肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

こんにちは、hachi8833です。今回は、自分が知りたかった、ActiveRecordモデルのリファクタリングに関する記事を翻訳いたしました。1年前の記事なのでRails 3が前提ですが、Rails 4でも基本的には変わらないと思います。リンクは可能なものについては日本語のものに置き換えています。 なお、ここでご紹介したオブジェクトは、app以下にそれぞれ以下のようにフォルダを追加してそこに配置します。 Value Object Service Object Form Object Query Object View Object Policy Object Decorator 肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳) (元記事: 7 Patterns to Refactor Fat ActiveRecord Models) Posted by @brynary on Oct 17th, 2012 (Code Climate Blog) Railsアプリケーションの品質を高めるためにチーム内でCode Climateを使用していれば、モデルの肥大化を自然と避けるようになるでしょう。モデルが肥大化(ファットモデル)すると、大規模アプリケーションのメンテナンスが困難になります。ファットモデルは、コントローラがドメインロジックで散らかってしまうよりは1段階だけましであるとはいえ、たいていの場合Single Responsibility Principle (SRP:単一責任の原則)の適用に失敗した状態であると言えます。 SRPの適用は、元々難しいものではありません。ActiveRecordクラスは永続性と関連付けを扱うものであり、それ以外のものではありません。しかしクラスはじわじわ成長していきます。永続性について本質的に責任を持つオブジェクトは、やがて事実上ビジネスロジックも持つようになるのです。1年2年が経過すると、User クラスには500行ものコードがはびこり、パブリックなインターフェイスには数百ものメソッドが追加されることでしょう。それに続くのはコールバック地獄です。 アプリケーションに何か本質的に複雑な要素を追加したら、ちょうどケーキのタネをケーキ型に流し込むのと同じように、それらを小規模かつカプセル化されたオブジェクト群(あるいはより上位のモジュール)に整然と配置することが目標になります。ファットモデルは、さしずめタネをケーキ型に流し込むときに見つかるダマ(混ざらなかった粉の固まり)のようなものでしょう。これらのダマを砕いて、ロジックが等分に広がって配置されるようにしなければなりません。これを繰り返し、それらが最終的に、シンプルで、きちんと定義されたインターフェイスを持つ一連のオブジェクトとなって、それらが見事に協調動作するようにしましょう。 そうは言っても、きっとこう思う人もいることでしょう。 “でもRailsでちゃんとOOPするのってめちゃくちゃ大変ぢゃなくね?!” 私も以前は同じことを思ってました。でも若干の調査と実践の結果、RailsというフレームワークはOOPを妨げてなどいないという結論に達しました。スケールできないでいるのはRailsのフレームワークではなく、従来のRailsの慣習・流儀の方です。より具体的に言えば、Active Recordパターンできちんと扱える範囲を超えるような複雑な要素を扱うための定番の手法がまだないのです。幸いにも、オブジェクト指向における一般的な原則とベストプラクティスというものがあるので、Railsに欠けている部分にこれらを適用することができます。 [その前に]ファットモデルからミックスインで展開しないこと 肥大化したActiveRecordクラスから単に一連のメソッドを切り出して “concerns” やモジュールに移動するのはよくありません。移動したところで、後でまた1つのモデルの中でミックスインされてしまうのですから。いつだったか、こんなことを言っていた人がいました。 “app/concerns ディレクトリを使っているようなアプリケーションって、だいたい後から頭痛の種になる(=concerning)んだよね” 私もそう思います。ミックスインよりも、継承で構成する方がよいと思います継承よりコンポジションの方がよいと思います。このようなミックスインは、部屋に散らかっているガラクタを引き出しに押し込めてピシャリと閉めたのと変わりません。一見片付いているように見えても、引き出しの中はぐちゃぐちゃ、どこに何があるのかを調べるだけでも大変です。ドメインモデルを明らかにするために必要な分解と再構成を実装するのも並大抵ではありません。 これはもうリファクタリングするしかないでしょう。 1. Value Object(link) Value Objectは、異なるオブジェクト同士であっても値が等しければ等しいと見なされる、シンプルなオブジェクトです。Value Objectは変更不可能であるのが普通です。Rubyの標準ライブラリにはDate、URI、Pathname などのValue がありますが、Railsアプリケーションでもドメイン固有のValue Objectを定義できますし、そうすべきです。ActiveRecordからValue Objectへの展開は、すぐにもメリットの得られるリファクタリングです。 Railsでは、ロジックが関連付けられている属性が1つ以上ある場合にはValue Objectが有用です。単なるテキストフィールドやカウンタ以上の要素は、何でもValue Objectの候補になりえます。 ちょうど著者が仕事をしている某テキストメッセージングアプリケーションには、PhoneNumber というValue Objectがあります。そして某eコマースアプリケーションではMoneyクラスを必要としています。私たちのCode Climateには RatingというValue Objectがあり、受け取ったクラスやモジュールのランキングをAからFまでの段階で表します。ここではRuby のStringクラスのインスタンスを使用することもできます(実際使用していました)が、このRatingを使用すると以下のように振る舞いとデータを一体化することができます。 class Rating include Comparable def self.from_cost(cost) if cost <= 2 new(“A”) elsif cost <= 4 new(“B”) elsif cost <= 8 new(“C”) elsif cost <= 16 new(“D”) else new(“F”) end end def initialize(letter) @letter = letter end def better_than?(other) self > other end def <=>(other) other.to_s <=> to_s end def hash @letter.hash end def eql?(other) to_s == other.to_s end def to_s @letter.to_s end end 次にすべてのConstantSnapshotでRatingのインスタンスをパブリックなインターフェイスに公開します。 class ConstantSnapshot < ActiveRecord::Base # … def rating @rating ||= Rating.from_cost(cost) end end これによりConstantSnapshotがスリムになるだけでなく、他にも多くの利点があります。 #worse_than?メソッドと#better_than?メソッドは、レートを比較する場合にはなどのRubyの組み込み演算子よりも適切です。 #hashや#eql?を定義しておけばRatingをハッシュキーとして使用できます。Code Climateではこれを使用して、定数をレートごとにEnumberable#group_byでグループ化しています。 #to_sメソッドを定義してあるので、Ratingを簡単に文字列やテンプレートに変換できます。 このクラス定義は、ファクトリーメソッドを導入する場合にも便利です。矯正コスト (=クラスの「臭い」を除去するのにかかる時間) に見合う、正しい Rating を得られます。 2. Service Object(link) アクションによってはService Objectを使用して操作をカプセル化することができるものがあります。著者の場合、以下の基準に1つでも合えばService Objectの導入を検討します。 アクションが複雑になる場合 (決算期の終わりに帳簿をクローズする、など) アクションが複数のモデルにわたって動作する場合 (eコマースの購入でOrder, CreditCard, Customer を使用する、など) アクションから外部サービスとやりとりする場合 (SNSに投稿する、など) アクションが背後のモデルの中核をなすものではない場合 (一定期間ごとに古くなったデータを消去する、など) … Continue reading 肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)