前置き
- あくまで、個人でやっていてやりやすい私的の設計である。仕事においては、業務の内容/フェーズに合ったコード設計を採用するべし
- CleanArchitectureを意識してはいるが、あくまで意識しているだけであるため原理からは異なっている可能性がある (原理主義者様からの意見/指摘は大歓迎です)
対象コード
- Discord上から対象のホストに対してpprofをとって結果をS3に上げるBot
- 将来的にはDBをSQLiteではなく別サーバーにしたい
- 将来的にはWebAPIとしても提供したい
前提に置く思想
- アプリケーションのビジネスロジックを副作用から守り、テストしやすい状態を維持する
- インフラ(DBや、DiscordBotの基盤そのもの)を置換可能な物とし、移植性を高める
CleanArchitectureに関する議論でよく「DBやフレームワークなんてそうそう変える物じゃないんだから、インフラの抽象化は過剰である」という話が上がるが、今回のアプリは将来的にWebAPIとしても提供したいし、DBもSQLiteから他のDBに移行したいのでインフラの抽象化は必要である。
ディレクトリ構成解説
.
├── application
├── cmd
├── config
├── contexts
├── controllers
│ ├── params
│ └── validators
├── db
├── handlers
├── infrastructures
├── models
├── presenters
├── repositories
└── services
アプリケーションのエントリポイントから近い順に解説していく
cmd
- アプリケーションのエントリポイント
- アプリケーション全体の初期化もここで行う
- DBオブジェクトの生成
- DiscordBotの初期化
- Webサーバーの立ち上げ
config
- アプリケーションの設定を管理
- 環境変数の値取得とかもここ
contexts
- アプリ内で使用する共通の値などを管理する (DiscordBotのSessionやWebAPIのcontextなど)
- これが参照されるのはcontrollerまでで、以降のアプリ部分では使用されない
handlers
- アプリケーションへの生リクエストを受け取り、パラメータへの変換および各機能へのハンドリングを行う
- DiscordBotなら、"DiscordHandler"、WebAPIなら"WebAPIHandler"のように動作基盤によって複数のハンドラーを作成する
- 動作基盤によってパラメータの渡し方が異なるので、複数作成する必要がある (例: WebAPIならパラメータを渡す時はパスパラメータやBodyだが、DiscordBotはOptionからパラメータを取得する)
- controllers/pramsが提供するパラメータの変換関数を用いてパラメータを呼び出すコントローラーに合った形に変換する
controllers
- アプリケーションの各機能を呼び出す
- パラメータに応じて呼び出す順序や条件などを制御する
- 動作基盤の差を吸収して、アプリケーションに純粋なパラメータを渡す
- DiscordBot用のコントローラーやWebAPI用のコントローラーを作成する
- パラメータのバリデーションを行う
- applicationの具体に依存する
正直controllerからもcontextの依存を抜いて移植可能な状態にするかどうか迷っている。
しかし、流石にHandlerの責務が重くなりすぎる気がする。
presenters
- アプリケーションの結果を動作基盤に基づいて変換しレスポンスを作成する
- WebAPIならHTMLを返し、DiscordBotなら返信を作成する
application (アプリケーション内部に閉じている)
- アプリのビジネスロジック
- Domainにおけるロジック
- これ単体では副作用が発生しないように、repositories/servicesよりも外の世界には依存しない
- controllerには具体を提供する (usecaseで抽象化されない)
models (アプリケーション内部に閉じている)
- アプリ内で使用する値の定義
- 必ずしもDBのテーブルと1対1になる必要はなく、あくまでアプリケーションの内部で使用する値の定義をおこなう
- Domainにおけるモデル
repositories/services (アプリケーション内部に閉じている)
- アプリケーション内で使用する外部への操作のinterface
- DB操作や、API呼び出し等
- アプリケーションはinterfaceに依存することで、外の世界への依存を避けることができる
- repositoriesは値の永続化系、servicesはそれ以外のものを詰め込んでいる
infrastructures
- アプリケーション内で使用する外部への操作の具体的実装
- repositoriesとservicesで区別していないが、膨らんできたら分割するかも
上記コード設計の工夫
- 上記の中で、application、models、repositories/servicesはアプリケーション内部の世界に閉じており、外部への依存は存在しない。
そのため、動作基盤やDBを変更する際はhandler、controllers、presenters、infrastructuresのみを変更すればよく、アプリケーションのビジネスロジックそのものは同じ動作が保証される。 - controllersはapplicationの具体に依存している(usecaseで抽象化されていない)が、これはビジネスロジックが置換されることについてアプリケーション内で差を吸収する必要がないと判断したためである (ビジネスロジックが変わるならそれはもう全部変えた方がいいため)
おわり
アプリを作っていると抽象化中毒になってきてどんどん抽象化しまくりたくなってくる。
実際は無限に抽象化すればいいわけではなく、ある程度具体に依存しなければアプリケーションの意味が希薄化してしまう(OS等のプラットフォームならまだいいが、業務アプリでは避けた方がいい)上に、とにかく書く量が増えてきて大変になる。