---
title: ビジネスルールを型システムで表現する
tags:  #jjug_ccc #java  
author: [Yohei Kajiro](https://image.docswell.com/user/yoheiyohei4)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/87DK8G15JG.jpg?width=480
description: ビジネスルールを型システムで表現する by Yohei Kajiro
published: May 30, 26
canonical: https://image.docswell.com/s/yoheiyohei4/57NJ3N-business-rules-in-the-type-system
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/87DK8G15JG.jpg)

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


# Page. 2

![Page Image](https://bcdn.docswell.com/page/VJPK83QDE8.jpg)

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


# Page. 3

![Page Image](https://bcdn.docswell.com/page/2EVVN45GEQ.jpg)

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


# Page. 4

![Page Image](https://bcdn.docswell.com/page/57GLK1PDEL.jpg)

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


# Page. 5

![Page Image](https://bcdn.docswell.com/page/4EQYND2XJP.jpg)

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


# Page. 6

![Page Image](https://bcdn.docswell.com/page/KJ4WGZL271.jpg)

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


# Page. 7

![Page Image](https://bcdn.docswell.com/page/LE1YDRLK7G.jpg)

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


# Page. 8

![Page Image](https://bcdn.docswell.com/page/GEWGY1VPJ2.jpg)

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


# Page. 9

![Page Image](https://bcdn.docswell.com/page/47ZLXP46J3.jpg)

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


# Page. 10

![Page Image](https://bcdn.docswell.com/page/YJ6W4M3LJV.jpg)

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


# Page. 11

![Page Image](https://bcdn.docswell.com/page/GJ5MQZLMJ4.jpg)

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


# Page. 12

![Page Image](https://bcdn.docswell.com/page/9E29PRLR7R.jpg)

sealed interface による解決


# Page. 13

![Page Image](https://bcdn.docswell.com/page/D7Y45DQ5EM.jpg)

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


# Page. 14

![Page Image](https://bcdn.docswell.com/page/VENYN6K4J8.jpg)

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


# Page. 15

![Page Image](https://bcdn.docswell.com/page/Y79PRLV4E3.jpg)

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


# Page. 16

![Page Image](https://bcdn.docswell.com/page/G78DWXPV7D.jpg)

After: usecase での switch 式
CanceledBill result = switch (bill) {
case IssuedBill b -&gt; billRepository.saveCancellation(b.cancel(cancellation));
case CanceledBill b -&gt; throw new CancelUseCaseException(&quot;既にキャンセル済みです &quot;);
case PaidBill b
-&gt; throw new CancelUseCaseException(&quot;支払済みのためキャンセルできません &quot;);
case RefundedBill b -&gt; throw new CancelUseCaseException(&quot;返金済みのためキャンセルできません &quot;);
};
全ケース漏れ → コンパイルエラー
戻り値の型が CanceledBill → 状態が型で保証される
16


# Page. 17

![Page Image](https://bcdn.docswell.com/page/L7LMN81RJR.jpg)

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


# Page. 18

![Page Image](https://bcdn.docswell.com/page/4EMYX6WPEW.jpg)

新状態追加時 → コンパイラが変更漏れを検出
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


# Page. 19

![Page Image](https://bcdn.docswell.com/page/PER9NPYYJ9.jpg)

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


# Page. 20

![Page Image](https://bcdn.docswell.com/page/P7XQN3G4EX.jpg)

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


# Page. 21

![Page Image](https://bcdn.docswell.com/page/37K9NY4P7D.jpg)

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


# Page. 22

![Page Image](https://bcdn.docswell.com/page/LJ3WV9L4J5.jpg)

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


# Page. 23

![Page Image](https://bcdn.docswell.com/page/8JDK8GN5EG.jpg)

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


