以下の記事の続きです。
Visitorパターンでは各モデルを処理するメソッドの引数を複数にしたい状況が結構あります。
手前のノードと現在のノードを比較する処理をしたいケースを例として説明します。
# 各モデルが visitor.visit(self) と呼び出す。
def visit(object)
if object.is_a?(Task)
task(object)
elsif object.is_a?(Step)
step(object)
end
end
def step(step)
-> (prev_step) {
# 前と後で比較する処理
}
end
def task(task)
prev_step = nil
task.steps.each{|step|
step.accept(self).(prev_step)
prev_step = step
}
end
この例ではstepメソッドがlambdaを返すので、呼出し側で追加の引数を指定して処理を行います。
これをカリー化して第一引数を固定した関数を返すようにすると以下のように書けます。
# 各モデルが visitor.visit(self) と呼び出す。
# curryingで第一引数にモデルを指定しておく。
def visit(object)
if object.is_a?(Task)
self.method(:task).curry.(object)
elsif object.is_a?(Step)
self.method(:step).curry.(object)
end
end
def step(step, prev_step)
# 前と後で差分を集計する等
end
def task(task)
prev_step = nil
task.steps.each{|step|
step.accept(self).(prev_step)
prev_step = step
}
end
Visitorの実装が増えてノードの扱いが複雑化してくると、#visitのディスパッチ時点でカリー化されているメリットを感じると思います。
以下、全体を通しで見たコードです。
module VisitorAcceptance
def accept(visitor)
visitor.visit(self)
end
end
class Project < ApplicationRecord
has_many :tasks
include VisitorAcceptance
end
class Task < ApplicationRecord
belongs_to :project
has_many :steps
include VisitorAcceptance
end
class Step < ApplicationRecord
belongs_to :task
include VisitorAcceptance
end
class Visitor
def visit(object)
if object.is_a?(Task)
self.method(:task).curry.(object)
elsif object.is_a?(Step)
self.method(:step).curry.(object)
elsif object.is_a?(Project)
self.method(:project).curry.(object)
end
end
end
class ConcreteVisitor < Visitor
private
def step(step, prev_step)
# 前と後で比較する処理
end
def task(task)
prev_step = nil
task.steps.each{|step|
step.accept(self).(prev_step)
prev_step = step
}
end
def project(project)
project.tasks.each{|task|
task.accept(self)
}
end
end
すっきり。