Best Practice Ruby on Rails Refactoringは、Rails開発を初めて半年のエンジニアにピッタリの本だった

Rails AntiPatterns: Best Practice Ruby on Rails Refactoring (Addison-Wesley Professional Ruby Series) (English Edition)

Rails AntiPatterns: Best Practice Ruby on Rails Refactoring (Addison-Wesley Professional Ruby Series) (English Edition)

  • 作者:Chad Pytel,Tammer Saleh
  • 出版社/メーカー: Addison-Wesley Professional
  • 発売日: 2010/11/09
  • メディア: Kindle版

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_xmlto_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のように呼び出すことができました。

これは、デメテルの法則に違反しています。はじめに紹介した方法でどのようにリファクタできるか考えてみてください。答えは、書籍に記載されています。

Rails AntiPatterns: Best Practice Ruby on Rails Refactoring (Addison-Wesley Professional Ruby Series) (English Edition)

Rails AntiPatterns: Best Practice Ruby on Rails Refactoring (Addison-Wesley Professional Ruby Series) (English Edition)

  • 作者:Chad Pytel,Tammer Saleh
  • 出版社/メーカー: Addison-Wesley Professional
  • 発売日: 2010/11/09
  • メディア: Kindle版

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