3
1

More than 1 year has passed since last update.

CleanArchitecture入門

Last updated at Posted at 2023-06-15

CleanArchitectureについて学ぶ目的

  • 可読性の高いコードをかけるようにする
  • チーム内の共通認識として持てるような実装方針を習得する

要約

  • CleanArchitectureとは、関心の分離に着目したソフトウェアの設計手法のこと
    • アーキテクチャといいつつインフラ設計ではなくコードの詳細設計や実装に関するもの
  • CleanArchitectureの規約はコード全体を4つの階層に分けて依存関係の向きをビジネスロジックと遠いものから近いものへと統一させることだけ
  • ビジネスロジックとフレームワークを遠ざけることで変更に強く寿命が長いコードを目指す

CleanArchitectureとは

68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3239333336382f37636531666231302d353034652d313665302d383933302d3237386238613766393432642e6a706567.jfif
ソフトウェアの設計手法の一つで、規約は以下の一つのみ

まずアプリを以下の4つの階層に分ける

  • Enterprise Business Rules
  • Application Business Rules
  • Interface Adapters
  • Frameworks & Drivers

そして依存関係をFrameworks & DriversEnterprise Business Rules
という方向に統一する

例を出すと
:o:

  • Frameworks & DriversInterface Adapters
  • Interface AdaptersEnterprise Business Rules

:x:

  • Enterprise Business RulesApplication Business Rules
  • Application Business RulesFrameworks & 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を記述するなど

変更に強いコード

上記の通りレイヤーを切って実装すると以下のようになる
1.png
変更頻度が高いレイヤーほど他レイヤーから依存されにくく、飛行頻度が低いレイヤーほど他レイヤーから依存されやすい

ここで、Enterprise Business RulesFrameworks & Driversそれぞれが変更されたケースを考える

Enterprise Business Rules

Enterprise Business Rulesが変更されたとき、このレイヤーはApplication Business Rulesに依存されているためこちらも変更する必要がある
さらに、Application Business RulesInterface Adaptersに依存されており...と芋づる式に次々と変更の必要性が生じてしまい大変
しかし、そもそも変更頻度が一番低いレイヤーなので(正しく設計できていれば)このリスクは十分許容範囲内

Frameworks & Drivers

Frameworks & Driversはライブラリのバージョンアップやインフラの移行に伴うDBドライバの換装など変更頻度が非常に多い
しかしこのレイヤーは他レイヤーから依存されることは一切ないため、変更の影響範囲はこのレイヤー内で完結する

CleanArchitectureの規約を遵守し変更頻度が高いモジュールを外側のレイヤーに配置することで影響範囲を最小限度にとどめることができる
これにより変更に強く寿命が長いコードを書くことが可能になる

CleanArchitectureのTIPS

CleanArchitectureの規約を守るため、あるいはこのアーキテクチャと特に相性がいいために使われるあれこれをまとめる

CleanArchitectureの規約は依存関係の向きを統一させることだけ
これ以降の話はこの規約を達成させるための手法、あるいは単に相性のいいツール類の紹介であってこのアーキテクチャの本質ではないことに注意

DIP(依存関係逆転の原則)

Interfaceを介すことによってクラス間の依存関係を逆転させること

CleanArchitectureにおいては以下のような問題を解決するために用いられる
例えば以下のような依存関係を考える

ユーザーを新規作成するCreateUserUsecaseの依存関係を表しているが、CreateUserUsecase→UserRepositoryUserRepository→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によってインスタンス生成の手間とパフォーマンス低下を軽減しよう

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1