11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Clean Architectureで、各レイヤの責務を考慮したバリデーションを備えたAPIサーバを実装してみる(TypeScript)

Last updated at Posted at 2021-02-28

概要

Clean Architectureとは、下図に代表されるような、アプリを数層のレイヤに分け、それらの間の依存関係を正しく守ったアーキテクチャである。

image.png

Clean Architectureで実用的なWebシステム、特にAPIサーバを作ろうと思うと、リクエストのバリデーションや認証が必要なときにアーキテクチャをどうするか?といった現実的な問題に突き当たる。

特にバリデーションに関しては、バリデーションはUse Case (Application Business Rulesレイヤ)の入り口で行うべきという考え方や、さらに外側のレイヤで行うべきという考え方など、様々な考え方がある。

その中で、個人的には(実際のプロダクトでいけるかは別として)、こちらの記事で解説されているように、

それぞれのレイヤごとに、それを受け付けてしまうと自分のレイヤの責務を果たすことができなってしまうような input がやってきた場合は弾くようにするというのがバリデーションである。

という考え方が、Clean Architectureに最も忠実で、責務の分離がしっかりできていると思う。

しかし、実際これをどう実装するか?となると、いろいろ悩む点があり、かつQiita等にも詳細な解説があまりない気がした。ということで実際に作ってみた。

使用した技術としては以下の通りである。

  • 言語: TypeScript
  • フレームワーク: Express (リクエストのルーティング等基本的な機能のみ使用)
  • DB: インメモリのモックDB

作ったコードはgithubに置いた。

作ったもの

ユーザのCRUDが行える、超単純なAPIサーバを作った。
ここでユーザは、以下のような特徴を持つものとする。

  • ユーザの属性は、idとnameの2つだけ
  • nameは、空文字列でないこと、他のユーザと重複していないこと、という2つの条件を満たさなければならない

また、APIのエンドポイントは以下の通り。認証等は特にない。

  • ユーザ作成 (POST /users)
  • 全ユーザ取得 (GET /users)
  • idを指定してユーザ取得 (GET /users/{id})
  • idを指定してユーザ更新 (PUT /users/{id})
  • idを指定してユーザ削除 (DELETE /users/{id})

各レイヤで行うバリデーション

上記のAPIサーバの場合、Clean Architectureの各レイヤで検出したいバリデーションをまとめると、以下のようになるかなと思う。

APIエンドポイント Interface Adapters (Controller) Application Business Rules (Use Case Interactor) Enterprise Business Rules (Entity)
POST /users リクエストパラメータ不足、型違反 nameが空文字列、nameが他のユーザと重複
GET /users リクエストパラメータ不足、型違反
GET /users/{id} リクエストパラメータ不足、型違反 指定のidを持つユーザが存在しない
PUT /users/{id} リクエストパラメータ不足、型違反 指定のidを持つユーザが存在しない nameが空文字列、nameが他のユーザと重複
DELETE /users/{id} リクエストパラメータ不足、型違反 指定のidを持つユーザが存在しない

上表は、基本的には、こちらの記事に解説されているような責務分けになっていると思う。以下、レイヤごとに簡単に解説する。

  • Enterprise Business Rules
    • ユーザの属性がビジネス的に満たすべき要件のチェック(=name空文字列チェックと重複チェック)を行なっている。
  • Application Business Rules
    • ユーザそのものがビジネス的に満たすべき要件ではないが、個々のアプリケーションユースケースの実行のために必要な要件のチェック(=指定のidを持つユーザが存在するかチェック)を行なっている。
  • Interface Adapters
    • ビジネスルールには直接関わらない、パラメータ不足や型違反などのリクエストの形式面のチェックを行なっている。

なお、一番外側のFrameworks & Drivers層でのバリデーションは、ここでは省略している。

全体アーキテクチャ

作ったAPIサーバのコンポーネント・モジュールと、その依存関係は下図の通り。
components.png
受信したAPIリクエストは、まずServerコンポーネントで受け付けられ、Controllers、Interactorsと処理が流れていく。ユーザーのモデルはEntitiesで定義され、DBとのやり取りはDataAccessが担う、という構成になっている。
また、EntityValidatorsというコンポーネントがあるが、これは後述するように、Enterprise Business Rulesレイヤでのバリデーション処理のために存在するものである。

