ビジネスルールを型システムで表現する

744 Views

May 30, 26

スライド概要

profile-image

エンジニアリングマネージャー at コドモン Kotlinをよく書いてます。Goも好きです。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

ビジネスルールを型システムで表現する JJUG CCC 2026 Spring 上代 洋平

2.

自己紹介 上代 洋平 かじろ ようへい 2022.12 コドモンに入社 現在 エンジニアリングマネージャー 2

3.

こんな経験ありませんか? ドメインモデルに複数の状態があるとき ● 新しい状態を追加するとき、どこを直せばいいか分からない ● null チェックがあちこちに散らばって、コードが読めなくなっている ● あるメソッドを「この状態で呼んでいいのか?」自信が持てない ● 状態を追加したら既存のロジックをこっそり壊していた 3

4.

他人ごとではなかった 悩んでいた側でもあり、作り出してしまっていた側でもありました。 今日は私が実際に取り組んだリファクタリングのアプローチをご紹介します。 4

5.

このセッションで得られること ● nullable フィールドによる状態管理の何が問題か → 問題を言語化できるようになる ● 型で状態を表現するとどう変わるか → 型でビジネスルールを守るコードが書ける 5

6.

今日の題材:請求ドメイン 6

7.

nullable フィールドによる状態管理の辛さ

8.

Before: nullable フィールドで状態を表現 public class Bill { private final Id id; private final BillNumber billNumber; // ... private final Cancellation cancellation; // nullable: キャンセル済みのみ private final Refund refund; private final Payment payment; // nullable: 返金済みのみ // nullable: 支払済みのみ // ... } 8

9.

Before: 状態が増えるたびにガード節が増える public Bill cancel(EmployeeId canceledBy) { if (isPaid()) throw new BillDomainException("支払済みのためキャンセルでき ません"); if (isCanceled()) throw new BillDomainException("既にキャンセル済みです"); if (isRefunded()) throw new BillDomainException("返金済みのためキャンセルで きません"); // ... } 9

10.

Before: 戻り値の型が状態を語らない Bill canceledBill = bill.cancel(input.employeeId()); // 戻り値の型は Bill のまま billRepository.saveCancellation(canceledBill); cancel() が成功しても、戻り値は Bill 呼び出し側から見ると何の状態かわからないオブジェクトが返ってくる 10

11.

この設計の問題 ● ガード節の増殖 ○ 状態が増えるたびに全メソッドへの追記が必要 ● null チェックの散在 ○ if (cancellation != null) が各所に重複 ● 型が状態を語らない ○ Bill canceledBill では「キャンセル済み」が伝わらない 11

12.

sealed interface による解決

13.

After: sealed interface の宣言 public sealed interface Bill permits IssuedBill, CanceledBill, PaidBill, RefundedBill { Id id(); BillNumber billNumber(); } sealed interface が「状態の種類の全リスト」になる 13

14.

After: cancel() は IssuedBill だけが持つ public final class IssuedBill implements Bill { // ... public CanceledBill cancel(String canceledBy) { return new CanceledBill(id, billNumber, amount, Cancellation.of(canceledBy)); } } CanceledBill や PaidBill に対して cancel() を呼ぼうとすると コンパイルエラー になる 14

15.

Before / After: 各状態のフィールド // Before: Bill(nullable フィールドが混在) public class Bill { private final Cancellation cancellation; // nullable } // After: CanceledBill(non-null が保証) public final class CanceledBill implements Bill { private final Cancellation cancellation; // non-null が保証される } 15

16.
[beta]
After: usecase での switch 式
CanceledBill result = switch (bill) {
case IssuedBill b -> billRepository.saveCancellation(b.cancel(cancellation));
case CanceledBill b -> throw new CancelUseCaseException("既にキャンセル済みです ");
case PaidBill b

-> throw new CancelUseCaseException("支払済みのためキャンセルできません ");

case RefundedBill b -> throw new CancelUseCaseException("返金済みのためキャンセルできません ");
};

全ケース漏れ → コンパイルエラー
戻り値の型が CanceledBill → 状態が型で保証される

16

17.
[beta]
After: DB からの復元は from() で一元化
// null の閉じ込め:ここより先は型で保証
static Bill from(
...,
Cancellation cancellation, // nullable
Refund refund,

// nullable

Payment payment

// nullable

){
if (cancellation != null) return new CanceledBill(...);
if (refund != null)

return new RefundedBill(...);

if (payment != null)

return new PaidBill(...);

return new IssuedBill(...);
}
17

18.

新状態追加時 → コンパイラが変更漏れを検出 ExpiredBill(期限切れ)を permits に追加するだけで... public sealed interface Bill permits IssuedBill, CanceledBill, PaidBill, RefundedBill, ExpiredBill { ... } error: the switch expression does not cover all possible input values CanceledBill result = switch (bill) { ^ 変更すべき箇所をコンパイラが全部教えてくれる 18

19.

リファクタリングで何が変わったか

20.

Before / After 比較 Before After 状態の表現 null フィールドの組 み合わせ 専用クラス cancel() の誤呼 び出し 実行時に例外 コンパイルエラー 戻り値の型 Bill (状態不明) CanceledBill (型で保証) 網羅性チェック 対応漏れに 気づけない コンパイルエラー ガード節のテスト 全状態分必要 型が保証 → 不要 20

21.

この設計が向いているとき ● 全ケースが定義時に確定している(状態数が増えるほどクラスも増えるので トレードオフに注意) ● 状態ごとに持つべきフィールドが異なる ● 「どの状態でどの操作が許可されるか」というルールがある 21

22.

まとめ ● nullable フィールドで状態を管理すると... → 型が状態を語らず、変更漏れをコンパイラが検出できない ● sealed interface + pattern matching で状態を型で表現すると... → コンパイラがビジネスルールを守ってくれる ● 結果として... → 実行時エラーからコンパイルエラーへのシフト、変更に自信を持てる ドになる コー 22

23.

ご清聴ありがとうございました!