ソフトウェア開発をしていると、ある部分を変更したら他の箇所がすべて壊れたという経験は、誰でも一度はあるのではないでしょうか。
これは**高い結合度(Coupling)**の典型的な問題です。保守が難しくなり、機能変更に伴うリスクが大きくなります。
では、カップリングを減らして構造的な設計を適用するにはどうすればよいのでしょうか?
カップリングとは?#
カップリング(Coupling)とは、モジュールやクラスがどの程度強く依存し合っているかを示す結合度のことです。
- 高い結合度:相互依存性が大きく、一つを変えると他の部分に連鎖的な影響
- 低い結合度:役割と責任がよく分離されていて、変更の影響を最小化
結局、目指すべき方向は「高い凝集度、低い結合度」を持つコードを書くことです。
凝集度とは、クラスやモジュール内部の構成要素が一つの目的にのみ集中している度合いを意味します。
カップリングの問題点
- 保守の難易度上昇
- 変更範囲の拡大
- テストとデプロイでの不安定性増加
この問題を解決するためにアーキテクチャを導入する理由が生まれます。
アーキテクチャは単にコード構造をきれいにすることではなく、変更に強いシステムを作るための約束事だからです。
カップリングの確認方法#
- 依存関係グラフ分析:クラスやモジュール間の接続性を視覚化
- コードレビュー & 静的解析ツールの活用
- **デメテルの法則(Law of Demeter)**の点検:オブジェクトの内部構造を外部に公開せずカプセル化を維持
SOLID原則とDI#
アーキテクチャの根幹にはSOLID原則があります。
その中でもSRP(単一責任の原則)とIoC(制御の反転)、**DI(依存性注入)**は結合度を下げる核心的な要素です。
DIには3つの方法があります:
- コンストラクタ注入(DI via Constructor) – 最も安全で推奨される方式
- Setterメソッド注入 – ランタイム時にエラーの可能性あり
- フィールド注入(Class field) – テストと保守が困難
コンストラクタDIの例
class SomeService {
final SomeDependency dependency;
SomeService(this.dependency);
}Setter DI
class SomeService {
SomeDependency? dependency;
set setDependency(SomeDependency value) => dependency = value;
}ServiceLocator (get_itパッケージの例)
import 'package:get_it/get_it.dart';
final getIt = GetIt.instance;
void setup() {
getIt.registerSingleton<SomeService>(SomeService(SomeDependency()));
}
// 使用
final service = getIt<SomeService>();コンストラクタDIを優先的に使用し、必要に応じてSetterやServiceLocatorを使用できます。
外部オブジェクトを直接生成せず、外部から受け取って使う習慣だけでもカップリングは大幅に減ります。
役割と責任の分離#
アーキテクチャ適用の核心は**役割(Role)と責任(Responsibility)**を区別することです。
- 役割:何をすべきか(抽象化)
- 責任:どのように行うか(具体化)
役割を分けずにすべての責任を一つのクラスに詰め込むと、変更の影響が大きくなり再利用性も低下します。
良い協力と悪い協力#
コード上では協力の方式によっても結合度の差が生まれます。
- 良い協力:Tell、Unidirectional、Weak
- 状態を取得するのではなく命令する(Tell)
- 単方向の依存性を維持
- 変更への影響を最小化する弱い依存性
- 悪い協力:Ask、Bidirectional、Strong
- 状態に直接アクセス(Ask)→ カプセル化の崩壊
- 双方向依存 → スパゲッティコードの原因
- 強い依存性 → 変更時に連鎖的なエラー発生
Flutterでのアーキテクチャ適用#
Flutter開発では関心の分離を通じて、主要領域をPresentation、Business Logic、Data Layerに区分することがあります。
- Presentation:画面(UI)とユーザー入力の処理
- Business Logic:計算とドメインルールの適用
- Data Layer:API、DB、キャッシングなどのデータ保存・取得
例えばControllerクラスに入力処理とデータロードをすべて入れると、巨大なクラスになりカップリングが深刻になります。
これを分割してレイヤーの役割ごとに分離することが、Feature-Firstアーキテクチャの核心です。
Feature-Firstアーキテクチャの例#
各機能ごとに以下のようにレイヤー分離を適用します。
lib/
├── features/
│ └── product/
│ ├── domain/
│ │ ├── entities/
│ │ └── usecases/
│ ├── data/
│ │ ├── models/
│ │ └── repositories/
│ ├── presentation/
│ │ ├── screens/
│ │ └── widgets/このように分離すれば各機能ごとに独立した開発・テストが可能になります。
データ受け渡しパターン#
Flutterでページ間のデータ受け渡しには2つの方式があります。
- extraでオブジェクトを渡す – 画面遷移時に直接データを渡す
- 利点:すぐに使用可能
- 欠点:存在しない、または空の値の場合に例外処理が必要
- 識別値を渡してデータを取得 – 次のページでIDで再取得
- 利点:フローが明確でデータの整合性を確保
- 欠点:追加のAPIコールが必要
アーキテクチャ評価:C4モデル#
C4モデルはシステムを4つのレベルに分けて視覚的に分析します。
- Context – システムと外部環境の相互作用
- Container – 実行可能な単位に分離
- Component – コンテナ内部の構成要素
- Code – 実際のクラスと関数レベル
これにより依存関係、責任範囲を視覚的に分析しながら、アーキテクチャの品質を定量的に評価できます。
Anemic Domain Modelからの脱出#
ビジネスロジックのないオブジェクトは貧血ドメインモデルです。これを防ぐには:
- Domainレイヤー:Entity、Value Object、Domain Eventなどの概念とルールの定義
- Applicationレイヤー:ユースケースの実行、ドメインと外部の接続
- Persistenceレイヤー:DB保存と取得
- Infrastructureレイヤー:外部SDK、メッセージング、エラーレポート
このように水平的(役割別)・垂直的(機能別)の分離を併用すれば、クリーンアーキテクチャを維持できます。
Wisdom is knowing what to do next; Skill is knowing how ot do it, and Virtue is doing it.
— David Jordan