LoginSignup
3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-27

さて、前回の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 内の関数の引数として適した形式に加工する

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

https___qiita-image-store.s3.amazonaws.com_0_293368_7ce1fb10-504e-16e0-8930-278b8a7f942d.jpeg

ⅲ. 理想の設計を、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 を活用することによって、
更新頻度の高い、『外部からのリクエスト形式』を、実際の処理に適した形式に変更するという部分を、
フレームワークから切り出すことができました。

https___qiita-image-store.s3.amazonaws.com_0_293368_7ce1fb10-504e-16e0-8930-278b8a7f942d.jpeg

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2