クリーンアーキテクチャとは?【ディレクトリ構成・コード例付き】

クリーンアーキテクチャとは?【ディレクトリ構成・コード例付き】

クリーンアーキテクチャとは?

クリーンアーキテクチャとは、ソフトウェア開発におけるアーキテクチャ設計の一つで、コードの保守性や拡張性を向上させるための原則や構造を提供しています。

このアーキテクチャには核となる考え方があり、「依存性の方向を内側に限定すること」です。

具体的には、ビジネスロジック(アプリケーションの中心的な部分)が外部(データベース、UI、フレームワークなど)に依存せず、逆に詳細がビジネスロジックに依存する構造を作ります。

これを視覚化したものが有名な「同心円」の図であり、アーキテクチャを複数の層(レイヤー)に分割します。

クリーンアーキテクチャ
  • エンティティ(Entities)
    • ビジネスルールを表現する層。アプリケーションの中心で、最も安定した部分。
  • ユースケース(Use Cases)
    • アプリケーションの具体的な操作や振る舞いを管理する層。
  • インターフェースアダプター(Interface Adapters)
    • データをユースケースやエンティティが理解できる形に変換する層。
  • 外部フレームワーク・データベース(Frameworks and Drivers)
    • データベースやUIなど、外部の技術的な詳細を担当する層。

これらの層は明確に分離され、依存関係は内側に向かってのみ許されるため、システム全体が柔軟で変更に強い設計になります。

依存関係は内側に向かってのみ許されるってどう言うこと?

具体的に説明すると、先ほど説明したそれぞれの層が以下のようなルールに従います。

  • 内側の層(エンティティやユースケース)
    • ここには、システムのコアな部分(ビジネスロジックやドメインルール)が含まれます。
    • 外部の技術や詳細に依存しない純粋なロジックを保つことが目的です。
  • 外側の層(UIやデータベースなど)
    • これらは、システムのコアである内側の層に依存します。つまり、UIやデータベースのような詳細は、ビジネスロジックに合わせて設計され、内側の層を呼び出す形で動作します。

つまり、外側の層(UIやデータベース)は内側の層に依存しますが、逆に内側の層が外側の詳細に依存することはありません。

これにより、データベースやUIフレームワークなどを変更しても、内側のビジネスロジックには影響を与えずに済むようになります。

このように、依存の方向は常に内側にあるということです。

クリーンアーキテクチャの分かりやすい例え話

たとえば、レストランを運営するアプリケーションを考えてみましょう。

  • エンティティ(内側)
    • メニュー、注文、顧客などの概念やルールを定義します。
    • 例:顧客が注文できるのは営業中のみ
  • ユースケース(内側)
    • 顧客が注文を行う処理や、注文をキャンセルする操作。
  • 外側の層(外側)
    • UI(アプリの画面)やデータベース(注文データを保存する仕組み)は、ユースケースが提供する機能を使って動作します。

UIを新しい技術(ReactからVueに変更など)に置き換えても、注文処理やメニューの管理ロジックはそのまま利用可能です。

これが「変更に強い設計」につながります。

「内側への依存」によって、技術の進化や要件変更に柔軟に対応できるのがクリーンアーキテクチャの強みとなっています。

クリーンアーキテクチャのメリット

クリーンアーキテクチャのメリットを4つ紹介します。

1. 保守性が高い

  • 各層が独立しているため、ある層に変更を加えても他の層に影響を与えにくい。
  • ビジネスロジックは外部の技術に依存しないため、UIやデータベースを変更しても影響を最小限に抑えられる。

2. 再利用性が高い

  • コアとなるビジネスロジック(エンティティやユースケース)は他のプロジェクトでも使い回し可能。

3. テストが容易

  • 各層が独立しているため、モックを利用してビジネスロジックやユースケースを単体テストしやすい。

4. 拡張性が高い

  • 外部システムや技術を差し替える場合でも、コア部分を変更せずに対応可能。

クリーンアーキテクチャのデメリット

クリーンアーキテクチャのデメリットを3つ紹介します。

1. 初期コストが高い

  • 複数の層に分けて設計するため、学習コストや開発の初期段階での工数が増える。
  • 小規模なプロジェクトでは過剰設計になることもある。

2. 設計を守る運用コスト

  • チーム全体でアーキテクチャの意図を理解し、一貫性を保つためのコミュニケーションが必要になる。
  • 層の境界を曖昧にすると、意図しない依存が生まれるリスクがある。

3. 実装の複雑さ

  • 各層間のインターフェースや依存関係を適切に管理するためのコード量が増加する。

クリーンアーキテクチャの採用基準

