⚖️

Clean-ish アーキテクチャ: Flutter×Firebaseで0→1を最速に、1→10を伸ばす3.5層設計!

に公開

🔥 「Clean-ish」アーキテクチャを提唱したい!

私はクリーンアーキテクチャやDDDが好きでよく個人開発などでも利用しています。
(完全に理解しているかと言われる危うい😓)

ただし、ふと思いました。
「(個人開発や小規模PJにおいて)こんなにクリーンを保つ必要があるのか?コードやアーキ特有のルールを増やして自分の負担でかくなってるだけなんじゃないか?」

そこで、クリーンさ、拡張性は残しつつも初期段階で爆速でアプリをリリースするためのモバイルアプリアーキテクチャ。
「Clean-ishアーキテクチャ(クリーンっぽいアーキテクチャ)」を考えました!

🏁 この記事で扱うもの

項目 内容
👤想定読者 Flutter と Firebase で MVP/個人開発を爆速で作りたい人
🏢規模感 1~3 名、ローンチ初期/PMF 探索中
🎯ゴール 最速で仮説検証 → 当たった後もリファクタしやすい プロジェクト構造を掴む

※PMF = Product Market Fitの略

🔎 なぜFlutter × Firebaseに向いていると思うか

そもそもFlutterとFirebaseを技術選定する場合のメリットは以下だと思います。

  • UI爆速(Web,iOS,Android)のクロスプラットフォームである。
  • サーバーレス爆速(サーバーの用意など不要)である。

そしてこのメリットが最も刺さるフェーズが以下だと思います。

  • ビジネス初期段階(スタートアップや、サービス検証フェーズ)
  • 個人開発
  • (小中規模アプリ)

この場合、ビジネスの転換などによってドメインのルールが変わる、UI/UX検証によって画面構成が変わる、などさまざまな変更が考えられます。
そのため、変更容易性を担保するために疎結合とするクリーンアーキテクチャが良い、と私は考えていたのですが、、、
なんにせよコード量(ファイル数)の増加や、クリーンアーキテクチャのルールに沿っているかを考える時間ネックでした。

そこで考えたのが
Clean-ishアーキテクチャです。

💎 Clean-ishアーキテクチャとは?

このアーキテクチャの思想は以下です。

クリーンさと初速の“いいとこ取り” を目指す 3.0〜3.5 層構成。
0→1 を 最短リードタイム で倒し、1→10 で 必要箇所だけクリーン度を濃く していく。

概念図

ディレクトリ構成

lib/
├─ data/  # Infrastructureでも可
│   └─ repositories/
│       └─ user_repository_impl.dart  # 実装クラス
├─ domain/  # Application + light Domain
│   ├─ models/  # Entity(Freezedで定義)
│   ├─ repositories/  # repositoryのinterface
│   └─ usecases/  # UserUseCase など
└─ presentation/
    ├─ view/
    └─ viewmodel/
レイヤ 0→1 で入れるもの 1→10 で追加するもの
presentation View / ViewModel Widget 拡張・UIテスト
domain Entity (Freezed) / UseCase / Repository interface Domain Service / パッケージ分割
data 最小 Repository Firestore impl LocalDB/別バックエンド impl

※repositoryの実装クラスは「infrastructure層」に置くプロジェクトも見かけます。

📝 ディレクトリ別 “なぜ作るのか?” メモ

Clean-ish のキモは “理由がある最小構成”。
各フォルダに置く意義を明文化しておくと、あとでチームメンバーが増えても迷いません。

domain/models ― Entity

お馴染みの(?)Freezedを使ったEntityです。


class User with _$User {
  const factory User({
    required String id,
    required String name,
    required int point,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);
}

❓なぜentityは作成するのか?

  • 型安全 & fromJson/toJson 自動生成
  • プロジェクト初期(小規模PJなら初期に限った話ではないかも?)はFirestoreのスキーマ=アプリ内でそのまま扱える場合が多いため、dtoなどは作成しない。
  • factoryを使えばドメインルールの記載も可能(clean-ishアーキテクチャでは必須ではないと考える)
    • 1→10でDDDっぽく倒すならvalue objectやドメインサービスに切り出すことも可能

domain/usecases ― UseCase

class UserUseCase {
  UserUseCase(this._repo);
  final UserRepository _repo;

  Future<User> createUser(String name) async {
    final user = User(id: uuid, name: name, point: 0);
    final res = await _repo.create(user);
    return res;
  }
}

❓なぜUseCaseは作成するのか?

  • viewModelで直接repositoryを呼ぶとviewModelが肥大化する
  • UI変更や画面に左右されずに再利用できる
    • 初期フェーズでは特に検証次第でUI/UXが変わる場面が多い

domain/repositories ― interface

abstract class UserRepository {
  Future<User> read(String id);
  Future<User> create(User user);
}

❓なぜrepository interfaceは作成するのか?

  • 今後の別 DB/API への 差し替えコストを下げる (実装クラスを替えるだけ)
    • interfaceの作成だけならそこまで手間ではないと考えている
  • テストの際にもDIしやすい

data/repositories ― impl

初期は、Firestoreを直書きするこのクラスだけで十分!
本来であればFirebaseFirestore.instanceはDIした方がテストなどは楽ですが、初期フェーズなので無視しています。
テストも最初から行うPJであれば、DIした方がいいと思います。

class UserRepositoryImpl implements UserRepository {
  final _col = FirebaseFirestore.instance.collection('users');

  
  Future<User> read(String id) async {
    final d = await _col.doc(id).get();
    return User.fromJson(d.data()!).copyWith(id: d.id);
  }

  
  Future<User> create(User user) => _col.doc(user.id).set(user.toJson());
}

❓なぜrepository実装クラスは作成するのか?

  • インフラ依存 (Firestore SDK) を domain から隔離
  • 実装ファイル単位で Supabase 版/LocalDB 版 に差し替えやすい
  • View 層は UserRepository しか知らない=依存の向きが守れる

🔑 Clean-ish を支える “3本柱”

せっかくの提唱(?)なので、かっこいい名称にしてみたw

一言で 実装イメージ
1. Velocity First
「まず走る」
Entity+UseCase+最小 Repo interface だけ置き、残りは直書き。 Freezed 即生成/Firestore 直書きで UX 検証を即日回す
2. Intentional Seams
「替えどころを 1 枚残す」
差し替え・モックが必要になり得る“未来の急所”にだけ interface を挟む。 UserRepository だけ DI で切替、他は YAGNI
3. Progressive Hardening
「育ってから固める」
複雑さが顕在化した箇所にだけレイヤ追加。 ロジック重複→Domain Service昇格/オフライン要求→LocalDataSource追加

✅ まとめ

  1. Entity+UseCase+最小 Repo interface だけで “走りながら守る” 土台ができる。
  2. 差し替えポイント を 1 枚仕込めば、当たった後の方向転換も怖くない。
  3. “クリーンっぽい” だけど プロトタイプ→スケール が滑らか、それが Clean-ish。

初速は殺したくない、初期リリースまで時間がない。。。
でも、後から改修が大変になるのは避けたい。。。

そんな折衷案が欲しい時は、ぜひClean-ishアーキテクチャを試してみてください。

ご意見や質問、実際に使ってみた!などコメント欄でお待ちしております!😆
(今後このアーキテクチャに関してはアップデートしていきたいと思います!)

Discussion