LoginSignup
8
8

More than 5 years have passed since last update.

RailsのモデルをVisitorパターンで辿る説明

Posted at

Railsで開発していると親子/兄弟の関係にあるモデルをノードのように扱って辿りたくなる時がありますよね。

そんなときに使うと便利なVisitorパターンの説明、Rails版です。

プロジェクト管理ツールの開発を行っているとして、以下3つのモデルが親子関係になっているとします。

  • プロジェクト (Project)
    • タスク (Task)
      • ステップ (Step)

まずモデルでビジターを受け入れるモジュールを作ります。

models/concerns/visitor_acceptance.rb
module VisitorAcceptance
  def accept(visitor)
    visitor.visit(self)
  end
end

各モデルはVisitorを受け入れるためにVisitorAcceptanceをincludeします。継承しているのがApplicationRecordなのはRails5をベースにしているためです。

models/project.rb
class Project < ApplicationRecord
  has_many :tasks
  include VisitorAcceptance
end
models/task.rb
class Task < ApplicationRecord
  belongs_to :project
  has_many :steps
  include VisitorAcceptance
end
models/step.rb
class Step < ApplicationRecord
  belongs_to :task
  include VisitorAcceptance
  def complete?
    actual_hours > 0 && estimation_hours > 0
  end
end

次にモデルを辿って処理をするVisitorの実装です。#visitで処理を振り分けます。

class ProjectVisitor

  attr_reader :completion_steps, :estimation_hours, :actual_hours

  def initialize
    @completion_steps = 0
    @estimation_hours  = 0
    @actual_hours = 0
  end

  def visit(object)
    if object.is_a?(Task)
      task(object)
    elsif object.is_a?(Step)
      step(object)
    elsif object.is_a?(Project)
      project(object)
    end
  end

  private

  def step(step)
    @completion_steps += step.complete?? 1 : 0
    @estimation_hours += step.estimation_hours
    @actual_hours += step.complete?? step.actual_hours : 0
  end

  def task(task)
    task.steps.each{|step|
      step.accept(self)
    }
  end

  def project(project)
    project.tasks.each{|task|
      task.accept(self)
    }
  end
end

最後に呼び出すときはModel#accept(visitor)を呼び出します。Project#acceptならtasks -> stepsと辿ります。Task#acceptならstepsを辿ります。

visitor = ProjectVisitor.new
project.accept(visitor) # task -> stepと辿る
p visitor.completion_steps
p visitor.estimation_hours
p visitor.actual_hours

visitor = ProjectVisitor.new
task.accept(visitor) # stepを辿る
p visitor.completion_steps
p visitor.estimation_hours
p visitor.actual_hours

モデルのデータ構造を辿って集計する処理がキレイにVisitorの中にまとまりましたね。

すっきり。

Visitorを使わないコードは以下のようになります。今回のように各ノードが一方向ならこれでも十分わかりやすいですね。

@completion_steps = 0
@estimation_hours  = 0
@actual_hours = 0
project.tasks.each{|task|
 task.steps.each{|step|
  @completion_steps += step.complete?? 1 : 0
  @estimation_hours += step.estimation_hours
  @actual_hours += step.complete?? step.actual_hours : 0
 }
}

ノードが循環したり入れ子になったりと複雑になって、それぞれで別の処理をするとなると、この記事のように独立したVisitorが欲しくなってきます。

質問などありましたらコメントかTwitter宛にお願いします。

8
8
0

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
8
8