Best Practice Ruby on Rails Refactoring
のモデル章を読ました。
本書は、Railsガイドの知識を読んだが実践でどうもいいコードにならないと思っているエンジニアにオススメできます。
学んだことを使える知識にしたいと思ったので言語化することにします。
デメテルの法則
デメテルの法則は、モジュール間の結合度を下げる手法で最小知識の原則とも呼ばれいます。基準となるオブジェクトは、隣接するオブジェクトの情報のみを知っているべきであるということです。Rubyの場合、オブジェクト間の通信において.
の数が一つだけになるようにすればいいです。
書籍では、以下のコードをリファクタすることでデメテルの法則が紹介されていました。
class Customer < ActiveRecord::Base
has_many :invoices
end
class Invoice < ActiveRecord::Base
belong_to :customer
end
InvoiceクラスからCustomerクラスの情報を取得する場合、以下のようになります。
@invoice.customer.name
これをデメテルの法則に従うようにリファクタリングすると、@invoice.customer_name となります。
class Customer < ActiveRecord::Base
has_many :invoices
end
class Invoice < ActiveRecord::Base
belong_to :customer
def customer_name
customer.name
end
end
しかし、この方法には欠点があります。取得した属性値に対して線形的にメソッドが増加するためModelが肥大化してしまいます。
class Invoice < ActiveRecord::Base
belong_to :customer
def customer_name
customer.name
end
def customer_street
customer.street
end
def customer_city
customer.city
end
end
この肥大化は、delegate
メソッドで解消できます。
class Invoice < ActiveRecord::Base
belong_to :customer
delegate :name,
:street,
:city,
to: :customer,
prefix: :customer
end
end
Fat Model
適切な責務が表現された状態で、Fat Modelになるのは致し方ありません。書籍では、分割統治法のアプローチとしてModuleに切り出すことでFat Modelに混在する役割を切り分けていました。
この手法はFat Modelを解決したことにはなりませんが、そもそもFat Modelであることは問題でないと思うので個人的に良い方法であると思います。
上記は、適切な責務を実現している場合に有効です。しかし、現実のプロダクトコードでは適切な責務が表現されていないことがあります。書籍では、この時の対処法が紹介されていました。新しいクラスに責務を委譲することです。
注文クラスであるOrderモデルが例として紹介されていました。
class Order < ActiveRecord::Base
def self.find_purchased
...
end
def self.find_wating_for_review
...
end
def to_xml
...
end
def to_json
...
end
def to_csv
...
end
end
上記のコードの問題点は、単一責任の原則(SRP)に反することです。to_xml
やto_json
注文クラスは、どのような形式で出力するかに責務を持っています。以下のように、別クラスに切り出すことで責務を明らかにできます。
class Order < ActiveRecord::Base
def self.find_purchased
...
end
def self.find_wating_for_review
...
end
def converter
OrderConverter.new(self)
end
end
class OrderConverter
attr_reader :order
def initialize(order)
@order = order
end
def to_xml
...
end
def to_json
...
end
def to_csv
...
end
end
@order.converter.to_pdf
のように呼び出すことができました。
これは、デメテルの法則に違反しています。はじめに紹介した方法でどのようにリファクタできるか考えてみてください。答えは、書籍に記載されています。
Association extensions
class PetsController < ApplicationController
def show
@pet = Pet.find(parames[:id])
@toys = @pet.find_cute_for_pet
end
end
class Pet < ActiveRecord::Base
has_many :toys
def find_cute_for_pet
self.where(cute: true)
end
end
上記のようなコードがある時、has_many
の中にメソッドを押し込むことでどのように利用するかを明らかにできます。また、メソッド名もシンプルになります。
class Pet < ActiveRecord::Base
has_many :toys do
def cute
self.where(cute: true)
end
end
end
toys
に対して複数のメソッドが定義されるようになったとき、Association extensionsが有効です。
class Pet < ActiveRecord::Base
has_many :toys, -> { extending ToyAssocationMethods }
end
module ToyAssocationMethods
def cute
where(cute: true)
end
end
まとめ
Railsのモデルに関するアンチパターンを紹介しました。
しかし、安易にリファクタリングに取り掛かってはいけません。
リファクタリングを行うことで、本当に生産性が上がるのかROI(投資利益率)を意識してチームやプロダクトオーナに提案する必要があると思います。
他に興味のあるトピックのみを読んでブログにしたい。
- Testing
- Using Third-Party Code
- Databases