TL;DR
DollarとFrancのtimesメソッドの重複を解消するために、小さなステップで TDD を進めた。- Factory Method パターンを導入してテストコードからサブクラスを隠し、既存のコードに影響を与えずに継承構造を変えられるようにした。
内容
サブクラス(Dollar と Franc)の times メソッドは処理が重複している。しかし、一度に解消するにはステップが大きいので、次のように少しずつ進めた。
timesメソッドのシグネチャを統一した- 親クラス
Moneyを抽象クラスにし、timesを抽象メソッドとして定義した - Factory Method パターンを導入し、テストコードからサブクラスの存在を隠した
3 によって、既存のコードに影響を与えずに継承の構造を変更できるようになった。その結果テストは冗長になったが、今はそのままにした。
キーワード
自分の理解
別物の通貨を共通化してよいのか
Dollar と Franc は現実世界では別物なのに、共通化を進めようとしている。「重複を避けるため」だけでは理由として弱くないか。2 つは明らかに別の通貨であり、為替レートも法的な位置付けも違う。親子の間は厳密に is-a の関係だが、子どうしは is-a の関係ではない。第7章でも「内部の値が同じなら Dollar と Franc の equals が true になってしまう」ことを問題にしていたはずである。
今の実装を見渡すと、これらのクラスがモデリングしているのは通貨そのものではなく、金額の計算という振る舞いである。それならインタフェースを切るべきではないかとも思った。もちろん早すぎる抽象化は良くないし、インタフェースはおそらく後の章で出てくるのだろう。
結局、テスト「駆動」開発なのだから、こうしたモデリングも必要に迫られたときにやればよい、と落ち着いた。大切なのは、テストを書き、最低限の実装をし、リファクタリングするサイクルを回すことである。詳細なロジックや設計はそのサイクルの中から生まれる(Clean Craftsmanship)ので、焦らなくてよい。
Factory Method を強制できないか
テストコードを Factory Method に置き換えている箇所があるが、置き換え忘れても気づけないのではないか。Java で Factory Method の使用を強制する(コンストラクタを使えなくする)ことはできるのだろうか。コンストラクタを private にすると親クラスから呼び出せなくなるので使えない。private なネストクラスにするのかもしれない。
Scala なら、コンパニオンオブジェクトを使って簡単に表現できた気がする。Claude Code に書かせてみたのが次のコードである。
sealed trait Money:
def amount: Int
object Money:
private case class Dollar(amount: Int) extends Money
private case class Franc(amount: Int) extends Money
def dollar(amount: Int): Money = Dollar(amount)
def franc(amount: Int): Money = Franc(amount)sealed trait は同じファイルの中でしか継承できないトレイト、object はシングルトンオブジェクトの定義、case class はイミュータブルなデータを表すクラスである。Scala 3 の機能を本格的に使うなら、enum や Opaque types を使うのかもしれない。スマートコンストラクタと呼ばれる手法にも近い。深入りすると沼なので、いったんここで止めた。