🪡

Clean Architecture の矯正装置を作った ― 動くコード ≠ 正しい設計

に公開

はじめに

まず、大前提として「このやり方が正しい!」というつもりは少しだけあるんですが、基本的に間違えている可能性も高いと思っています。

私が問題と思っている点を説明すると、実際にCAをやる際に

「コード上では依存方向が守られている」

実装レベルで見れば、定義場所や参照方向などがあっているケースが多いと思います。
ただ、そうだったとしても設計として見てみると、依存関係が狂っているケースが多いと思っています。

私のCAの理解として

  • Domain ー ビジネスルールやドメインなどの構造定義
  • Usecase ― 純粋なビジネスロジックであり、外側の影響を受けない
  • Controller/Presenter ― 外界とUsecaseの間を調整するAdapter(翻訳機)
  • Gateway(Repository) ー 記録装置やデバイスとUsecaseを繋ぐ中継装置

と考えています。

コレを主軸に考える場合、Usecaseにあわせて設計されるべき境界部分、つまり Usecaseの IN/OUT は、Usecaseの都合で決めるものと考えています。
Usecaseは純粋なビジネスロジックであり、外側の影響を受けない(外の都合など知らない)という状態が健全であると考えています。

ただ、Controllerの都合で UsecaseのINが決まってしまっていたり、Presenterの都合で整形された情報が、UsecaseのOUTとして出ていく実装になっていることが結構あるかなというのが体感でして
このあたりの思想が入り込んでしまった結果、再利用可能なはずのUsecaseが使い回せないのはもったいない!と、思想の乱れを構文で正す矯正装置を考えてみたわけです。

矯正であれば「正しい依存とは何か」が必要

一番問題だと思っていたのは「どこから作るのか?」だと思っています。

私の体感で申し訳ないのですが、

まず、UIが出来ていて、UIにあわせてControllerが作られ、その入力をUsecaseに渡して処理させて、Presenterに送り見た目に持っていく

UI → Controller → Usecase(Repository → Database)→ Presenter → View

こんなイメージです。
その結果、Controllerの出力に構造につられてUsecaseが作られることで、コードを書く場所は正しいけど、設計としてはControllerの思想がUsecaseに入り込むという事象が起きるのではないかと考えました。

「ControllerがAPI入力を整形するためにDTOをねじ込んだ瞬間、Usecaseは外界のフォーマットを知ってしまう。」

みたいな、ヤバい状態を作り出さないために、私が正しいと考える形は

Usecase 以下、順不同(Controller、Presenter、Repository/Gateway)

これはなぜかというと、
前述の通り「Usecaseは純粋なビジネスロジックである」この一言に尽きます。

Usecaseは純粋なビジネスロジックであり、外側の影響を受けない。
そして、Clean Architecture全体の“責務の中心”として、まわりの構造を定義する。

つまり、Usecaseが自分の都合の良い入出力を定義して、まわりがそれにあわせて作られていくという形が、Clean Architecture の思想を守るうえで重要だと考えました。

というわけで、「Usecaseからしか作れなくしてしまえ」と今回の仕組みを作ったのです。

コード

https://github.com/risk/ts-playground/blob/main/src/caFramework/caFabricator.ts
こちらに全体があります。特に依存等ありませんので、お手元の環境で実行できると思います。
npm install tsxnpx tsx caFabricator.ts で動きますので、実際にお試しいただくことも出来ます。

今回のコードは「構造を編む」というテーマで作っていまして、それっぽい名前にしてます。

fabric ― 編まれた構造そのもの
fabricator ー 構造の編み機

って感じですね。今回はコレを使ってClean Architectureを編んでいきます。

編んでみたところ

https://github.com/risk/ts-playground/blob/ecfdfeecf7a2a76cd4b113eb043070f446c602d1/src/caFramework/caFabricator.ts#L184-L208
実際に編んでるところがこの辺です。

この仕組みのポイントとして、必ずUsecaseからしか作れない仕組みにしています。
まわりのことは考えずに、まず純粋なユースケース部分を作りましょう。
Usecaseが出来上がると、そこに付随するものを編み込めるようになります。

weaveController であれば、Controllerを編み込み
weavePresenter であれば、Presenterを編み込み
weaveRepositories であれば、Repositoryを編み込みます。

この時に重要なのは、Usecaseが定義した入出力でしか、これらのメソッドを編み込めなくなっているという点です。

Controllerであれば、入力は自由に決められますが、出力はUsecaseの入力に縛られます。
Presenterであれば、出力は自由に決められますが、入力はUsecaseの出力に縛られます。
Repositoryであれば、Usecaseがこう使いたい という形のものしか受け入れてくれません。

つまり、

Usecaseが一番都合のいい形で定義したものにたいして、まわりが合わせるしか無い

という状態になっているわけです。
矯正装置という意味がおわかりいただけたでしょうか。

実際の中身はどうなってんの?

こうなってます。
https://github.com/risk/ts-playground/blob/ecfdfeecf7a2a76cd4b113eb043070f446c602d1/src/caFramework/caFabricator.ts#L47-L182

軽く解説すると、

仕組みとしては、メソッドチェインを返すために、各weave系メソッドは this を返すのですが、その時に追加された型情報を更新して、fabricatorを作り直しています。
(Repositoriesだけは、渡されたものをクロージャーでまとめてしまい、メソッドをカリー化しちゃいます)

構造を編んでいくと、段々型情報が揃っていき、最終的に実行可能になるという感じですね。

実際に実行する run 部分は、呼び出しと結果を次のメソッドに渡す動作だけです。

Controller → Usecase → Presenter に情報が順番に渡っていくことで処理が行われます。

最後に

私が「Clean Architecture」をプロジェクトに適用するのが難しいと思っている部分は、コードをどう作るのか? ではなく、設計の思想を一貫させる のが難解であり、そここそが重要なアークテクチャだと思っています。

作る際、定義を考える時に「そのコードがどんな意味を持つのか」であり、「コードでは動くけどなんか違う、めんどくさい」になってしまうと、結構な手間をかけて作ったものが台無しになります。
そうではなく、「綺麗に分割できてめっちゃ気持ちいい!」って感じられるようになっていければと思っています。

で・・・

元々ライブラリ化しようと思ったのですが、あまりにガチガチに固めすぎて、

使いやすい ではなく 決められた方法以外は絶対に許さん

という、かなり思想が偏った出来になってしまったので、一旦コード公開に留めることにしました。
r-fabricという名前で「構造を織る」をコンセプトに作ってたものの断片でして、クラスやメソッドの名前がなんかカッコつけようとしているのはその名残ですw

Discussion