【実務・中級編】Null安全下での『DI(依存性の注入)』:Provider/RiverpodにおけるNull許容型の扱い – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Dartの型システムを飼い慣らせ:Sound Null Safety下での堅牢なDI設計

現場のコードレビューで「とりあえず `?` を付けておけばエラーは消える」という安易なNull許容型の乱用を見るたびに、私はDartの型システムが本来持っている「静的安全性」という強大な武器がドブに捨てられていると感じる。

特にProviderやRiverpodを用いたDI(依存性の注入)において、初期化タイミングの不確実性をNull許容型で誤魔化すのは、ランタイムでの予期せぬクラッシュを自ら招いているに等しい。今日は、DartのSound Null Safetyを最大限に活用し、コンパイル時に「絶対安全」を証明する設計の極意を伝授しよう。

—

1. 「遅延初期化」という名の逃げ道:`late` と Null許容型の境界線

多くの開発者が陥る罠は、DIコンテナ内での非同期初期化を `T?` で表現することだ。

// ❌ アンチパターン:Null許容型による状態管理
class AuthProvider extends ChangeNotifier {
User? user; // 使うたびに user! と書く羽目になる

Future init() async {
user = await authService.getCurrentUser();
}
}

このコードの何が問題か。`user` にアクセスするすべての場所で「本当に値が入っているのか?」を呼び出し側がケアしなければならない。これは型システムの敗北だ。

正解:型による状態の表現(States as Types)

DIコンテナでは「未初期化」「初期化済み」という状態を、Null許容型ではなく 「型そのものの階層」 で表現すべきだ。

// ✅ 推奨パターン:状態を型で封じ込める
sealed class AuthState {
const AuthState();
}

class AuthUninitialized extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
const AuthAuthenticated(this.user);
}

// Riverpodなら StateNotifier で管理
class AuthNotifier extends StateNotifier {
AuthNotifier() : super(AuthUninitialized());

Future init() async {
final user = await authService.getCurrentUser();
state = AuthAuthenticated(user);
}
}

このように設計すれば、UI層では `switch` 文による網羅的チェックが強制される。`null` チェックを忘れてクラッシュする余地は、コンパイラによって完全に排除される。

—

2. 非同期DIの「待ち」を型で安全にする

Webフロントエンドでは、APIのレスポンス待ちでDIコンテナが空になる瞬間がある。これを `late` で強引に解決しようとするのは非常に危険だ。`late` は初期化前にアクセスすると `LateInitializationError` を吐き、これは制御不能なランタイム例外となる。

代わりに、「依存の解決を待つための専用プロバイダー」 を構築せよ。

// API連携を伴うDIの堅牢な例
final apiClientProvider = Provider((ref) => ApiClient());

// 依存解決を待機するプロバイダー(AsyncValueを活用)
final userProvider = FutureProvider((ref) async {
final client = ref.watch(apiClientProvider);
return await client.fetchUser();
});

`FutureProvider` を使えば、`AsyncValue` が持つ `when` メソッドを通じて、`loading`, `error`, `data` の各状態を強制的にハンドリングできる。Dartの型システムが、ビジネスロジック以前に「データの不在」を処理するよう開発者を強制する。これこそが、大規模開発における保守性の正体だ。

—

3. コンパイル時最適化とパフォーマンスへの視点

Dart VMは、非Null型に対して最適化されたコードを生成する。`T?` を使った場合、VMは実行時にタグチェック(その値が `null` か否か)を行う必要がある。これは極めて微細だが、ホットパスにおいては無視できないコストになる。

逆に、`late final` や `non-nullable` な型で設計を固めれば、VMは不要な分岐を排除し、直接的なメモリアクセスが可能になる。「型を厳格にすることは、実行速度を速くすることと同義である」 という事実を忘れてはならない。

—

4. プロダクションコードにおける「禁じ手」

最後に、現場でよく見かける「やってはいけないこと」を記しておく。

1. `late` の無計画な使用: `late` は「初期化が完了していると確信できる場合」にのみ使う。DIコンテナの初期化フローにおいて、もし「初期化が完了しているか怪しい」なら、それは `late` ではなく `AsyncValue` 等で状態管理すべきだ。
2. `! `演算子の乱用: `!` は「ここには絶対値がある」というコンパイラへの命令だ。これを見たら「設計上の敗北」とみなせ。安全なコードであれば、`!` を書く必要は原則として存在しない。

まとめ:型は「守り」ではなく「攻め」の武器

多くの開発者は、Null安全を「クラッシュを防ぐための守りの制約」と捉えている。だが、真のシニアエンジニアにとって型システムは、「コードの意図をコンパイラに伝え、正当性を自動検証させるための攻めの武器」 だ。

DIコンテナを設計する際は、常にこう自問してほしい。
「このNull許容型は、状態遷移を表現するために必要か? それとも、ただ設計をサボっているだけではないか?」

Nullを追放し、型を厳格に定義した先にこそ、大規模プロダクトを支える静かなる堅牢性が宿る。Dartという言語の性能を100%引き出すための、洗練された設計を追求してほしい。

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