Rails: Active RecordでRepositoryパターンを実装する(翻訳)
Repositoryは本質的に、ドメインオブジェクトをその永続化方法から切り離して、それらにアクセスするための限定的なインターフェイスを提供します。Repositoryは戦術的なパターンの一種であり、本記事の導入部で私が説明したい内容よりずっと詳しくMartin FowlerやEric Evansが解説しています。Repositoryパターンが推し進めるものは、いわゆるActive Recordパターンと真逆です。なぜパターンをわざわざ別のものに変換するのでしょうか?
Active Recordパターンは両刃の剣であり、その問題はまさに最大の長所から発しています。Active Recordパターンは、「個人起業家」が短期間で製品のプロトタイプを作り上げるときには大変有用であり、規律を守れる精鋭チームに適した柔軟性を提供してくれます。しかし、比較的大規模なレガシーアプリケーションに複数のチームが組織横断的に取り組んでいる場合は制御不能になってしまいます。
素のActiveRecord::Base
では、350個のインスタンスメソッドがpublicインターフェイスで公開されます。ここに、よく使われるActiveRecord::Relation
のメソッドが496個追加されます。このように大規模なActive Recordモデルで可能なすべての利用パターンをカバーする大規模なリファクタリングを実行するとなると悪夢です。以下はリファクタリングで最初に行うチェックリストの一部です。
- 膨大なクエリAPIの調査
- さまざまなコールバックの調査
- リレーション、およびその拡張機能と従来の振る舞いの調査
- Gemfileで
ActiveRecord::Base
を拡張しているgemの調査 - 追加されたメソッドや、変更された振る舞いの調査
これはカバーすべき重要な範囲です。お金を稼いでくれるproductionシステムに手を加えるには、それなりの時間とエネルギー、そして自信を必要とします。
以前私の同僚が、大規模なコードベースで表面化したActive Recordのカバー範囲を制御しようと何度か試みていたことが思い出されます。Active Recordの境界表現を支援するnot_activerecord gemや、読み取りに対応するさまざまなQuery Objectパターン(記事1、記事2)がありました。
以前Adam PohoreckiがDRUG meetupで述べていた、20%の労力をかけてActive Recordを以下のような形に整えれば、Repositoryで80%のメリットを得られるという話も何となく思い出されます。
class Transaction
def self.of_id(id)
find(id)
end
def self.last_not_pending_of_user_id(user_id)
where.not(status: "pending").where(user_id: user_id).order(:id).last
end
end
この方法、すなわちActiveRecord::Base
のメソッドを"private"として扱い、アプリケーション固有のクラスメソッドだけを使ってモデルにアクセスするという方法は、チームの規律の高さに大きく依存しています。
以下は今回私が作成するRepositoryです。ここには、既にフレームワークにある外部依存関係は含まれていません。
class TransactionRepository
class Record < ActiveRecord::Base
self.table_name = "transactions"
end
private_constant :Record
Transaction = Data.define(Record.attribute_names.map(&:to_sym))
class << self
def of_id(id)
as_struct(Record.find(id))
end
def last_not_pending_of_user_id(user_id)
as_struct(Record.where.not(status: "pending").where(user_id: user_id).order(:id).last)
end
private
def as_struct(record)
Transaction.new(**record.attributes.symbolize_keys)
end
end
end
このサンプルを少し分析してみましょう。
- このAPIを形成しているのは、
TransactionRepository
とそのpublicメソッドです。ここには依存関係はなく、ライフサイクル内でステートを保持することもないので、メソッドはシングルトン上に存在します。これらはデータにアクセスする唯一の方法であり、露出は極めて限定的となっています。 -
TransactionRepository::Record
はActive Recordのクラスになっています。この名前空間はRailsフレームワークの仕組みから「かけ離れている」ので、データベーステーブルをself.table_name
で指定しなければなりません。このRepository内ではRecord
を利用することと、そこに機能を実装することが許されています。この定数はRepositoryの外からはアクセスできません。つまりカプセル化が達成されています。 -
Repositoryクエリからの戻り値はイミュータブルな構造体になっています。
ActiveRecord::Relation
でもなければActiveRecord::Base
のインスタンスでもありません。
この方法に欠点はあるでしょうか?もちろんあります。他のどんな方法でもそうですが、何を選択し、何を捨てるかが腕の見せ所です。ある分野における利便性を犠牲にする代わりに、予測可能性とメンテナンス性を手に入れることになるので、効果があるかどうかは場合によりけりです。
Active Recordの膨大なAPI露出面積が最も重宝される場所は、ビューレイヤ、そしてその上に構築される多数のヘルパーですが、私たちの構成ではこうしたメリットを得られなくなります。これについては、ActiveModel::Naming
の振る舞いをinclude
することで多少取り戻せるでしょう。
他にも方法はあるでしょうか?場合によっては、実行可能な選択肢にCQRS1、すなわち「モデルへの書き込みと読み出しを切り離す手法」も入ってくるかもしれません。書き込みと読み出しを異なる形で実装するときを考慮すると、読み出しをActive Recordに担当させるのが万全です。私の好みは、Railsの非正規化されたSQLデータベース上にRead Modelを実装する方法です。
概要
元サイトの許諾を得て翻訳・公開いたします。