Rails
active_type

ActiveTypeで機能別モデルを実現

More than 3 years have passed since last update.

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特有の記法に直す必要がある、とのことです。


models/no_db_model.rb

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