Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ビジネスロジックを堅牢に~Scala 3のドメインプリミティブ~

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

ビジネスロジックを堅牢に~Scala 3のドメインプリミティブ~

かとじゅん

November 13, 2026

Transcript

9 slides
  1. Scala 2 でも不正な値は作れないが、定型記述が多い DueDate は、期限の日付とルールを持つ小さな型。これをドメインプリミティブと呼ぶ。 // import java.time._ / java.time.format._

    // ParseError の定義は省略 final class DueDate private (val value: LocalDate) { override def equals(other: Any): Boolean = other match { case that: DueDate => value == that.value; case _ => false } override def hashCode: Int = value.hashCode override def toString: String = s"DueDate($value)" } object DueDate { // MaxLength = 10 / DateRegex / Formatter は省略 def apply(value: String, clock: Clock): Either[ParseError, DueDate] = if (value.isEmpty) Left(ParseError.EmptyInput) else if (value.length != MaxLength) Left(ParseError.InvalidLength) // ... 文字種 → 正規表現 → 実在日付 → 未来かどうか ビジネスロジックを堅牢に 3
  2. opaque type:実体を隠し、別の型として扱う コンパイル時は DueDate と LocalDate を区別し、実行時は LocalDate と同じ表現になる。 //

    DueDate.scala のトップレベル opaque type DueDate = LocalDate // 利用側の別ファイル(import と clock の用意は省略) val raw: LocalDate = LocalDate.of(2026, 11, 13) val due: DueDate = raw // コンパイルエラー:別の型なので代入できない val result = DueDate.parse("2026-11-13", clock) // 検証して生成する 普通の type DueDate = LocalDate は別名なので、直接代入できてしまう この例では定義側で実体を扱い、別ファイルの利用側には DueDate として公開する 実行時に値を包むクラスは不要。等価性の判定も LocalDate のものをそのまま使う opaque type は型を区別する仕組み。日付の検証は次の parse に書く ビジネスロジックを堅牢に 4
  3. DueDate を Scala 3 で書く:生成と検証 enum ParseError: case EmptyInput, InvalidLength,

    InvalidCharacters, InvalidFormat, InvalidDate, MustBeInFuture opaque type DueDate = LocalDate object DueDate: // MaxLength = 10 / DateRegex / Formatter(STRICT)は省略 def parse(value: String, clock: Clock): Either[ParseError, DueDate] = if value.isEmpty then Left(ParseError.EmptyInput) // 1 サイズ else if value.length != MaxLength then Left(ParseError.InvalidLength) else if !value.forall(c => c.isDigit || c == '-') then // 2 文字種 Left(ParseError.InvalidCharacters) else if !DateRegex.matches(value) then Left(ParseError.InvalidFormat) // 3 構文 else try val date = LocalDate.parse(value, Formatter) if date.isAfter(LocalDate.now(clock)) then Right(date) // 4 意味(未来) else Left(ParseError.MustBeInFuture) catch case _: DateTimeParseException => Left(ParseError.InvalidDate) 検証は4層:サイズ → 文字種 → 構文 → 意味。「2027-02-30」は実在する日付かの検証で弾かれる 生成時の「未来かどうか」は、外から渡した Clock の時刻で判定する 文字列を受け取る境界で検証し、成功した Right の DueDate を内部へ渡す ビジネスロジックを堅牢に 5
  4. DueDate の振る舞い(Scala 2) // 振る舞いはクラスの中に同居させるのが基本 final class DueDate private (val

    value: LocalDate) { def isOverdue(today: LocalDate): Boolean = today.isAfter(value) def extendBusinessDays(n: Int, holidays: Set[LocalDate]): DueDate = LazyList.iterate(value.plusDays(1))(_.plusDays(1)) .filter(DueDate.isBusinessDay(_, holidays)) .take(n) .lastOption .map(new DueDate(_)) .getOrElse(this) // businessDaysFrom・equals / hashCode / toString はリポジトリ参照 } ビジネスロジックを堅牢に 6
  5. DueDate の振る舞い(Scala 3) object DueDate: private def isBusinessDay(d: LocalDate, holidays:

    Set[LocalDate]): Boolean = d.getDayOfWeek != DayOfWeek.SATURDAY && d.getDayOfWeek != DayOfWeek.SUNDAY && !holidays.contains(d) extension (due: DueDate) def isOverdue(today: LocalDate): Boolean = today.isAfter(due) def extendBusinessDays(n: Int, holidays: Set[LocalDate]): DueDate = LazyList.iterate(due.plusDays(1))(_.plusDays(1)) .filter(isBusinessDay(_, holidays)) .take(n) .lastOption .getOrElse(due) 拡張メソッドを定義する extension (due: DueDate) で、 due.isOverdue(today) のように呼べる DueDate を受け取るメソッドとして、期限切れ判定と営業日での延長を定義する 延長は元の値を書き換えず、新しい DueDate を返す。今日の日付と祝日は外から渡す ビジネスロジックを堅牢に 7
  6. 小さな型に、業務の意味とルールを集める 定型記述が減ると、業務の概念ごとに小さな型を定義しやすくなる。 DueDate で表したいこと 日付に「期限」という名前を付け、他の日付と区別する 生成時は未来の日付だけを受け付ける 期限切れ判定・営業日での延長を提供する Scala 3 での書き方

    opaque type で型を分ける parse で検証する extension でメソッドを定義する DueDate のような型を作るたびに、包むクラスや等価性の定義を書かずに済む 引数の型を DueDate にすると、日付ではなく期限を受け取ると型で伝えられる 業務の意味ごとに、型とルールを表す ビジネスロジックを堅牢に 8

More Decks by かとじゅん

Other Decks in Programming