アーキテクチャ上の課題と解決案

アーキテクチャにおいて、特にEnterprise Business Rulesレイヤのバリデーションで、個人的に悩ましかったポイントがあった。

課題

今回のAPIサーバでは、インメモリでDBのモックを作ってそこにユーザのデータを永続化する形とした。
永続化に関しては、

  • 実際のメモリへのデータ書き込みは、Interface AdaptersレイヤのDataAccessコンポーネントに担わせる
  • そしてそのインターフェースは、Application business rulesレイヤのDataAccessInterfacesコンポーネントに配置する

という方法を取ろうとした(なお、今回はRDBへの接続等は行わないので、Frameworks & Driversレイヤにはデータ永続化関連のコンポーネントは無い)。

また、Enterprise Business Rules内のバリデーション処理については、エンティティであるUserクラスの中に組み込もうとしていた。

図で表すと以下のような感じである。
starndard.png

データ永続化関連のコンポーネントの置き場所に関しては、Clean Architecture本のChapter 22の例や、こちらの記事こちらの記事にも同様の構成があり、一般的かとは思う。

しかし、Userクラスのバリデーション処理の中で、「ユーザのnameは他のユーザと重複してはいけない」というやつが問題になる。
このバリデーションのためには、DB内の他のユーザデータにアクセスする必要があり、そのためには、Application Business RulesのDataAccessInterfacesに依存しなければならないからである。

つまり、Enterprise Business Rules内のバリデーションロジックが、外側のレイヤに依存してしまうという、DIPを完全に破る不具合が生じてしまう。

素直な解決案(?)

それなら、下図のように、DataAccessInterfacesをEnterprise Business Rulesに移せば良いのでは?というのが素直な方法である。

alternative1.png

しかし、この方法でも、よく考えると不具合が生じかねない。

例えば、今考えている超単純なCRUDのユースケースに加えて、「nameが10文字以上のユーザの全一覧を取得したい」というユースケースが加わったとする。
このとき、UserDataAccessには、例えばlistAllUsersWithNameLengthMoreThan(len: number): Promise<User[]>のようなメソッドが加わると思われる。

しかし、このメソッドは、完全に特定のユースケースに特化したものであり、Entityであるユーザ自身のビジネスルールから生まれたものであるとは言い難い。
つまり、Enterprise Business Rules内のコンポーネント(DataAccessInterfaces)が、外側のレイヤの都合で変更されてしまうという、間接的にDIPを破る事態が起こってしまう。

今のところ良さそうな解決案

悩んだ末、以下のような構成を考えた。
alternative2.png

この構成では、Enterprise business ruleレイヤでのバリデーションの処理の実体は、Application business rulesレイヤのUserValidatorImplクラスが担う。

一方、UserValidatorImplのメソッド一覧は、Enterprise business rulesレイヤのUserValidatorインターフェースに宣言し、Userエンティティはそのインターフェースのみ参照する。

こうすることで、「何を」バリデーションするかを内側のレイヤで宣言しつつ、「どうやって」バリデーションするかは外側のレイヤに委ねることができる。(具体的な実装は以下で説明)

各レイヤの詳細

以下、各レイヤの実装の詳細を、コードを含めて説明する。なお、簡潔さのため、コードは省略している箇所も多いので、全体はgithubを参照。

全レイヤ共通で使うもの

レイヤごとの説明をする前に、全レイヤ共通で使う、エラー表現に関するクラスを説明する。

本実装では、何かメソッドがエラーを吐くときには、こちらの記事で紹介されている、Either型のようなResultクラスを用い、メソッドの返り値として、正常終了結果と、エラーのいずれかを返せるようにした。Resultクラスには、isSuccessisFailureメソッドが備わっており、そのインスタンスがエラーなのか正常結果なのかを判別できるようになっている。

src/result.ts
export type Result<T, E> = Success<T, E> | Failure<T, E>;

