COMSBIのアプリケーション開発を紹介

COMSBIのアプリケーション開発を紹介

  • このエントリーをはてなブックマークに追加

はじめに

こんにちは、COMSBIのエンジニアをやっているみなとです。
今回はCOMSBIでのアプリケーション開発を事例を交えて紹介します。

COMSBIの開発では主に開発手法としてDDD(ドメイン駆動開発)を用いています。
アーキテクチャはオニオンアーキテクチャをベースにCOMSBI用にチューニングしながら開発を進めています。
和田卓人氏の「質とスピード」に感銘を受け、その観点から前述した開発手法・アーキテクチャを用いるようになりました。

※注意点: 本記事(2023年7月時点)で紹介するCOMSBIのアプリケーション開発では「DDD」「オニオンアーキテクチャ」の個人的な解釈を交えていますので、もちろん「絶対正解」ではなく「一つの考え方」として紹介します。
厳密には違ってくる部分や妥協してる部分もありますが、みんなで議論してより良い開発を行ってきたいと思ってます。

弊社のプロダクトであるCOMSBIについての詳細は、COMSBIのサイトをご覧ください。

前提

  • COMSBIで作ってるAPIの一例
  • 実行環境はAWS Lambda
  • 実装はTypeScript
  • routingとかはexpress

フォルダ構成

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とかのフォルダがあったりする

一連の流れ

  1. controllerでrequestを受け取る
  2. controllerでusecaseを呼び出す
  3. usecaseでrepositoryを呼び出す
  4. repositoryで外部とやりとりする
  5. usecaseでresponseを作る
  6. controllerでresponseを返す

Controller

1. controllerでrequestを受け取る
2. controllerでusecaseを呼び出す
6. controllerでresponseを返す

~~~~~~~~~~~~~~~~~~~前略~~~~~~~~~~~~~~~~~~
{
  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を作る

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で外部とやりとりする

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には具体的な業務内容のほかメンバーインタビューも掲載しております。ぜひご覧ください。

  • このエントリーをはてなブックマークに追加

記事作成者の紹介

みなと(エンジニア)

comsbiでエンジニアやってます。 趣味はダーツとボルダリングです。 よろしくお願いします!

関連するSONICMOOVのサービス

システムエンジニア募集中!

×

SNSでも情報配信中!ぜひご登録ください。

×

SNSでも
情報配信中!
SONICMOOV Facebookページ SONICMOOV Twitter
Wantedly 採用情報はこちら

新着の記事

mautic is open source marketing automation