Layered Design for Ruby on Rails Applications を読んだ時のメモ用
Layered Design for Ruby on Rails Applications
気になっていたので読んでみる。
zennのスクラップを使ってみたかった && 気になった箇所とか軽くメモしたり、感想を書いてみようかなと思った。
Chapter1: Rails as a Web Application Framework
概要
この章はRailsが用意してくれてる抽象化についての説明。
- Railsがwebリクエストを処理するときの抽象化レイヤー
- バックグラウンド処理の抽象化レイヤー
- データベース周りの抽象化レイヤー
の大枠3つ
気になったことメモ & 感想
- trace_location (https://github.com/yhirano55/trace_location) のgem使ってみたい。
# こんな感じで特定の処理をtraceしてくれるっぽい
TraceLocation.trace(format: :log) do
Rails.application.call(request)
end
- バックグラウンド処理のとこでGVLとスレッド数の関係の話があった。社内勉強会でもそういえばやったなー。単純にスレッド数をあげてもスループットは上がらない。 GVLTools(https://github.com/Shopify/gvltools)あたりで最適なスレッド数がわかる(こっそり今度やってみたい)
- 上記の代替策としてfibers (https://rubyapi.org/3.2/o/fiber)というものがあるらしい?後で調べてみる。(railsでは使えないらしいが)
- sidkiqの紹介もされてたけど、sidkiqって知れば知るほど難しい
- databaseの抽象化のとこでcancancan(https://github.com/CanCanCommunity/cancancan)が紹介されてた。初めて知ったがこれは認可を設定できるDSLを提供してるらしい
- アプリーケーションコードだけじゃなくて、監査目的で抽象化を定義でいる。PaperTrail (https://github.com/paper-trail-gem/paper_trail)、Logidze (https://github.com/palkan/logidze)はそのgemの例。
まとめ
- 改めてRailsが何をどうやって抽象化し、我々を守ってくれるか認識できた
- Rackはミドルウェア(関数の実行をラップするcomponent)でリクエストやレスポンスの抽象化してる
- バックグランドレイヤーではジョブとキューを扱いやすいよう抽象化してる
- etc...
- 改めて抽象化にはトレードオフあるよね...
- 知らないgemがいっぱいでてきた。面白い
chapter2: Active Models and Records
概要
ActiveRecordとActiveModel周りの抽象化について掘り下げよう的な。
気になったことメモ & 感想
- ActiveなんちゃらとActionなんちゃらがrailsにはたくさんあるが、Aユーザーとのインタラクションに関連するものにはAction、モデル関連のライブラリはActiveを使うらしい。意識したことなかったけどたしかにと思った。
- ORMには2つの主要な抽象化(Active RecordパターンとData Mapperパターン)がある
- Active Recordパターンは、永続化オブジェクトとビジネスモデル・オブジェクトの両方を実装しているため関心事の分離原則に違反しているという批判もあるが、DataMapperに比べて生産性がバカ高い。
- hanamiはDataMapperパターンらしい。ActiveRecordに比べて大変だ。。
- Ruby Object Mapperはデータマッパーパターンを実装した最も有名なライブラリ
- model層にはたくさん役割あるよね〜。validationは別クラスに分けても依存しやすいし、context使ってもそれはそれで増えたらつらい。(経験あり。。。。)
- database_validationsというgemは、対応する制約がデータベースに存在することが確認できるっぽい?
- database_consistencyツールは、バリデーションとデータベース制約を比較し、一貫性がない場合に通知する?
- Railsの 、#redirect_toや #link_toなどのヘルパーの多くは、Active Modelインターフェイスに依存してる
- Active Record Store使うとJSONとかいい感じにできるっぽい。Mysql5系使ってたのであんま恩恵なかったかも。でも便利そう。
- composed_ofマクロとmappingオプションが値オブジェクトつくるとき便利。
- ベンチマーク計測でbenchmark-ipsとbenchmark-memoryが便利そう
- structと比べるとActivemodelはやっぱ重いんだなあ
- churnは、あるファイルがどれくらいの頻度で変更されているかを表す指標らしい
- Flog やAttractor 使うと複雑さをいい感じに算出できる
まとめ
- ActiveRecord便利だけどGod ObjectになりやすいのはActiveRecordパターンを採用したトレードオフだよね〜
- ActiveModelはActionPackとActiveRecordの間に入っていい感じにしてくる。
- 読めば読むほど知らなかった or あんまつかったことなかった実装いっぱいでてきて勉強になるぞ〜
Chapter3: More Adapters, Less Implementaions
概要
ActiveJobとActiveStorageの抽象化に関するお話がメイン
メモ
- ご存知のようにActivejobはキュー、perfromというインターフェイス、実行ルールに関する抽象化を提供する
- ActiveJobのおかげで目的は同じで異なる実装のadapterを選べる(sidkiq, goodjob)
- sikkiqしかしらんけど、色んなgem紹介されてる(GoodJob (https://github.com/bensheldon/good_job) etc...)
- ActiveJobはAdapterパターン
- 別の実行環境(アプリーケーション側とバックグランド側など)でオブジェクトを移動し再構築する技術をシリアライズという(昔どっかで見た気がする)。workerの引数をオブジェクトじゃなくてidとか使えばテストはできるがdbに依存するようになるらしい。
- Rubyには標準のMarshalライブラリもあり、オブジェクトをバイナリストリームに変換できるらしいがActiveJobはカスタムなシリアライズをしてるらしい(GlobalIDのシステムの話)
- ActiveStorageも一種のAdapterパターン,service: disk, service: s3とかできるので。upload, downloadメソッドのインターフェイスも提供してる
- ActiveStorageにはpluginパターンで画像の変換等のこともできる。Adapterとpluginの違いはpluginは追加機能を提供すること
- 必要によりけりだけど、独自でサードパーティツールの抽象化(adapterとかwrapper)を定義して作ることもできるよ〜。それはそれで運用の難しさとかもありそうだけど
まとめ
- activejobとかseriarizeの内部的な定義とかはじめて意識した
- 徐々に抽象化のコード例がでてきておもしろい
Chapter4: Rails Anti-Patterns?
概要
Rails wayのなかでも論争が起こる部分, callbackやconcern、global stateについてのお話。
メモ
- callbackが親クラスの内部に依存すると予期せぬエラー発生するよね。httpリクエストに依存してるのでcontroller内のcallbackはまあいけるが、modelにcallbackが大量にあると死ぬ。(経験あり)
- ActiveRecordには19種類のcallbackがあるらしい。まじかよ。
- 例で出されてるコードが地獄みたいで草。
- modelからcallbackを全部駆逐すべき思想(個人的には結構同意)もあるが、この書籍では分離すべきかしないべきか5段階に評価している・
- transforming callback(データの変換的なやつ)★5
- normalization callback(正規化)★4
- normalizes :content, with: -> { _1.squish }が7.1から使える
- belongs_to :post, touch: true, counter_cache: true(内部的にcallbackが使われる)など技術的要件のためのcallback★4
- 関係ないけどPost.increment_counter(:comments_count, post_id)` みたいにincrement_counterなるものがあるらしい
- Operations and event handlers★1 or ★2
- callbackに条件やmodle以外のオブジェクトが出現したらきな臭いぞ!!!
- イベントソーシングパターン(イベント駆動とは異なる)は、イベントの変更のストリームとして保存する
- イベント駆動パターンActive Support Notificationsでpub/subが簡単につくれるらしい。
- イベント駆動に便利なThe dowsntream (https://github.com/bibendi/downstream) があるらしい
- 上記のパターンたちはそれはそれでmodel自身がpublisherであるべきか論争にもなるし、イベント発火の副作用的なものではなくビジネスロジックそのものに組み込みたい場合は適してないよね
- 昔concernの使い方で注意された(
https://blog.willnet.in/entry/2019/12/02/093000
)が、この本でもconcernに詰め込みすぎたり、使いすぎはよくないといわれてる(特に主要なロジックをconcernに書いてしまうとまとまりがなくなる)。behaviorsの抽出こそconcern化すべきだといってる(大事) - concernのファイルは名前空間つけて管理しよう
- concernはmoduleのためプライバシーの欠如, 命名の難しさ、テストの複雑さの問題あり
- 肥大化したconcern(複数のdbやAPIに依存)をclassとして抽出し、concernを残したままリファクタするコードはなるほど〜って感じ(concernを残して委譲させるのはよいリファクタ感ある)
- 同じくData構造が同じだがパターンが複数ある場合はValueObjectが良さそう。Data.defineの使い方なるほどなー
- 上記のの委譲, Valueを使っても下位レイヤーでの中小抽出はレイヤーの境界簡単に超えちゃうよねー(多分ここらへんの話がpart2のメイン?)
-
ActiveSupport::CurrentAttributes
はじめてみた。(実行contextでglobalな属性が定義できる) - GlobalStateはよほどじゃない限り使いたくない。。
まとめ
Concernやcallbackはあるあるというか簡単に使えるがゆえの管理の難しさがあるよね。。。
Chapter4: When Rails Abstractions Are not Enough
概要
RailsのMVCの限界とその症状。あとはMVCから逸脱した抽象化であるサービスオブジェクトについて
memo
- controllerなりmodelがfat/thinになるのは宿命....
- requestテストはmodelテストより高価よね(抽象度高いとその分テスト大変)
- modelの classメソッドがcontrollerのpayloadの形式に依存してると逆依存
- サービスオブジェクトの定義は結構曖昧だし、運用のされ方もたくさんある
- インターフェイスとかは整えて良さそう
- 基本
-Service
という命名 - controllerとmodelのfatを防ぐ一時的な手段ともいっていた
- serviceを使うとAnemic models(modelの貧弱)や手続きになりすぎるデメリットがあるし、なんでもserviceObject化されてしまう
- 5章にして本書のタイトルである「Layered architecture」と抽象化について伏線が回収される
- Layered architectureは水平な論理レイヤーに分割すること(Presentation, Application,Domain layer, Infrastructure layer)。上のレイヤーには依存しないのがポイント。
- 抽象化は実装の詳細を隠すこと。ポイントはアーキテクチャーの境界を越える抽象化レイヤーを持ってはならないということ。
chapter6: Data Layer Abstractions
概要
ActiveRecord周りの新たな抽象化について。クエリオブジェクトとリポジトリらへんがメインかな?
メモ
- クエリオブジェクト
- CTE(.withメソッド)というのを使うとパフォーマンスよくなる?参考
- queryの呼び出しはscopeなりクラスメソッドなりをmodelに書けばある程度重複削除や分離という意味ではいいけどmodelはfatになるよね〜。そのためにqueryオブジェクト!
- クエリオブジェクトの主な責任は、ドメインから永続化レイヤーを分離すること。ドメインレベルのオブジェクトを受け取ってクエリを構築するオブジェクト
- ただ抽象化には規定も必要。のでベースとなる共通のクエリオブジェクトを作ろう!(単一のインターフェイス、コンストラクタの共通の引数etc...)
- scopeとも役割似てるけど、scopeは使いすぎるとconflict起きやすくなるよね。。。ただscopeのほうが可読性の観点からは良さそう。
- scopeにqueryオブジェクトをアタッチできる
- sqliteにはjson_eachなるものがあるらしい?
- queryの共通のApplicationQueryはちょっとおしゃれなことしないといけなそうだな(model名の取得とか)。それ許容できるならありだけど、実務だと際どそう。。。
- Arelに慣れてないよ〜
- queryオブジェクトはmodel配下に置くのが良さそう?_query.rbとかファイル名つけて
- Repositoryパターン
- ActiveRecordRepositoryという抽象化を導入してみて、DataMapperのアイディアを試す感じ
- リポジトリはデータとドメインモデルの中間
- 1つのドメインモデルに複数のリポジトリを導入することもできる
- リポジトリの各メソッドの返り値はActiveRecordにしない(実装の詳細を隠す)
- モジュラーモノリス、マイクロサービスしたいRailsならありな気がする。(単純な大規模モノリシックでやっていく場合は....)
chapter7: Handling User Input outside of Models
概要
ユーザー主導の操作を処理するための抽象化
formオブジェクトとフィルターオブジェクトについて
メモ
- フォームオブジェクト
- フォーム・オブジェクトは、データ送信を伴う特定のユーザー・インタラクションの処理を担当する
- フォームオブジェクトはプレゼンテーション層に属する(どっちかというとcontrollerとview寄り)
- ビューフォームのビルダーとかビューコンポーネントとは区別する必要がある
- include ActiveModel::API, include ActiveModel::Attributes、ちゃんと使う部分だけincludeするのは良さそうだ。
- フォームオブジェクト用の抽象クラス
ApplicationForm
はありなアイデアだ!(毎回個別のformを作ってる気がする) - ActionModelのAPIを意識することで、viewとcontroller側もフォームオブジェクトでだいぶシンプルに書けるな。これはいい発見。使っていきたい。
- フィルターオブジェクト
- フィルターオブジェクトは、ユーザーから与えられたパラメーターに基づいてデータセットを変換する役割を持つオブジェクト。検索とか並び替えのやり方が複数ある場合とかに有効
- 仮にフィルターオブジェクト使わないとcontrollerやmodelがfatになる可能性が高い
- 本ではRubanok (https://github.com/palkan/rubanok)を使ってる
- フィルターオブジェクトは、ユーザーから与えられたパラメーターに基づいてデータセットを変換する役割を持つオブジェクト。検索とか並び替えのやり方が複数ある場合とかに有効
chapter8: Pulling Out the Representation Layer
概要
modelから表示関連のロジックを切り離す方法
プレゼンターとシリアライザー(API onlyの場合)がmain
memo
- 原則的にやってはだめだけどmodelに特定のviewに依存するようなものが書けちゃう(まあまあよく見るきはする)
- helperはグローバルっていうのが注意(グローバルじゃなくすることもできるらし)
- helperは単純なmoduleとmethodが定義できるだけで、記述が増えると保守が大変。全ファイル共通のユーティリティとかの定義にはもってこい。(よく実務ではオリジナルのsimple_format的なやつとか定義してた気がする)
- decoratorとpresenterはmodelとviewの橋渡しをし、特定のmodelをwrapすることでviewで使える便利な拡張ドメインオブジェクトが使えるようになる
- presenterの場合のデメリットは委譲するメソッドを追跡する必要があったり、表現ロジックと関係ないユーティリティメソッドを公開しないといけないことがあったりすること
- デコレータは与えられたオブジェクトをラップし、与えられたクラスの他のインスタンスに影響を与えたり新しいクラスを作成したりすることなく、動的に新しい振る舞いを追加できる。また明示的に定義していないメソッドも、ラップされたオブジェクトに委譲される(presenter層の一種のパターンとしてdecoratorパターンを使えるという感じ)
- decoratorパターンはデザインパターンとして有名だが、Railsではプレゼンター周りのこととして使われる
- Rubyには
SimpleDelegator
という組み込みのデコレータ作成クラスがある - プレゼンターの概念は、一度に複数のオブジェクトに簡単に適用できる
- 抽象レイヤーとしてのプレゼンターの規約
- 接尾辞に-Presenterをつける(model名から自動推測するのがよい)
- app/presenter二配置
- 同じオブジェクトに新しいプレゼンターを割り当てないようキャッシュする
- keynote (https://github.com/rf-/keynote)を利用するのもよさそう
- プレゼンターはをあまり早い段階でその表現に置き換えるべきではない。コントローラでデコレートしたくなるが副作用もある。(早めに呼びすぎるとcontrollerが他のレイヤーを呼び出しているところに対して副作用が起きる)
- SerializerはAPI用。as_json使えばオブジェクトをjsonにできるけど、返すデータに無駄が生じる。(as_jsonをオーバライドすれば解決するが....)。それにmodelとjsonの結合が強い。
- 個人的jbuilderは好かない。
- Presenterにas_jsonを定義すればシリアライザーにできるがツラミ。
- alba gem (https://github.com/okuramasafumi/alba) は便利。好き。
chapter 9: Authorization Models and Layers
概要
認可・認証まわりの抽象化のお話
memo
- 認可・認証の実行経路に注意。抽象化レイヤーのどこでもやっていいわけではない。
- 認可はプレゼンテーション層で行われるが、ドメインオブジェクトに依存する
- 認可専用のmodelをまず作る必要はなく、
if post.user_id == current_user.id
でいける - RBACはroleベースの認可モデル。userは複数のroleを持つことができ、roleを持つことで特定のドメインにアクセスできる
- アプリーケーションが複雑になるとrole管理が大変なのでPermissionを使う。
- それでもroleが多くなると大変。
- modelが肥大化したら、Valueオブジェクトを使うと良い
- ABACはAttribute-basedの認可モデル。RBACの一般化みたいなイメージでRoleだけでなく、他の属性にも注目する
- 例えばAというroleを持っているuserが、10~17時(属性)のあいだにアクセスできる的な意味。
- 柔軟だけどその分複雑性は増えるよね。
- 本書の立場的に認可のルールはビジネスロジックにいれたい!それはレスポンス形式に依存すべきでない!一方で認可の実施はプレゼンテーション層にとどまる必要がある!認可はmodelに属すべきではない、ドメインオブジェクトは認可されたコンテキスト上で存在すべきため
- Polycyオブジェクトパターンは、与えられたコンテキスト内でどの操作を実行できるか、または実行できないかを記述するビジネスルールをカプセル化する
- Action Policy (https://github.com/palkan/action_policy) 便利。
- 認可classのメソッドは#show?, #update?, and #destroy?.あたりを使うとcontrollerと対応して良い。読み取りと書き込み的な分け方で#view?と#manage?もあり。(ActionPolicyはそう)
- ActinonPolicyの命名に従ってれば制約はその分あるが、裏で色々やってくれる
- assert_authorized_toというテストで使える便利なメソッドがあるらしい
- 認可が失敗すると例外が起こるようになってる
- allowed_to?はviewファイルで呼べるヘルパー
- 気をつけないとパフォーマンス影響もあるけど、preload, eager_loadのインターフェイスも用意する、キャッシュを使うで対処しよう。
chapter10: Crafting the Notifications Layer
概要
Mailer周りの抽象化に関する内容
memo
- modelのコールバックでmail送信はレイヤードアーキテクチャ違反!(個人的にも嫌い)
- 最近は通知先がメールだけじゃなくてたくさんあるから大変。サービスオブジェクトに送信メソッドを生やすとその中身がえげつない量になる。
- 基本的にはすべてのチャネルに送信したいので、通信の詳細を隠した抽象化レイヤーほしいよねって感じか
- 単純なリファクタリング(再配置)も役に立つ
- ApplicationNotifierによる抽象化
- .plug DSLのメソッド
- インターフェイス はnotifyだけ。
- active_deliveryとnoticedの2つがサードパーティライブラリとして存在する
- active_deliveryはActionMailerの使い方に似ている
- 電子メール以外の場合はAbstractNotifierを使うことができる
- Noticedというgefは通知と対応する通知オブジェクトが1対1
- NotecedはNotiferとDeliveryが一緒で, active_deliveryはNotiferとDerliveryを分離してるイメージ。
- User modelに...enabled?などの要素を追加することによって、modelはGodになりやすくなってしまう
- Bit fields and value objectsが1つ解決方法としてある
chapter11: Better Abstractions for HTML Views
概要
viewファイル周りのおはなし
memo
- RailsのViewは優れているけど肥大化すると保守性が乏しくなるよねってお話
- viewはhtmlでhtmlは平たくいえばテキストファイル。使い方に対するAPIを提供しない。
- 部分テンプレートにインスタンス変数をおくこともできるので、そうするとcontroller二依存するようになる
- rails7.1からは
<%# locals: (quiz:, result:, prev_result: nil) -%>
みたいに部分テンプレートにマジックコメントを書くと、ローカル変数を呼び出し側で強制できる - erb-lintを使って対策する方法もある
- 基本的にUIを抽象化するのがデフォルトのRailsだとパーシャルとhelperしかない。ボタンやフォーム単位のパーツが抽象化されないのはそのためだし、ActionView使って、cssだけ変えればなんとかなるから
- vue, reactと同じようにcomponentで考えよう
- rubyのclassを使ってコンポーネントを記述する。そうすることで状態に依存せt図にUIを構築できる
- render_inはActionViewのAPIで任意のコンテキストでレンダリングできる
- ViewComponent (https://viewcomponent.org) というgemがあり、いい感じにrailsでのcomponentの抽象化をしてくれる
- componentはviewのパーシャルより10倍早い
Configuration as a First-Class Application Citizen
概要
middlewareとかインフラレイヤーの設定周りのお話
memo
- 環境変数が主要な設定ソースとして使われているよね
- クラウド, コンテナ普及により色んな環境でコードを動かすにはハードコードするより環境変数。とか色々メリットがあるため- 一方で環境変数地獄になる
- デフォルト値の設定(開発・テスト用)、fetchを利用して全環境で設定しないといけないことを防ぐこともできる
- 定数、秘匿値、設定値それぞれの特性に応じて、設定をする
- これらの値にアプリケーションコードが依存するのはよくないよね
- 設定値を集約するyamlファイルをつくる
- yaml二複雑さを隠すほうがコードに複雑さを隠すよりもよさげ??
- Rails.env?がアプリケーションコード内にあるのはきな臭い。
- Anyway Config (https://github.com/palkan/anyway_config)を使うと、yamlの複雑さとconfig_forのグローバルを避けられる。設定値をオブジェクト化する
chapter13: Cross-Layers and Off-Layers
概要
ロギング、監視、例外追跡などなど...インフラレイヤ周り
規約やレイヤーをまたいだ抽象化
memo
- Active Recordでは、下層の抽象化をたどっていくとActiveRecord::Base.connectionがあるように、高レイヤーから低レイヤーに辿ってく道にニーズがある
- ログはRailsの仕組みとしてある程度用意されてる。それでも足りない場合カスタムログを定義する
- cross layer(あらゆる抽象化をまたぐ)として使われる
- puts が1番簡単な方法(情報は足りないが)
- Rails.loggerがrailsがデフォルトで用意しているlogの抽象化
- Rails.loggerにはActiveSupport::TaggedLoggingモジュールが組み込まれていて、ログをフィルタリングできる
-
Rails.logger.tagged("★")
とかconfig.log_tags = [ :request_id, proc { |request| request.headers["X-USER-ID"] }]
とか使える
-
- 例外通知はrails7からgemの固有の実装に依存せずにすることができる
-
Rails.error.report(error, handled: true)
みたいな感じ。
-
- メトリクスを集める、instrumentation
- Active Support Notificationsがrailsには用意されている。これでイベントをpub/subできる 。
- Yabeda(https://github.com/yabeda-rb/yabeda)はRubyアプリケーションのための計測フレームワーク
- 低レベルな抽象化と中間の抽象化の切り離しはパフォーマンスにとってもメリットがある。そうすることで低レベルなものと他の抽象化の組み合わせが自由になる
最後難しかったけど読了!!!
おもしろかった。また見返したい!!!