export class Success<T, E> {
  constructor(readonly value: T) {}
  type = 'success' as const;
  isSuccess(): this is Success<T, E> {
    return true;
  }
  isFailure(): this is Failure<T, E> {
    return false;
  }
}

export class Failure<T, E> {
  constructor(readonly value: E) {}
  type = 'failure' as const;
  isSuccess(): this is Success<T, E> {
    return false;
  }
  isFailure(): this is Failure<T, E> {
    return true;
  }
}

また、Resultクラスのインスタンスが使うエラークラスは、typescript組み込みのErrorクラスを継承したカスタムエラークラスを定義し、それを用いることとした。
レイヤ共通で使うエラークラスとして、HTTPステータスコード400, 404, 409などに対応する抽象クラスをざっくり作っておき、より詳細なエラーを表すクラスは、各レイヤの中で、ここで定義した抽象クラスを継承する形で作成した。(それらのコードは省略)。

src/errors/validationErrors.ts
export abstract class ValidationError extends Error {}

// ステータスコード400に対応するバリデーションエラーの抽象クラス。
export abstract class InvalidInputValidationError extends ValidationError {
  constructor(message: string) {
    super(message);
    this.name = new.target.name;
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

// 以下、404や409など、各ステータスコードに対応するバリデーションエラー抽象クラスの定義が続く。
// ここでは簡潔さのため省略。

Enterprise Business Rulesレイヤ

このレイヤでは、ユーザEntityの定義と、Entityのビジネス要件として存在するバリデーションを行うクラスのインターフェース定義を行なっている。

UserValidator

UserValidatorインターフェースは、以下のように、ユーザが満たすべきビジネス的要件をチェックするメソッド一覧を定義したものである。
ここでは、前述したように実際のバリデーション処理は行わず、あくまで「何を」バリデーションするかのみを定義している。これにより、DBへのアクセスを含む具体的な処理を外側のレイヤに委ねることができ、Enterprise Business Rulesレイヤをクリーンな状態に保つことができる。

src/entities/interfaces/userValidator.ts
export interface UserValidator {
  isNameNotEmpty(name: string): boolean;
  isNameUnique(id: string, name: string): Promise<boolean>;
}

User

EntityであるUserクラスは、ユーザの属性であるidnameを定義する。
また、UserValidatorインターフェースを実装したインスタンスを引数として受け取るvalidateというメソッドを持ち、UserValidatorで定義されたメソッドを実行することで、バリデーションを行う。
なお、validateメソッドの返り値は、前述のResultクラスのインスタンスであり、バリデーションエラーの場合には、前述のコード内にあったInvalidInputValidationErrorを継承したカスタムエラークラスであるUserNameEmptyValidationErrorなどを返す。

src/entities/user.ts
import { ValidationError } from 'src/errors/validationErrors';
import { Failure, Result, Success } from 'src/result';

import {
  UserNameEmptyValidationError,
  UserNameNotUniqueValidationError,
} from './errors';
import { UserValidator } from './interfaces/userValidator';

export class User {
  private _id: string;
  private _name: string;

  public get id(): string {
    return this._id;
  }

  public set id(id: string) {
    this._id = id;
  }

  public get name(): string {
    return this._name;
  }

  public set name(name: string) {
    this._name = name;
  }

  constructor(id: string, name: string) {
    this._id = id;
    this._name = name;
  }

  // バリデーションを行うメソッド
  public async validate(
    validator: UserValidator
  ): Promise<Result<null, ValidationError>> {
    // nameが空文字列でないかチェック
    if (!validator.isNameNotEmpty(this._name)) {
      return new Failure(new UserNameEmptyValidationError());
    }
    // nameが他のユーザと重複していないかチェック
    if (!(await validator.isNameUnique(this._id, this._name))) {
      return new Failure(new UserNameNotUniqueValidationError(this._name));
    }

    return new Success(null);
  }
}

Application Business Rulesレイヤ

このレイヤでは、Enterprise Business Rulesレイヤで定義されたユーザのバリデーションメソッドの実装、ユーザのCRUDを行うユースケースの処理実装と、DBへのアクセスに用いるインターフェースの定義を行なっている。

UserValidatorImpl

Enterprise Business RulesレイヤのUserValidatorインターフェースで宣言された実際の個々のバリデーションの実装は、Application Business Rulesレイヤにある、UserValidatorImplクラスで行う。
このクラスでは、同じくApplication Business RulesレイヤのUserDataAccessインターフェースを介して、DBにアクセスし、ユーザのnameの重複チェックなどを行なっている。

src/entityValidators/userValidatorImpl.ts
import { UserDataAccess } from 'src/dataAccessInterfaces/userDataAccess';
import { UserValidator } from 'src/entities/interfaces/userValidator';

export class UserValidatorImpl implements UserValidator {
  private readonly dataAccess: UserDataAccess;

  constructor(dataAccess: UserDataAccess) {
    this.dataAccess = dataAccess;
  }

  public isNameNotEmpty(name: string): boolean {
    if (name.length === 0) {
      return false;
    } else {
      return true;
    }
  }

  public async isNameUnique(id: string, name: string): Promise<boolean> {
    const user = await this.dataAccess.findUserByName(name);
    if (!user) {
      return true;
    } else if (user.id === id) {
      return true; // user with same name exists, but it is the user to validate here
    } else {
      return false;
    }
  }
}

UserDataAccess

UserDataAccessインターフェースは、DBへのアクセスを行うメソッド一覧を定義したものである。実際のDBへのアクセス処理は外側のレイヤに委ねることで、Application Business Rulesレイヤをクリーンに保つことができる。

src/dataAccessInterfaces/userDataAccess.ts
import { User } from 'src/entities/user';

export interface UserDataAccess {
  saveUser(user: User): Promise<void>;
  listAllUsers(): Promise<User[]>;
  findUserById(id: string): Promise<User | void>;
  findUserByName(name: string): Promise<User | void>;
  deleteUser(id: string): Promise<void>;
}

UserInteractor

UserInteractorは、個々のユーザーCRUDのアプリケーションユースケースを実行するinteractorのインターフェースである。なお、...InputDataや、...OutputDataといった型は、interactorへの入力・出力を表すオブジェクトの型である(型の定義コードは省略)。

src/interactors/interfaces/userInteractor.ts
import { ValidationError } from 'src/errors/validationErrors';
import { Result } from 'src/result';

import {
  CreateUserInputData,
  UpdateUserInputData,
} from '../types/userInputData';
import {
  CreateUserOutputData,
  FindUserByIdOutputData,
  ListAllUsersOutputData,
} from '../types/userOutputData';

export interface UserInteractor {
  createUser(
    input: CreateUserInputData
  ): Promise<Result<CreateUserOutputData, ValidationError>>;
  listAllUsers(): Promise<Result<ListAllUsersOutputData, ValidationError>>;
  findUserById(
    id: string
  ): Promise<Result<FindUserByIdOutputData, ValidationError>>;
  updateUser(
    input: UpdateUserInputData
  ): Promise<Result<null, ValidationError>>;
  deleteUser(id: string): Promise<Result<null, ValidationError>>;
}

UserInteractorImpl

UserInteractorImplクラスは、ユースケースの実行の処理を実際に実行するクラスである。
以下では、説明の簡潔さのため、ユーザーの更新のユースケースに対応するメソッド(updateUser)のみ掲載している。
このメソッドでは、まず、リクエストされたidに対応するユーザーがDBに存在するかをチェックし、存在しなかったらエラーを吐く(Application Business Rulesレイヤの責務であるバリデーション)。
また、更新されたユーザーをDBに保存する前に、Enterprise Business Rulesレイヤでのバリデーション処理を呼び出している。

src/interactors/userInteractorImpl.ts
import { UserDataAccess } from 'src/dataAccessInterfaces/userDataAccess';
import { UserValidator } from 'src/entities/interfaces/userValidator';
import { User } from 'src/entities/user';
import { ValidationError } from 'src/errors/validationErrors';
import { Failure, Result, Success } from 'src/result';
import { v4 as uuidv4 } from 'uuid';

import { UserWithIdNotFoundValidationError } from './errors';
import { UserInteractor } from './interfaces/userInteractor';
import {
  CreateUserInputData,
  UpdateUserInputData,
} from './types/userInputData';
import {
  CreateUserOutputData,
  FindUserByIdOutputData,
  ListAllUsersOutputData,
} from './types/userOutputData';

export class UserInteractorImpl implements UserInteractor {
  private readonly dataAccess: UserDataAccess;
  private readonly validator: UserValidator;

  constructor(dataAccess: UserDataAccess, validator: UserValidator) {
    this.dataAccess = dataAccess;
    this.validator = validator;
  }

  // 簡潔さのため、ユーザー情報の更新ユースケースのみここでは掲載。
  // その他のユースケース(ユーザー作成など)のメソッドは省略。
  public async updateUser(
    input: UpdateUserInputData
  ): Promise<Result<null, ValidationError>> {
    // まず、リクエストされたidに対応するユーザーが存在するか、という、
    // interactorの責務であるバリデーションを実施するとともに、当該ユーザー情報をDBから取得
    const user = await this.dataAccess.findUserById(input.id);

    if (!user) {
      return new Failure(new UserWithIdNotFoundValidationError(input.id));
    }

    // DBから取得したユーザー情報を書き換え、Enterprise Business Rulesレイヤでの
    // バリデーション実施
    user.name = input.name;
    const validResult = await user.validate(this.validator);
    if (validResult.isFailure()) {
      return new Failure(validResult.value);
    }

    // バリデーション成功したらユーザーデータをDBに保存
    await this.dataAccess.saveUser(user);
    return new Success(null);
  }

}

Interface Adaptersレイヤ

このレイヤでは、インメモリのモックDBへのアクセス処理の実装と、HTTPリクエストを処理するコントローラの実装を行う。

InMemoryUserDataAccessImpl

InMemoryUserDataAccessImplクラスは、Application Business RulesレイヤのUserDataAccessインターフェースを実装し、インメモリのモックDBに対して、ユーザーデータの永続化処理を行う。

src/dataAccess/inMemoryUserDataAccessImpl.ts
import { UserDataAccess } from 'src/dataAccessInterfaces/userDataAccess';
import { User } from 'src/entities/user';

export class InMemoryUserDataAccessImpl implements UserDataAccess {
  private users: User[];

  constructor(users: User[]) {
    this.users = users;
  }

  public async saveUser(user: User): Promise<void> {
    await this.wait();

    // before saving, delete existing user with same ID
    this.users = this.users.filter((u) => u.id !== user.id);
    this.users.push(user);
  }

  public async listAllUsers(): Promise<User[]> {
    await this.wait();

    // to prevent contamination of in-memory data after returning users,
    // return cloned users (same for other methods).
    return this.users.map((u) => new User(u.id, u.name));
  }

  public async findUserById(id: string): Promise<User | void> {
    await this.wait();

    const user = this.users.find((u) => u.id === id);
    if (user) {
      return new User(user.id, user.name);
    }
  }

  public async findUserByName(name: string): Promise<User | void> {
    await this.wait();

    const user = this.users.find((u) => u.name === name);
    if (user) {
      return new User(user.id, user.name);
    }
  }

  public async deleteUser(id: string): Promise<void> {
    await this.wait();

    this.users = this.users.filter((u) => u.id !== id);
  }

  // wait for some time to emulate actual DB
  private async wait(): Promise<void> {
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
}

UserController

UserControllerインターフェースは、ユーザーCRUD処理のリクエストを受け付けるコントローラのインターフェースである。

src/controllers/interfaces/userController.ts
import { Request } from '../types/request';
import { Response } from '../types/response';

export interface UserController {
  createUser(request: Request): Promise<Response>;
  listAllUsers(request: Request): Promise<Response>;
  findUserById(request: Request): Promise<Response>;
  updateUser(request: Request): Promise<Response>;
  deleteUser(request: Request): Promise<Response>;
}

なお、上のコードの中で使われている、RequestResponse型は、以下のように、HTTPリクエストやレスポンスを抽象化したような型である。
ここでは、「Clean Architectureでは、Interface Adapterレイヤより内側では、特定のWebフレームワークに依存しない方が良い」というプラクティスに則り、RequestResponse型は、Expressなどのフレームワークには依存しない形にしている。

src/controllers/types/request.ts
export type Request = {
  body: { [key: string]: unknown };
  params: { [key: string]: unknown };
  headers: { [key: string]: string };
  query: { [key: string]: unknown };
};
src/controllers/types/response.ts
export type Response = {
  body: { [key: string]: unknown } | unknown[];
  status: number;
};

UserControllerImpl

UserControllerImplクラスには、実際のコントローラの処理を記述する。
以下では、説明の簡潔さのため、ユーザーの更新に対応するメソッド(updateUser)のみ掲載している。
このメソッドでは、まずTypescriptのassertion function機能を使って、リクエストパラメータが指定の構造・型であるかどうかのバリデーションを行なっている。
このバリデーションを通過したら、interactorを呼び出してユースケースを実行し、そのあと、レスポンスデータを生成している。

src/controllers/userControllerImpl.ts
import { UserInteractor } from 'src/interactors/interfaces/userInteractor';

import { UserController } from './interfaces/userController';
import { Request } from './types/request';
import { Response } from './types/response';
import {
  assertIsCreateUserRequest,
  assertIsDeleteUserRequest,
  assertIsFindUserByIdRequest,
  assertIsUpdateUserRequest,
} from './types/userRequests';
import { buildErrorResponse } from './util';

export class UserControllerImpl implements UserController {
  private readonly interactor: UserInteractor;

  constructor(interactor: UserInteractor) {
    this.interactor = interactor;
  }


  // 簡潔さのため、ユーザー情報の更新に関するメソッドのみここでは掲載。
  // その他(ユーザー作成など)のメソッドは省略。
  public async updateUser(request: Request): Promise<Response> {
    // まず、リクエストパラメータの型チェックなど、コントローラの責務であるバリデーションを実施
    try {
      assertIsUpdateUserRequest(request);
    } catch (error) {
      return buildErrorResponse(error);
    }

    // interactorでのユースケース実行
    const result = await this.interactor.updateUser({
      id: request.params.id,
      name: request.body.name,
    });

    // レスポンス生成
    if (result.isFailure()) {
      return buildErrorResponse(result.value);
    }
    return {
      body: {},
      status: 204,
    };
  }
}

上のコードで使っている、assertIsUpdateUserRequest関数の定義は以下。

src/controllers/types/userRequest.ts
import { InvalidInputFormatValidationError } from '../errors';
import { Request } from './request';

// 簡潔さのため、ユーザー情報の更新に関するリクエスト型定義とassertion functionのみ掲載。
// その他は省略。
export type UpdateUserRequest = Request & {
  params: { id: string };
  body: { name: string };
};

export function assertIsUpdateUserRequest(
  request: Request
): asserts request is UpdateUserRequest {
  if (!request.params.id) {
    throw new InvalidInputFormatValidationError('id is required');
  } else if (typeof request.params.id !== 'string') {
    throw new InvalidInputFormatValidationError('id must be string');
  }

  if (!request.body.name) {
    throw new InvalidInputFormatValidationError('name is required');
  } else if (typeof request.body.name !== 'string') {
    throw new InvalidInputFormatValidationError('name must be string');
  }
}

また、buildErrorResponse関数の定義は以下。
バリデーションエラーを受け取り、そのクラスの種類に応じて、ステータスコードをつけたHTTPレスポンスデータを生成している。

src/controllers/util.ts
import {
  InvalidInputValidationError,
  NotFoundValidationError,
  ResourceConflictValidationError,
} from 'src/errors/validationErrors';

import { Response } from './types/response';

export function buildErrorResponse(error: Error): Response {
  if (error instanceof InvalidInputValidationError) {
    return errorResponse(error, 400);
  } else if (error instanceof NotFoundValidationError) {
    return errorResponse(error, 404);
  } else if (error instanceof ResourceConflictValidationError) {
    return errorResponse(error, 409);
  } else {
    return errorResponse(error, 500);
  }
}

const errorResponse = function (error: Error, status: number): Response {
  return {
    body: { message: error.message },
    status,
  };
};

Frameworks & Driversレイヤ

このレイヤでは、HTTPサーバを立ち上げ、リクエストのルーティングを行う。また、内側のレイヤで定義したクラスのインスタンス化を行う。
このレイヤで初めて、Webフレームワーク(Express)への依存が出てくる。

App

Appクラスは、HTTPのリクエストルーティングを定義し、ExpressのApplicationインスタンスを作る。
Appクラスは、インスタンス化するときにコントローラのインスタンスを受け取り、各ルーティング定義の中で、そのコントローラを呼び出して処理を行わせている。

src/server/app.ts
// to enable async handler in express, partially disable eslint
/* eslint-disable @typescript-eslint/no-misused-promises */

import { Server } from 'http';

import express from 'express';
import { UserController } from 'src/controllers/interfaces/userController';
import { Request } from 'src/controllers/types/request';

export class App {
  private readonly _app: express.Application;

  public get app(): express.Application {
    return this._app;
  }

  constructor(controller: UserController) {
    this._app = this.buildApp(controller);
  }

  public listen(port: number, callback: (() => void) | undefined): Server {
    return this._app.listen(port, callback);
  }

  private buildApp(controller: UserController): express.Application {
    const app = express();
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));

    // 簡潔さのため、ユーザー情報の更新に関するルートのみここでは掲載。
    // その他(ユーザー作成など)のルートは省略。
    app.put(
      '/users/:id',
      async (req: express.Request, res: express.Response) => {
        const response = await controller.updateUser(req as Request);
        res.status(response.status).json(response.body);
      }
    );

    return app;
  }
}

ControllerFactory

ControllerFactoryクラスは、Appクラスに渡すためのコントローラインスタンスを作成するクラスである。コントローラインスタンスを作るために必要な、interactorやdataAccessのインスタンス化もここで行なっている。
なお、コントローラインスタンス作成時に、APIサーバ立ち上げ時に元から存在する初期ユーザーを渡すこともできる。

src/server/controllerFactory.ts
import { UserController } from 'src/controllers/interfaces/userController';
import { UserControllerImpl } from 'src/controllers/userControllerImpl';
import { InMemoryUserDataAccessImpl } from 'src/dataAccess/inMemoryUserDataAccessImpl';
import { User } from 'src/entities/user';
import { UserValidatorImpl } from 'src/entityValidators/userValidatorImpl';
import { UserInteractorImpl } from 'src/interactors/userInteractorImpl';

export class ControllerFactory {
  public makeInMemoryUserController(initialUsers: User[]): UserController {
    const dataAccess = new InMemoryUserDataAccessImpl(initialUsers);
    const validator = new UserValidatorImpl(dataAccess);
    const interactor = new UserInteractorImpl(dataAccess, validator);
    const controller = new UserControllerImpl(interactor);
    return controller;
  }
}

index

indexは、Appクラスをインスタンス化し、Webサーバーを立ち上げる。

src/server/index.ts
import { User } from 'src/entities/user';

import { App } from './app';
import { ControllerFactory } from './controllerFactory';

// initial user of server
const initialUser = new User(
  '931378e7-976b-4e36-87e9-cdfe9a5b85ce',
  'initial-user'
);

// make controller with in-memory database and initial user
const factory = new ControllerFactory();
const controller = factory.makeInMemoryUserController([initialUser]);

// server start
new App(controller).listen(8000, () => {
  console.log('server started');
});

感想

今回、バリデーションまで考慮したClean Architectureを作ってみたが、(自分がClean Architecutre & Typescriptに不慣れなせいもあり)色々と悩むポイントが多かった。
特に、Enterprise Business Rulesレイヤでのバリデーションをどうするか、という点が悩ましかった。

一応実装はしてみたが、まだまだDRYに書けていない部分や、しっくりこない部分もあるので、よりよい実装等あれば是非コメント・ご指摘お願いいたします。

11
7
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
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?