こんにちは!FlutterやDartでの開発、日々の試行錯誤お疲れ様です。先輩フルスタックエンジニアの私です。
今回は、Dart 3で導入された「パターンマッチング」を使って、巨大で複雑になりがちな `if-else` ブロックを美しく解体し、「単体テストが容易な小さな関数へとリファクタリングする極意」についてお話しします。
他の言語(RustやSwiftなど)でパターンマッチングの味をしめた方も、Dart 3のそれは独自の洗練された構文を持っていて、非常に強力です。ここをクリアすれば、あなたの書くコードの保守性とテスト容易性は見違えるほど跳ね上がりますよ。ぜひ一緒にマスターしていきましょう!
—
なぜ巨大な if-else は「悪」なのか?
実務の現場で、次のようなコードに出会ったことはありませんか?
// 良くある巨大な if-else の迷宮
String getShippingFee(Map
if (order[‘type’] == ‘book’) {
if (order[‘weight’] < 1.0) {
if (order['isMember'] == true) {
return '無料';
} else {
return '300円';
}
} else {
return '500円';
}
} else if (order['type'] == 'denshi_書籍') {
return '0円(デジタル)';
} else {
// さらに続くネストの嵐...
return '1000円';
}
}
こういうコード、見ているだけで頭痛がしてきますよね。何が問題かと言うと、「条件の組み合わせが爆発し、どこをどうテストしていいか分からない(網羅できない)」という点です。結合テストのように全パターンを流すのは大変ですし、変更を入れるたびにデグレ(バグの混入)の恐怖に怯えることになります。
これをDart 3のパターンマッチングで華麗に料理していきましょう。
—
Dart 3 パターンマッチングの基本:switch エクスプレッション
Dart 3では、`switch` が「文(Statement)」から「式(Expression)」に進化しました。これにより、値を直接返すスマートな書き方ができるようになりました。
まずは基本の形を確認しておきましょう。
int getScoreBonus(String rank) {
return switch (rank) {
‘Gold’ => 100,
‘Silver’ => 50,
‘Bronze’ => 10,
_ => 0, // どのパターンにも一致しない場合(デフォルト)
};
}
C#やJavaにあるような `switch` と似ていますが、Dartの真骨頂は「データの構造そのものを分解(destructuring)しながら条件分岐できる点」にあります。
—
実践:複雑な条件を「小さな純粋関数」に分割する
先ほどのECサイトの送料計算を例に、テストしやすい形へリファクタリングする手順を解説します。
リファクタリングの黄金律は、「1つの関数には1つの責任を持たせ、複雑な条件判定は小さなプライベート関数に切り出すこと」です。
ステップ1: 扱うデータをモデル化する
まずは闇雲な `Map` ではなく、型安全なレコード(Records)やクラスを使います。今回はDart 3の機能であるレコードを使いましょう。
ステップ2: 条件をパターンマッチングで切り出す
次のように、判定ロジックを独立した小さな関数に分割します。
// 送料を計算するためのドメインモデル(注文種別、重さ、会員フラグ)
// レコード型を使って簡潔に表現しています
typedef OrderInfo = ({String type, double weight, bool isMember});
/// 1. 書籍カテゴリの送料を判定する責務を持つ関数
String _evalBookFee(double weight, bool isMember) {
// パターンマッチングのガード節(when)を活用
return switch ((weight, isMember)) {
(var w, true) when w < 1.0 => ‘無料’,
(var w, false) when w < 1.0 => ‘300円’,
_ => ‘500円’,
};
}
/// 2. メインのエントリポイント(ここから各ロジックを呼び出す)
String calculateShippingFee(OrderInfo order) {
return switch (order.type) {
‘book’ => _evalBookFee(order.weight, order.isMember),
‘digital’ => ‘0円(デジタル)’,
_ => ‘1000円’,
};
}
どうでしょう? `switch` の中でタプル `(weight, isMember)` を受け取り、`when` 句(ガード節)を使って非常に宣言的に条件を書けていますよね。
—
なぜこの分割が「テスト容易性」を生むのか?
上記のコードの最大のメリットは、「`_evalBookFee` という関数が、外部の状態に依存しない純粋関数(Pure Function)になったこと」です。
これに対する単体テスト(Unit Test)を書いてみましょう。Flutter/Dartの `test` パッケージを使うと、以下のように圧倒的にシンプルに書けます。
import ‘package:test/test.dart’;
void main() {
group(‘書籍の送料判定ロジック (_evalBookFee)’, () {
test(‘1kg未満の会員なら無料であること’, () {
final result = _evalBookFee(0.5, true);
expect(result, ‘無料’);
});
test(‘1kg未満の非会員なら300円であること’, () {
final result = _evalBookFee(0.5, false);
expect(result, ‘300円’);
});
test(‘1kg以上の場合は重さに関わらず500円であること’, () {
expect(_evalBookFee(1.5, true), ‘500円’);
expect(_evalBookFee(2.0, false), ‘500円’);
});
});
}
巨大な `if-else` の中に入り込んでいた複雑な条件が、個別の関数として独立したことで、すべての分岐網羅(C1/C2テスト)が驚くほど簡単になったのがお分かりいただけるはずです。
—
陥りがちな文法エラーと注意点
ここで、Dartのパターンマッチングを使い始めた初心者がよくハマるポイントをいくつか共有しておきますね。網を張っておくことで、無駄なハマり時間をゼロにできます。
1. 網羅性チェック(Exhaustiveness checking)のエラー
Dartの `switch` 式は非常に賢いため、「すべての可能性が網羅されているか」をコンパイル時にチェックします。
もし `_`(デフォルトケース)を書き忘れたり、enumやboolの全パターンを網羅していないと、コンパイルエラーになります。
> エラー例:
> `The type ‘String’ is not exhaustively matched by the switch cases.`
対策: 想定外の値に対するフォールバックとして、必ず最後に `_ => 処理` を記述する癖をつけましょう。
2. `when` 句の書き間違い
ガード節を書く際、`=>` の手前に `when` を書く必要があります。
// ❌ 間違い:コンパイルエラーになります
(var w, true) => ‘無料’ when w < 1.0,
// 〇 正しい:パターンマッチの条件の後に when を置く
(var w, true) when w < 1.0 => ‘無料’,
—
まとめ
今回は、Dart 3のパターンマッチングと `switch` 式を使って、複雑な条件分岐をテスト可能な小さな関数にリファクタリングする手法を解説しました。
- 巨大な `if-else` のネストは、コードのテスト容易性と保守性を著しく下げる。
- Dart 3の `switch` 式とレコード型を組み合わせることで、複雑な条件を美しく宣言的に記述できる。
- 条件判定を小さな純粋関数に切り出すことで、単体テストの網羅が容易になる。
- コンパイラの網羅性チェック(Exhaustiveness checking)を味方につけて、安全なコードを書く。
ここをクリアすれば、あなたの書くDartコードはワンランク上のクオリティになりますよ。ぜひ今日の開発から取り入れてみてくださいね。それでは、また次回の極限の知見でお会いしましょう!