🐭

【Ruby on Rails】モデル共通化の方法

2023/04/30に公開

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するとメソッドがそのクラスに追加されるため、そのクラスの責務が減るわけではありません。

メソッドの衝突

例えば、Module1Module2を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' → 意図せぬ挙動になりうる

まとめ

共通化には様々な手法がありますが、それぞれのメリデメを理解し、適切に手法を選択することが重要だと思っています。
皆様の参考になれば幸いでございます!

参考

https://www.amazon.co.jp/オブジェクト指向設計実践ガイド-~Rubyでわかる-進化しつづける柔軟なアプリケーションの育て方-Sandi-Metz-ebook/dp/B01L8SEVYI/ref=sr_1_1?__mk_ja_JP=カタカナ&crid=4V5OXLC3BVFV&keywords=オブジェクト指向設計&qid=1682834205&sprefix=オブジェクト指向せっけ%2Caps%2C225&sr=8-1

https://blog.willnet.in/entry/2019/12/02/093000

Discussion