仕様変更が想定される場合の Fizz Buzz のドメインモデリングについて
前の記事 「Fizz Buzz と税率とタイムゾーンの話 (ドメインレイヤとアプリケーションレイヤの話、あるいは時間変化する値をモデリングする話)」 でもちょっと言及した下記のついーと。
これはプロダクトの文脈(FizzBuzzをどんな用途で使うか)によるかと。
— フェイ=サン@Y!の人 (@fei_kome) 2019年2月15日
その文脈上で不変性が高いならばEntityやValueObjに、低いならばUseCaseかなと。
例えば今後「4の倍数ならhuzzが返る」的な仕様追加が容易に想像できるならばUseCaseにしますねー。
変化しやすい箇所を依存関係の外側に定義するというのはクリーンアーキテクチャの教えとして正しいのだけど、ドメインやユースケースを捉えるという意味では「仕様変更されやすそうならユースケース」 というのは本質的ではなく、その点については賛成できない。
一方で「仕様変更されやすそうなときはどうするの?」 というのは良い問題提起だなーと感じた。 というわけで 『今後 「4 の倍数なら huzz が返る」 的な仕様追加が容易に想像できる』 場合に自分だったらどうするか考えてみる。
自分だったらどうするか?
想定される変化に今のドメインモデルでは対応できない、ということは、ドメインモデル上で本来変更されやすくなっているべき箇所 *1 が変更されやすくなっていないということだ。 おそらくドメインモデルに改善の余地があると思われるので、ドメインを捉えなおしてドメインモデリングをやり直したい。
「4 の倍数なら huzz が返る」 的な仕様追加が容易に想像できる、という感じであれば、Fizz Buzz 問題を下記のように捉えなおすことができそうである。
条件と文字列の組 (「設定項目」 と呼ぶ) が複数与えられる。 数字を数え上げていき、その数字がいずれかの設定項目の条件に合致する場合はその設定項目の文字列を表示すること。 合致するものがない場合はその数字をそのまま表示すること。 ただし、合致する条件を持つ設定項目が複数ある場合は、それらの文字列をすべて連結して表示すること。
ここで、設定項目は次のとおりである。
- 条件 : 数値が 3 の倍数; 文字列 : Fizz
- 条件 : 数値が 5 の倍数; 文字列 : Buzz
問題を上のように捉えなおすことでドメインモデルも変化する。 以下のような感じである。
class FizzBuzzConfigItem( /** この条件が真になる場合に [expression] が出力に含まれる。 */ val predicate: (Int) -> Boolean, /** 条件が真の場合に出力される文字列。 */ val expression: String ) class FizzBuzzCalculator(private val config: List<FizzBuzzConfigItem>) { fun calculate(inputNumber: Int): String = run { val expressions = config.filter { it.predicate(inputNumber) } .map(FizzBuzzConfigItem::expression) if (expressions.isEmpty()) { inputNumber.toString() } else { expressions.joinToString("") } } }
これで通常の Fizz Buzz に対応することも 「4 の倍数なら Huzz」 という仕様を追加することも容易になった。
// 3 の倍数なら Fizz、5 の倍数なら Buzz を返す。 val fizzBuzzCalculator = FizzBuzzCalculator(listOf( FizzBuzzConfigItem({ it % 3 == 0 }, "Fizz"), FizzBuzzConfigItem({ it % 5 == 0 }, "Buzz") )) // 3 の倍数なら Fizz、4 の倍数なら Huzz、5 の倍数なら Buzz を返す。 val fizzHuzzBuzzCalculator = FizzBuzzCalculator(listOf( FizzBuzzConfigItem({ it % 3 == 0 }, "Fizz"), FizzBuzzConfigItem({ it % 4 == 0 }, "Huzz"), FizzBuzzConfigItem({ it % 5 == 0 }, "Buzz") ))
「3 の倍数なら Fizz」 というような設定項目をどこに定義するかは難しいところだが、カスタマイズ性がそこまで高くなくて良いなら (仕様変更がたまにであれば) ドメイン層の中にべた書きしてしまって良いだろう。 もしユースケースごとに異なるのであればユースケース層に定義すると良いし、ユーザーごとに変更する必要があるならそもそもコード上に定義するのではなくてストレージに保存することになるだろう。
コード全体は fizzbuzz_configurable.kt においてある。
ここまで書いたけど
ドメインレイヤの API が変わらないなら別に実装が変わっても困らないので、必ずしも上のようにカスタマイズ性を高める必要はない気もする。 それよりも、Fizz Buzz をさらに抽象的にして 「数字を数え上げて、文字列を出力するゲーム」 みたいに捉えて、ドメインモデルとしては数字から文字列の変換のインターフェイスと実装を別に提供して切り替えやすくする、みたいな感じにしても良さそう。
(追記) ここでは事前に仕様変更について書いていたが、「2019-02-16 UseCase とは何か | wada811.com」 に書かれている通り、問題領域に変更が入ってからドメインモデリングしなおすというのでも全然問題ないと思う。
実際の開発におけるドメインモデルの変更
実際の開発においてもドメインモデルの再構築が必要になることはままある。 ドメインモデルの再構築が必要だと気付くのは大体開発の途中なので、再構築するのはちょっと大変だったりする。 だけど場当たり的な対応をするとどんどん崩壊していく (繰り返すうちにどんどん変更が難しくなっていく) ので、必要になったらしっかりドメインモデルの再構築をしていく方が良いと思う。 きれいなドメインモデルを保っていれば変更もそこまで苦ではないはず。
ちなみに前の記事で時刻によって状態が変化する場合のドメインモデリングについて書いたが、これは今開発している製品でもともと時刻に応じた状態を持っていなかったところに時刻に応じた状態を持たせるというドメインモデルの変更を行う必要があって、そのときにいろいろ考えたときのことをベースに書いた。 (具体的に言うと、人と組織の所属関係としてもともと 「所属していない」 「所属している」 「所属していた」 という状態しかもっていなかったところに、本来は 「所属期間」 という概念が必要だったということがわかってドメインモデルを変更した。) そのときはもともとのドメインレイヤの設計がめちゃくちゃだったのですごく苦労したのだけど、きれいなドメインモデルを作って実装に落とし込めていれば多分そこまで苦労せずにドメインモデルを変更できたはず。