Dart 3 レコードとパターンマッチングで魅せる:名前付き引数の限界を超える「型安全かつ柔軟な関数設計」
コードレビューをしていると、次のような関数シグネチャに度々遭遇する。
// どこにでもある、しかし拡張性の低い関数
Future
required String userId,
bool includeMetadata = false,
bool includePermissions = false,
CachePolicy cachePolicy = CachePolicy.networkOnly,
Duration? timeout,
}) async {
// …
}
一見して綺麗に書かれているように見えるこのコード。しかし、フロントエンドのコンポーネント設計や、複雑なAPI連携レイヤーにおいて、この「名前付き引数の多用」は保守性のボトルネックへと変貌する。
なぜか? Dartの名前付き引数は第一級オブジェクト(First-class citizen)ではないからだ。引数の束をそのまま変数に代別したり、別の関数へ「一部を変更してスプレッド的に」渡したり、動的に構築して転送することが言語仕様上できない。
ここで、Dart 3で導入されたレコード(Records)とパターンマッチングの出番となる。
今回は、コンパイル時の型安全性を一切妥協することなく、関数シグネチャの柔軟性を極限まで高めるアーキテクチャパターンを伝授しよう。
—
なぜ「名前付き引数の多用」はスケールしないのか?
Dartの名前付き引数は非常に強力だが、以下のユースケースで破綻する。
1. 引数束(パラメータバンドル)の再利用ができない:複数の関数が同じようなオプション引数のセットを要求する場合、typedefを駆使してもボイラープレートが増える。
2. 動的な合成が困難:条件に応じて引数を追加・変更(マージ)しながら関数に渡すといった「関数のパイプライン処理」が書けない。
3. 部分適用やカリー化の欠如:共通のコンテキスト(設定値など)を保持したまま、一部のパラメータだけを差し替えた関数のインスタンスを作ることが難しい。
これを解決するのが、「引数をレコード型として1つにまとめ、関数側でパターン分解する」という設計アプローチだ。
—
実装パターン:プロダクションコードで見る「レコード引数」
実際のWebフロントエンドやAPIクライアント層を想定したコードを見てほしい。
オプション設定やクエリパラメータをレコードとしてカプセル化し、関数内では構造的パターンマッチングで安全に受け取る。
import ‘dart:async’;
// 1. キャッシュポリシーの定義
enum CachePolicy { networkOnly, cacheFirst, staleWhileRevalidate }
// 2. クエリパラメータを表す「名前付きレコード型」の型エイリアス
// これにより、ドメイン固有のコンテキストを第一級の型として扱える
typedef UserQueryOptions = ({
bool includeMetadata,
bool includePermissions,
CachePolicy cachePolicy,
Duration? timeout,
});
/// ユーザープロフィールを取得する堅牢なAPIクライアント
/// 引数を1つのレコード(opts)として受け取ることで、柔軟な合成が可能になる
Future
String userId, {
// デフォルト値を担保しつつ、レコードとして一括受領
UserQueryOptions opts = (
includeMetadata: false,
includePermissions: false,
cachePolicy: CachePolicy.cacheFirst,
timeout: null,
),
}) async {
// Dart 3 のパターンマッチングを用いた分解(Destructuring)
// コンパイル時にフィールドの存在が完全に保証されるため、実行時エラーの心配はゼロ
final (:includeMetadata, :includePermissions, :cachePolicy, :timeout) = opts;
print(‘[API] Fetching user: $userId’);
print(‘[API] Config -> Cache: $cachePolicy, Timeout: ${timeout?.inMilliseconds}ms’);
// モックとしての遅延処理
final stopwatch = Stopwatch()..start();
await Future.delayed(const Duration(milliseconds: 100));
if (timeout != null && stopwatch.elapsed > timeout) {
throw TimeoutException(‘User profile fetch timed out.’);
}
return UserProfile(
id: userId,
metadata: includeMetadata ? {‘lastLogin’: ‘202X-XX-XX’} : null,
permissions: includePermissions ? [‘read’, ‘write’] : [],
);
}
// ダミーのデータクラス
class UserProfile {
final String id;
final Map
final List
UserProfile({required this.id, this.metadata, required this.permissions});
@override
String toString() => ‘UserProfile(id: $id, metadata: $metadata, permissions: $permissions)’;
}
—
この設計がもたらす圧倒的な優位性
1. データの「合成(Composition)」と「差分更新」が自由自在
従来の名前付き引数では不可能だった「設定の結合」が、レコードの構文(Record syntax)を使うことで極めてエレガントに記述できる。
main() async {
// ベースとなる標準オプション
const UserQueryOptions defaultOptions = (
includeMetadata: false,
includePermissions: false,
cachePolicy: CachePolicy.cacheFirst,
timeout: Duration(seconds: 5),
);
// 管理者画面用:メタデータだけをtrueに書き換えた「新しいレコード」を瞬時に生成
// レコードはイミュータブルであり、構造的型付けの恩恵を受ける
final UserQueryOptions adminOptions = (
…defaultOptions, // スプレッド的なレコードの再構築(Dart 3のレコード構文)
includeMetadata: true,
includePermissions: true,
);
final profile = await fetchUserProfile(‘user_999’, opts: adminOptions);
print(profile);
}
> AOTコンパイラとメモリの視点:
> Dartのレコードは、軽量なインラインデータ構造としてアロケートされる。クラス(`class`)をわざわざ定義してヒープ領域を汚染するコストを払う必要がなく、ガベージコレクション(GC)の負荷を最小限に抑えられる。VMの最適化により、フィールドアクセスは直接レジスタまたはオフセット参照にコンパイルされるため、パフォーマンスのペナルティは一切ない。
2. パターンガードと組み合わせた高度なバリデーション
受け取ったレコードをそのまま分解するだけでなく、パターンマッチングのガード節(`when`)を組み合わせることで、不正なパラメータの組み合わせをコンパイル時、あるいは関数の入口で完全にシャットアウトできる。
void executeQuery(UserQueryOptions opts) {
// レコードの構造をパターンマッチで検証
switch (opts) {
// タイムアウトが設定されているのにキャッシュファーストなのは許容しないドメインルール等
case (:cachePolicy, :timeout) when cachePolicy == CachePolicy.cacheFirst && timeout != null:
throw ArgumentError(‘CacheFirst policy cannot be used with a strict timeout.’);
default:
print(‘Query options are valid.’);
}
}
—
チーフアーキテクトからの実践的アドバイス:いつこの手法を採用すべきか?
すべての関数をレコード引数に書き換える必要はない。過剰設計(Over-engineering)はコードベースを難解にするだけだ。以下の基準で判断してほしい。
1. パラメータが4つ以上あり、将来的に拡張が見込まれる場合:迷わずレコード型(`typedef`付き)を導入せよ。
2. 複数の関数間で「共通のコンテキストやオプション」を使い回す場合:オブジェクト指向の継承に頼るのではなく、レコードの型合成を活用せよ。
3. UIコンポーネントのプロパティ(Props)構造を設計する場合:Flutterのウィジェット引数や、状態管理のディスパッチ関数において、状態の塊をそのままレコードとしてハンドリングすると、コードの結合度が劇的に下がる。
Dart 3のレコード型とパターンマッチングは、単なる「便利なシンタックスシュガー」ではない。言語の表現力を一段上のレイヤーへと引き上げるための強力なアーキテクチャツールである。
今日のコードレビューから、無機質な名前付き引数の羅列を見直そう。あなたの書くコードは、もっと美しく、もっと拡張性を持てるはずだ。