改めて振り返る Fat Controller / Fat modelとの戦い
先週の銀座Rails #15で @yalabさんがserviceクラスに関するネタで登壇していました。
www.slideshare.net
このスライドのP.16, P.17で「なぜserviceクラスを使うようになったか、それは fat modelをなんとかするため」というような記載があります。serviceクラスに関する話は定期的に出ていますが、このスライドにも出ているように「なぜserviceクラスが流行ったのか」という話もよく聞く話のような気がします。
私の知る限り、serviceクラスが(少なくともRailsに関する日本語圏)で流行したのは 7 Patterns to Refactor Fat ActiveRecord Models がきっかけではなかったかと思います。
この記事を改めて読み返すと、中々に良い記事である一方、この記事にあるいくつかのパターンだけがそれぞれ独り歩きしている感もあるので、今回は本記事を久々に振り返ってみようと思います。幸い、この記事はTechRachoに翻訳記事がありますので、ありがたく利用させていただきます。
パターンが紹介される前に何が書かれていたか
まず、対象記事の最初の段落には以下のように書かれています。
モデルが肥大化(ファットモデル)すると、大規模アプリケーションのメンテナンスが困難になります。 ファットモデルは、コントローラがドメインロジックで散らかってしまうよりは1段階だけましであるとはいえ、 たいていの場合Single Responsibility Principle (SRP:単一責任の原則)の適用に失敗した状態であると言えます。
最近はRailsでのFat Controller / Fat model問題はよく知られていてそれほど頻度高く聞かれる用語ではなくなっていると思いますが、この記事が出た当時は多くのRailsプロジェクトで悩みのタネになっていたように感じます。
Fat Controller / Fat model問題について軽く振り返っておきましょう。Fat controllerはその名の通り、コントローラのコードが膨れ上がる状態で、Railsを始めた人なら誰でも通る道と言えるでしょう。RailsではRESTfulインタフェースに対応したメソッド(アクション)がコントローラに用意されていますが、現実のアプリケーションでは必ずしもRESTfulインタフェースだけでうまく切り分けられなかったり、仮にインタフェースをうまく切ることが出来たとしてもコントローラ内部で行う処理が膨大になる可能性もあります。こういった問題に対して、コントローラにプライベートメソッドを増やして(場合によってはメソッド分割もせずに!)ひたすら実装をしていくと、当たり前ですがコードの行数は増えていって、コントローラの見通しが悪くなります。また、複数のコントローラを跨ぐような処理をうまく切り分けられず、あちこちに似たようなコードが分散している、なんて状況にもなったりします。
Fat modelもその名の通り、モデルのコードが膨れ上がる現象です。対象記事では「Fat controllerよりマシ」とされていますが、ここは人によって評価が分かれるところでしょう。Fat modelにはどういう問題があるのか、次の段落で述べられています。
ActiveRecordクラスは永続性と関連付けを扱うものであり、それ以外のものではありません。 しかしクラスはじわじわ成長していきます。永続性について本質的に責任を持つオブジェクトは、 やがて事実上ビジネスロジックも持つようになるのです。1年2年が経過すると、User クラスには 500行ものコードがはびこり、パブリックなインターフェイスには数百ものメソッドが追加される ことでしょう。それに続くのはコールバック地獄です。
私が見てきた限りで、コールバック地獄まで行って生きて帰ってきたプロジェクトはそれほど多くないという印象です。それらのプロジェクトが安らかに眠っていることを祈ります(たぶん現役で動いていると思いますが)。この引用箇所にある通り、Fat modelの問題はビジネスロジックを持つようになってしまうという点にあると思います。Rails自体は複数のActiveRecordクラス(正確には継承したクラス)を扱う方法については開発者に委ねているため、ActiveRecordクラスだけでアプリケーションを作ろうとすると、このようなことが起きがちです。「クラスAがクラスBとクラスCを引数に取るメソッドを持っていると思ったら、クラスCもクラスAとクラスBを引数にとるメソッドがある」みたいなこともよくありました。
では、どうすればこのメタボRails地獄から抜けられるのか。そのための方針が次の段落に以下のように書かれています。
アプリケーションに何か本質的に複雑な要素を追加したら、ちょうどケーキのタネをケーキ型に 流し込むのと同じように、それらを小規模かつカプセル化されたオブジェクト群(あるいはより 上位のモジュール)に整然と配置することが目標になります。
端的にまとめれば 「ちゃんとオブジェクト指向設計しましょうね」 ということで、これを受けて以下のように続きます。
> “でもRailsでちゃんとOOPするのってめちゃくちゃ大変ぢゃなくね?!” 私も以前は同じことを思ってました。でも若干の調査と実践の結果、Railsというフレームワークは OOPを妨げてなどいないという結論に達しました。スケールできないでいるのはRailsのフレームワーク ではなく、従来のRailsの慣習・流儀の方です。より具体的に言えば、Active Recordパターンできちんと 扱える範囲を超えるような複雑な要素を扱うための定番の手法がまだないのです。幸いにも、オブジェクト 指向における一般的な原則とベストプラクティスというものがあるので、Railsに欠けている部分にこれらを 適用することができます。
この対象記事は2012年に書かれたものですが、これは今でも通じることだと思いますし、The Rails Doctrine の "The menu is omakase", "No one paradigm", "Provide sharp knives" に表されている点でもあるように感じますが、それはさておき、確実なのはRailsが用意しているもの以上の部分は我々Railsを利用する開発者が考えねばならない、ということです。そして、次の段落で書かれている安易な逃げ道(そして往々にして行き止まり)である app/concerns
の利用に関する警告が書かれていますが、先程も書いたとおり結局は真面目にオブジェクト指向設計と向き合わねばならない、ということです。
が、我々は安易な逃げ道を模索する
残念ながら、自分自身も含め、対象記事ではここまでの解説よりも、ここから紹介されているパターンのほうが注目されているといえるでしょう。その結果がどうなったかといえば、各所で指摘されるServiceクラスの濫用でしょう。個人的には、その後ろを
- 責務がよくわからなくなったFormオブジェクト
- 便利そうで入れたけどメンテされなくなって部分的に使われているView Object
- 拡張性が確保できず足かせとなってしまっている自前のDecorator
- いざ使ってみたらHashで十分だった Value Object
- 試しに使ってみたけど必ずしもビシッとはまらなかったその他のパターン
などが列を成しているのではないかという気がします。もちろん、中にはプロジェクトにピタリとハマった方法もあるとは思いますが、どちらかといえば死屍累々、のほうが近いかもしれません。
ここに紹介された方法が有用であるにも関わらず、なぜそれが成功しなかったのか、成功しないどころかアンチパターンとして槍玉に挙げられるようになってしまったのか。それはひとえにFat controller / Fat modelを生み出した背景が変わっていないというだけだったのではないかと思います。つまり、手元にある良さそうに見えるソリューションに安易に頼ってしまったということです。
これは一言でいえばドメインモデル貧血症なわけですが、Railsを採用したプロジェクトは特にサービスを小さく作って(会社が潰れたりしなければ)大きく育てるというアプローチに用いられることが多く、適宜設計を見直していないとあっと今にドメインモデル貧血症になりやすいといえるでしょう。
足がかりのその先へ
該当記事が出たのは2012年10月のようですが、先程も書いたとおり、ドメインモデル貧血症、及びそこから派生する諸問題についてなにかマジカルなツールやアプローチが登場してRails開発がぐんと楽になる、という状況は現時点では訪れていません。
しかし、現在はオブジェクト指向設計実践ガイドのように初学者にもわかりやすい書籍も出ています。そんな簡単にいく話ではないですが、対象記事に挙げられているような解決方法はあくまで1つの足がかりとし、サービスの性質に合わせて自分たちで考えていく、というのが結局最短ルートになるのではないでしょうか。
最後に、ジョーカーさんの悲痛な叫び声が音声入りで脳内再生されることで有名な以下の記事から一部引用させていただき、結びとさせていただきたいと思います。
長々と書いたけど、結局の所言いたいのは、サービスクラス等という単語に踊らされる前に、 オブジェクト指向プログラミングに関する知識を貯めて、真剣にクラス同士の関係性を設計し ロジックを表現する努力をしようということだ。