はじめに
クリーンアーキテクチャを学んでいる上で、どのようなディレクトリ構成にしようか参考になる記事が意外と少なかったので執筆しました。
個人的に技術の詳細とビジネスに関わる重要な部分を疎結合にできる良さげなディレクトリ構成を見つけたのでご紹介します。
また、エラーハンドリングやリポジトリの処理など一部書き途中の部分はありますがご了承ください。
サンプルコードをGitHubで確認したい方はこちら
ついでにクリーンアーキテクチャとは何ぞや?という方向けに記事を書いたので、参考にしてみてください:クリーンアーキテクチャを少し説明できるようになれる記事
クリーンアーキテクチャは図の通りしっかり全部層に分ける必要性や完璧なディレクトリ構成にすることが重要ではありません。
依存関係をコントロールし、内側の層が外部の層に依存しないようにすることが一番重要です。
ディレクトリ構成
メインとなるsrc配下をご覧ください。
.
├── README.md
├── api
│ ├── Dockerfile
│ ├── node_modules
│ ├── nodemon.json
│ ├── package-lock.json
│ ├── package.json
│ ├── src
│ │ ├── application ← Application Business Rules層
│ │ │ └── usecases
│ │ │ └── users
│ │ │ └── CreateUser.ts
│ │ ├── domain ← Enterprise Business Rules層
│ │ │ ├── gateways
│ │ │ │ └── UserGateway.ts
│ │ │ └── models
│ │ │ └── users
│ │ │ ├── User.ts
│ │ │ ├── UserId.ts
│ │ │ └── UserName.ts
│ │ ├── infra ← Frameworks & Drivers層
│ │ │ ├── db
│ │ │ │ └── mysql
│ │ │ │ ├── database.js
│ │ │ │ └── repositories
│ │ │ │ └── UserRepository.ts
│ │ │ └── http
│ │ │ └── express
│ │ │ ├── adapters
│ │ │ │ └── user
│ │ │ │ └── toCreateRequest.ts
│ │ │ ├── app.ts
│ │ │ ├── bin
│ │ │ │ └── www
│ │ │ ├── injector
│ │ │ │ └── UserInjector.ts
│ │ │ ├── public
│ │ │ │ ├── index.html
│ │ │ │ └── stylesheets
│ │ │ │ └── style.css
│ │ │ └── routes
│ │ │ └── user.ts
│ │ └── interfaces ← Interface Adapters層
│ │ ├── controllers
│ │ │ └── api
│ │ │ └── v1
│ │ │ └── user
│ │ │ └── UserCreateController.ts
│ │ └── requests
│ │ └── api
│ │ └── vi
│ │ └── user
│ │ └── UserCreateRequest.ts
│ └── tsconfig.json
└── docker-compose.yml
このディレクトリ構成は黄色、赤、緑、青の層の単位で切っています。
MySQLやExpressという詳細な技術を知っているのは、infra層だけです。
詳細な技術をinfra層に閉じ込めておくことによって、Expressから別のフレームワークに変更されようがデータの保存先がMySQLからスプレッドシートに変更されようが、interface層から中心の層は影響を受けることはありません。
つまり、この記事ではExpressとTypescriptを題材にしていますが、他のフレームワークや言語でも十分構造を参考にできるはずです。
各ディレクトリについて詳細に説明していきます。
src/domain (Entrerprise Business Rules層)
このディレクトリには、クリーンアーキテクチャの図でいう黄色い部分を示しています。
この層は最も重要な層で、外側の層の影響を受けてはいけません。
ビジネスルールに関わるオブジェクトが配置されており、ドメイン駆動設計の思想を強く受けます。
src/domain/gateways
リポジトリのインターフェイスが置かれています。
他にもsrc/domain/repositories/IUserRepository.ts
という代替案もありました。
しかし個人的にRepositoryはデータベースの意味合いが強く感じます。
永続化の手段としてはDB以外にもスプレッドシートやテキストファイルなど手段はたくさんあるため、gatewayという広域なデータリソースを指し示すワードにしました。
import { User } from "@/domain/models/users/User";
export interface UserGateway {
create(user: User): void;
}
src/domain/models
ドメインモデルなどビジネスルールにまつわるオブジェクトを置く場所です。
User, UserId, UserNameといったドメインモデルを今回は配置しています。
import { UserId } from "@/domain/models/users/UserId";
import { UserName } from "@/domain/models/users/UserName";
export class User {
constructor(
private readonly name: UserName,
private readonly userId?: UserId
) {}
}
export class UserName {
private static readonly MaxNameLength = 50;
private static readonly MinNameLength = 1;
constructor(private readonly name: string) {
UserName.validate(name);
}
private static validate(name: string) {
if (
!name ||
name.length < UserName.MinNameLength ||
UserName.MaxNameLength < name.length
) {
throw new Error(
`ユーザー名は${UserName.MinNameLength}〜${UserName.MaxNameLength}文字で入力してください`
);
}
}
}
export class UserId {
constructor(private readonly id: number) {}
}
src/infra(Frameworks & Drivers層)
このディレクトリには、クリーンアーキテクチャの図でいう青い部分を示しています。
ExpressやMySQLなど、詳細なフレームワークや技術が置かれています。
src/infra/db/mysql
MySQLの設定ファイルなど、MySQLに依存するオブジェクトたちが所属します。
src/infra/db/mysql/repositories
domain層に置かれたリポジトリのインターフェイスの実装クラスが所属します。
保存メソッドなどの具体的な書き方はMySQLに依存するため、mysql配下にリポジトリの実装クラスを配置しました。
イメージとしては以下です。
import { UserGateway } from "@/domain/gateways/UserGateway";
import { User } from "@/domain/models/users/User";
export class UserRepository implements UserGateway {
create(user: User): void {
// TODO: mysqlの保存処理を書く
}
}
src/infra/http/express
Expressに関するファイルなどが所属しています。
src/infra/http/express/adapters
Expressのリクエストオブジェクトをアプリケーション固有のリクエストオブジェクト(UserCreateRequest)に変換する役割を持った処理が置かれます。
このアダプタを経由することで、Expressという詳細な技術にとらわれないプレーンなリクエストオブジェクトに変換することができます。
コードを見ればわかると思いますが、import { Request } from "express";
というExpressに依存した記述をする必要があるためinfra層に配置しました。
import { Request } from "express";
import { UserCreateRequest } from "@/interfaces/requests/api/vi/user/UserCreateRequest";
export function toUserCreateRequest(req: Request): UserCreateRequest {
return new UserCreateRequest(req.body.name);
}
src/infra/http/express/injector
DIを行う役割を持ったファイルたちが配置されます。
di.tsの方がしっくりくる方もいるかもしれませんね。
DIコンテナなどDIを便利に行う方法は他にもありますが、アプリケーションの規模が小さいため簡単なinjectorというファイルを設けることにしました。
ここでRepositoryやUsecase、Controllerをnewしています。
getUserCreateController()
をルーターから叩く想定です。
import { UserRepository } from "@/infra/db/mysql/repositories/UserRepository";
import { CreateUser } from "@/application/usecases/users/CreateUser";
import { UserCreateController } from "@/interfaces/controllers/api/v1/user/UserCreateController";
export class UserInjector {
constructor() {}
getUserCreateController(): UserCreateController {
const userRepository = new UserRepository();
const usecase = new CreateUser(userRepository);
return new UserCreateController(usecase);
}
}
src/infra/http/express/routes
Expressのルーティングに関わるファイルが配置されます。
前述したtoUserCreateRequest(req);
をというアダプターをここで実行し、アプリケーション固有のプレーンなリクエストオブジェクトに変換しています。
そしてInjectorのgetUserCreateControllerメソッドを読んでコントローラーを手に入れ、そのhandleメソッドを呼びます。
コントローラーのhandleメソッドを実行することでusecase→repositoryといった流れでデータを永続化できます。
import express from "express";
const router = express.Router();
import { UserInjector } from "@/infra/http/express/injector/UserInjector";
import { toUserCreateRequest } from "@/infra/http/express/adapters/user/toCreateRequest";
const injector = new UserInjector();
router.post("/", (req: express.Request, res: express.Response) => {
const userCreateRequest = toUserCreateRequest(req);
const response = injector.getUserCreateController().handle(userCreateRequest);
// ここは適当
res.send("respond with a create" + JSON.stringify(response));
});
module.exports = router;
src/interfaces( Interface Adaptors層)
このディレクトリには、クリーンアーキテクチャの図でいう緑色の部分を示しています。
src/interfaces/controllers/api/v1/user
コントローラーが属しています。
拡張性を考慮してapi/v1というバージョンを指定してディレクトリを切りました。
CRUDの各単位でコントローラーを用意すると単一責任の原則が保たれ、かつ見通しも良くなるのでおすすめです。
また、コントローラーでUserというドメインモデルを作成してusecaseに渡すことによって、usecaseはプレーンなドメインモデルを扱うことができます。
import { User } from "@/domain/models/users/User";
import { UserName } from "@/domain/models/users/UserName";
import { CreateUser } from "@/application/usecases/users/CreateUser";
import { UserCreateRequest } from "@/interfaces/requests/api/vi/user/UserCreateRequest";
export class UserCreateController {
constructor(private usecase: CreateUser) {}
handle(req: UserCreateRequest): User {
const user = new User(new UserName(req.name));
return this.usecase.handle(user);
}
}
src/interfaces/requests/api/v1/user
Expressなどの詳細な技術に左右されない、アプリケーション固有のプレーンなリクエストオブジェクトが配置されます。
どのようなフレームワークや技術を使ってリクエストをされても、ここのリクエストオブジェクトに変換されたらusecaseには影響を与えません。
また、ビジネス的なバリデーションではなく、空文字は入っていないか?などのバリデーションを行います。
リクエストオブジェクトは完全コンストラクタにすることで、usecaseは整合性の取れた正しいリクエストオブジェクトを受け取ることができます。
export class UserCreateRequest {
constructor(private readonly _name: string) {
UserCreateRequest.validateRequest();
}
get name(): string {
return this._name;
}
private static validateRequest() {
// リクエストのバリデーション処理
}
}
src/usecases(Application Business Rules層)
このディレクトリには、クリーンアーキテクチャの図でいう赤色の部分を示しています。
内側のEntrerprise Business Rulesに所属するドメインオブジェクトなどを操り、アプリケーション固有のビジネスルールを定義します。
api/src/application/usecases/users
ユーザーに関するユースケースが配置されます。
今回はCRUDなので、基本的に作成する、取得する、削除するなどのユースケースになる想定です。
以下のユーザーを作成するというユースケースは、リポジトリを用いて永続化処理を行なっています。
import { User } from "@/domain/models/users/User";
import { UserGateway } from "@/domain/gateways/UserGateway";
export class CreateUser {
constructor(private readonly userRepository: UserGateway) {}
handle(user: User) {
this.userRepository.create(user);
return user;
}
}
処理の流れ
ざっくり全体像としては、
router→Injector→Controller→Usecase→Repository
という流れです。
さらに詳細化すると以下の通りになります。
- クライアントがリクエストを送信
- routerで適切なエンドポイントが受け取る
i .toUserCreateRequest()
でプレーンなリクエストオブジェクトに変換 -
Injector.getUserCreateController()
が適切なコントローラーを取得 -
UserCreateController.handle()
メソッドが呼び出される -
CreateUser.handle()
メソッドが呼び出される(ユースケース) -
Repository.create()
でデータがリポジトリに保存される
3のInjector.getUserCreateController()の時点ではexpressという詳細な技術は知っていない状態になります。
なので、expressから別のフレームワークに変えても、toUserCreateRequest()というアダプターのおかげでinterface層に響くことはありません。
個人的にこの箇所の存在が変更への強さに繋がるのだと思います。
最後に
長々と書いてしまいましたが、Injectorの役割とtoCreateRequest.tsの役割を抑えていただければ十分です。
最初にもお伝えしましたが、クリーンアーキテクチャではディレクトリ構成やファイル名などは重要ではありません。
チームや個人で適切だと考えた構成にすることが理想です。
正解がないからこそ、一つの例として私のディレクトリ構成を参考にしていただければと思います。