アプリケーションアーキテクチャ設計に関する覚書(Python)
1. 前提条件
- (1) WEB APIを提供するマイクロサービス
- (2) 補助機能としてCLIやジョブスケジューラを備える
- (3) マイクロフレームワークを利用
- (4) アプリケーションはコンテナで実行される
2. 基本的な考え方
MVC + Uアーキテクチャ(Model/View/Controller + Usecase)
- 基本的なアーキテクチャはMVCを踏襲
- アプリケーション層とドメイン層のファサードとして
usecase
層を配置
- 少し前は
Service
層と呼ばれていたもの
ドメイン層はアプリケーション層/インフラ層から独立
- ドメイン層のモジュールはアプリケーション層やインフラ層に依存しない
- ドメイン層だけ引き抜いて移植可能な設計を目指す
- ドメイン層を独立させておく考え方は
Clean Architecture
やオニオンアーキテクチャ
のそれと同じ
3. プロジェクト構造
プロジェクトディレクトリの構造
.
├─ doc/
├─ docker/
├─ schema/
├─ src/
├─ static/
├─ template/
├─ test/
├─ .env
├─ .gitignore
├─ docker-compose.yaml
├─ poetry.lock
├─ poetry.toml
├─ pyproject.toml
└─ README.md
doc/
- アプリケーション固有のドキュメントを格納する
- 原則的にMarkdownで記述する
- ADR的なドキュメントが格納されることを想定
docker/
- dockerに関する設定ファイルやDockerfileが格納される
schema/
- DBのDDLや初期データなど
- マイグレーションツールは任意のツールを使う
src/
static/
template/
- テンプレートエンジンのテンプレートファイルを格納する
- HTMLやXMLのテンプレートが格納されることを想定
test/
.env
-
.env
設定ファイル
- 環境毎にファイルを作る(
.env.prod
、.env.local
)
.gitignore
docker-compose.yaml
- docker-composeの設定ファイル
- 基本的にローカル開発用
poetry.lock
poetry.toml
-
poetry
の設定ファイル
- ほぼ
poetry
の.venv
ディレクトリをプロジェクトディレクトリ内に作る設定しか入れていない
[virtualenvs]
in-project = true
pyproject.toml
-
poetry
の設定ファイル
- プロジェクトが依存するライブラリのリスト
README.md
4. src以下の構造
src以下のディレクトリツリー
src/app
├─ config/
├─ controller/
├─ exception/
├─ middleware/
├─ model/
├─ repository/
├─ usecase/
├─ validator/
├─ view/
├─ app.py
├─ cmd.py
└─ job.py
config
- アプリケーションの設定を管理
- 典型的には
conifig/config.py
だけが配置される
-
conifig/config.py
は環境変数から設定値を読み込んだり、アプリ側が使いやすい構造の提供を行う
- こんな感じ↓
import os
BASE_URL = os.environ.get('BASE_URL', '')
APP_HOME = os.environ.get('APP_HOME', '')
TASK_STATUS = [
{
'name': 'new',
'screen_name': '新規',
'code': '01',
'order': 1,
},
{
'name': 'working',
'screen_name': '着手',
'code': '02',
'order': 2,
},
...
]
...
controller
- MVCにおける
Controller
- Clean Architectureやオニオンアーキテクチャにおける
Application Service
- リクエストハンドリングとレスポンスを担当する
- アプリケーションの境界層でもある
- HTTPリクエストに関するロジックは
controller
で完結させる
- 典型的な実装ではフレームワークのルーターにマッピングされる関数になる
- リクエストハンドリング/ユースケース呼び出し/レスポンスデータ生成など責務が集中しやすいポイントなので、ロジックは極力ヘルパー関数/クラス化することが重要
exception
- アプリケーション独自例外が配置される
- 典型的には
exception/exceptions.py
内に独自例外が列挙される
- アプリケーションの規模によっては作らないこともある
middleware
- アプリケーション層やドメイン層のカテゴライズされないモジュールが配置される
- 主にフレームワークの拡張やDB接続、メール送信などインフラ層に関するモジュールなど
-
middleware
に配置されたモジュールは他の全てのレイヤーから利用してもよいが、依存関係には注意を払う必要がある
- 以前なら
common
やutil
と命名されていたような名前空間
-
middleware
という名称はLaravel
やNuxt.js
から借用した。正直common
でもutil
でも何でもいいと思う
- 割とゴチャゴチャしやすいところでもあるので、モジュールの依存関係には注意が必要
model
repository
- インフラ層を抽象化したモジュールが配置される
- DB操作やAPIリクエストなどが配置される
- 任意のORマッパーを使ってもいいし、SQLを直接記述してもよい(ただし、ドメイン層に影響を与えてはいけない)
usecase
- アプリケーション固有のビジネスロジックのファサードが配置される
-
controller
などアプリケーション層から呼び出されることを想定
- ドメイン層の処理をカプセル化し、アプリケーション層向けのインターフェイスを備えたモジュールとなる
-
usecase/view_toppage.py
のように動詞_目的語
の命名が付けられる
-
usecase
層はアプリケーション層から独立していなければならない
- つまり
usecase
内でHTTPリクエストに直接アクセスしたり、Cookie情報にアクセスしてはなず、必要なデータはパラメータとして与えられる必要がある
validator
- バリデーションロジックが配置される
- 好みのバリデーションライブラリを使ってもいいし、自前で実装してもよい
view
- MVCのViewに関するロジックが配置される
- とはいえ、APIを実装する場合は空のままにしておくことも多い
- HTMLを生成する場合は、テンプレートエンジンのラッパーとヘルパー関数を配置している
app.py
- WEBアプリケーションのエントリーポイント
- ルーティングとリクエストハンドラ関数をマッピングする
- 大抵のフレームワークにはルーティングとコールバック関数をマッピングする機能があるのでそれを利用する
# Flaskの場合
app.add_url_rule("/", view_func=index)
# Bottleの場合
import bottle
app = bottle.Bottle()
app.route('/', ['GET'], index)
- 起動時の初期化処理が必要なこのモジュールに記述するか、このモジュールから呼び出すようにする
- フレームワークの拡張機能(フックやミドルウェア)もこのモジュールで設定する
cmd.py
# coding: utf-8
import click
from usecase import xxx_usecase
@click.group()
@click.pass_context
def main(context):
pass
@main.command()
def xxx():
xxx_usecase.run()
if __name__ == '__main__':
main()
job.py
# coding: utf-8
import schedule
import time
from usecase import xxx_usecase
# 毎時0分に処理実行
schedule.every().hour.at(":00").do(xxx_usecase.run)
if __name__=='__main__':
while True:
schedule.run_pending()
time.sleep(1)
5. バリデーション
外部入力のバリデーション
- ユーザー入力のチェックやフォーマットのバリデーションはController層で検証する
データの整合性の関するバリデーション
- ドメインモデルの健全性のチェックはユースケース層で検証する
6. CQRS
更新系と参照系ではドメインモデルを分ける
- 登録/更新系の処理はドメインモデルを用いて型チェックを駆使する
- 一方、参照系は変化が激しかったり、UIや利用先アプリケーションの事情に依存することもあるので出力データとドメインモデルが一致しないことも多い
- 参照系とドメインモデルを一致させることは潔く放棄して参照系の出力データはそれ専用のデータ構造を用意する
7. テスト
ユニットテスト
- ユニットテストはUseCase層以下を主たる対象にする
- コントローラ層は結合テストでカバーする
- コントローラ層のテストはモック対象が多くなりがちで有効なテストが行いにくい
- コントローラのインプットとアウトプットの試験はもはやアプリケーションの結合試験と同義になることが多い
結合テスト
- 実際にアプリを動作させて挙動を確認するテスト
- 適切に動作することを確認する
- APIであれば実際にリクエストを送信して動作確認を行う
- APIの場合、リクエストを自動的に実行する仕組み(テストスイート)を作成する必要がある
8. FAQ
Clean Architectureにしていない理由は?
- Clean Architectureは過剰である
- 現実的な開発案件でClean Architectureが提供するレベルの柔軟性が要求されることは少ない
- また、アプリケーションの構造も複雑化しがちで引き継ぎに問題を抱えているケースが多い(ように観測される)
- そのような柔軟性が要求された時、初めてClean Architectureを検討すればよい
- 一方、MVCアーキテクチャは多くのエンジニアにとって馴染みがあるので、経験の浅いエンジニアでも少ない学習コストで戦力化することが期待できる
リポジトリが永続化手段に依存しているのはよいのか?
- DBを丸ごと差し替えたいという要求はあまり一般的ではないのでYAGNI原則に従い、必要にならない限り直接依存させても問題ないと思う
- そのような要求が発生した時に考えれば済むだろうという判断
コントローラの責務が多いように見えるが?
- ある程度は仕方がない
- コントローラ関数に処理を長々書くことはせず、ヘルパーモジュールにロジックを逃がすことを重視する
何故○○(任意のフルスタックフレームワーク)を使わないのか?
- フルスタックフレームワークの流行は常に流動的である
- ブームが過ぎ去ると一気に人気が落ちたり、開発が停滞してしまう
- フルスタックフレームワークのバージョンアップは面倒(影響範囲が広いので気軽にやりたくない)
- フルスタックフレームワークのバージョンアップは影響範囲が大きい割にビジネス的な利益に直結しないので後回しにされがち
- そんなわけで、稼働中のアプリケーションのフレームワークのバージョンアップは行われないことが多く、気が付いたらバージョンがかなり古くなっていたという事態が発生しがち
- マイクロフレームワーク + 各種ライブラリの組合せで構成されたマイクロサービスなら個々のバージョンアップを小さなタスクとして行うことが可能で持続可能性の点で優れている
- マイクロサービスはそもそも規模が小さいのでフルスタックフレームワークは機能過剰という点も大きい
各モジュールの名前空間は複数形にしないのか?