
層ごとにprovider置いたら参照違反になるの、なんでなん?💔
これ絶対あるじゃん??
- Clean Architecture + MVVMでフォルダ分けたのに、
UseCaseProviderがRepositoryProviderをimportしたら「application層からinfrastructure層への参照違反」って言われた - 仕方なくprovider定義をdomain層に移したら、今度は「domain層がRiverpodに依存してる」って言われた
- 「じゃあどこに置けばいいの??」ってなって全部
lib/直下にぶち込んだ - アーキテクチャ図はキレイなのに、importの矢印がぐちゃぐちゃ
これがCross-layer Provider Definition(provider定義の層またぎ)、以降「配線バラまき」って呼ぶね、が原因なんだよね。
てかシンプルに言うとね
provider(配線コード)を各層のフォルダの中に直接置いちゃったから、層の壁をぶち破るimportができちゃう。
「それの何がまずいの? 動いてるし」ってなる気持ち、わかる!
でもそのimport1本のせいで「層に分けた意味が全部消えてる」話、ちゃんとするね。
ちょっと待って、先に全然関係ない話していい?🏠
会社の話するね。
「みらい商事」っていう中堅の商社があったんだけどさ。ここ、部署間のルールがめっちゃ厳しかったの。
「部署同士は直接やり取りしない。必ず受付を通す。」
営業部は倉庫部の中身を知らないし、倉庫部は経理部のやり方を知らない。それぞれバラバラに動けるようにするためのルールなんだよね。
ただ、連絡先の管理でやらかしたの。
各部署が、他の部署の担当者の内線番号とか席の場所を、自分の部署のホワイトボードに直接書いてたんだよね。
営業部のホワイトボード:
- 「在庫確認 → 倉庫部の山田さん、2F奥の席、内線2345」
- 「商品スペック → 企画部の佐藤さん、3F会議室横、内線3456」
ある日、総務部長がこう言ったの。
「来月から倉庫部は4Fに移動します。内線番号も変わります。」
営業部の田中さんが真っ先に気づいた。
「え、うちのホワイトボードに『倉庫部は2F、内線2345』って書いてあるんだけど……」
田中さんは内線2345にかけた。繋がらない。席が変わって番号も変わったらしい。
「じゃあ新しい番号、どこで確認すんの?」
倉庫部に直接行って、向こうのホワイトボードを見るしかなかった。
ちょっと待って? 「部署同士は直接やり取りしない」ってルールだったよね? なのに新しい番号を知るために、倉庫部の中に入り込んでるじゃん。
ルール、ぶっ壊れてるんだよね。
しかもこれ、営業部だけの問題じゃなくて。経理部も、カスタマーサポートも、全部自分とこのホワイトボードに「倉庫部は2F」って書いてたの。移動のたびに全部署のホワイトボードを書き直すハメになったんだよね。
3日後、サポート部がお客さんに「在庫あります」って古い情報で回答しちゃってクレームが来た。経理は1週間後にやっと気づいた。
「連絡先を各部署のホワイトボードに書いてたから、こうなったんだ……」
これが本番で起きてるやつが、配線バラまきってこと。
じゃあコードで見てみよっか👀
Flutter + Riverpodで、Clean Architecture + MVVMをやってるプロジェクトを想像してみて。
// ❌ 各層にprovider定義を置いちゃったやつ
// ===== domain層 =====
// lib/domain/repositories/user_repository.dart
abstract class UserRepository {
Future<User> findById(String id);
}
// ===== infrastructure層 =====
// lib/infrastructure/repositories/user_repository_impl.dart
class UserRepositoryImpl implements UserRepository {
@override
Future<User> findById(String id) async {
final doc = await FirebaseFirestore.instance
.collection('users').doc(id).get();
return User.fromJson(doc.data()!);
}
}
// lib/infrastructure/providers/user_repository_provider.dart
// ↓ infrastructure層にprovider定義を置いた
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepositoryImpl();
});
// ===== application層 =====
// lib/application/usecases/get_user_usecase.dart
class GetUserUseCase {
final UserRepository repository;
GetUserUseCase(this.repository);
Future<User> execute(String id) => repository.findById(id);
}
// lib/application/providers/get_user_usecase_provider.dart
import '../../infrastructure/providers/user_repository_provider.dart';
// ↑ ここ!application層からinfrastructure層をimportしてる!
final getUserUseCaseProvider = Provider<GetUserUseCase>((ref) {
final repo = ref.watch(userRepositoryProvider);
return GetUserUseCase(repo);
});
見えた? application/providers/からinfrastructure/providers/へのimportが発生してるじゃん。
application層(ビジネスの判断をする場所)が、infrastructure層(FirestoreとかDBの実装がある場所)の中身を知っちゃってるの。
さっきのみらい商事でいうと、営業部のホワイトボードに「倉庫部の山田さん、2F、内線2345」って書いてある状態。連絡先を知るために、相手の部署に入り込んでるんだよね。
「いやproviderを層のフォルダに置くのが自然じゃない?」って思うよね。わたしも最初そう思った。でもこの「自然」が罠なんだよね。
やばいのまだあんだけど、バグに気づけない問題😭
配線バラまきの怖さって「参照違反になる」だけじゃなくて、**「正しいか確認する方法まで壊れてる」**のがほんとにやばいの。
さっきのみらい商事で考えてみて? 各部署のホワイトボードが正しいか、どうやって確認する?
- 営業部のボード → 倉庫部に行って照合
- 経理部のボード → 倉庫部に行って照合
- サポート部のボード → 倉庫部に行って照合
確認するためにルールを破らなきゃいけないじゃん。ちゃんとなってるか調べたいだけなのに、調べるにはルール破るしかないの。もう詰んでるんだよね。
コードでも同じなんだよね。
// テストでrepositoryを差し替えたい
// でもprovider定義がinfrastructure層にあるから...
// test/application/get_user_usecase_test.dart
import '../../infrastructure/providers/user_repository_provider.dart';
// ↑ テストコードまでinfrastructure層に依存してる
void main() {
test('ユーザーが見つかる', () {
final container = ProviderContainer(
overrides: [
// infrastructure層のproviderをoverrideしてるから
// infrastructureの中身が変わるとテストのimportまで壊れる
userRepositoryProvider.overrideWithValue(MockUserRepository()),
],
);
});
}
FirestoreからSupabaseに切り替えたとする。infrastructure/providers/のファイル名もクラス名も変わるよね。そしたらテストファイルのimportも全部書き直しじゃん。
配線バラまきは壊れやすいだけじゃなく、テストでも層のルールを破る設計ってこと。
分けてた会社の話もするね〜✨
同じ業界に「あおぞら商事」ってとこがあったんだけどさ。部署間のルールは同じく厳しいんだけど、連絡先の管理がまるで違ったの。
1Fの受付に「社内連絡先ノート」が1冊だけある。 各部署のホワイトボードには、連絡先は一切書いてない。
営業部が在庫を確認したいときは、受付に「在庫確認の担当者につないで」って言うだけ。受付がノート見て「倉庫部の山田さんですね、おつなぎします」ってなる。
営業部は山田さんが2Fにいるか4Fにいるかを知らなくていいの。
倉庫部が4Fに移動した日。受付がディレクトリの1行を書き換えた。それだけ。
営業部のホワイトボード? 何も変わってない。
経理部? 何も変わってない。
サポート部? 何も変わってない。
誰もルールを破らずに、全部署が正しい相手に繋がり続けたんだって。
コードに戻すとこういうことなんだよね。ちょっと見てみて?
あおぞら商事の「受付の連絡先ノート」= Composition Root。providerの定義をどの層にも属さない1箇所にまとめるの。
// ✅ Composition Rootパターン
// ===== domain層(変わらない) =====
// lib/domain/repositories/user_repository.dart
abstract class UserRepository {
Future<User> findById(String id);
}
// ===== application層(変わらない) =====
// lib/application/usecases/get_user_usecase.dart
class GetUserUseCase {
final UserRepository repository;
GetUserUseCase(this.repository);
Future<User> execute(String id) => repository.findById(id);
}
// ===== infrastructure層(変わらない) =====
// lib/infrastructure/repositories/user_repository_impl.dart
class UserRepositoryImpl implements UserRepository {
@override
Future<User> findById(String id) async {
final doc = await FirebaseFirestore.instance
.collection('users').doc(id).get();
return User.fromJson(doc.data()!);
}
}
// ===== Composition Root(どの層にも属さない) =====
// lib/providers/injection.dart
import '../domain/repositories/user_repository.dart';
import '../infrastructure/repositories/user_repository_impl.dart';
import '../application/usecases/get_user_usecase.dart';
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepositoryImpl();
});
final getUserUseCaseProvider = Provider<GetUserUseCase>((ref) {
final repo = ref.watch(userRepositoryProvider);
return GetUserUseCase(repo);
});
各層のフォルダにprovider定義は1個もないじゃん。lib/providers/injection.dartが「受付の連絡先ノート」として、全部の配線を1箇所で管理してるの。
どの層もComposition Rootをimportしてこないし、全ての層をimportしていいのはここだけなんだよね。だから参照違反にならない。
// テストもスッキリ
// test/application/get_user_usecase_test.dart
import '../../lib/providers/injection.dart';
// ↑ infrastructure層のimportは一切ない
void main() {
test('ユーザーが見つかる', () {
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(MockUserRepository()),
],
);
// FirestoreがSupabaseに変わっても、このテストは1行も変わらない
});
}
で、実務だとどういうパターンがあるの?🌸
「Composition Rootはわかった。でも実際のプロジェクトだとどうすんの?」ってなるよね。よくある3パターンを紹介するね。
パターン1:小〜中規模(1パッケージ)
一番シンプルなやつ。lib/providers/に全provider定義をまとめるだけ。
lib/
domain/ # インターフェース
application/ # ユースケース
infrastructure/ # 実装
presentation/ # UI(ViewModelとView)
providers/ # ← ここがComposition Root
injection.dart # 全providerを定義
ファイルが大きくなってきたらinjection.dartを分割してもOK。providers/auth_providers.dart、providers/user_providers.dartみたいにね。場所はproviders/の中。層のフォルダには絶対入れない。
パターン2:大規模(マルチパッケージ)
パッケージを分けてる場合、各パッケージはクラスだけ外から使えるようにして、配線はアプリ本体でやるの。
packages/
domain/ # UserRepositoryのインターフェースだけ公開
application/ # GetUserUseCaseだけ公開
infrastructure/ # UserRepositoryImplだけ公開
app/
lib/
providers/ # ← アプリ本体のComposition Root
injection.dart
各パッケージは他のパッケージのproviderを知らない。繋ぎ込みはアプリ本体の仕事ってこと。
パターン3:feature-firstフォルダ構成
「層じゃなくて機能で分けてるんだけど?」ってケースもあるよね。
lib/
features/
auth/
domain/
application/
infrastructure/
presentation/
user/
domain/
application/
infrastructure/
presentation/
providers/ # ← feature横断のComposition Root
auth_providers.dart
user_providers.dart
feature内にproviderフォルダを置きたくなるんだけど、そうするとfeature間の配線で同じ参照違反が起きる。だからproviderはfeatureの外に出すのが正解なんだよね。
ちゃんとした名前もあるから一応言うね📚
Composition Root(コンポジションルート)とは、「誰が誰を使うか」の配線を全部1箇所にまとめた場所っつーこと。
Mark Seemannって人が2011年の『Dependency Injection in .NET』でちゃんとまとめたやつ。2019年の第2版で.NET以外にも使える話に広げてる。
難しそうに聞こえるけど、要は「providerの定義、どの層のフォルダにも入れないで1箇所にまとめろ」を問い続けるだけなんだよね。
これだけ覚えて帰って🥺
合言葉はこれ。hbng(配線バラまきNG)
provider定義を書くとき、これだけ問いかけてみて。
「このprovider、別の層のproviderをimportしてない?」
たとえばgetUserUseCaseProviderを書くとき——
このファイル、application/フォルダにあるよね? で、userRepositoryProviderをimportしてる。そのprovider、infrastructure/フォルダにあるよね?
application → infrastructureのimportしちゃってるじゃん。 これ、営業部のホワイトボードに倉庫部の内線番号を直接書いてるのと同じじゃん。
「このprovider定義、今いるフォルダの層より外側のproviderをimportしてる?」——答えがYESなら、そのprovider定義はComposition Rootに移動するべきっつーこと。
まとめるとこういうことね
| 観点 | 配線バラまき(各層にprovider) | Composition Root(1箇所にまとめる) |
|---|---|---|
| provider定義の場所 | application層やinfrastructure層のフォルダ内 | どの層にも属さない専用フォルダ |
| importの方向 | application → infrastructure(参照違反) | Composition Root → 全層(唯一許可された方向) |
| 層の実装を差し替えたとき | 複数ファイルのimportを書き直す | Composition Rootの1ファイルだけ変える |
| テスト | テストコードまでinfrastructure層をimportする | Composition Rootだけimportすれば済む |
| 新メンバーの理解 | 配線があちこちに散らばって追えない | 1ファイル見れば全体の繋がりがわかる |
「このprovider定義、層をまたぐimportしてない?」——それを問い続けるだけで、参照違反は消えるっつーこと。
沼りたい人はこっちも読んで📖
権威ある書籍
- Mark Seemann『Dependency Injection in .NET』(2011) — Composition Rootの考え方をちゃんとまとめた最初の本
- Mark Seemann, Steven van Deursen『Dependency Injection: Principles, Practices, and Patterns』(2019, 第2版) — .NET以外にも使える形に広げた改訂版
- Robert C. Martin『Clean Architecture』(2017) — 層の向きとComposition Rootを含む設計全体の話
一次資料
- Mark Seemann blog — Composition Root: https://blog.ploeh.dk/2011/07/28/CompositionRoot/
- Wikipedia — Dependency injection: https://en.wikipedia.org/wiki/Dependency_injection
- Riverpod公式ドキュメント: https://riverpod.dev/
無料で読める入門記事
- Zenn・Qiitaで「Composition Root Flutter」「Riverpod Clean Architecture」で検索すると日本語の実装例が見つかる
Dev-Here「ギャルでも分かる設計の勘ドコロ!」シリーズ 第13回