クリーンアーキテクチャのUseCase Inputについて見直してみた
ターゲット
クリーンアーキテクチャ 初級者~中級者
概要
WebAPI開発時はバックエンドのアーキテクチャを考えて実装すると思いますが、取り入れるアーキテクチャの一つにクリーンアーキテクチャがあります。
私自身もなんとか少しずつ考え方を取り入れようとしているのですが、UseCaseのInput Dataに関してはずっともやもやがあったので、「単に自分の理解が間違っているだけなのではないか?」「一回考え方を見直した方が良いのではないか?」という思いから今回見直してみることにしました。結果、見事にわかっていなかったようです。今でも理解しているかと言われると怪しいですが。。
参考文献は、クリーンアーキテクチャの原本である**Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series)**です。
THE DEPENDENCY RULE
クリーンアーキテクチャでは、THE DEPENDENCY RULEというものがあり、各役割を持つ4つの層はお互いに独立してあるべきだ、依存に関係にあってはならない、というコアな考え方がありますね。
4つの層は、 Enterprise Business Rules, Apllication Business Rules, Interface Adapters, Framework & Driversです。
今回取り上げるのは、Apllication Business Rulesに当たるUseCasesのInput Data, Input Boundary, Use Case Interactorのところです。
Input Data, Input Boundary, Use Case Interactor
Input Data, Input Boundary, Use Case Interactorは何者なのか、を説明しようかと思いましたが、コードを見た方がわかりやすい気がするので書いてみました。TypeScriptです。
※全部を1ファイルに詰め込んだように書いています。
※クリーンアーキテクチャを実装するためのサンプルなので、Entityの作成など適当なところはお許しください。
// Entityの型定義
interface IUser {
userId: number;
name: string;
}
// Entity
class UserEntity implements IUser {
readonly userId: number;
readonly name: string;
constructor(userId: number, name: string) {
this.userId = userId;
this.name = name;
}
}
// [Input Data<Data Structure>]
// Controllerが受けたrequestBodyからUseCaseが使用するオブジェクトに変換にすることでUseCaseがControllerに依存しないようにする
class UserCreateInputData {
readonly name: string;
constructor(userCreateReqBody: TUserCreateReqBody) {
this.name = userCreateReqBody.firstName + userCreateReqBody.lastName;
}
}
// [Input Boundary<Interface>]
// UseCaseが持つ関数などのインターフェースを定義する抽象クラス
abstract class IUserCreateUseCase {
abstract create(inputData: UserCreateInputData): IUser;
}
// [UseCase Interactor]
// 実際にオブジェクトを操作するUseCaseの具象クラス
class UserCreateInteractor implements IUserCreateUseCase {
// ※DBなどの外部とのやりとりする場合はconstructorでRepositoryをDIP(Dipendency Inversion Principle)で実装
public create = (inputData: UserCreateInputData) => {
return new UserEntity(1, inputData.name);
};
}
//Controller
class UserController {
private readonly userCreateUseCase: IUserCreateUseCase;
constructor(userCreateUseCase: IUserCreateUseCase) {
this.userCreateUseCase = userCreateUseCase;
}
public CreateUser(userCreateReqBody: TUserCreateReqBody) {
const inputData = new UserCreateInputData(userCreateReqBody);
return this.userCreateUseCase.create(inputData);
}
}
動きの確認
type TUserCreateReqBody = {
firstName: string;
lastName: string;
};
// routerでHttpRequestからrequestBodyを取得したと仮定[※]
const userCreateReqBody: TUserCreateReqBody = {
firstName: "Type",
lastName: "Script",
};
// UseCaseの作成
const useCaseInteractor = new UserCreateInteractor();
// Controllerの作成
const controller = new UserController(useCaseInteractor);
const result = controller.CreateUser(userCreateReqBody);
console.log(result);
console結果
[LOG]: UserEntity: {
"userId": 1,
"name": "TypeScript"
}
[※]たとえば、Node.js(express)ではAPIサーバはwebからrouterを介してHttpRequestを受け取ります。そのリクエストの一部(bodyなど)を、データのアウトプットを作り出すControllerへ渡したいのですが、ControllersとWebは層が違うのでHttpRequestをそのまま渡すのは考え方に添いません。そこで、userCreateReqBody
のようなオブジェクトを作成しControllerへ渡すことで、Controllerが外側のFramework & Drivers層に依存しない様にします。
気になっていること
Input Dataは何層?
Input DataであるUserCreateInputData
はUseCases層として考えて良いのか?ということです。UseCases層からInterface Adapters層へのDTO的な働きであるので、正直どちらかに属するという考えがわかっていませんが、本書の絵だとUseCases層にあるように書いてますよね。ここの理解ができていません。仮に、UseCases層であるとしたら、外側のInterface Adapters層にあるべきTUserCreateReqBody
を引数としているのはNGなはずです。なんか私の考え方が間違っている気がしています。
リクエストパラメータのバリデーションはどこで行う?
リクエストパラメータのバリデーションをするならUserCreateInputData
に実装しそうな気がしていますが、どうなのでしょうか?個人的にはここで行っておき、UseCaseが受け取るパラメータは既にバリデートした値、つまり本来受け取るべき値がくることが前提となるように実装した方がしっくりはきます。静的型付け言語だと実装もしやすそうです。ただ、本書のBusiness RulesのサンプルではUseCasesでバリデーションを行っており、なんやねん、と思っています。
感想
- いろいろ読んでわかったことは、要はしっかりと責務を分離して依存関係を守っておけば、多少構造が変わっても(4層が5層みたいになったとしても)それはそれでありでということです。これは本書にも書いてあります。
There's no rule that says you must always have just these four. However, the Dependency Rule always applies. Source code dependecies always point inward.
Reference: Clean Architecture: A Craftsman's Guide to Software Structure and Design (Robert C. Martin Series)
- めちゃくちゃ多人数で大規模開発するときは分離されている分単体テストとかもしやすいのでかなり使えそうだけど、少人数で行う場合ってコード量増えるしどうなんだろうか。なんかチームがやりやすいようにエセクリーンアーキテクチャでも良い気がしてきたが、それはダメんだろうな。
Discussion
気になっていること
の項、僕も前に悩んだ経験があります。その上で、僕の考えが解決の参考になれば幸いです。クリーンアーキテクチャに厳密に従うのは結構大変なので解釈やルールをプロジェクトに柔軟に合わせることが大切だと思います。今回のコードの例で言えば、コンストラクタの引数が
TUserCreateReqBody
になっているのが問題で、冗長でもそれぞれをコンストラクタで渡す必要があります。Usecase層の外側が Web API でも CLI でも動くことを目指す実装が望ましいです。(下記、コードを拝借致しました)
検証したいパラメータが正しいことを知っているのがどこであるべきかだと思います。(少し話は広がりますが)例えば Web API において JSON のシンタックスを保証するのは Usecase ではないはずです。
では各 Usecase が受け取る値が正しいかどうかを知っているのがどこかというと、Usecase だと思います。各値それぞれが正しいかどうかだけでなく値の組み合わせ、場合によっては既存データとの照合を必要とする場合など、これらはビジネスルールに該当するので Usecase でなければ値の検証はできません。
検証失敗時に Presenter(Webフレームワークと相性が悪いことが多いですが...)にエラーに関する情報を渡すことも考えると、データの流れとしては Usecase でバリデーションをするのが自然だろうと現時点では考えています。
ご回答ありがとうございます!!
すごく納得できる説明でした。
そうですね。確かに、プロジェクトによって力を入れるべき項目をどこにするか、どういった実装方法でそれらに対応するかなどはチーム内で統一された認識を持っておくことがまずは大事ですね。(本当は全部できた方が良いのでしょうが。)
単に冗長になるのが嫌という理由でそうすることを避けていたのかも知れません。おっしゃる通りだと思います!
その通りですね。InputDataはシンプルなデータストラクチャでしかなく、Entityでは対処できない制約のあるビジネスロジック(validateなど)は全てUseCaseに任せるのが自然ですね。
改めてご回答ありがとうございました。
勉強になりました!取り入れてみたいと思います。