仕様(Specification)(DDD)

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

ACLが典型的な例だが、「AオブジェクトとBオブジェクトとの間に関係Xは成り立つか?」というような真偽値を返すメソッドは、AオブジェクトとBオブジェクトのどちらに作っても座りが悪い場合がある。

携帯の料金プランを例に考える。大別して「契約者」と「機種」と「基本プラン」と「オプション」の4つがあり、有効な組み合わせと無効な組み合わせが存在する(例えば高校生の契約者は学生割オプションが使えるが、ガラケーの機種ではスマホ用の基本プランは使えない)。

契約者クラスに料金プランが妥当かチェックするメソッドをのせる方法もあるが、妥当チェックのためのメソッドが大量に作られ契約者クラスがFatになるのは想像に難くない。また、契約者が関係しない「機種」と「基本プラン」の関係をチェックするのは契約者クラスの責務外である。

別の方法として、契約者に関するチェックは契約者クラスに、機種に関するチェックは機種クラスにとった具合にチェック処理を分散させる方法もある。しかし、契約者クラスなどがFatになりやすいことは変わりがない。さらに「機種とオプションの組み合わせチェック」や「契約者と基本プランとオプションの組み合わせチェック」といったチェックは「ここに実装するのが自然」と言い切れる場所がない。「この機種にこのオプションが適用できるか?」と捉えれば機種クラスにチェックメソッドを作るだろうし、「このオプションが適用できる機種か?」と捉えればオプションクラスにチェックメソッドを作ることになる。それゆえに、改修のためにコードを読もうとしたとき、どこにチェック処理があるのか予想するのが難しくなり、同じ処理が別々のクラスに実装されるといったことが発生しやすくなる。

すでにあるクラスにチェック処理をのせようとするとこういった問題が発生するため、「契約者と基本プランの関係は成り立つか?」といった組み合わせに関する責務を持ったクラスを用意することで解決する。

class RatePlanSpec
  def valid?(customer, phone, base_plan, option)
    # 契約者とオプションの組み合わせは妥当か?
    return false unless RatePlanCustomerOptionSpec.new.valid?(customer: customer, option: option)
    # 機種と基本プランの組み合わせは妥当か?
    return false unless RatePlanPhoneBasePlanSpec.new.valid?(phone: phone, base_plan: base_plan)
    # ...
  end
end

class RatePlanCustomerOptionSpec
  attr_accessor :customer, :option
  def initialize(customer: nil, option: nil)
    self.customer = customer
    self.option = option
  end

  def valid?(customer: nil, option: nil)
    target_customer = self.customer
    target_customer ||= customer
    target_option = self.option
    target_option ||= option

    # 契約者とオプションの組み合わせチェック
  end
end

上記の例だとクラスメソッドでも良いが、ある契約者が利用できるオプションの一覧を取得する処理を書く場合はインスタンスメソッドの方が都合が良い。

spec = RatePlanCustomerOptionSpec.new(customer: customer)
options = Option.all.select { |option| spec.valid?(option: option) }

あるいは、Strategyパターンを適用してSpecクラスを差し替えるなら、やはりインスタンスメソッドの方が都合が良いだろう。

以降は私個人の考えていることである。

エリック・エヴァンスのドメイン駆動設計では、「検証」「選択」「要求に応じた構築」の3例があげられているが、これらは使い方の例であってパターンとして覚えるべきものではない。あくまで重要なのは、エンティティや値オブジェクトにのせるには不相応な処理を独立させるという点である。上記の例は、複数のオブジェクトにおける例だったが、単一のオブジェクトでも仕様パターンを適用した方が良い場合も当然ある。

また、仕様パターンはビジネスルールのファクトや制約をコードで表現したパターンと言え、*Specクラスではなく*Ruleクラスにして計算のビジネスルールなども同じように独立させた方が良いだろうと考えている。計算処理もどこかのクラスにのせるとFatになりやすく、どこにのせるべきか迷いやすい処理だからだ。詳細は要求分析駆動設計 ビジネスルールに書いているのでそちらを参照。