1. Top
  2. Clean Architecture を学習した

Clean Architecture を学習した

2024/08/02

Clean Architecture を読んで学習したので、個人的な備忘録として記録を残します。
設計を学習する入門として、とても良い本だと感じました。

また、kansai.ts というイベントで登壇した際にスライド形式でお話もしているので、よければそちらもご覧ください。

https://speakerdeck.com/ryounasso/clean-architecture-by-typescript-and-nestjs

目次

Clean Architecture

Clean Architecture は、ビジネスルールを中心に置き、フレームワークやデータベースなどの外部要素に依存しないように設計することを目指す設計手法です。そして、ソフトウェアをレイヤーに分割することで関心の分離を図ります。

ビジネスルールは、ソフトウェアシステムが存在する理由であり、中心的な機能です。周辺の関心事の変更によってビジネスルールが不必要に変更されることは望ましくありません。そしてソフトウェアは、ステークホルダーの要望に合わせて振る舞いを簡単に変更できる状態であるべきです。この状態の構築に役立つのがソフトウェアアーキテクチャであり、その一つがこの Clean Architecture です。

Clean Architecture でよく見るのが以下の図だと思います。

clean architecture
The Clean Architecture

このアーキテクチャのそれぞれのレイヤーには以下のような役割があります。

  • Enterprise Business Rules
    Entities と呼ばれる、ビジネスルールをカプセル化したものが存在するレイヤー
  • Application Business Rules
    システムのユースケースがカプセル化・実装されているレイヤー
  • Interface Adapters
    円の内側に便利な形から、Web や DB などに便利な形に変換するレイヤー
  • Frameworks & Drivers
    フレームワークやツールで構成されるレイヤー

本では 4 つのレイヤーで構成されていますが、必ずしも 4 つでなくても良いとのことです。

このアーキテクチャにおいて最も重要なルールは、依存関係の方向 です。ソースコードの依存性は、内側だけに向かっている必要があり、円の内側は外側について何も知らない状態であるべきです。

そして、各レイヤーをどのようにして超えるべきかは、画像の右下で表現されています。この図ではある処理の一連の流れを表現しています。処理の中で Use Case Interactor がレイヤーを超えるために、依存関係逆転の原則 (DIP) を用いています。interface を定義して、境界を超えるところでソースコードの依存関係と処理の流れを逆転させています。

Todo アプリで実践

Clean Architecture の概念を理解しましたが、実際に実装に落とし込むところにはまた違った難しさがあります。
そこで、Todo アプリを作成して Clean Architecture を実践してみました。
使用した技術は以下の通りです。選定に深い意味はありません。

  • TypeScript
  • NestJS
  • Prisma

リポジトリはこちらです。

https://github.com/ryounasso/todo-app-by-clean-architecture

実装の全体像を、クリーンアーキテクチャの書籍に掲載されている Java システムの典型的なシナリオに当てはめると以下のようになるかなと思います。

System_overall

実装詳細

Entity

※ contructor, getter は省略

export class Todo {
  private id: number;
  private title: string;
  private status: string;
  private userId: number;
  private createdAt: Date;
  private finishedAt: Date | null;

  public updateTitle(newTitle: string): void {
    if (newTitle.length <= 0) {
      throw new Error("title should not empty");
    }
    this.title = newTitle;
  }

  public start(): void {
    this.status = "doing";
  }

  public done(): void {
    this.status = "done";
    this.finishedAt = new Date();
  }
}

UseCase

export interface TodoService {
  getTodoList(
    userId: number,
    fields?: string[],
    exclude_done_todo?: boolean
  ): Promise<TodoListDto>;

  addTodo(addTodoDto: AddTodoDto): Promise<TodoDto>;

  updateTitle(id: number, newTitle: string): Promise<TodoTitleDto>;

  startTodo(id: number): Promise<StartDto>;

  done(id: number): Promise<DoneDto>;
}

Interface

Controller
@Controller("todo")
export class PostController {
  constructor(
    @Inject("TodoService") private readonly todoService: TodoService
  ) {}

