【WIP】 Rails でドメインロジックの実装方法まとめ

このエントリは Ruby on Rails Advent Calendar 2014 の 7 日目のエントリです。 前日は seri_k さんの「Turbolinksさんと上手く付き合う10の方法」でした。

お詫び

WIP です。公開期限に間に合わない可能性があるため、まだ途中ですが先に公開してしまいました。 サンプルコード等を後ほど追記する予定です。

Rails のファットモデル問題

Rails で構築したアプリケーションが大規模になり機能が増えていくにつれてモデルが大きくなり、そのうち手がつけられなくなる問題は古くから指摘されています。これについてはもはや詳細を述べるまでもないと思うので割愛しますが、この問題は 2014 年になった今でも多くの開発チームを悩ませていると感じています*1

このエントリでは、普段 Rails を業務で使いながら OO 厨、デザパタ厨、DDD 厨、モデリング厨という"プログラマの麻疹”*2を一通り経験した僕がいろいろなドメインロジックの実装方法を模索した結果と感想をまとめてみようと思います。

SQL アンチパターンの「マジックビーンズ」アンチパターン

書籍「SQL アンチパターン」ではモデルが ActiveRecord そのものであるというフレームワークのあり方がまさにアンチパターンとして紹介されています。先日、この書籍の読書会でこのアンチパターンについて発表したので、そのときの資料を紹介しておきます。

特に ActiveRecord は DB だけでなくフォームとも密結合である点、アンチパターンに陥らないための現実的な落とし所などについて簡単に紹介しています。

このエントリではこれらについてもう少し詳細にご説明したいと思います。

対策1: ActiveSupport::Concern で見た目の複雑度を下げる

Rails の強みを最大限に生かしつつ、現実的な範囲で複雑性を下げていくのはActiveSupport::Concernextendしたモジュールを各モデルの責務ごとに実装してそれをincludeすることです。

これは他の手段に対して比較的安全な手段といえます。

メリット

  • Rails の規約に最も素直に従う実装手法なのでハマりどころが少ない。
  • 上記の理由により基本的に Rails や利用している Gem のバージョンアップ等の影響を受けにくい。

デメリット

  • あくまで見た目の複雑性が下がっているだけであり、実体としては巨大なモデルが出来る上がることに変わりない。つまり本質的には何も解決してない。
  • 巨大なオブジェクトができるわけなのでメソッド名の命名にそれなりに気を使う必要がある。異なるコンテキストで同じメソッド名を使うといったことができない。

対策2: サービスオブジェクトを利用する

2 つ目の方法はサービスオブジェクトを持ち込む方法です。これについては昨年の Rails Advent Calendar で書きました。

Railsでサービスとフォームを導入してみる話 - assertInstanceOf('Engineer', $a_suenami)

複数のモデルにまたがる処理をサービスとしてまとめ、専用のクラスを作成します。

メリット

  • サービスとは手続き的な処理をまとめたものに他ならず設計の難易度が比較的低いため、プログラマのレベルにばらつきがあっても極端にひどいコードになりにくい。

デメリット

  • 設計の難易度が高くないことの裏返しであるが、それほどレベルの高くないプログラマが多くいるとサービスクラスだらけになりドメインモデル貧血症に容易に陥る。
    • このアンチパターンに陥ると単にコントローラにあった処理がサービスクラスに移動するだけになる。*3
  • 「サービス」というデザインパターンは PofEAA のサービスとエリック・エヴァンスの DDD におけるドメインサービスがあり、チーム内で認識を合わせておかないとそのレイヤーの役割がまったく違うものになる。

対策3: DCI ( Data Context Interaction )

対策1 の派生として DCI を用いるという手もあります。DCI とは何かという説明はここでは割愛しますのでご存知でない方はぐぐってください。

ざっくり言うと、あるエンティティの存在そのものとそのエンティティのあるコンテキストにおける振る舞いを分離し、アプリケーションの設計をドメインエキスパートやプロダクトオーナーのメンタルモデルに近づけようとする設計手法です。

メリット

  • コンテキストごとにロールが与えるため、異なるロールであれば同名の振る舞い(=メソッド)を持つことが許される。
    • 対策1 の問題が解消される。
  • コンテキスト、ロール、ロールに実装されるメソッドの名前はユーザのメンタルモデルにある語彙が選ばれる。それはユビキタス言語を構築していることに等しい。

デメリット

  • モジュールの extend が動的に行われるため、性能上の懸念がある。
  • DCI は一時期流行した気がするが最近はあまり聞かなくなったし、あまりメジャーにはならなかった?

その他

「エリック・エヴァンスドメイン駆動設計」に感銘を受けて Rails に頼ることなく PORO ( Plain Old Ruby Object )でドメインモデルを構築しようと思ったこともありますし、永続化層とのインターフェースとしてリポジトリパターンを適用しようと思ったこともありましたが、Rails の流儀から外れすぎてしまうとどうしてもうまくいかず途中で断念しました。

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践)

また、昨今ではマイクロサービス化の流れもあるため、モノリシックな Rails アプリケーションで立ちゆかなくなった際にはいっそ複数のアプリケーションに分割するという方向性もアリかもしれません。

結び

このエントリは Ruby on Rails Advent Calendar の 7 日目のエントリであり、「Railsでドメインロジックをモデルに書くのは、果たして良い設計なのだろうか?」へのアンサーエントリでもあります。

Rails は開発効率やプログラマの生産性という意味では抜群に優れていて実績もあるフレームワークなので、その強みをそのままに、保守性や拡張可能性、大規模化したときの運用効率などを維持するためのデザインパターンや設計ノウハウが普及し、さらなる発展を遂げることを願います。

*1:少なくとも僕のまわりではそういう声がいまだにあります。

*2:知らない人はぐぐってください。

*3:それでもサービスクラスに名前がつけられるだけまだマシではありますが…