CleanArchitectureについて学ぶ目的
- 可読性の高いコードをかけるようにする
- チーム内の共通認識として持てるような実装方針を習得する
要約
- CleanArchitectureとは、関心の分離に着目したソフトウェアの設計手法のこと
- アーキテクチャといいつつインフラ設計ではなくコードの詳細設計や実装に関するもの
- CleanArchitectureの規約はコード全体を4つの階層に分けて依存関係の向きをビジネスロジックと遠いものから近いものへと統一させることだけ
- ビジネスロジックとフレームワークを遠ざけることで変更に強く寿命が長いコードを目指す
CleanArchitectureとは
まずアプリを以下の4つの階層に分ける
- Enterprise Business Rules
- Application Business Rules
- Interface Adapters
- Frameworks & Drivers
そして依存関係をFrameworks & Drivers→Enterprise Business Rules
という方向に統一する
例を出すと
- Frameworks & Drivers→Interface Adapters
- Interface Adapters→Enterprise Business Rules
- Enterprise Business Rules→Application Business Rules
- Application Business Rules→Frameworks & Drivers
各階層について
上で整理した階層についてそれぞれの役割を確認する
Enterprise Business Rules
CleanArchitectureの最も内部にあるレイヤー
主にモデルと呼ばれるUIやDB等と一切依存関係がないビジネスロジックをカプセル化したクラスを定義する
例
- WEBサービスのユーザークラス
- 名前、メールアドレス、年齢、名前のバリデートメソッドetc
export class User {
public name: string;
public age: number;
public email: string;
constructor(name: string, age: string, email: string) {
// init
}
public validateName(): boolean {
// validate
}
}
Application Business Rules
Enterprise Business Rulesの外側にあるレイヤー
ユースケースを書くことになる
ユースケースとはモデルを操作する一連の処理をまとめたクラスないし関数のこと
例
- ユーザーを新規作成するユースケース
- ユーザーのインスタンス生成→DBやファイルストレージへ保存→完了メッセージを返すなど
export class CreateUserUsecase {
private userRepository: UserRepository;
public execute() {
const user = new User();
this.userRepository.add(user);
}
}
ある処理をモデルに書くかユースケースに書くかの判断基準としては以下の通り
- UIやDBと依存しない→モデル
- UIやDBと依存する→ユースケース
ユーザーを新規作成する場合はそのユーザーを何らかのリポジトリに保存する必要がある=DBとの依存関係があるのでユースケース
Interface Adapters
Application Business Rulesの外側にあるレイヤー
クライアントやDBなどアプリの外側にあるものとアプリ本体との橋渡し役
HTTPリクエストやHTML Formイベントのバリデート、DBのレコードとプログラムのオブジェクトの相互変換など
また、ライブラリもFrameworks & Driversに置くことになるのでそれの抽象化をしたい場合もここ
export class UserRepository {
private datebase: Database;
public add(user: User) {
this.database.add(user);
this.database.flush();
}
}
Frameworks & Drivers
CleanArchitectureで一番外側にあるレイヤー
各種フレームワークやDBに関する処理を記述する
MySQLにコネクションを貼る、WEBフレームワークで仮想DOMを記述するなど
変更に強いコード
上記の通りレイヤーを切って実装すると以下のようになる
変更頻度が高いレイヤーほど他レイヤーから依存されにくく、飛行頻度が低いレイヤーほど他レイヤーから依存されやすい
ここで、Enterprise Business RulesとFrameworks & Driversそれぞれが変更されたケースを考える
Enterprise Business Rules
Enterprise Business Rulesが変更されたとき、このレイヤーはApplication Business Rulesに依存されているためこちらも変更する必要がある
さらに、Application Business RulesはInterface Adaptersに依存されており...と芋づる式に次々と変更の必要性が生じてしまい大変
しかし、そもそも変更頻度が一番低いレイヤーなので(正しく設計できていれば)このリスクは十分許容範囲内
Frameworks & Drivers
Frameworks & Driversはライブラリのバージョンアップやインフラの移行に伴うDBドライバの換装など変更頻度が非常に多い
しかしこのレイヤーは他レイヤーから依存されることは一切ないため、変更の影響範囲はこのレイヤー内で完結する
CleanArchitectureの規約を遵守し変更頻度が高いモジュールを外側のレイヤーに配置することで影響範囲を最小限度にとどめることができる
これにより変更に強く寿命が長いコードを書くことが可能になる
CleanArchitectureのTIPS
CleanArchitectureの規約を守るため、あるいはこのアーキテクチャと特に相性がいいために使われるあれこれをまとめる
CleanArchitectureの規約は依存関係の向きを統一させることだけ
これ以降の話はこの規約を達成させるための手法、あるいは単に相性のいいツール類の紹介であってこのアーキテクチャの本質ではないことに注意
DIP(依存関係逆転の原則)
Interfaceを介すことによってクラス間の依存関係を逆転させること
CleanArchitectureにおいては以下のような問題を解決するために用いられる
例えば以下のような依存関係を考える
ユーザーを新規作成するCreateUserUsecaseの依存関係を表しているが、CreateUserUsecase→UserRepository
とUserRepository→TypeOrm
の依存関係がCLeanArchitectureの規約違反となってしまっている
このように規約違反となる依存関係にせざるを得ない問題を以下のようにinterfaceを介して解決する
このように依存先をクラス本体ではなくインターフェースにすることで自身の外側ではなく同じレイヤーに依存させることができCleanArchitectureの規約を遵守することができる
外側のレイヤーに依存せざるを得ない場合には同レイヤーにインターフェースを切ってそれに依存させ、外側のレイヤーでそのインターフェースを実装したクラスを書くようにして解決する
DI(依存性注入)
あるプログラムが別のプログラムに依存している状態を切り離し、外部からその依存性を注入すること
例えば以下のようなコード
class CreateUserUsecase {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
public execute() {
this.userRepository.add(new User());
}
}
このコードではクラスのコンストラクタ内で依存関係のあるUserRepository
のインスタンスを生成している
このような依存のさせ方では外部から依存関係のあるインスタンスを注入することができないため単体テスト等が非常に面倒になってしまう
そのため以下のように書き換える
class CreateUserRepository {
private userRepository: IUserRepository;
constructor(userRepository: IUserRepository) {
this.userRepository = userRepository;
}
public execute() {
this.userRepository.add(new User());
}
}
こうすることでこのクラスを初期化する際に依存するモジュールを外部から注入することができるようになる
// 通常の使い方
const createUserRepository = new CreateUserRepository(new UserRepoisotry());
// UTでモックを突っ込んでしまう
const createUserRepository = new CreateUserRepository({add: addMock});
tsyringe
TypeScriptの場合tsyringeというライブラリを使うことでDIを実現できる
DIコンテナと呼ばれるクラスに対して依存性を登録し、依存性の注入対象をアノテーションで指定することによって自動的に依存関係を解決してくれる
import { container } from 'tsyringe';
// コンテナに依存性を登録
container.register('UserRepository', { useClass: UserRepository });
// 依存関係を解決
const usecase = container.resolve(CreateUserUsecase);
import { injectable, inject } from 'tsyringe';
@injectable() // 依存性を注入する対象であることを示す
class CreateUserUsecase {
constructor(
@inject('UserRepository') // 依存性をこのフィールドに注入
private userRepository: UserRepository
) {}
public execute() {
this.userRepository.add(new User());
}
}
CleanArchitectureに則ったプログラミングではどうしてもファイル数、クラス数が嵩むのでDIによってインスタンス生成の手間とパフォーマンス低下を軽減しよう