[CleanArchitecture with Python] Part4: Interface Adapters 層: Controllers の登場


さて、前回のPart3では、main.py は、

MemoHandlerクラスを、

  • Application Business Rules
  • Enterprise Business Rules

に分割しました。

この記事では、前回の章で作成した下記のコードをベースとして解説を進めています。

Part3: https://qiita.com/y_tom/items/9dba5615eb00cd2cc639

1. 成果物に対して、仕様変更依頼を受ける

ここで、

「JSON 形式で POST リクエストを受け付けるように仕様を変更してくれ!!」

という依頼があったとします。

なお、現在は下記のような form 形式での post リクエストしか受け付けていません。

shell script
curl --location --request POST 'localhost:5000/memo/1' \
--form 'memo=momomo'

2. 現在の設計のままで仕様変更依頼に対応する際の懸念点

現状の設計で変更を加える場合のコーディング

現在の設計のまま、jsonを受け付けることができるよう修正するのであれば、下記のようになるでしょう。

@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id: int) -> str:
    #   memo = request.form["memo"]
    memo: str = request.json["memo"] # NEW

    return jsonify(
        {
            "message": MemoHandleInteractor().save(memo_id, memo)
        }
    )


現状の設計で変更を加える場合のコーディングの懸念点

まず第一に、既存のコードを修正する形式での変更になってしまいます。

仕様変更の際、routerのコードも誤って変更しかねないのは、懸念点として挙げられます。

また、リクエスト body の形式だけではなく、今後 json のキーの値が変更・追加になるかもしれません。

例えば

  • memomemo_text 名でフロントから投げたい
  • memo 以外に、memo_author キーを追加する

等です。

リクエスト body 形式の変更(Form・json 等)については、変更の頻度は少ないかもしれませんが、
上記のようなキーの変更や、キーの追加は、サービスを運用していく上では往々にしてあると思います。



@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id):
    # memo = request.form["memo"]
    # memo = request.json["memo"]
    memo = request.json["memo_text"] # NEW
    memo_author = request.json["memo_author"] # NEW
    ...

これらの仕様変更があるたびに、router 内の、

受け取ったリクエストから、キーによって値を抽出する

部分の修正が必要になります。

実際の開発では、

  • フレームワークの仕様変更の頻度
  • リクエスト body の形式、及びキー名の形式変更の頻度

は異なり、リクエスト body の形式、及びキー名の形式変更の頻度のたびに、
フレームワークを管理する router.py ファイルを変更するのは避けたいです。

3. 依頼に対して、どのような設計だったら、スムーズに仕様変更できたかを、CleanArchitecture ベースで考えてみる

i. 設計上の懸念点を再整理

今回、Flask フレームワークでは、requestオブジェクト内から値を引き出すようになっています。

@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id: int) -> str:
    # memo = request.form["memo"]
    memo: str = request.json["memo"] # NEW

ⅱ. どのような設計になっていれば、懸念点を回避して仕様変更できたか

現在、flask_router.py内で、requestオブジェクト内から引き出した値を、MemoHandleInteractor 内の関数に、引数として渡していますが、

flask_router.py から、

requestオブジェクト内から、MemoHandleInteractor 内の関数の引数として適した形式に加工する

部分を、フレームワーク部分を担うファイルから切り出せると良さそうです。

ⅲ. 理想の設計を、CleanArchitecture で解釈した場合

外部から受け取った値を、実際の処理に適した形式に変換するという責務を担う役割として、CleanArchitecture 内では、Interface Adapters 層の、Controllers が存在します。

Interface Adapters層について : https://blog.tai2.net/the_clean_architecture.html

このレイヤーのソフトウェアは、アダプターの集合だ。これは、ユースケースとエンティティにもっとも便利な形式から、データベースやウェブのような外部の機能にもっとも便利な形式に、データを変換する。(省略...)

今回は、

フレームワークから受け取った値(外部の機能にもっとも便利な形式)を、

MemoHandleInteractor.save(ユースケースとエンティティ)に渡すため、

MemoHandleInteractor.save の引数に適した形(ユースケースとエンティティに最も便利な形)に変換する

ためのController を用意します。

Controller の 抽象的なイメージ については、下記記事内の画像がとてもわかり易かったです。

TODO : ゲームの Controller の図を記載すること

ⅳ. 実際のコーディング

.
├── application_business_rules
│   └── memo_handle_interactor.py
|
├── enterprise_business_rules
│   ├── __init__.py
│   ├── entity
│   │   ├── __init__.py
│   │   └── memo_object.py
│   └── memo_repository.py
|
├── frameworks_and_drivers
│   ├── __init__.py
│   └── web
│       ├── __init__.py
│       ├── fastapi_router.py
│       └── flask_router.py
|
├── interface_adapters
│   ├── __init__.py
│   ├── __pycache__
│   └── controllers
│       ├── __init__.py
│       ├── fastapi_controller.py
│       └── flask_controller.py
|
└── main.py


Interface adapters 層 : controller

新たに FlaskController という、request から値を取得し、MemoHandleInteractor の引数に合う形式で戻り値を返す関数を持つ class を用意します。

これにより、外部から受け取った値を、実際の処理に適した値に加工する場合は、この Controller のみを変更すれば大丈夫です。

interface_adapters/controllers/flask_controller.py

class FlaskController:
    def save(self, memo_id, request) -> str:
        # memo = request.form["memo"]
        memo: str = request.json["memo"]
        return MemoHandleInteractor().save(memo_id, memo)

Framework & driver 層

flask_router.py からは、この Controller 呼び出します。
これにより、仮に外部からの値の受け取り方が変更になっても、フレームワークを管理するrouterを変更せずに済みます。

frameworks_and_drivers/web/flask_router.py

@app.route('/memo/<int:memo_id>', methods=['POST'])
def post(memo_id: int) -> str:
    return jsonify(
        {
            "message": FlaskController().save(memo_id, request)
        }
    )

Part4の全てのコードは下記です。

https://github.com/y-tomimoto/CleanArchitecture/tree/master/part4

4. 設計の変化によって、どのような仕様変更に耐えうるようになったか?

さて、今回は、Interface Adapters 層の Controller を活用することによって、
更新頻度の高い、『外部からのリクエスト形式』を、実際の処理に適した形式に変更するという部分を、
フレームワークから切り出すことができました。

これにより、アプリケーションで受け入れることのできるリクエストの形式を変更する際、
既存のWebアプリケーションフレームワークや、ビジネスルールを考慮せずに、コードの修正を行うことができるようになりました。