TL;DR

  • Dollar と Franc の times メソッドの重複を解消するために、小さなステップで TDD を進めた。
  • Factory Method パターンを導入してテストコードからサブクラスを隠し、既存のコードに影響を与えずに継承構造を変えられるようにした。

内容

サブクラス(Dollar と Franc)の times メソッドは処理が重複している。しかし、一度に解消するにはステップが大きいので、次のように少しずつ進めた。

  1. times メソッドのシグネチャを統一した
  2. 親クラス Money を抽象クラスにし、times を抽象メソッドとして定義した
  3. 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 を使うのかもしれない。スマートコンストラクタと呼ばれる手法にも近い。深入りすると沼なので、いったんここで止めた。