記事の内容

オニオンアーキテクチャの構成とデータの流れの解説の概要を説明
ありがちなそれぞれの責務に関して少し詳細に設計理念を記載し、理解を深められることを期待している
また、これもありがちだがアプリケーション層の肥大化問題に対し、presentation層を追加することでアプローチしている
そういった意味での少し改造版の意味

対象者

・バックエンドでチーム開発を行なっている人
・クリーンアーキテクチャなど、設計を勉強中の人
・オニオンアーキテクチャを勉強し始めている人

データの流れ

以下のmain_table とは、最も主体となるテーブル名を指す
例えばユーザーテーブルと、ユーザーに紐づくユーザーインフォテーブルが存在したと仮定し、
その場合のメインはユーザーテーブルがメインのため、
main_tableはユーザーインフォテーブルではなくユーザーテーブルとなる。

markdown
main.py
↓
v1 / router.py
↓
end_points / <<main_table>>.py
※ domainは基本DBのテーブルに準拠する(e.g. book)
↓
handler / <<main_table>>.py
↓
application / <<main_table>> / presentation.py
※ entityへのマッピングとバリデーション
↓
application / <<main_table>> / service.py
↓
infra / dynamo_db / <<main_table>> / repository.py
※ infrastractureよりinfraの方が短くていいかなと。
e.g. infra / oracle / <<main_table>> / repository.py
↓
application / handler / <<main_table>>.py
↓
return

責務は以下に記載

※中間パスは簡単のため割愛する

handler層

  • エラーハンドリングと、statusコードの設定もこの層の責務とする
    • (例:fastAPIのstatusモジュールをimportしての使用もOK)
  • リクエストのパラメータ、もしくはリクエストボディをaplication層に渡す
  • 上記でEntitiyとしてマッピングされた返り値をservice層に渡す
  • 返り値を再度presentation層に渡して、加工した返り値をレスポンスとして返却する
  • handlerのクラスとそのコンストラクタの定義の責務がある

※当初は名前をinterface層としたかったが、
interfaceという名前が被りやすいのでhandler層とする

presentation層

  • リクエストのEntityへのマッピングとバリデーション
    • ただし、validationを@validator(pydentic:BaseModel)にて実装する場合、command_model層などでvalidation処理を実装することも許容とする
  • レスポンスの整形、処理(e.g.レポジトリーから取得したデータを暗号化する等)
  • メソッドのみの実装(handler層、service層が使用する)

service層

  • ビジネスロジックを記載する
  • e.g. crud処理を記載
    • (crud処理に限らない処理を作成可能だが、コード規約に基づいたメソッドの行数制限の範囲で適度に分割する、e.g. find_all も許容)
  • service層のクラス(インターフェース)は使用するレポジトリーのインスタンスを受け取り生成される(endpointが実施)
    • コンストラクタの引数として受け取って、生成する
    •  ※ constructer等で定義
  • serviceの実態のコンストラクタの実装
  • 基本query_***とcommand_***にわける。
    • POST系などリクエストボディを扱う場合はcommand_modelにリクエストボディを受け取るクラスの定義を実装し、presentation層でバリデーションを行う。
      • それ以外はquery_modelやquery_serviceを用いる。
        • (リクエストボディと合致させるため、基本パスパラメータや、クエリーパラメータはcommand_modelに含めてはいけない)
  • どういったエラーメッセージを出したいかも、ビジネスの責務なので、この層で実装する
    • 例えば、DTOに依存するNoResultFoundなど(from sqlalchemy.exc import NoResultFound)をインフラ層で使用、判別し、service層には、それに応じた結果を変更し値を返す
      • それを受けたサービス層がエラーメッセージを振り分ける形が望ましい
  • 補足:
    query_model内にレスポンスの型だけを定義する案もあるが、
    それは実装して不便を感じたらそちらにしても良い、
    ただごちゃるのが一番杞憂すべきことなので、簡単にリクエストパラメータを使う時はcommand_**を使うんだなという理解でしばらく進むのをお勧めする。
    validatorをfastAPIのendpointに寄せ切れる場合、query_model層を使わない方針もなしではない。(ただしそれでもcommand_modelとcommand_queryは必須想定)

