COMSBIのアプリケーション開発を紹介
はじめに
こんにちは、COMSBIのエンジニアをやっているみなとです。
今回はCOMSBIでのアプリケーション開発を事例を交えて紹介します。
COMSBIの開発では主に開発手法としてDDD(ドメイン駆動開発)を用いています。 アーキテクチャはオニオンアーキテクチャをベースにCOMSBI用にチューニングしながら開発を進めています。 和田卓人氏の「質とスピード」に感銘を受け、その観点から前述した開発手法・アーキテクチャを用いるようになりました。
※注意点: 本記事(2023年7月時点)で紹介するCOMSBIのアプリケーション開発では「DDD」「オニオンアーキテクチャ」の個人的な解釈を交えていますので、もちろん「絶対正解」ではなく「一つの考え方」として紹介します。
厳密には違ってくる部分や妥協してる部分もありますが、みんなで議論してより良い開発を行ってきたいと思ってます。
弊社のプロダクトであるCOMSBIについての詳細は、COMSBIのサイトをご覧ください。
前提
- COMSBIで作ってるAPIの一例
- 実行環境はAWS Lambda
- 実装はTypeScript
- routingとかはexpress
フォルダ構成
1 2 3 4 5 6 7 8 9 10 11 12 13 |
domain ∟ entities -> modelEntityやDTOなどロジックを持たないクラス ∟ repositories -> repositoryのinterface群 infrastructure -> 外部とのやりとりは基本的にここ ∟ {接続先(databaseなど)} ∟ repositories -> repositoryの実装 presentation -> 最初に動く場所 ∟ controllers -> controller ∟ requests -> requestのラッパー的なクラス(domainに入れるか毎回悩む) usecases -> ビジネスロジック群(基本的に1controllerに1usecaseでいいと思ってる) |
基本domain、infrastructure、presentation、usecasesの4つは変わらず中のフォルダは結構案件による部分はある。
例えば、汎用的なロジックとかはusecasesの中にserviceとか作って入れたり
domainの中にerrorとかresponseとかのフォルダがあったりする
一連の流れ
- controllerでrequestを受け取る
- controllerでusecaseを呼び出す
- usecaseでrepositoryを呼び出す
- repositoryで外部とやりとりする
- usecaseでresponseを作る
- controllerでresponseを返す
Controller
1. controllerでrequestを受け取る 2. controllerでusecaseを呼び出す 6. controllerでresponseを返す
1 2 3 4 5 6 7 8 9 |
~~~~~~~~~~~~~~~~~~~前略~~~~~~~~~~~~~~~~~~ { const service = new GetApiKeyService( <- usecase作って new GetApiKeyRepository() ); const response = await service.execute(channel_id); <- usecase呼び出して res.status(response.status).json(response.json); <- 上で返ってきたのをresponseにのせる } ~~~~~~~~~~~~~~~~~~~後略~~~~~~~~~~~~~~~~~~ |
controllerでusecaseを呼び出す以外のことは基本的にやらない if分岐とかが入ってきたらおかしいと思って考え直して欲しい
usecase
3. usecaseでrepositoryを呼び出す 5. usecaseでresponseを作る
1 2 3 4 5 6 7 8 9 10 11 12 |
export default class GetApiKeyService { constructor( private readonly getApiKeyRepository: GetApiKeyRepositoryInterface, ) {} async execute(channel_id: string): Promise<ApiResponse> { const apikeyEntity: ApiKey = await this.getRepository.execute(channel_id); <- repository呼び出し return new ApiKeyApiResponse(200, apikeyEntity.apikey); <- 上でもらったdomainEntityをもとにresponse整形 } } |
ロジックは全部usecaseで賄えばいいと思ってるが、そうするとusecaseが肥大化する気がする。 でもusecaseが肥大化するのは1APIの中でやることが多すぎるということなので、設計を見直すきっかけにもなると思うから問題ないかな。….多分
repository
4. repositoryで外部とやりとりする
1 2 3 4 5 6 7 8 9 10 11 12 |
export default class GetApiKeyRepository implements GetApiKeyRepositoryInterface { async execute(channel_id: string): Promise<ApiKey> { const apikey: ApiKeys | null = await ApiKeys.findOne({ <- DBから取得 where: { channel_id: channel_id }, limit: 1, }); return new ApiKey(apikey?.api_key || null, channel_id); <- domainEntityに詰め替え } } |
repositoryの役割としては外部データ等をentityに詰め替えることだと思ってる。 でもって、repositoryをinterface作るようになってるのは、外部データの取得方法が変わった時にinterfaceを実装したクラスを変えるだけでいいようにするため。 usecaseで使う時に接続先がDBなのかredisなのかとかを気にしなくていいようにするため。 controllerでnew してるからそこで判断するとif分岐入るじゃんとかなるから、その時はfactoryみたいなの作ればいいかなと。
以上が簡単な一連の流れになると思う。
まとめ
- controllerはrequestを受け取ってusecase呼び出してresponseを返すだけ
- ロジックは全部usecaseでやる
- repositoryは外部データをentityに詰め替えるだけ
- repositoryはinterfaceを作っておく
- 案件規模等によって妥協するとかは全然あり(ただ案件内で統一は必要)
ソニックムーブは一緒に働くメンバーを募集しています
Wantedlyには具体的な業務内容のほかメンバーインタビューも掲載しております。ぜひご覧ください。