こんにちは。テックリードの私だ。
コードレビューをしていて、最近よく見かける光景がある。ドメインのプリミティブな値(ユーザーIDや金額など)を保護するために、やれ `class UserId { final String value; … }` とラップし、その結果として無数のインスタンスがヒープにばら撒かれ、GC(ガベージコレクション)のプレッシャーを上げているコードだ。
「型安全性は欲しい。だが、パフォーマンスは犠牲にしたくない」
Webフロントエンドであれ、複雑な非同期API連携を行うクライアントであれ、このジレンマに直面した開発者は多いはずだ。
Dart 3の Extension Types(拡張型) と パターンマッチング は、この永遠の課題に対するDartチームからのファイナルアンサーである。今回は、これらを完璧に融合させ、実行時オーバーヘッドを完全にゼロにしつつ、コンパイラにドメインの制約を強制させる究極のドメインモデル構築術を伝授しよう。
—
なぜ通常のラッパクラスはドメイン設計の罠となるのか?
まずは現実を見よう。次のようなコードはよくある悪夢だ。
// 従来のクラスによるラップ
class Amount {
final int value;
const Amount(this.value);
}
一見、プリミティブな `int` が直接露出するのを防げているように見える。だが、Dart VMの内部構造を知る者からすれば、これは「コストの割に合わない抽象化」だ。
このクラスは、たとえ `const` でインスタンス化されたとしても、大規模なドメインロジックやコレクション内ではヒープアロケーションを誘発し、GCのサイクルを早める。さらに、`null` チェックや不正な値(負の金額など)のバリデーションをコンパイル時に強制することはできない。
ここで登場するのが Extension Types だ。
—
Extension Typesとは何か?(VMの視点から見た真実)
勘違いしている人が多いが、Extension Typesは TypeScript の `type` エイリアスや単なるシンタックスシュガーではない。
extension type const Amount(int value) {
// コンパイル時制約(バリデーション)を仕込める
validate() {
if (value < 0) throw ArgumentError('金額は0以上でなければなりません');
}
}
Dart VMのAOT/JITコンパイラにおいて、`Amount` 型の変数は、実行時には完全にただの `int` として扱われる。 ラッパーオブジェクトの生成は「ゼロ」だ。
コンパイル時にのみ型チェックが行われ、生成される機械語レベルではプリミティブ型にコンパイルされる。これが、パフォーマンスを1ミリも犠牲にしない理由である。
—
実践:パターンマッチングと組み合わせた堅牢なドメインモデル
では、APIレスポンスの処理や、状態管理における複雑なドメインモデルをこの仕組みで構築してみよう。
以下のコードは、実務の現場ですぐに使える、型安全かつ堅牢な設計パターンの実例だ。
import ‘dart:convert’;
// — 1. プリミティブを包む Zero-Cost な Extension Types —
extension type const UserId(String value) {
bool get isValid => value.startsWith(‘usr_’);
}
extension type const Amount(int value) {
// ドメインロジックを内包する
Amount operator +(Amount other) => Amount(value + other.value);
}
// — 2. ドメインの状態を表現する Sealed Class —
sealed class PaymentStatus {}
class Pending extends PaymentStatus {
final DateTime requestedAt;
Pending(this.requestedAt);
}
class Completed extends PaymentStatus {
final String transactionId;
Completed(this.transactionId);
}
class Failed extends PaymentStatus {
final String reason;
Failed(this.reason);
}
// — 3. 集約ルート(Aggregate Root)としてのドメインモデル —
class PaymentOrder {
final UserId userId;
final Amount amount;
final PaymentStatus status;
const PaymentOrder({
required this.userId,
required this.amount,
required this.status,
});
}
// — 4. 非同期API連携とパターンマッチングの融合 —
Future
// 非同期APIからのデータフェッチをシミュレート
await Future.delayed(const Duration(milliseconds: 100));
final Map
// コンパイル時の型安全性と実行時のゼロコスト抽象化の恩恵を受ける
return PaymentOrder(
userId: UserId(data[‘userId’] as String),
amount: Amount(data[‘amount’] as int),
status: switch (data[‘status’] as String) {
‘pending’ => Pending(DateTime.parse(data[‘requestedAt’] as String)),
‘completed’ => Completed(data[‘transactionId’] as String),
‘failed’ => Failed(data[‘reason’] as String),
String s => throw FormatException(‘未知のステータスです: $s’),
},
);
}
// — 5. ビジネスロジックの処理(Dart 3 パターンマッチングの真骨頂) —
String processPaymentWorkflow(PaymentOrder order) {
// ガード節や冗長な if-else はもう不要。
// exhaustiveness(網羅性)チェックにより、新しいステータス追加時のバグをコンパイルエラーで防ぐ。
return switch (order) {
PaymentOrder(userId: var uid, amount: var amt, status: Completed(transactionId: var tx))
when uid.isValid =>
‘成功: ユーザー ${uid.value} が ${amt.value} 円を支払いました。(TX: $tx)’,
PaymentOrder(status: Pending(:var requestedAt)) =>
‘処理中: ${requestedAt.toIso8601String()} にリクエストされました。’,
PaymentOrder(status: Failed(:var reason)) =>
‘失敗: 理由 -> $reason’,
_ => ‘無効なオーダーまたは未定義の状態です。’,
};
}
// — 実行テスト —
void main() async {
const jsonResponse = ”’
{
“userId”: “usr_99812”,
“amount”: 5000,
“status”: “completed”,
“transactionId”: “tx_xyz_777″
}
”’;
print(‘=== 決済ドメインモデルの処理開始 ===’);
final order = await fetchPaymentOrder(jsonResponse);
// 実行時オーバーヘッドなしでドメインロジックが安全に走る
final result = processPaymentWorkflow(order);
print(result);
}
—
コードレビューの視点:なぜこの設計が優れているのか?
私のチームでこのコードが提出されたら、私は迷わず「LGTM(Approved)」を出す。その理由は以下の3点だ。
1. 実行時のメモリ効率(Zero-Cost)
`UserId` も `Amount` も、実行時にはただの `String` と `int` として扱われる。オブジェクトのラップによるGCプレッシャーが完全に消失する。数万件のリストデータを扱うフロントエンドのステート管理や、高頻度で実行される非同期APIのシリアライゼーションにおいて、これは絶大なパフォーマンス上のアドバンテージを生む。
2. Dart 3 網羅性チェック(Exhaustiveness Checking)によるバグの根絶
`switch` 式によるパターンマッチングでは、`PaymentStatus` の派生クラス(`Pending`, `Completed`, `Failed`)のいずれかをハンドリングし忘れた場合、コンパイルエラーになる。将来、仕様変更で `Refunded` ステータスを追加した時、ハンドリング漏れによる本番障害を未然に防ぐことができる。
3. ドメイン知識の局所化
「`UserId` は `usr_` から始まるべき」「`Amount` 同士は加算できる」といったドメインのルールが、Extension Typeの定義内に美しくカプセル化されている。プリミティブ型をそのまま関数にばら撒くことで起きる「引数の順序間違い」や「不正な値の混入」という人災を、型システムによって完全にハメ殺すことができる。
—
アーキテクトからのメッセージ
型の力を信じろ。だが、パフォーマンスを言い訳にして脆弱なプリミティブ型に逃げるな。
Dart 3の `extension types` と `pattern matching` を使いこなせば、「圧倒的な実行パフォーマンス」 と 「妥協のない型安全性」 は両立できる。
今日のコードレビューから、君のプロジェクトのドメインモデルをアップデートしてほしい。プロダクションコードの美しさと強靭さに、チームメンバーも必ず驚くはずだ。