  @Post("start.json")
  @UsePipes(new ValidationPipe({ transform: true }))
  async run(@Body() body: UpdateInputForm): Promise<StartDto> {
    return await this.todoService.startTodo(Number.parseInt(body.getId()));
  }

  @Post("done.json")
  @UsePipes(new ValidationPipe({ transform: true }))
  async done(@Body() body: UpdateInputForm): Promise<DoneDto> {
    return await this.todoService.done(Number.parseInt(body.getId()));
  }
}
Repository
export interface TodoRepository {
  findById(id: number): Promise<Todo>;

  findTodoList(userId: number): Promise<Todo[]>;

  findTodoListExcludeDone(userId: number): Promise<Todo[]>;

  insert(todo: AddTodoDto): Promise<Todo>;

  update(todo: Todo): Promise<Todo>;
}

このアプリで守りたいユースケースは、Entity と UseCase に記述されています。
Usecase 層に作成した TodoService を見ると分かるように、このアプリケーションでできることは以下の通りです。

  • Todo の一覧取得
  • Todo の追加
  • Todo のタイトル変更
  • Todo の開始
  • Todo の完了

実装振り返り

実際に実装してみて、以下のようなメリットがあると感じました。

  • ビジネスルールが周辺の関心事に引っ張られない
    例えば、DB の変更があっても、ビジネスルールに変更がない場合は、ビジネスルールに変更を加える必要がない
  • テストがしやすい
  • 各レイヤーの意図を明確にして、それらの間に境界を設けて管理することで、コードの可動性が向上する

実装していく中で、何度か立ち止まって考えた箇所がありました。

Todo 一覧の取得

このアプリケーションの Todo は、Entity を見る限り以下の情報を持っています。

  • ID
  • 名前
  • ステータス
  • ユーザー ID (これは必要なかったです :( )
  • 作成日時
  • 完了日時

そして開発当初、以下のような Todo 一覧を取得するユースケースを作成しました。

getTodoList(userId: number, exclude_done_todo?: boolean): Promise<Todo[]>;

すなわち、Todo の情報全てを返しています。しかし、実際にユーザーが Todo の一覧を見るときに本当に全ての情報が欲しいでしょうか。私なりに考えた結果、必要なさそうだと判断しました。
実際に、Trello のようなアプリの一覧画面を考えたときに、全ての情報が表示されていると見づらいと感じるケースもあると思いました。
そして、最終的には以下の形のように、必要なフィールドを指定でき、指定されたフィールドの値を返すように変更しました。

getTodoList(
  userId: number,
  fields?: string[],
  exclude_done_todo?: boolean
): Promise<TodoListDto>;

アプリケーション固有のビジネスルールをいつ・どのようにクライアントが利用するのかをしっかりと考えることの重要性を感じました。また、このような違和感に気づけるのも、各レイヤーの意図を明確にしているからだと感じました。

Presenter と Controller

クリーンアーキテクチャの書籍内で紹介されている Java システムの例では、Controller とは別に Presenter が存在していました。
しかし、今回の私のシステムでは Presenter は存在しておらず、Service クラスが Usecase 層に存在している Dto を生成して return しています。
作成しないことによって、Usecase が外側に依存してしまう形になってしまうのであれば避けるべきだと思いますが、現状は外向きの依存は発生していないので、この方針にしました。また、現状の Controller の実装が複雑になっていないので、今後複雑になってきた場合は Presenter を導入することも考えられます。

まとめ

今回、クリーンアーキテクチャを学んで実装してみて、ソフトウェアの設計においての重要性を改めて感じました。
特に、ビジネスルールを中心において設計することで、ビジネスルールが周辺の関心事に引っ張られないというメリットは大きいと感じました。
また、各レイヤーの意図を明確にして、それらの間に境界を設けて管理することで、コードの可動性が向上することも感じました。
今後の開発に生かしていきたいと思います。

参考

share this article with X
profile

ryounasso