前提
本記事は、クリーンアーキテクチャとレイヤードアーキテクチャの基本的な設計思想は知っていることを前提としています。
分からない方は、参考資料 を読んでいただければと思います。
はじめに
アプリケーション開発のアーキテクチャにおいて、クリーンアーキテクチャで使われるSOLID原則の依存性逆転 (Dependency Inversion) は、特に大規模なシステムにおいて有用な設計パターンです。
しかし、必ずしもすべてのプロジェクトにおいて必要かというと、そうではないかもしれません。特にチーム規模が小さかったり、開発が集中している場合には、レイヤードアーキテクチャでも十分に機能する場合が多いです。
今回は、依存性逆転の必要性や、それがどのようにテストや開発の効率を改善するのかを、TypeScriptのコード例を交えて考えていこうと思います。
クリーンアーキテクチャ vs レイヤードアーキテクチャ
クリーンアーキテクチャとレイヤードアーキテクチャの違い
まず、クリーンアーキテクチャとレイヤードアーキテクチャの本質的な最大の違いは、依存性逆転 (Dependency Inversion) の有無です。
レイヤードアーキテクチャでは、各層が下方向に依存しており、UI層はアプリケーション層に、アプリケーション層はドメイン層およびインフラ層に依存します。これに対して、クリーンアーキテクチャでは、ドメイン層を中心に据え、アプリケーション層はドメインのビジネスルールを利用し、インフラ層はドメイン層が定義したインターフェースに依存します。
レイヤードアーキテクチャの依存関係
クリーンアーキテクチャの依存関係(依存性逆転)
依存性逆転のメリット
依存性逆転を採用する最大のメリットは、テストの柔軟性や並行開発の効率化にあります。開発者がフロントエンドとバックエンドで分業をしている場合、バックエンドの実装が進んでいなくても、フロントエンド側でテスト用のモックをDI(依存性注入)して開発を進めることができます。
また、リポジトリの実装が完了する前に、ユースケース層のテストを進めることができるため、単体テストが容易に行えるようになります。
依存性逆転のデメリット
- インターフェースの定義が増えるため、初期の実装コストが上がる
- 依存性注入の仕組み(DIコンテナなど)が必要になる
- 小規模なプロジェクトではオーバーエンジニアリングになる可能性がある
依存性逆転を使ったTypeScriptのテスト例
以下のコードは、依存性逆転を活用して、リポジトリの実装を差し替えてテストを行う例になります。
依存性逆転なしの場合
まず、依存性逆転がない場合のコードを示します。ここでは、直接的に具象クラスのリポジトリをDI(依存性注入)しているため、リポジトリ側の処理を書かないとユニットテストができません。
class UserRepository {
public getAllUsers() {
return ['Alice', 'Bob', 'Charlie'];
}
}
class UserService {
private userRepository: UserRepository;
// 具象クラスをDIさせている
constructor(userRepository: UserRepository) {
this.userRepository = userRepository;
}
public getUsers() {
return this.userRepository.getAllUsers();
}
}
// 実際のリポジトリを注入
const realRepository = new UserRepository();
const userService = new UserService(realRepository);
console.log(userService.getUsers()); // ['Alice', 'Bob', 'Charlie']
依存性逆転を使用した場合
依存性逆転を活用することで、リポジトリを差し替えてテストを行うことが可能になります。
以下のコードは、インターフェースを使ってリポジトリの実装をDI(依存性注入)する例です。
interface IUserRepository {
getAllUsers(): string[];
}
class UserRepository implements IUserRepository {
public getAllUsers() {
// 実際のデータベースアクセス処理はまだ未実装
return [];
}
}
class MockUserRepository implements IUserRepository {
public getAllUsers() {
// テスト用のモックデータ
return ['Test User 1', 'Test User 2'];
}
}
class UserService {
private userRepository: IUserRepository;
// インターフェースをDIさせている
constructor(userRepository: IUserRepository) {
this.userRepository = userRepository;
}
public getUsers() {
return this.userRepository.getAllUsers();
}
}
// テストコード
const mockRepository = new MockUserRepository();
const userService = new UserService(mockRepository);
console.log(userService.getUsers()); // ['Test User 1', 'Test User 2']
このように、UserService は IUserRepository に依存しており、テスト時にはモックリポジトリに差し替えることができます。
これにより、リポジトリ層の実装に依存せずに(実際の処理が未実装でも)、ユースケース層のテストを行うことができるようになります。
依存性逆転が不要なケース
依存性逆転は、並行開発やテストの効率化において大きなメリットを発揮しますが、すべてのプロジェクトに導入する必要があるわけではありません。
特に、以下のような場合にはレイヤードアーキテクチャでも問題ないことが多いです。
- 小規模なチームや単一の開発者が開発を行っている場合
- 開発が集中(APIとフロントエンドの開発者が同一など)していて、並行開発が少ない場合
- テストのカバレッジが十分に確保されている場合(E2EやFeatureテストで十分)
結論
依存性逆転は、並行開発やテストの柔軟性を向上させるために有効なパターンですが、すべてのプロジェクトで導入するべきというわけではありません。特に、小規模なチームや開発が集中している場合には、レイヤードアーキテクチャで十分なことが多いです。
大規模なシステムや分業が進んでいるプロジェクトでは、クリーンアーキテクチャと依存性逆転の導入が有益ですが、それ以外の場合はレイヤードアーキテクチャでも十分に機能するケースが多いと考えられます。
なので、自分たちのプロジェクトの規模・開発の進め方なども含めて、どのアーキテクチャを採用すべきかを検討して選定していくのが良いかと思います。