Specifications パターンと匿名関数の部分適用
カリー化や部分適用の使いどころの例として Specifications パターンを挙げたい。
Specifications パターンは、EvansとFowler1によって提案されたオブジェクト指向のデザインパターン。コレクションからのオブジェクトの抽出(Selection)、ビジネスルールへの適合のバリデーション(validation)、要求に応じたオブジェクトの生成(Requirement)の3つのパターンに用いられる。
パターンに特徴的なのは、Specification
クラスの isSatisfiedBy( aContract )
というメソッド。引数にDDDでいうところのエンティティ(例えば契約とか患者とかドメイン次第でさまざま)を取り、ブーリアンを返す。Specification
はルールを表現し、エンティティはルールから独立して存在する。Validationの場合、エンティティは自由にふるまえるが、ルール違反を起こしたときにそれがわかるという関係にある。Selectionの場合、エンティティは様々であるものの、ルールに適合したエンティティだけを選び出すことができる。Requirementの場合、ルールに適合したエンティティを生成できる。
いずれも、着目すべきは矢印の向きで、エンティティの側がルールを知るのではなく、ルールがエンティティを知るという関係。
ルールそれ自体がオブジェクトであり、コンテキストによりルールが変わる。そこで、isSatisfiedBy
メソッドはクラスメソッド(スタティックメソッド)ではなくインスタンスメソッドとなる。Specification
の生成にてコンテキストを与える。オブジェクト指向的には Specification
を生成するファクトリでコンテキストを注入(インジェクション)する(Evans2 )。ルールの適用開始日は典型例。法律の場合附則に施行日が書かれるし、社内規則も同じような仕組みだろう。
さて、Elixirで Specifications パターンを実装するには、コンテキストとなる引数を固定化した匿名関数を返すということになるだろう。
以下の例は、2024/04/01より損金算入の条件が変わった接待交際費の判定。関数 specification/2
は日付(コンテキスト)によって接待交際費となる基準が変わり、5,000円未満または10,000円未満ならば会議費として認められる。認められる場合には true
、認められない場合には false
を返す。関数 new/1
は、日付を固定して(コンテキストをインジェクションして)バリデーション用の関数を生成する。
defmodule ConventionExpense do
def specification( date, amount ) do
case Date.compare(date, ~D[2024-04-01]) do
:lt -> amount < 5000
_ -> amount < 10000
end
end
@doc """
Creates a new specification for convention expenses.
iex> is_convention_expense = ConventionExpense.new(~D[2024-04-15])
iex> is_convention_expense.(5000)
true
"""
def new(date) do
fn amount -> specification(date, amount) end
end
end
Specificationを部分適用された匿名関数として実現すると、コンテキストを捨象してルールを適用することができる。オブジェクト指向の Specifications パターンと同様に、宣言的なルールの記述、ルールの生成知識、処理とルールの分離などが実現できる。ルールが変わったのであれば ConventionExpense.specification/2
を変更すればよい。日付ではないコンテキストが増えたのなら(大企業と中小企業でルールが違うなど)new/2
などを追加すればいい。判定タイミングがバリデーションではなく、データ抽出に使うとなっても、再利用が容易。