Skip to content

第 7 章: ケースクラスとトレイトによるポリモーフィズム

7.1 はじめに

第 1 部〜第 2 部で TDD の基本サイクルと開発環境を整えました。ここからは FizzBuzz に追加仕様を導入し、オブジェクト指向設計を進めます。

今回の追加仕様は、FizzBuzz のタイプバリエーションです。

  • Type01: 通常の FizzBuzz(3 の倍数で Fizz、5 の倍数で Buzz、15 の倍数で FizzBuzz)
  • Type02: 数値のみ(常に数値文字列を返す)
  • Type03: FizzBuzz のみ(15 の倍数のみ FizzBuzz、それ以外は数値文字列)

Scala では case classtrait を組み合わせることで、型安全なポリモーフィズムをシンプルに表現できます。

7.2 TODO リスト

既存 TODO に加えて、タイプバリエーションに関する TODO を整理します。

  • ~~1 を渡したら "1" を返す~~
  • ~~3 の倍数のときは "Fizz" を返す~~
  • ~~5 の倍数のときは "Buzz" を返す~~
  • ~~15 の倍数のときは "FizzBuzz" を返す~~
  • ~~1 から 100 までのリストを返す~~
  • Type01 のルールを実装する
  • Type02 のルールを実装する
  • Type03 のルールを実装する
  • 未定義タイプで例外を送出する

7.3 値オブジェクト(case class)

単一値の表現には case class FizzBuzzValue を使います。

case class FizzBuzzValue(number: Int, value: String):
  require(number > 0, s"数値は正の整数でなければなりません: $number")
  override def toString: String = value

case class の主な特徴は次のとおりです。

  • 不変データを簡潔に定義できる
  • equals / hashCode / toString が自動生成される
  • パターンマッチに自然に対応できる

また、require により「正の整数であること」という事前条件をコンストラクタ境界で保証しています。

7.4 ファーストクラスコレクション

複数値の表現には FizzBuzzList を使います。

case class FizzBuzzList(values: List[FizzBuzzValue]):
  require(values.nonEmpty, "リストは空であってはなりません")
  def toStringList: List[String] = values.map(_.value)
  def count: Int = values.length

List[FizzBuzzValue] を直接扱わず、FizzBuzzList にラップすることで、コレクション操作をドメインの語彙で表現できます。

実装ではコンパニオンオブジェクトに create を持たせ、生成ロジックを集約しています。

object FizzBuzzList:
  def create(count: Int, fizzBuzzType: FizzBuzzType): FizzBuzzList =
    val values = (1 to count).map { n =>
      FizzBuzzValue(n, fizzBuzzType.generate(n))
    }.toList
    FizzBuzzList(values)

7.5 トレイトによるポリモーフィズム

FizzBuzz のタイプごとの差し替えポイントは FizzBuzzType で抽象化します。

sealed trait FizzBuzzType:
  def generate(number: Int): String

実装は 3 つの case object です。

object FizzBuzzType:
  case object Type01 extends FizzBuzzType:
    def generate(number: Int): String =
      number match
        case n if n % 15 == 0 => "FizzBuzz"
        case n if n % 3 == 0  => "Fizz"
        case n if n % 5 == 0  => "Buzz"
        case n                => n.toString

  case object Type02 extends FizzBuzzType:
    def generate(number: Int): String = number.toString

  case object Type03 extends FizzBuzzType:
    def generate(number: Int): String =
      number match
        case n if n % 15 == 0 => "FizzBuzz"
        case n                => n.toString

sealed を付けることで実装候補を同一ファイルに閉じ込められ、コンパイラの網羅性チェックが効くようになります。

7.6 テスト

TypeSpec では各タイプとファクトリメソッドを検証しています。

class TypeSpec extends AnyFunSuite:
  test("Type01: 3 の倍数で Fizz を返す") {
    assert(FizzBuzzType.Type01.generate(3) === "Fizz")
  }

  test("Type02: 15 を渡すと 15 を返す") {
    assert(FizzBuzzType.Type02.generate(15) === "15")
  }

  test("Type03: 15 の倍数で FizzBuzz を返す") {
    assert(FizzBuzzType.Type03.generate(15) === "FizzBuzz")
  }

  test("create: 未定義のタイプで例外が発生する") {
    assertThrows[IllegalArgumentException] {
      FizzBuzzType.create(4)
    }
  }

タイプごとの振る舞いを個別に固定することで、追加仕様時の回帰を防げます。

7.7 まとめ

この章では、case classtrait を使って FizzBuzz の型安全な設計を行いました。

  • FizzBuzzValue で値を明示的に表現
  • FizzBuzzList でコレクション操作をカプセル化
  • FizzBuzzType のポリモーフィズムでタイプ差分を分離

次章では、matchsealed trait をさらに掘り下げ、Scala らしい分岐設計を整理します。