Ruby on Railsでは、ビジネスロジックはモデルに載せるということになっていますが、どうしてもモデルが肥大化しがちです。そんな時に使える切り分け手法として、ActiveType
があります。
Fat Controller→Fat Model→クラスごと分ける
何も考えずにRailsでコードを書いていくと、ついついControllerにビジネスロジックを寄せてしまうことになります。これは、「Fat Controller」と呼ばれて、あまり良くないパターンとされています。
それでは、これらをモデルに移していくとどうなるでしょうか。同じようなパターンの処理をモデルとしてくくり出せるうちはいいのですが、すべての場合の処理をモデルに出していっても、それはそれで今度はモデルが肥大化してきてしまいます。これもこれで、コードの動作がわかりづらくなってしまいます。
ということで、いちばんの理想形は、「実現すべきビジネスロジックに対応して、細かくモデルを切り出す」ということになります。ActiveTypeは、それを支援するための強力なツールとなります。
テーブルに対応しないモデル…ActiveType::Object
テーブルに対応しないモデルを作るときに、別に基底クラスはなくても構わないのですが、そうするとフォームヘルパーやバリデーションといった、ActiveRecord::Base
で用意されている機能が全く使えないモデルとなってしまいます。情報取得だけの場合など、それでも全く支障がないこともあるのですが、とりわけ入力を受け付けるような場合には不便極まりありません。いちおう、ActiveModel
の各種モジュールをinclude
すればそれらの機能にも対応はしますが、正しく書くだけでひと仕事になってしまいます。
そこで、基底クラスとしてActiveType::Object
を使えます。これは、実際にActiveRecord::Base
を継承した上で、DBアクセスだけを殺したものとなっています。ということで、バリデーション、コールバック、リレーションといった、DB書き込み以外のすべての機能を通常のActiveRecordと同様に使えます。DBがないので、属性はattribute
を使って宣言する必要があります。
なお、accepts_nested_attributes_for
については、ActiveType特有の記法に直す必要がある、とのことです。
class NoDbModel < ActiveType::Object
attribute :name, :string
attribute :price, :integer
attribute :parent_id
validates :price, numericality: {greater_than: 0}
belongs_to :parent
end
テーブルに対応したモデルを継承する…ActiveType::Record[BaseClass]
Railsで作るような業務システムの根幹は、データの変換にあります。入力フォームへ行った人間の入力からモデルのデータへ、そしてDBのレコードにしたり、逆に表示の時はDBからモデルへ変換し、そしてモデルからHTMLやJSONとしてデータを取り出す、そういうのがメインの流れです。
ときには、イレギュラーな変換が入ることがあります。たとえば、CSVや他サービスからのデータ取り込みなどです。もちろんこれらもモデルが担うべき部分ではあるのですが、全部1本のモデルに書いていては冗長になってしまうだけなので、「特殊化」として、クラスを継承してそちらに実装できればすっきりします。
ただ、ふつうにActiveRecordのクラスを継承すると、シングルテーブル継承として働いてしまうため、「同じモデルに特殊な機能を加える」という、意図した効果は得られません。
ここでもActiveTypeの出番です。クラスをActiveType::Record[BaseClass]
から継承させると、データベースの読み書きはBaseClass
として行われるのですが、継承した先のクラスとして、特殊な機能が使えるようになります。正しい継承関係なのかどうかは確認していませんが、ここで継承したクラスのインスタンスについて、.is_a?(BaseClass)
もtrueを返すので、ビューで表示させるときなどにそのまま通常のモデルがわりに使っても問題ありません(ActiveType.cast
を使うと1オブジェクト、あるいはリレーション単位でキャストもできます)。
# user.rb
class User < ActiveRecord::Base
end
# csv_user_importer.rb
class CsvUserImporter < ActiveType::Record[User]
def self.import
# 略
end
end