はじめに
Railsで開発をしていると頭を悩ます3つのしばしばがあります。
- 
1つのモデルで複数のモデルを操作してしまう
 - 
冗長なコードが複数のモデルまたはコントローラに出てくる
 - 
表示のためだけに、モデルにメソッドを追加する
 
これらを解決するためには、オブジェクト指向とデザインパターンによる考え方ができるとよいです。
この記事では、Railsでよりオブジェクト指向的に、より構造が単純で、より DRYに書くためのパターンを学びます。
環境は Rails 4 です。
キーワードは3つ。
- 複雑なビジネスロジックを分離する Non associated Model
 - 関心の分離を保ちつつ処理をまとめる Model/Controller Concerns
 - 表示ロジックをモデルから分離する Decorator
 
Non associated Model
解決する問題
- 1つのモデルで複数のモデルを操作してしまう
 
導入する理由
ActiveRecordは、1つのモデルが1つのテーブルにひもづくので、あるモデルに複数のモデルを操作するような処理は書くべきでない。
さらに言うと、複数のモデルを操作するクラスは、テストしづらい。
回答
ActiveRecordにひもづかないクラスを作り、複雑なロジックは分離する。
例
車モデルに対して、複数の部品を組み立てる build_parts というアクションを考える。
# cars_controller.rb
class CarsController < ApplicationController
  before_action :set_car, only:[:show, :update, :edit, :delete]
  :
  def build_parts
    @car.build_parts
    respond_to do |format|
      :
    end
  end
  private
    def set_car
      @car = Car.find(params[:id])
    end
end
# car.rb
class Car < ActiveRecord::Base
  def build_parts
    self.class.transaction do
      self.suspension.create
      self.tire.create
      self.body.create
      :
      複雑なロジック
    end
  end
end
↓
# cars_controller.rb
class CarsController < ApplicationController
  :
  def build_parts
    car_builder = CarBuilder.new(@car)
    car_builder.build_parts
    respond_to do |format|
      :
    end
  end
  private
    def set_car
      @car = Car.find(params[:id])
    end
end
# car.rb
class Car < ActiveRecord::Base
end
# car_builder.rb
class CarBuilder
  attr_reader :car
  def initialize(car)
    @car = car
  end
  def build_parts
    car.class.transaction do
      car.suspension.create
      car.tire.create
      car.body.create
      :
      複雑なロジック
  end
end
テーブルに関連づいているCarモデルは、自分のことしか見ないクラスになりました。
例における実装は、builderパターンが元になっています。
参考 Builderパターン
Model/Controller Concerns
解決する問題
- 冗長なコードが複数のモデルに出てくる
 
導入する理由
ActiveRecordは、1つのモデル/コントローラが1つのテーブルにひもづくので、モデル単位でロジックを意識する。
これが行き過ぎると、モデル/コントローラごとに同じようなメソッドが出てきて、DRYではなくなってくる。
回答
app/controllers/Concerns、または、 app/models/concernsに共通メソッドをまとめる。
例1(models/concerns)
写真と動画にコメントしたいケースを考える。
# comment.rb
class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
    :
end
# picture.rb
class Picture < ActiveRecord::Base
  has_many :comments, as: :commentable
  #duplicate code
  def comments_by_user(id)
    comments.where(user_id: id)
  end
end
# movie.rb
class Movie < ActiveRecord::Base
  has_many :comments, as: :commentable
  #duplicate code
  def comments_by_user(id)
    comments.where(user_id: id)
  end
end
↓
# app/models/concerns/commentable.rb
module Commentable
  extend ActiveSupport::Concern
  included do
    has_many :comments, as: :commentable
  end
  def comments_by_user(id)
    comments.where(user_id: id)
  end
end
または
module Commentable
  def self.included(base)
    base.class_eval do
      has_many :comments, as: :commentable
    end
  end
  def comments_by_user(id)
    comments.where(user_id: id)
  end
end
----
# picture.rb
class Picture < ActiveRecord::Base
  include Commentable
    :
end
----
# movie.rb
class Movie < ActiveRecord::Base
  include Commentable
    :
