【入門編】Dartの「パターンマッチング」を用いた複雑な条件分岐のテスト可能な単位への分割 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartを使った開発を楽しんでいますか?

他のプログラミング言語からDartの世界に飛び込んできた方から、よくこんな相談を受けます。
「画面の状態やAPIからのレスポンスに応じて、条件分岐がものすごく巨大になってしまいました……。これ、どうやってテストを書けばいいんでしょう?」

分かります。業務アプリ開発でよくある、JSONのパースや複雑なステート管理。気がつくと、数千行もある巨大な `switch` 文が誕生していて、誰も手を加えられない「魔神の祠」のようになってしまうんですよね。

でも、ご安心ください。Dart 3で導入された「パターンマッチング」を使いこなせば、その巨大な魔神を美しく、かつ完全にテスト可能な小さな部品たちへと解体できるんです。

今回は、先輩フルスタックエンジニアの私が、Dart 3のパターンマッチングを武器にした「テストしやすいコードへのリファクタリング手法」を優しく、かつ深く伝授します。ここをクリアすれば、あなたのDartのコードは見違えるほど洗練されますよ!

—

1. なぜ巨大な `switch` 文はテスト地獄になるのか?

まずは、よくある「アンチパターン」を見てみましょう。
サーバーから送られてきたユーザーの権限やステータスを判定して、表示するメッセージを決定する処理をイメージしてください。

// 【アンチパターン】すべてを抱え込んだ巨大なswitch文
String getDashboardMessage(Map userJson) {
final role = userJson[‘role’];
final status = userJson[‘status’];
final loginCount = userJson[‘loginCount’];

// ここにすべてのビジネスロジックが集中している
if (role == ‘admin’) {
if (status == ‘active’) {
return ‘管理者様、おかえりなさいませ。本日のシステム稼働率は正常です。’;
} else if (status == ‘suspended’) {
return ‘アカウントが一時停止されています。サポートにお問い合わせください。’;
}
} else if (role == ‘user’) {
if (status == ‘active’ && loginCount > 10) {
return ‘いつもご利用ありがとうございます!’;
} else if (status == ‘active’) {
return ‘ようこそ!まずはプロフィールを設定しましょう。’;
}
}
return ‘ゲスト様、サインインしてください。’;
}

このコード、何が問題か分かりますか?
1. 認知負荷が高すぎる: 条件が複雑にネストしていて、人間が脳内でシミュレーションするのが困難です。
2. ユニットテストが書きにくい: 「特定の条件の組み合わせ」をテストするためだけに、わざわざ巨大な `Map` を組み立てて関数全体を呼び出す必要があります。網羅性を担保しようとすると、テストケースが爆発します。

これを、Dart 3のパターンマッチングを使ってスッキリと分割していきましょう!

—

2. Dart 3 パターンマッチングの基本をおさらい

Dart 3では、値の「構造」や「データ型」をそのままマッチさせる強力なパターン構文が導入されました。switch文やswitch式(Expression)の中で、変数をエレガントに分解できます。

イメージとしては、「パズルのピースの形がピタリとハマる場所を探す」ような感覚です。

// switch式を使った美しいパターンマッチングの例
String evaluateScore(int score) => switch (score) {
100 => ‘満点です!’,
>= 80 && < 100 => ‘よくできました!’,
>= 60 => ‘合格です。’,
_ => ‘がんばりましょう。’, // どの条件にも当てはまらない場合(デフォルト)
};

従来の `switch-case` 文と違って、`switch` が「値を返す式」としても使えるのが Dart 3 の大きな特徴です。

—

3. 実践!巨大な分岐を「小さな純粋関数」に分割する

それでは、先ほどの巨大な判定ロジックを、「パターンマッチングを使う小さな関数(プライベートヘルパー)」の集まりにリファクタリングしてみましょう。

プログラミングの鉄則は、「1つの関数には1つの責任を持たせること」です。

// 1. 扱うデータを表すドモデル(イミュータブルなレコードやクラス)
// ここではシンプルにレコード(Record)を使ってみましょう
typedef UserContext = ({String role, String status, int loginCount});

// 2. メインの関数は、各パーツを組み合わせる「司令塔」にする
String getDashboardMessageRefactored(UserContext context) {
return switch (context) {
// 管理者グループのパターン
(role: ‘admin’, status: var s) => _handleAdminStatus(s),

// 一般ユーザーグループのパターン(ガード節も活用)
(role: ‘user’, status: ‘active’, loginCount: var count)
when count > 10 => ‘いつもご利用ありがとうございます!’,

(role: ‘user’, status: ‘active’, _) => ‘ようこそ!まずはプロフィールを設定しましょう。’,

// デフォルト(フォールバック)
_ => ‘ゲスト様、サインインしてください。’,
};
}

