LoginSignup
108

More than 5 years have passed since last update.

オブジェクト指向 Rails しばしば出てくる3つの悩みを解決する。

Last updated at Posted at 2016-03-18

はじめに

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 というアクションを考える。

sample-model.png

bad

#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

good

#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)

写真と動画にコメントしたいケースを考える。

IMG_6120.JPG

not_dry

#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

dry

#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)

先ほどの例で、写真と動画のサムネイルをそれようのパスに保存したいケースを考える。

not_dry

#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

dry
#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/にモデル単位で表示するための固有のロジックをまとめる。

日付をフォーマットして表示したいケースを考える。

bad
#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

good
#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のメソッドを呼び出すことができます。

call_native_model_method.rb
  :
 @sample_decorator.find(params[:id])
  :

参考:デザインパターン
Decorator

補足:ヘルパーについて
この議論をするとき、よくヘルパーに切り出す、という記事を見つけます。
この方法でも、モデルを汚さずに、かつ、手軽に表示ロジックを切り出せますが、ネームスペースを汚してしまう。というデメリットもあります。
Decoratorとして、明示的にclassに切り出せるほうが、より局所化できるので、(OOP的には)ベターな方法だと思います。

DecoratorをパッケージングしてくれているDraperというGemがあります。
scaffoldするたびに、Decoratorを自力定義する必要がないので、効率化できます。

最後に

よりDRYに、またオブジェクト指向でいうところの関心の分離ができたのではないかと思います。
Railsは本当によくできたフレームワーク(お前が言うな!)なので、逆にOOPを意識しなくてもある程度形になってくれるのですが、OOPをある程度知ってなかったら、このような気づきはなかったと思います。

あ、例に出したコードや記事の内容について、もちろん異論はありです。
いろんな意見を聞かせていただければと。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
108