end
polymorphic 関連がわからない、という人は、以下の記事がわかりやすいです。
Rails4でポリモフィックのリレーションを実装する
ActiveSupport::Concernを使う例のほうが、すっきり見えるので、私は好んで使っています。
例2(controllers/concerns)
先ほどの例で、写真と動画のサムネイルをそれようのパスに保存したいケースを考える。
# pictures_controller.rb
class PicturesController < ApplicationController
  def create
      :
    #duplicate code
    thumb_path = "/thumb/#{@picture.file_name}"
    thumb = @picture.make_thumbnail thumb_path
    thumb.save
  end
end
# movies_controller.rb
class MoviesController < ApplicationController
  def create
      :
    #duplicate code
    thumb_path = "/thumb/#{@movie.file_name}"
    thumb = @movie.make_thumbnail thumb_path
    thumb.save
  end
end
↓
# app/controllers/concerns/previewable.rb
module Previewable
  def thumbnail(content)
    thumb_path = "/thumb/#{content.file_name}"
    thumb = content.make_thumbnail thumb_path
    thumb.save    
  end
end
# pictures_controller.rb
class PicturesController < ApplicationController
  include Previewable
  def create
      :
    thumbnail(@picture)
  end
end
# movies_controller.rb
class MoviesController < ApplicationController
  include Previewable
  def create
      :
    thumbnail(@movie)  
  end
end
Decorator
解決する問題
- 表示のためだけに、モデルにメソッドを追加する
 
導入する理由
表示のためのロジックがモデルに増えてくると、肝心のビジネスロジックが見づらくなってしまう。
回答
app/decorators/にモデル単位で表示するための固有のロジックをまとめる。
例
日付をフォーマットして表示したいケースを考える。
# sample.html.haml
 :
= "created_at #{@sample.formatted_date}"
 :
# samples_controller.rb
class SamplesController < ApplicationController
  before_action :set_sample, only: [:show, :update, :edit, :delete]
    :
  def show
  end
  private
  def set_sample
    @sample = Sample.find(params[:id])
  end 
end
# sample.rb
class Sample < ActiveRecord::Base
  def formatted_date
    self.created_at.strftime("%Y/%m/%d %H:%M:%S")
  end
end
↓
# sample.html.haml
 :
= "created_at #{@sample_decorator.formatted_date}"
 :
# app/decorators/sample_decorator.rb
class SampleDecorator
  attr_reader :sample
  def initialze(sample)
    @sample = sample
  end
  def method_missing(method_name, *args, &block)
    sample.send(method_name, *args, &block)
  end
  def respond_to_missing?(method_name, invalid_private = false)
    sample.respond_to(method_name, invalid_private)
  end
  def formatted_date
    sample.created_at.strftime("%Y/%m/%d %H:%M:%S")
  end
end
# samples_controller.rb
class SampleController < ApplicationController
  before_action :set_sample, only: [:show, :update, :edit, :delete]
    :
  def show
  end
  private
  def set_sample
    @sample_decolator = SampleDecorator.new(Sample.find(params[:id]))
  end
end
# sample.rb
class Sample < ActiveRecord::Base
end
ポイントは、method_missingとrespond_to_missingをDecoratorに実装すること。
これにより、sample_decoratorから、sampleのメソッドを呼び出すことができます。
  :
 @sample_decorator.find(params[:id])
  :
参考:デザインパターン
Decorator
補足:ヘルパーについて
この議論をするとき、よくヘルパーに切り出す、という記事を見つけます。
この方法でも、モデルを汚さずに、かつ、手軽に表示ロジックを切り出せますが、ネームスペースを汚してしまう。というデメリットもあります。
Decoratorとして、明示的にclassに切り出せるほうが、より局所化できるので、(OOP的には)ベターな方法だと思います。
DecoratorをパッケージングしてくれているDraperというGemがあります。
scaffoldするたびに、Decoratorを自力定義する必要がないので、効率化できます。
最後に
よりDRYに、またオブジェクト指向でいうところの関心の分離ができたのではないかと思います。
Railsは本当によくできたフレームワーク(お前が言うな!)なので、逆にOOPを意識しなくてもある程度形になってくれるのですが、OOPをある程度知ってなかったら、このような気づきはなかったと思います。
あ、例に出したコードや記事の内容について、もちろん異論はありです。
いろんな意見を聞かせていただければと。

