原典
- 7 Patterns to Refactor Fat ActiveRecord Models
-
肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)
- FatModelをリファクタリングするパターンを示し、議論の先駆けとなった記事の模様。
Service/Form/Decoratorについて
Service
自分的解説
- 以下のいずれかとして利用されるように思われる。
- ユースケースごとに複数のコンテキスト(ユーザーの更新とメールの送信など)を組み合わせて記述するレイヤー。(= アプリケーションサービス)
または - モデルに実装すると不自然なドメインロジック(複数のオブジェクトを組み合わせて表現するロジックなど)を記述するレイヤー。(= ドメインサービス)
- パーフェクトRails増補改訂版ではこちら。
- ユースケースごとに複数のコンテキスト(ユーザーの更新とメールの送信など)を組み合わせて記述するレイヤー。(= アプリケーションサービス)
参考記事
-
似非サービスクラスの殺し方 / #ginzarb
- RailsはDDDに親和性がないため、サービスクラスは複数のモデルにまたがるトランザクション管理のためのクラスと考えておけば概ね大丈夫、とのこと。※ 下2つの記事はこれと意見が異なる。
-
俺が悪かった。素直に間違いを認めるから、もうサービスクラスとか作るのは止めてくれ
- DDD文脈におけるドメインサービスをapp/servicesに作るのでなく、オブジェクト指向プログラミングを理解して app/models にPOROを作れるようになりましょうという話(であるという理解。上の記事と著者が同じため、上の記事に対する、ドメインサービスとしてサービスクラスを作るのはやめましょうといったような訂正の意図が含まれていそう)。
-
Rails サービスクラス再考 / have a rethink on Rails service class
- サービスクラスを app/services においてレイヤーを作る構成にするならアプリケーションサービスとして扱うべきで、ビジネスロジックはあくまで app/models に作るべきという話。
- サービスクラスはあくまで app/models 配下のpublic method をハンドリングするだけ。以下挙げられているアンチパターン。
- 肥大化したモデルのメソッドをただサービスクラスに移す
- ロジックの重複が起こりやすい。
- 複数モデルを触るのでサービスクラスにする
- オブジェクトの生成条件が守れなくなっており、これも本来クラスが持つべき責務が漏れ出してしまっている。
- 複数のモデルを触るとき〜のような画一的なルールではなくて、誰が担う責務なのかをまず考えるべき。
- 集約の境界を意識する。
- 肥大化したモデルのメソッドをただサービスクラスに移す
-
Railsの太ったモデルをダイエットさせる方法について
- サービスクラスの実装例。
-
Railsのデザインパターン: Interactorオブジェクト
- サービスクラスの実装例。
-
https://github.com/mastodon/mastodon/tree/main/app/services
- サービスクラスの実装例。
-
Railsで処理を別クラスに切り出す方法について
- POROの実装例。
Form
自分的解説
- 各フォーム(
form_with
やform_for
,form_tag
)によって異なるバリデーションやコールバックのロジックを、モデルから分離して記述するレイヤー。 - 複数リリースの同時保存・更新を行う方法として(
accepts_nested_attributes_for
と比較して)推奨される。 - 自分の経験としては、コントローラがAPIのエンドポイントであった場合、APIのリクエストパラメータをフォームのパラメータに見立ててFormオブジェクトを使用していたRailsアプリケーションにも何度か出くわした。
- Formオブジェクトという呼ばれ方をされつつも、コントローラのパラメータを(集約などで)上手に扱うための(アプリケーション層の)レイヤーとして利用するイメージ。
- また個人的に、1つのsaveメソッドで集約のバリデーションを実行できるのが良い。
- texta.fm #2によると、バリデーションは画面(UI)に引っ張られるので、フォームオブジェクトは本来バリデーションが行われる場所。
- パーフェクトRails増補改訂版では、アプリケーションサービス/インタラクターが相当するとされている。
参考記事
-
accepts_nested_attributes_forを使わず、複数の子レコードを保存する
- Formオブジェクトの実装例。例のコードが綺麗な気がする。
-
Model と画面上の form が1対1で一致しない場合、どのように実装するのが綺麗なのか?
- Formオブジェクトの実装例。
-
form objectを使ってみよう
- Formオブジェクトの実装例。
-
Railsのデザインパターン: Formオブジェクト
- Formオブジェクトの実装例。
-
https://github.com/mastodon/mastodon/tree/main/app/models/form
- Formオブジェクトの実装例。
Decorator
自分的解説
- ユーザに表示する画面の内容や、データや処理の結果をどのように表現するかというロジックを、ビューヘルパーの代わりにモデルのメソッドとして記述するレイヤー。
参考記事
-
Railsでやってしまいがちな保守性を下げてしまうコードとその解決策
- (引用)デコレータに使うのは特定のビューに依存したものではなくモデルを表示用に整形するときだけが望ましいと思います、という良い注意点。
-
https://github.com/drapergem/draper
- 代表的なgemの1つ。
-
https://github.com/amatsuda/active_decorator
- 代表的なgemの1つ。
-
https://nekorails.hatenablog.com/entry/2018/10/13/070437
- Decoratorの実装例。
Railsエンジニア向けDDD・レイヤードアーキテクチャ・クリーンアーキテクチャ・PoEAA用語集
Railsのアーキテクチャについての記事でも関連用語が頻出するので、どういうものを指しているか理解できるようにする。
パーフェクトRuby on Rails 増補改訂版 Part5 エキスパートRailsより
ドメイン
- アプリケーションが対象とする問題領域のこと。
ドメインモデル
- ドメインを分析して構成概念を抽出(モデリング)した結果得られた概念のこと。
ドメインロジック(ビジネスロジック)
- ドメインモデルに関連する属性と振る舞いのこと。ドメインモデルはこれらを持ったオブジェクトとして定義される。
エンティティ
- 属性の値に関わらず一意に識別されるオブジェクトのこと。
- 例: Railsのモデルのインスタンスは「id」を識別子とするエンティティ。
- texta.fmによると、Railsのモデルというのは不正な状態があり得る(invalid)ので、厳密にはエンティティではない。
- 信頼できないユーザーの入力を受け付ける。入力が不正であるときにエラーにできないことから。
値オブジェクト
- 値が同じであればアプリケーション上は同じものとして扱われるオブジェクトのこと。
- 例: 「性」「名」「生年月日」など。
アクティブレコード
- データの取得・保存処理とドメインロジックを合わせてカプセル化するアーキテクチャパターン。
- データベースのテーブル = クラス
- データベースのレコード = クラスのインスタンス
- データベースのカラム = インスタンスの属性
ユースケース
- 何らかの目的を達成するために行われるユーザとアプリケーションの間の一連のやり取りを表したもの。
- 例: GitHubアカウントでログインをクリックするとGitHubのアプリケーション認証画面に遷移する、イベント登録フォームで間違った入力をするとエラーメッセージが表示される
- texta.fm #4によると、Webアプリケーションでは、ユーザーとアプリケーションの間のやり取りはHTTPを介して行われるため、どのようなやり取りができるかはそのアプリケーションのルーティングによって決まる。どういうことをシステムを通じてやろうとしているのか、を実現しようとしている道具がユースケース。
アプリケーションサービス / インタラクター(interactor)
- ユースケースのロジックを実装するオブジェクト。
- ユースケースのロジックとは、特定のユースケースに固有であり、対象のユースケースがなければそもそも存在しないような振る舞いのこと。ドメインロジックとは異なる種類のロジック。
- 例: サービスの利用規約の同意にチェックを入れているかどうかを検証する、ユーザ登録が完了したらウェルカムメールを送信する
- ユースケースの一部または全体の処理に対応するメソッドを持つ。
ドメインサービスを書く時の判断基準と大事にしていることより
ドメインサービス
- そのドメインに特化したタスクをこなす、ステートレスな操作のこと。実行すべき何かの操作があって、それを集約や値オブジェクトのメソッドにするのは場違いだと感じたときは、ドメインモデルの中でサービスを作る。
- ドメインオブジェクトに持たせるには不自然なふるまいであるビジネスルール。
混乱しがちなサービスという概念についてより
ドメインサービス
- ドメインモデルを入出力にとる関数。
- オブジェクトとしてモデル化すると不自然なもの。
- モデルの言語を用いてインターフェイスを定義し、操作名が必ずユビキタス言語の一部になるようにする。
- サービスには状態を持たせない。
アプリケーションサービス
- ユースケースに強く依存する。
- ビジネスロジックそのものではない。
- ドメインモデルやインフラストラクチャサービスなどのやるべき作業を調整し進捗管理をするためだけの存在。
- I/Oの整合性を保証するためのトランザクション制御や、特定の処理完了を告げるためにプッシュ通知サービスを呼び出したりすることがある。
- 単純なユースケースであれば、コントローラなどにこのようなロジックを直接書くかもしれないが、意図をより明確にするために別の名前をつけたメソッドに切り出されたもの。
DDDにおける集約 - Martin Fowler's Bliki (ja)より
集約
- ひとまとまりとして扱えるようなドメインオブジェクトの小集合。
- 例: 「注文」と「品目」は別々のオブジェクトになるが、注文を(品目と一緒に)集約として扱うと便利になる。
- ひとつのコンポーネントであるオブジェクトを集約ルートとして持つ。
- 集約の外部からの参照は集約ルートにのみ行くべき。
- 集約はデータストレージ転送の基本要素となり、集約の単位でロードや、保存を要求する。
- トランザクションは集約の境界は超えてはいけない。
- DDDにおける集約はドメインコンセプト。
- 例: 注文、診察受診、プレイリスト
- 多くの場合、集約には簡単なフィールドと複数のコレクションが含まれる。
クリーンアーキテクチャ完全に理解したより
- クリーンアーキテクチャはDDDの概念や用語を流用しているが、レイヤーに関する考え方はDDDとは異なる。
- ヘキサゴナルアーキテクチャ、オニオンアーキテクチャ、クリーンアーキテクチャが本質的に同じである。
レイヤードアーキテクチャ
ユーザインターフェース/プレゼンテーション
- ユーザに情報を表示して、ユーザのコマンドを解釈する責務を負う。外部アクタは人間のユーザではなく、別のコンピュータシステムのこともある。
アプリケーション
- ソフトウェアが行うことになっている仕事を定義し、表現力豊かなドメインオブジェクトが問題を解決するように導く。
- ビジネスルールや知識を含まず、やるべき作業を調整するだけで、実際の処理は、ドメインオブジェクトによって直下のレイヤで実行される共同作業に委譲する。
ドメイン
- ビジネスの概念と、ビジネスが置かれた状況に関する情報、およびビジネスルールを表す責務を負う。ビジネスの状況を反映する状態はここで制御され使用されるが、それを格納するという技術的な詳細は、インフラストラクチャに委譲される。この層がビジネスソフトウェアの核心である。
インフラストラクチャ
- 上位のレイヤを支える一般的な技術的機能を提供する。これには、アプリケーションのためのメッセージ送信、ドメインのための永続化、ユーザインタフェースのためのウィジェット描画などがある。インフラストラクチャ層は、ここで示す4層間における相互作用のパターンも、アーキテクチャフレームワークを通じてサポートすることがある。
クリーンアーキテクチャの4つのレイヤー
Enterprise Business Rules
- Entities
- 値オブジェクト
- Domain Service
Application Business Rules
- Use Cases
- Interactor
Interface Adapters
- Controllers / Presenters / Gateways
Frameworks & Drivers
- Web / UI / External INterfaces / Devices / DB
Ruby on Railsの正体と向き合い方 / What is Ruby on Rails and how to deal with it?より
クリーンアーキテクチャ
Enterprise Business Rules (例: Entity, Value Object)
- オブジェクト(や関数)の形でアプリケーションの ビジネスロジックをカプセル化する
Application Business Rules (例: Interactor)
- Enterprise Business Rulesのオブジェクト群を 使って各ユースケースの処理の流れを組み立てる
Interface Adapters (例: Controller, Presenter, Repository)
- 内側の円から外側(WebやDBなど)の円、または その逆方向へデータの形式をよしなに変換する
Active Recordから考える次世代のRuby on Railsの方向性 / Directions for the next generation of Ruby on Rails: From the viewpoint of its Active Record より
PoEAAにおける3つのレイヤーについて。
1. プレゼンテーション
ユーザーからのコマンド(例: HTTPリクエスト)を下位レイヤーの呼び出しへ変換、ユーザーへの情報表示。
※ おそらくRailsではコントローラとビューが相当する。
2. ドメイン
入力データの妥当性確認、入力・格納データにもとづく計算。
トランザクションスクリプト
- プレゼンテーションレイヤーからの1つの要求を処理する手続き
ドメインモデル
- アプリケーションが対象とする問題領域(ドメイン)から抽出された概念をオブジェクトとして定義したもの
サービスレイヤー
- ドメインレイヤーにおけるドメインモデルの上位レイヤー。
- 複数のドメインモデルやデータソースを協調させて、 プレゼンテーションレイヤーからの要求に応える役割を担う
3. データソース
データベース、メッセージングシステムなどとの通信。
(テーブル|行)データゲートウェイ
- RDB内の(テーブル|レコード)への操作をカプセル化するオブジェクト
アクティブレコード
- ドメインロジックを実装した行データゲートウェイ
※ RailのActiveRecordは、パターンとしてのアクティブレコードを利用したORマッパー。
データマッパー
- ドメインモデルとRDB内のレコードを相互変換するもの。
- texta.fm #4によると、既存のデータベースを使っていたり、データベースの構造を変えられないときに、ドメインモデルを引数にしてsaveとかを行うもの。
Smart UI パターンが再評価される世界より
- DDD(エリック・エヴァンスのドメイン駆動設計)の説明。
Smart UIパターン
- デメリット
- 画面ごとの .cgi ファイルに、上の方にデータロードのコードが、下の方に UI のコードが書いてある。何なら混ざり合ってる。
- 例: KENT WEB時代ぐらいのPerlやPHPのCGI。
- メリット
- 画面ごとに分かれているので、影響が局所化される。
- もっともコピペが横行していて、その分労働集約的に並列作業できる、他には影響を与えずに改修できる。
トランザクションスクリプト
- template とは分離された、依然として手続き的なコード。
- 各 Action 間でのコピペは引き続き横行している。
PoEAAの説明
テーブルデータゲートウェイ
- ほぼテーブル単位
- すべてのCRUDはこのゲートウェイを通る
- SQLや、クエリビルダの組み立てが書かれるクラス
行データゲートウェイ
- 行単位のインスタンス
- insert/update/delete は行インスタンス自身が知っている
- アクティブレコードパターンと類似しているが、アクティブレコードパターンは何らかのドメインロジックもレコードに持たせるところが違う。
- 行データゲートウェイはただのゲートウェイ。
- トランザクションスクリプトを行データゲートウェイとともに使用する場合、複数のスクリプトで繰り返されるビジネスロジックこそが、行データゲートウェイに必要なロジックであることがわかる。
- ロジックを移動することによって、行データゲートウェイは段階的にアクティブレコードへと変化し、ビジネスロジックの重複を軽減する効果をもたらす。
アクティブレコード
- トランザクションスクリプトからロジックをゲートウェイ自身に持たせたらActiveRecordパターンになる。
データマッパー
- レイヤー化しようと思うとデータマッパーパターンになりがち
- だけど、密結合してエイッてやるともっと楽だよというのが Rails が示した道。
- 各レイヤー間を疎結合にしようとすると DTO による詰め直しが必要になる。
トランザクションスクリプト
- Smart UI パターンよりモデル化していきやすいので、何らかのコードをトランザクションスクリプトに抜き出すのは推奨されている。
レイヤー分けについて
- どういうレイヤー分けを行うかは正解がない。
- 何もレイヤー化しないときはすべてをControllerに書く
- Fat Controller, Skinny Model
- むしろモデルは存在しなくてデータソースだけの場合もある。
- ISUCON のコードでよく見るパターン。
- 最も一般的な方法=テーブル単位でクラスを作ること
- 一番分かりやすい分離ポイント。
- なんだけど、元々はトランザクション境界を置くからトランザクションスクリプトという名前にしているのに、このパターンだとテーブルをまたぐ処理に弱い。
- もう一つの方法=<動詞>Service
PoEAAに書かれているドメインロジックを構築する方法3種類
- オブジェクト指向の考え方を用いてどんどんモデル化することでコードを進化させていく、という行為は、入り口がどれでも同じ。
- ドメインオブジェクトを抽出しても手続き的なコードも必要になる。
トランザクションスクリプト
- データロードとTemplateを分離する。
- トランザクションスクリプトからでもオブジェクト指向の考え方で育てていける。
テーブルモジュール
- リレーショナルデータベースの2次元の表をうまく扱うクラスみたいなもの。でドメインモデルを使うもの。
ドメインモデル
- データと振る舞いを1箇所に集めようとしているのが「ドメイン」。
いまさらきけない「ドメインモデル」と「トランザクションスクリプト」より
トランザクションスクリプト
- 著者の最初の主張
- 「アクションより起動される一連の手続き」
- ユースケース(アクション)中心にオブジェクト(メソッド)を組み立てる。
- DIを使ったトランザクションスクリプトは、手続き型の悪い設計ではなくオブジェクト指向の原則にのっとった筋の良い設計。
- ユーザの要件であるユースケースとその実装であるサービスクラスが一対一に結びついていたほうが、要件の変更が入ったときに、どのクラスを修正すればいいか一目瞭然なので、メンテナンス性が高い。
- 手続き分割していくわけではなく、ユースケースごとに最も責任を持つクラスに分割しているし、DIによってオープンクローズの原則(OCP)もきちんと間持ている。
- 複数のユースケースの間でロジックが重複しても気づかず、同じようなロジックが分散しやすい。
- 著者の最後の主張
- 「DDDのサービスパターンを使ったドメインモデル」としてビジネスロジックを実装する。
- 「ドメインで扱う概念の中には、1つの機能や処理が単体で存在していて、もの(オブジェクト)として扱うのが不自然なもの」もある。そうしたものは、サービスという形でユビキタス言語に組み込む。サービスは基本的に状態をもたない(stateless)
- 「DDDのサービスパターンを使ったドメインモデル」としてビジネスロジックを実装する。
ドメインモデル
- 「ドメイン内の名詞によって体系化されたモデル」
- ドメインオブジェクトを中心にオブジェクトを組み立てる。
エヴァンスの分類 - Martin Fowler's Bliki (ja)より
エンティティ
- アイデンティティを持ち、時間経過によって形を変えるオブジェクト。
- 例: 「顧客」「出荷」「賃貸契約」など大きなもの。
バリューオブジェクト
- 属性の組み合わせに意味があるオブジェクト。同じ値(バリュー)を持つ2つのバリューオブジェクトは、どちらもすべての属性が等しいと見なされる。
- 例: 「時間」「お金」「データベースクエリー」など小さなもの。
サービス
- ドメインにおける独立したオペレーション。サービスオブジェクトは1つ以上のサービスを持つ。実行文脈におけるサービスオブジェクト型のインスタンスは、通常は1つだけである。
- 例: 「データベースコネクション」「メッセージングゲートウェイ」「レポジトリ」「プロダクトファクトリ」といった外部リソースにアクセスするもの。
- グローバル変数やクラス変数(Robert Martinの言うmonostates)、あるいはシングルトンで実装されることが多い。
- 「ただ1つ」というのは「ひとつの処理内において」という意味であり、 マルチスレッド環境では「ひとつのスレッド内で」という意味になる。
- 他のドメインオブジェクトから見えないようにして、実装メカニズムを容易に変更できるようにしなければならない。
- 今は必ずしもステートレスではなくてもよい、ということになっている。 もちろん、ステートレスにできるなら、したほうがよい。
サービスレイヤ- Martin Fowler's Bliki (ja)より
- データにアクセスしたり、データを操作したり、ビジネスロジックを呼び出したりと、アプリケーションとの相互作用が必要である点で共通している。
- アプリケーション境界とクライアント層から利用可能な操作を定める。
- 操作の実装のなかで、アプリケーションのビジネスロジック、トランザクション制御、レスポンスの取りまとめをカプセル化する。
その他参考リンク
Railsのアーキテクチャについての読み物
パーフェクトRuby on Rails 増補改訂版 Part5 エキスパートRailsより
複雑なドメインを表現する
値オブジェクト
- 値オブジェクトをモデルから分離することで、複数のモデルの間で再利用できるようになり、モデルが不要に肥大化することがなくなる。
- ActiveRecord::Aggregations::ClassMethods#composed_of
サービスオブジェクト
- 複数のオブジェクトを組み合わせて表現するロジックを独立したオブジェクトとして定義したもの。
- 例: ユーザー認証サービス、合計金額計算サービス
- ドメインロジックそのもの。
- 自身の動作を変更するような状態を持たない。
- 入力が同じであれば常に同じ結果を返す。
- インスタンス変数をはじめとした自身の状態を更新しない。
- モデルに実装すべきドメインロジックまで実装しないように注意する。
- 引数のオブジェクトの属性の値を直接参照・更新しているような箇所があれば、その操作を対応するモデルのインスタンスメソッドとして定義する。
- クラスの名前はある1つのドメインロジックを指すものにする。
イベントを表現するモデルを導入する
- サービスオブジェクトを利用しない方法。
- コールバックを使用することでActiveRecord::Base#transactionの呼び出しが必要なくなる。
- 例: 口座間の送金という事実。
複雑なユースケースを実現する
データベースと紐付かないモデルを作る
- Railsでユースケースを実現する代表的なものがバリデーションとコールバック。
- RailsはURLで表されるリソースとデータベースのテーブルの一対一関係を前提として、モデルに異なる2種類のロジック(バリデーションとコールバック?)を実装できるようにしているが、機能要求が複雑になるにつれて、あるテーブルのある種類のデータ操作が複数のユースケースで行われるようになったり、対応するテーブルが存在しないリソースのCRUD操作を提供する必要が出てくる。
ユースケースのロジックを実装するレイヤーを導入する(ActiveModel/フォームオブジェクト)
- コントローラやビューのメソッド(form_withなど)と連携できるようにActiveModelを利用する。
- フォームオブジェクト
- form_withとの連携に必要なインタフェースを持たせたもの。
- モデルのユースケースのロジック(バリデーションやコールバック)を分離できる。
- ActiveModel::EachValidatorやActiveModel::Validatorを利用して、フォームオブジェクトやモデルの共通のバリデーションルールを定義することもできる。
プレゼンター
- Railsでは、あるモデルが持つ属性やロジックを利用して、表示に関するロジックを実装するオブジェクトのこと。
- この中でデータや処理の結果をどのように表現するか、というビューヘルパーの実装は、全てのコントローラのビューで利用できてしまうため、コントローラ単位ではなくビューに渡すオブジェクトの単位で表示に関するロジックを整理することでビューヘルパーの問題点を解決する。
- ActiveDecoratorは、モジュールとして定義されたプレゼンターを対応するモデルの各インスタンスで動的にextendすることで、ビューに渡したモデルのインスタンスをレシーバとしてDecoratorのメソッドを呼び出すことができる。
- ビューの中でしか利用できないことに注意。
複雑なデータ操作を実装する
Concern
- 特定の概念や機能に関するロジックをモデルやコントローラとは分けて実装でき、複数のモデルやコントローラの間でこれらを再利用できる。
- 例: PhotoモデルとVideoモデルに同じタグ機能を追加したいときに、TaggableというConcernとして抽出する。
- 以下に注意すると、Concernはおのずとデータ操作に関するロジックの実装と関連付けの宣言を行うレイヤーになる。
- モデルでConcernを利用する前に、抽出したいロジックを値オブジェクやサービスオブジェクトとして実装すべきではないかを考えてみる。
- コントローラでConcernを利用する前に、そのロジックをモデルやフォームオブジェクトに実装すべきではないか考えてみる。
コールバックオブジェクト
- モデルとは独立したクラスとして定義されるため、複数のモデルの間で再利用できる。
- Concernと併用することで、コールバックオブジェクトの設定を容易にすることができる。
- Concernのみでも同様のことを実現できるが、コールバックを実装するメソッドやブロック内のselfが自身を利用するオブジェクトになり、テストダブルへの置き換えが難しいため、自身を利用するオブジェクトが外から渡され、テストダブルに置き換えることが容易なコールバックオブジェクトを利用した方が良い。
Ruby on Railsの正体と向き合い方 / What is Ruby on Rails and how to deal with it?より
RestfulルーティングとActiveRecordパターン
- これにより、URLで表されるリソースからDB上のテーブルまでが密結合する構造になったのがRails。
Model = Interactor
- Entity = Model
- ビジネスロジックの記述
- Interactor = Model(AR Callbacks)
- ユースケースの組み立て
- Interactor = Model(AR Validations)
- 入力値のバリデーション
- Repository = Model(AR Query Interface)
- DBアクセス・データ変換
- Railsの限界。
- 「あるModelが複数の異なるユースケースでCRUD操作されるようになったとき、あるModelに書かれたValidations/Callbacksは特定のユースケースと密結合しているために限界を迎える。」
- Railsの限界の解決策。
- コードレベル
- 責務に応じてDB上の同一テーブルを参照する複数のモデルを作る(ActiveRecordの分割)
※ serviceクラスをやめようサブクラスを使おう も近いアプローチかも?
※ 加えて、そもそもテーブル分割から行うような解決策 2020-08-17 パーフェクトRails著者が解説するdeviseの現代的なユーザー認証のモデル構成について
。 - DB上のテーブルに紐付かないController専用のModelを作る(Application Modelの導入)
- Controllerからの単一メソッド呼び出しで処理一式を実行する層を作る(Form/Service)
- 責務に応じてDB上の同一テーブルを参照する複数のモデルを作る(ActiveRecordの分割)
- アーキテクチャレベル
- アプリケーションが対象とするドメインを小さくする
- 限界を迎える前に複数のサブシステムへ分割する
- コードレベル
Active Recordから考える次世代のRuby on Railsの方向性 / Directions for the next generation of Ruby on Rails: From the viewpoint of its Active Recordより
トランザクションスクリプト + (テーブル|行)データゲートウェイ vs ドメインモデル + アクティブレコードwithサービスレイヤー
- 前者ではドメインロジックをスクリプト内にベタ書きする。
ActiveRecord
- アクティブレコードパターン + バリデーションとコールバックを導入してサービスレイヤーの実装を可能にした。
Prisma
- トランザクションスクリプト + テーブルデータゲートウェイとしてのPrisma。
- アクティブレコードではないため、ドメインが複雑になると開発生産性が急落する。
- が、そのままでも良いと考える人が増えるかも。
Sequelize、TypeORM
- それぞれアクティブレコードの実装が存在するが、TypeScript対応が不十分。
JavaScriptのORM vs Rails
- ドメインレイヤーの開発生産性の必要性 vs ユーザー体験の向上の必要性
Fat Modelの倒し方 / how to deal with fat modelより
Fat Modelを倒す3つのアプローチ
Rails Way
- Concern、Validation Class、Callback Class
- STI、Polymorphic
- Sub-Rails Way
- gemを使う
- draper、active_decortor
- pundit、banken、cancancan
- interactor-rails
- discard、paranoia、acts_as_paranoid
- acts-as-tagglable-on
- active_hash など
- gemを使う
- Non-Rails Way
- Form Model
- PORO
- Service Class
- 1 Table Multiple Models
中規模Web開発のためのMVC分割とレイヤアーキテクチャより
- FatView → BEMのパーシャル単位で分割する。
レイヤアーキテクチャとRailsのマッピング
- ユーザインタフェース層
- (View) views view_objects
- アプリケーション層
- (Controller) controllers observers decorators parameters
- ドメイン層
- (Model) models mailers callbacks validators policies queries services value_objects factories
Realworld Domain Model on Railsより
DDD概念とRailsのマッピング
- エンティティ > Model (AR or not AR)
- 値オブジェクト > Model (not AR)
- リポジトリ > ActiveRecord
- アプリケーションサービス > Controller
- ドメインサービス > Model (Service Class)
- Formオブジェクト
- 集約を行う。
バリデーションに関して
- ActiveRecordで行えるものはActiveRecordに任せる。
エンティティレベルでの複雑さ
- deviseの例
- 悪い点
- NULLABLEカラムの嵐になる
- 状態によって整合性を管理しなければならない範囲が変化する
- 登録完了時には必須だが、confirm待ちや招待の承認前は不要など
- 解決策
- User、UserRegistration、UserInvitationに分割し、Formオブジェクトで整合性を確認する
- 悪い点
ふつうのRailsアプリケーション開発より
- Controller, Model, View, FormObject以外はほとんど使わないService層はやりきる覚悟があるなら使ってよし。