【Ruby on Rails】モデル共通化の方法
Ruby on Railsにおけるモデルの共通化の方法をまとめてみたいと思います。
本記事は私の経験則から書いておりますので、間違っていればご指摘頂けますと幸いです。
モデルの共通化
基本的に、モデルを共通化する方法としては大きく分けて以下の3つの方法があります。
- 委譲(集約)→
has_a
- 継承 →
is_a
- Mixin →
behaves_like_a
これら3つを適切に使い分けることが重要です。
それぞれのやり方の具体例を説明していこうと思います。
委譲
委譲では、共通のひとかたまりの処理を別クラスに切り分けます。
モデルとの関係性としては、has_a
の関係になります。
例えば、商品が売られたりユーザーが購入したら、スラック通知するみたいな処理が複数のモデルであったとします。
class Product < ApplicationRecord
def sold
# 売る処理
slack_api = SlackApi.new
slack_api.channel = 'xxx'
slack_api.by_user = User.find_by(email: 'hoge')
slack_api.send
end
end
class User < ApplicationRecord
def buy
# 買う処理
slack_api = SlackApi.new
slack_api.channel = 'yyy'
slack_api.by_user = User.find_by(email: 'fuga')
slack_api.send
end
end
こういったときは、別クラスに委譲することが多いです。
class Product < ApplicationRecord
def sold
# 売る処理
SlackNotifier.new(channel: 'xxx', by_user: User.find_by(email: 'hoge')).send
end
end
class Cart < ApplicationRecord
def buy
# 買う処理
SlackNotifier.new(channel: 'yyy', by_user: User.find_by(email: 'fuga')).send
end
end
class SlackNotifier
def initialize(channel:, by_user:)
@api = SlackApi.new
@api.channel = channel
@api.by_user = by_user
end
def send
@api.send
end
end
上記のようにすることで、スラック通知に関する処理がSlackNotifierに委譲されます。このクラスに例外処理など色々な処理が追加されても凝集度が高く良いクラス設計なります。(テストもしやすい)
委譲で解決できるシーンは多く、副作用が少ないので一番よく使う手法です。
継承
継承はis_a
の関係になるときに使います。
複数のモデルで基本的な振る舞いが同一である場合に使うことが多いです。
以下は、商品の中で本や服などのモデルがあり、親クラスに商品モデルを作成する例です。
class Book < ApplicationRecord
def author_name
author&.name
end
def price_with_tax
price * 1.08
end
def display_name
"#{id}:#{name}"
end
def sold?
is_sold
end
end
class Cloth < ApplicationRecord
def bland_name
bland&.name
end
def price_with_tax
price * 1.08
end
def display_name
"#{id}:#{name}"
end
def sold?
is_sold
end
end
上記のコードでは、BookとClothモデルは両方とも商品の具象クラスであり、基本的なメソッドが共通なので継承を用いることができます。
class Product < ApplicationRecord
self.abstract_class = true
def price_with_tax
price * 1.08
end
def display_name
"#{id}:#{name}"
end
def sold?
is_sold
end
end
class Book < Product
def author_name
author&.name
end
end
class Cloth < Product
def bland_name
bland&.name
end
end
注意点として、ActiveRecord::Baseクラスの継承元で、親クラスとして使用する場合はself.abstrct_class = true
を指定します。これは、自動でモデル名からテーブルを探しに行かないようにするためです。
継承のデメリット
デメリットとしては、親クラスとの依存です。
子クラスを変更するときは親クラスを把握していなければなりません。
例えば、親クラスを把握していない状態でメソッドを追加すると、意図せず親クラスのメソッド(privateもpublicも)をオーバーライドしてしまうことがあります。
特に子クラスでは親クラスのprivateメソッドまでオーバーライドできてしまうので、その際は意図せぬ挙動になるかもしれません。
継承ツリーが深くなっていくにつれて、辛みがましていくので、継承のしすぎには注意すべきだと思っています。
Mixin
Mixinはbehaves_like_a
の関係を得ることができます。
RailsのMixinで共通化する場合はConcerns(関心事の分離)を使用することが多いです。
多くの場合では、Concernを使うのであれば他の手法を選択したほうがいいケースが多いと思っています。
Mixinのデメリット
デメリットは以下が考えられます。
- 責務が分離できていない
- メソッドの衝突
責務が分離できていない
Mixinの実態は多重継承です。includeするとメソッドがそのクラスに追加されるため、そのクラスの責務が減るわけではありません。
メソッドの衝突
例えば、Module1
とModule2
をincludeしたときに、両者あるいはinclude先で同名のメソッドがあった場合、どれが優先されるかを追うことは難しいです。includeするモジュールが増えれば増えるほどメソッドの衝突を追うことは難しくなっていきます。
class Module1
def run
puts 'run method of Module1'
end
end
class Module2
puts 'run method of Module2'
end
class User < ApplicationRecord
include Module1
include Module2
def run
puts 'run method of User'
end
# 色んなメソッド
end
user = User.first
user.run # => 'run method of User' → 意図せぬ挙動になりうる
まとめ
共通化には様々な手法がありますが、それぞれのメリデメを理解し、適切に手法を選択することが重要だと思っています。
皆様の参考になれば幸いでございます!
参考
Discussion