クリーンアーキテクチャは、小規模なプロジェクトや短期間で使い捨てる予定のコードには、過剰な設計になりやすいため不向きです。

中〜大規模なプロジェクトや、長期間にわたって保守する必要があるプロジェクトに向いています。

また、以下のようなケースで特に効果を発揮するでしょう。

  • 頻繁にUIやデータベースなどの外部要素が変更される可能性が高い
  • コードのテストを重視している
  • 他のプロジェクトでコアなビジネスロジックを再利用したい

クリーンアーキテクチャのディレクトリ構成

Flutterでクリーンアーキテクチャを実装する場合、以下のようなディレクトリ構成となります。

lib/
|-- core/              # アプリ全体で使用する共通機能やユーティリティ
|   |-- network/       # ネットワーク関連の処理
|   |-- error/         # エラーハンドリングクラス
|   |-- usecase.dart   # ユースケースのベースクラス
|
|-- features/          # 機能ごとのフォルダ
|   |-- auth/          # 認証機能
|   |   |-- data/      # データ層
|   |   |   |-- datasources/   # データソース(APIやローカルDB)
|   |   |   |-- models/        # モデルクラス
|   |   |   |-- repositories/  # リポジトリの実装
|   |   |-- domain/    # ドメイン層
|   |   |   |-- entities/      # エンティティ(ビジネスルール)
|   |   |   |-- repositories/  # 抽象リポジトリ
|   |   |   |-- usecases/      # ユースケースクラス
|   |   |-- presentation/      # UI層
|   |       |-- bloc/          # 状態管理(例: Blocパターン)
|   |       |-- pages/         # 画面ウィジェット
|   |       |-- widgets/       # 小さなウィジェット
|
|-- injection_container.dart   # 依存性注入(DI)
|-- main.dart                  # エントリーポイント

クリーンアーキテクチャのコード例

以下は、認証機能(auth)を例にしたクリーンアーキテクチャの実装例です。

1. Domain層

エンティティ: lib/features/auth/domain/entities/user.dart

class User {
  final String id;
  final String name;

  User({required this.id, required this.name});
}

ユースケース : lib/features/auth/domain/usecases/login_usecase.dart

import '../repositories/auth_repository.dart';
import '../entities/user.dart';

class LoginUseCase {
  final AuthRepository repository;

  LoginUseCase(this.repository);

  Future<User> call(String email, String password) async {
    return await repository.login(email, password);
  }
}

リポジトリのインターフェース : lib/features/auth/domain/repositories/auth_repository.dart

import '../entities/user.dart';

abstract class AuthRepository {
  Future<User> login(String email, String password);
}

2. Data層

データソース : lib/features/auth/data/datasources/auth_remote_data_source.dart

import '../models/user_model.dart';

abstract class AuthRemoteDataSource {
  Future<UserModel> login(String email, String password);
}

class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  @override
  Future<UserModel> login(String email, String password) async {
    // APIリクエストの模擬
    return UserModel(id: '123', name: 'John Doe');
  }
}

モデル : lib/features/auth/data/models/user_model.dart

import '../../domain/entities/user.dart';

class UserModel extends User {
  UserModel({required String id, required String name})
      : super(id: id, name: name);
}

リポジトリ実装 : lib/features/auth/data/repositories/auth_repository_impl.dart

import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../datasources/auth_remote_data_source.dart';

class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remoteDataSource;

  AuthRepositoryImpl(this.remoteDataSource);

  @override
  Future<User> login(String email, String password) async {
    return await remoteDataSource.login(email, password);
  }
}

3. Presentation層

Bloc(状態管理): lib/features/auth/presentation/bloc/auth_bloc.dart

import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/login_usecase.dart';

class AuthBloc extends Cubit<void> {
  final LoginUseCase loginUseCase;

  AuthBloc(this.loginUseCase) : super(null);

  Future<void> login(String email, String password) async {
    final user = await loginUseCase(email, password);
    print('User logged in: ${user.name}');
  }
}

まとめ

クリーンアーキテクチャは、メリットでも紹介した通り、拡張性を高める強力な設計手法です。

ただその一方で、実装には初期コストや運用コストがかかるため、プロジェクト規模や目的に応じて適切に採用していきましょう。

僕自身もまだ使い慣れないので、小規模なプロジェクトで試しながら、段階的に理解を深めていきたいです。

Anycloudではプロダクト開発の支援を行っています

プロダクト開発をお考えの方はぜひAnycloudにご相談ください。

まずは相談する

記事を書いた人

Matsuura

エンジニア

Matsuura

Twitter

Anycloudでエンジニアしてます!主にFlutterやWebフロントをやっていて、最近はバックエンドやインフラに挑戦・苦戦中。