// 3. 細分化された小さなパターンマッチング関数(ここがテストの狙い目!)
String _handleAdminStatus(String status) => switch (status) {
‘active’ => ‘管理者様、おかえりなさいませ。本日のシステム稼働率は正常です。’,
‘suspended’ => ‘アカウントが一時停止されています。サポートにお問い合わせください。’,
_ => ‘管理者権限が確認できません。’,
};

どうでしょう?
メインの `getDashboardMessageRefactored` は全体の「ルーティング(経路制御)」だけに責任を持ち、具体的なメッセージの決定は `_handleAdminStatus` などの小さな関数に綺麗に委譲(スプリット)されました。

—

4. ユニットテストが劇的に書きやすくなる理由

このようにコードを分割する最大のメリットは、「テストの粒度が細かくなり、意図が明確になること」です。

テストコードを書いてみましょう。Flutter/Dartの標準テストフレームワーク (`test` パッケージ) を使います。

import ‘package:test/test.dart’;

void main() {
group(‘Dashboard Message Tests’, () {

// 小さく分割した関数ごとにテストを書けるので、網羅性が一気に高まる
group(‘_handleAdminStatus tests’, () {
test(‘activeな管理者のメッセージが正しく返ること’, () {
final result = _handleAdminStatus(‘active’);
expect(result, contains(‘本日のシステム稼働率は正常です’));
});

test(‘suspendedな管理者のメッセージが正しく返ること’, () {
final result = _handleAdminStatus(‘suspended’);
expect(result, contains(‘アカウントが一時停止されています’));
});
});

group(‘Main Routing tests’, () {
test(‘ログイン回数が10回を超えるアクティブユーザーの判定’, () {
const UserContext context = (role: ‘user’, status: ‘active’, loginCount: 15);
final result = getDashboardMessageRefactored(context);
expect(result, ‘いつもご利用ありがとうございます!’);
});
});
});
}

巨大な `Map` をモックしたり、複雑な前提条件を何行も書く必要がなくなりました。引数にタプル(レコード)をポーンと渡すだけで、関数単体の挙動をサクサクとテストできます。これが「テスト可能な単位への分割」の真髄です。

—

5. 初学者が陥りがちな文法エラーと注意点

ここで、Dartのパターンマッチングを使い始めた頃に誰もがハマりがちな「罠」をいくつかご紹介しておきますね。

① 网羅性(Exhaustiveness)のチェックに引っかかる

Dart 3のswitch式は非常に賢く、「すべてのパターンが網羅されているか」をコンパイル時にチェックします。もし、想定外の値が入りうるケースで `_`(デフォルトケース)を書き忘れると、次のようなコンパイルエラーになります。

> Error: The switch is missing some cases.

対策: 必ず最後の行には `_ => …` を置いて、予期せぬ値に対するフォールバックを用意する癖をつけましょう。

② 変数のバインド構文の書き間違い

パターンマッチング内で変数をキャプチャ(取得)するとき、`var s` や `final s` の書き順に戸惑うことがあります。

// ❌ エラーになりやすい書き方
(role: var String role) => …

// ⭕️ 正しい書き方(フィールド名にマッチさせて、値を変数にバインドする)
(role: ‘admin’, status: var s) => …

Dartのパターンでは、型を指定する場合は `String role` のように書き、型推論に任せる場合は `var s` のように書きます。この構文のルールは最初は少し奇妙に感じるかもしれませんが、慣れると手放せなくなります。

—

まとめ

今回は、Dart 3のパターンマッチングを活用して、複雑な条件分岐をテスト可能な小さな関数へと分割するテクニックを解説しました。

  • 巨大な `switch` や `if-else` のネストは、コードの認知負荷を高め、テストを困難にする。
  • Dart 3の `switch` 式とパターンマッチングを使うことで、データを美しく分解できる。
  • 条件分岐を「ルーティングの司令塔」と「具体的な処理を行う小さな純粋関数」に分割することで、ユニットテストが驚くほど書きやすくなる。

ここをクリアできれば、あなたの書くDartコードの品質はワンランクもツーランクも上がります。保守性が高く、変更に強いコードベースを一緒に作っていきましょう!

それでは、また次回の技術解説でお会いしましょう。バッチリマスターしてくださいね!

タイトルとURLをコピーしました