はじめに
クリーンアーキテクチャやヘキサゴナル、オニオンといろんなアーキテクチャがあって、頭がこんがらがります。
ぶっちゃけみんな言ってることは一緒で同じことを違う切り口から言ってるだけのような・・・
今回、複数のモジュールを作成したので、その中でもライトなものとヘビーなものの2つのアーキテクチャの構成を備忘録も兼ねてまとめておくことにしました。
インフラについてはこちらにまとめています。
作ったのはどんなプロダクトか?
作成したプロダクトの概要としては
「顧客のwebアプリ上でサーベイを配信する」 ことが出来るプロダクトです。
もちろんただ配信するのではなく以下の機能などもあります。
- 高精度なユーザーの属性ターゲティング
- 適切なタイミングで配信するための行動ログターゲティング
- 広告配信システムのようなフリークエンシーなどの多数の機能
※ 複数サーバーがありますが、配信サーバが一番の重要な要です。
配信サーバの構成
❰hisakawa❙~/Documents/work/xxx(git:main)❱✔≻ tree -d
.
├── adapter
│ ├── controller // input,outputの変換処理をpresenterにお願いしつつ、usecaseを呼び出します。
│ └── presenter // handlerからのinputをusecaseに渡す引数に変換。usecaseからの返却値をhandlerに渡すオブジェクトに変換。
├── cache // 今回リクエスト数が多かったので、x秒日1度RDBから取得しin-memoryキャッシュに乗せる仕組みを作りました。
├── config // 環境変数や環境差分は、全てこちらで吸収するようにしてグローバルに参照される状態にしています。
├── domain // DDDで言うところのDomain層を参考にしています。
│ ├── model
│ │ ├── question // 可視性のためある程度の枠組みごとmodelの中でもパッケージを分けていました。
│ │ ├── survey
│ │ ├── targeting
│ │ └── user
│ └── repository // Interfaceとして定義しています。
│ └── service // surveyとuserなど複数のmodelに跨る処理はservice層に押し込むようにしていました。
├── infrastructure
│ ├── handler // 最低限のリクエストパラメータのValidationなどを行い、controllerを呼び出します。
│ └── repository // domain.repositoryの具象パッケージで、データベースへの接続部分。返却値はdomainに定義したEntity
├── injection // レイヤーごとに疎結合にするためにレイヤーを跨ぐ際はインタフェースに依存させるようにしたました。なのでDIをここで全て定義しています。
├── usecase // domainの操作の流れを意識して書いていましたが、domain.serviceの内容も結構入っちゃってることがありました。最初はそうして徐々にdomain.serviceに切り出していくのがいいのかな?と勝手に思ってます。
└── util // ライブラリを導入するほどでもない、共通して薄く利用する関数をこちらに定義していました。
❰hisakawa❙~/Documents/work/xxx(git:main)❱✔≻
※ memoがき程度に、コードで迷って部分などを残しておきます。全て変数名など書き換えているので実際のものとは異なります。
domain.repository層
ここだけすごく悩んだので残しておきます。
- 今回x秒に1度RDBのデータをcacheに積み、repositoryではcacheに対して問い合わせるような形を取っています。
- cacheに既に存在するuser情報と、リクエスト時に受け取る最新のuserの情報を1つにしてuser Entityとして扱いたかったです。
- ここは自信がないですが、repository層をdomain factoryのようなものと捉えました。
- なので引数に必要な最新情報を渡し、repository内でcacheの情報と引数の情報を1つにしてvalidationするようにしました。
- リクエスト時のスピードを加速させるために、cache時にvalidationしてましたが、
- 2点目のパターンの時は、このrepository内でvalidationしたり、validationをどのタイミングでやるかが非常に悩みました。
type UserRepository interface {
FindById(userId string, arg xxx.リクエストの最新情報) (*user.User, error)
}
最低限の大きさのモジュールでの構成
lambdaなどにデプロイするプロジェクトでは、コード量もそこまで多くないので、シンプルに以下のような構成にしました。
❰hisakawa❙~/Documents/work/xxx(git✱main)❱✔≻ tree -d
.
├── config // 環境変数や環境差分は、全てこちらで吸収するようにしてグローバルに参照される状態にしています。
├── dao // dbへの接続部分と戻り値はmodel層に定義したEntityとして返却されるようにしています。
├── client // firebaseやrdbなどの接続に必要な情報を書くようにしています。
├── handler // main.goなどはこちらに配備し、cleanで言うところのcontroller,presenter,usecaseの内容を記載してまいます。
└── model // model層だけテストコードも書きたいのでしっかり目に作り、他は手を抜いています。
❰hisakawa❙~/Documents/work/xxx(git✱main)❱✔≻
最後に
プロジェクトを作るごとに、パッケージ構成にすごく悩むので、備忘録として残しておきました。
ぶっちゃけチームで修正を行うならこの構成でも意味があったかと思いますが、結果1人で修正することばかりだったので、
もう少しシンプルな構成でも良かったかも・・・と思ったりしています。
初期からdomain層部分だけは、非常に丁寧にテストも書くようにしたので、感覚ですがバグも以前と比べ格段に少なくできたと思います。