Facadeパターン(GoF)

参考書籍一覧(Amazon アソシエイトリンク)

以下のような時に適用するパターン。

  • 複雑なクラス関係の取り扱いや手続きを
    • 意識せずに済むようにしたい場合
    • 定型処理として隔離したい場合
    • 主題の処理から切り離したい場合

人事評価システムを開発していて、評価結果を表示する処理を以下のように書いたところだとする。

class AssessmentsController < ApplicationController
  # 評価確定前のプレビュー
  # Employee, ManagerAssessmentはRDBへの問い合わせクラス(ActiveRecord)
  # AssessmentLogic, ManagerAssessmentLogicは評価計算のクラス(Entity、ValueObject、DomainModel)
  # ManagerAssessmentは上司の評価とする
  def preview
    employee = Employee.find(params[:employee_id])
    assessment = AssessmentLogic.new(employee.id, employee.name, employee.rank)
    manager_assessments = ManagerAssessment.where(employee_id: employee, year: params[:year])
    manager_assessments.each do |manager_assessment|
      manager_assessment_logic = ManagerAssessmentLogic.new(manager_assessment.xxx_score,
                                                            manager_assessment.yyy_score,
                                                            manager_assessment.zzz_score)
      assessment.manager_assessments << manager_assessment_logic
    end

    @result_score = assessment.score
  end

  def conclusion
    # 評価の確定。previewアクションとの違いは評価を保存するかどうかだけ
    # 重複するので省略...
  end
end

これはいわゆるFat Controllerというもので、Controllerの主な責務であるどのViewを表示するかやViewに値を渡すことの他に、具体的な評価処理がpreviewアクションのコードに含まれてしまっている。

また、previewアクションとconclusionアクションで同じ処理が繰り返されてしまっている。privateメソッドを作って、共通処理を押し込むことも可能だが、Controllerの責務外の処理であることは変わりがない。

これらの問題に対処するため、以下のようにする。

class AssessmentsController < ApplicationController
  # 評価確定前のプレビュー
  def preview
    employee = Employee.find(params[:employee_id])
    @result_score = AssessmentService.new(employee).score(params[:year])
  end

  def conclusion
    employee = Employee.find(params[:employee_id])
    result_score = AssessmentService.new(employee).score(params[:year])
    # 評価を保存する処理が続く...
  end
end

class AssessmentService
  def initialize(employee)
    @employee = employee
  end

  def score(year)
    assessment = AssessmentLogic.new(@employee.id, @employee.name, @employee.rank)
    manager_assessments = ManagerAssessment.where(employee_id: @employee, year: year)
    manager_assessments.each do |manager_assessment|
      manager_assessment_logic = ManagerAssessmentLogic.new(manager_assessment.xxx_score,
                                                            manager_assessment.yyy_score,
                                                            manager_assessment.zzz_score)
      assessment.manager_assessments << manager_assessment_logic
    end

    assessment.score
  end
end

これにより、Controller側にとって複雑な処理を意識せずに済むようになり、主要な責務でない処理を切り離すことができた。また、定型処理を隔離することができた。

conclusionアクションの評価の保存処理が複雑なら、同じようにServiceクラスを作るか、AssessmentServiceクラスにconclusionメソッドを追加してしまえば良い。