勉強不足を晒すことになりますが。 仕事で "Specification" なるオブジェクトが出てきたときに、しばらくは漠然としたイメージしか持てませんでした。
当面は直接関わることがなかったのでそのままにしていたのですが、原典に当たったことですっきりしました。
仕様とは、あるオブジェクトが何らかの基準を満たしているかどうかを判定する述語である。
なるほど、述語のオブジェクトということのようです。
その応用については Martin Fowler のサイトで公開されている論文に詳しく書かれています。
そこで、この論文に出てくる CompositeSpecification
を Ruby で書いてみました。
module CompositeSpecification class AndSpecification include CompositeSpecification def initialize(left, right) @left = left @right = right end def satisfied_by?(object) @left.satisfied_by?(object) && @right.satisfied_by?(object) end end class OrSpecfication include CompositeSpecification def initialize(left, right) @left = left @right = right end def satisfied_by?(object) @left.satisfied_by?(object) || @right.satisfied_by?(object) end end class NotSpecification include CompositeSpecification def initialize(specification) @specification = specification end def satisfied_by?(object) !@specification.satisfied_by?(object) end end def and(another) AndSpecification.new(self, another) end def or(another) OrSpecfication.new(self, another) end def ! NotSpecification.new(self) end end
使い方の例です。
まず、 1 から 30 の数値のうち、仕様を満たす値のみを出力する関数を用意します。
def satisfy(spec) puts (1..30).select { |i| spec.satisfied_by?(i) }.join(' ') end
次に 3 の倍数であることを FizzSpecification
、5 の倍数であることを BuzzSpecification
と定義します。
require_relative './composite_specification' class FizzSpecification include CompositeSpecification def satisfied_by?(n) (n % 3).zero? end end class BuzzSpecification include CompositeSpecification def satisfied_by?(n) (n % 5).zero? end end
それぞれの Specification オブジェクトを適用してみます。
fizz_spec = FizzSpecification.new satisfy(fizz_spec) #=> 3 6 9 12 15 18 21 24 27 30 buzz_spec = BuzzSpecification.new satisfy(buzz_spec) #=> 5 10 15 20 25 30
CompositeSpecification
を include したこれらの Specification は合成することができます。
FizzSpecification
と BuzzSpecification
を合成して、新しい Specification オブジェクトを作ります。
fizz_buzz_spec = fizz_spec.and(buzz_spec)
satisfy(fizz_buzz_spec)
#=> 15 30
どんどん合成できます。
satisfy(fizz_spec.or(buzz_spec)) #=> 3 5 6 9 10 12 15 18 20 21 24 25 27 30 satisfy(!fizz_spec.or(buzz_spec)) #=> 1 2 4 7 8 11 13 14 16 17 19 22 23 26 28 29 satisfy((!fizz_spec).and(!buzz_spec)) #=> 1 2 4 7 8 11 13 14 16 17 19 22 23 26 28 29
「エリック・エヴァンスのドメイン駆動設計」にはこうも書かれています。
論理プログラミングは、 「述語」と呼ばれる、独立した結合可能なルールオブジェクトの概念を提供するが、この概念をオブジェクトで完全に実装するのは面倒である。
なるほど。 それでは論理プログラミングなら簡単なのかも。
試してみます。
そんなわけで。 久々の Prolog プログラミングです。
fizz/1
と buzz/1
を定義して specification.prolog
というファイル名で保存します。
fizz(N) :- Rem is N mod 3, Rem == 0. buzz(N) :- Rem is N mod 5, Rem == 0.
REPL を起動します。 使うのはいつものように GNU Prolog です。
$ gprolog
プログラムを読み込みます。
| ?- [specification].
1から30までの間で fizz/1
を満たす N
をすべて見つけます。
| ?- findall(N, (between(1, 30, N), fizz(N)), Ns). Ns = [3,6,9,12,15,18,21,24,27,30]
1から30までの間で buzz/1
を満たす N
をすべて見つけます。
| ?- findall(N, (between(1, 30, N), buzz(N)), Ns). Ns = [5,10,15,20,25,30]
Prolog ではコンマ( (',')/2
)は論理積を、セミコロン( (;)/2
)は論理和を表す演算子です。
ちなみに否定を表す演算子は (\+)/1
です。
ですので fizz_buzz/1
はこのように定義できます。
fizz_buzz(N) :- fizz(N), buzz(N).
| ?- findall(N, (between(1, 30, N), fizz_buzz(N)), Ns). Ns = [15,30]
他にも:
fizz_or_buzz(N) :- fizz(N); buzz(N).
| ?- findall(N, (between(1, 30, N), fizz_or_buzz(N)), Ns). Ns = [3,5,6,9,10,12,15,15,18,20,21,24,25,27,30,30]
not_fizz_or_buzz(N) :- \+fizz_or_buzz(N).
| ?- findall(N, (between(1, 30, N), not_fizz_or_buzz(N)), Ns). Ns = [1,2,4,7,8,11,13,14,16,17,19,22,23,26,28,29]
確かに合成は簡単、…というかこれは論理プログラミングそのものですね、確かに。
合成の部分に関しては。 例えば次のように演算をオブジェクトして遅延評価したいときに顔をだす構造。
module Calculable class Add include Calculable def initialize(left, right) @left = left @right = right end def eval @left.eval + @right.eval end end class Sub include Calculable def initialize(left, right) @left = left @right = right end def eval @left.eval - @right.eval end end def add(another) Add.new(self, another) end def sub(another) Sub.new(self, another) end end class Int include Calculable def initialize(n) @n = n end def eval @n end end three = Int.new(3) two = Int.new(2) puts (three.add(two)).sub(three.sub(two)).eval #=> 4
これは Ruby on Rails の ActiveRecord のクエリメソッドで、普段からお世話になっている仕組みですね。
述語の詳細をオブジェクトで表現しなければならい状況に遭遇しないと、なぜこれが有意義なのかわかりにくいですが、よくよく調べてみると確かにそういった状況があると納得し、その解決方法の一つが Specification なのだとようやく腑に落ちたのでした。