application / common_process層

  • サービス層に存在する範囲での共通処理を記載する。例としてcommon_process/logger.pyにlogger関連の処理を定義する
  • ビジネスロジックは極力この層には持ち込まないことを推奨するし、気持ち悪いという論調がチームで強まったら削除をしても良い

repository(dynamo_db)層

  • EntitiyをDTOへ変換する処理を記載する
    • **dto.pyに記載
    • DTOクラスのメソッドとして、to_entity(self)とfrom_entity(self)を実装する
  • service層のビジネスロジックを実現するための実際のormapper処理を行う (**repository.pyに記載)
  • この層から見てdomain層に対してのみ参照、利用可能。それ以外を参照してはならない。
  • 基本は使用されるだけとする
  • i_repositoryのインターフェイスに対する実態のクラス及びコンストラクタの実装を行う

domain / entity層

  • DB種類によらずプログラム内で取り回しが行いやすいentityクラスを定義し提供する
    • DTOとは異なる

domain / i_repository層

  • infra / repository層で扱うインターフェースを定義し提供する(クラスのメソッドを含み)
    • 実態(implementation)はinfra / dynamoDB層配下に定義し、endpointがnewする。
    • 異なったDBにするなどといった変更の場合もこの層の修正は発生しない設計

domain / <> / error.py層

  • エラーメッセージやレスポンスのコードを定義する
  • domain / error / common.pyに共通のエラーメッセージを作成することを許容とする

end_points層

  • 該当のドメイン(DB)に関する以下のインスタンス生成を行いエンドポイントを生成、routerに渡す
    • リポジトリーのインスタンスの生成(ormのread or writerインスタンスを渡して生成)
      • 並びにコンストラクタの実行
    • サービスのインスタンスの生成(リポジトリーのインスタンスを渡して生成)
      • 並びにコンストラクタの実行
    • インターフェースのインスタンスの生成(サービスのインスタンスを渡して生成)
      • 並びにコンストラクタの実行

test層

  • APIのテストはマスト。
  • 単体テストについてはテストのコスパを鑑み、プロジェクト的に有用と判断できたタイミングで以下ディレクトリ構成を参考に追加していく。
  • テストをどこまで書いていくかはまた一ヶ月後などに議論予定
    • (APIのテストでカバーできない範囲は書いたほうがいいかも)

e.g.

tests/books/mock.py
tests/books/test_api.py
tests/books/test_ application.py

今後の発展

e.g.
初期ではValueObject、schemeなど省き、
初期の理解速度と開発速度を考慮し少し簡易なアーキテクチャでのスタートとする。
ビジネス要件や開発者のスキルセットに応じて、スキーマを拡張することは推奨される。
ただし最低限オニオンアーキテクチャの構成、理念を理解した上での拡張が必須と思われる。

今後の課題

oracleなどにアクセスするときに、requestの設定を共通で使う場合があったら、
どこにその設定を置くべきか論はまた、コードができたらコードベースでの議論が望ましいと思われる。

アーキテクチャ採用理由

e.g.
担当しているシステムの特徴として、DBの構成としてDynamoDBが望ましい側面があったり、一方で外部システムとしてOracleのRDBを使用することや、
それ以外の他外部システムの要件を整理するのに一定の時間がまだ継続して必要であった。
そのためDB種類を変更する必要が将来的に発生した場合に、
柔軟に対応でき尚且つそれ以外の部分に関しては、変更の必要がなく統一性を高められる設計であるオニオンアーキテクチャを採用した。

オニオンアーキテクチャのメリット

複数あるが、最も大きいのは、ビジネスロジックがDBの種類に依存しなくて良くなること
サービス層がDB種類と直接関係のない、i_repositoryを用いて実装を行えることが最大のメリット
これをドメイン層のインターフェースを介することで柔軟に対応できるようにしたこと
(付随して、処理の一方向性や、役割の分担などさまざまなメリットがある)
処理の流れが片方向で限定されるので、コードが難しくなりづらい。