5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[CleanArchitecture with Python] Part6: Interface Adapters 層: Presenter の登場

Last updated at Posted at 2020-09-27

前回のPart5では、DTOを活用して、
各レイヤー間の値のやりとりをスムーズにしました。

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

Part5 : https://qiita.com/y_tom/items/cd1410a9cc1f228ea609

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

ここで、

「各エンドポイントのレスポンスに、1 行広告をつけられるようにしよう!!」

という仕様変更依頼を受けたとします。

例えば、現在は、/memo/123 というエンドポイントに対して、メモを取得するためのGETリクエストを投げたとき、

memo : [test]

というレスポンスを返していますが、今後は、

memo : [test] (ad : 今なら70円引き!!XXXXマート!!)

という形式でレスポンスを返してほしいとのことです。

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

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

Part3 では、memo_repository.py からの response を 汎用的に扱える MemoData とすることで、
memo_handle_interactor.py で適切なレスポンスの形式に変更していました。

application_business_rules/memo_handle_interactor.py

class MemoHandleInteractor:
    ...
    def get(self, memo_id):
        result: MemoData = MemoRepository().get(memo_id)
        return f'memo : [{result.memo}]'
    ...

現在の設計であれば、最終的にクライアントに表示したいUIを生成する場合、このmemo_handle_interactor.pyに記載することに良さそうです。

class MemoHandleInteractor:
    ...
   def get(self, memo_id):
        result: MemoData = MemoRepository().get(memo_id)
-       return f'memo : [{result.memo}]'
+       return f'memo : [{result.memo}] (ad : 今なら70円引き!!XXXXマート!!)'
    ...

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

では、このまま運用していたとして、

  • 毎月広告メッセージを更新にしよう
  • 広告枠を増やしたいので、カンマ区切りで3つの会社の広告を載せよう

などの話を受けたとします。

この類の変更依頼は、本質的には UI に関わるなのにも変更にも関わらず、
Application Business Rules を司る memo_handle_interactor.py を変更することになります。

実際の開発で、UI の変更頻度、そして、Application Business Rules の変更頻度はどちらも高いです。

UIに対する変更にも関わらず、memo_handle_interactor.py に対して修正を行うことは避けたいです。

また、両方同時に修正が入ってしまうと、memo_handle_interactor.py がコンフリクトしてしまうなどありそうです。

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

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

出力する UI を生成する処理が、Application Business Rules を司る、memo_handle_interactor.py 部分に内包されてしまっています。

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

UI を生成する処理が、Application Business Rules 層 memo_handle_interactor.py から切り離せていると良さそうです。

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

最終的な UI を描画する役割として、Interface Adapter 層には、Presenter が存在します。

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

この層に、UI を生成する処理を切り出します。

ⅳ. 実際のコーディング

Interface Adapter 層

interface_adapter/presenters/ad_presenter.py

from enterprise_business_rules.entity.memo_object import MemoObject


class AdPresenter:
    def __init__(self):
        self.ad_message = "今なら70円引き!!XXXXマート!!"

    def create_view_for_get(self, memo_data: MemoData) -> str:
        return f'memo : [{memo_data.memo}] (ad : {self.ad_message})'

    def create_view_for_save(self) -> str:
        return f'saved. (ad : {self.ad_message})'
    
    def create_view_for_get_by_day(self, memo_data: MemoData) -> str:
        return f'本日のメモは [{memo_data.memo}] です!(ad : {self.ad_message})'

Application Business Rules 層

application_business_rules/memo_handle_interactor.py

from interface_adapters.presenter.ad_presenter import AdPresenter


class MemoHandleInteractor:
    def __init__(self, presenter):
        self.presenter = presenter

    def get(self, memo_id) -> str:
        result: MemoData = MemoRepository().get(memo_id)
        return self.presenter.create_view_for_get(result)

    def save(self, memo_data: MemoData) -> str:
        MemoRepository().save(memo_data)
        return self.presenter.save(create_view_for_save)

    def get_by_day_number(self) -> str:
        ...
        return self.presenter.create_view_for_get_by_day(result)

ⅳ. Output Boundary の登場

新たにPresentersを生成する際の問題点

さて、ここで

「今後課金ユーザーだけ広告を表示しないようにしたいと思っているので、その切替ができるようにしてくれ!!」

と言われたとします。

この場合、広告を付与せずにUIを生成する NonAdPresenter のようなものを用意すると良さそうです。

interface_adapter/presenters/non_ad_presenter.py

from enterprise_business_rules.dto.input_memo_dto import MemoData


class NonAdPresenter:
    def get(self, memo_data: MemoData):
        return f'memo : [{memo_data.memo}]'

    def save(self):
        return 'saved.'
    
    def create_view_for_get_by_day(self, memo_data: MemoData) -> str:
        return f'本日のメモは [{memo_data.memo}] です!'

このPresenterを採用する場合は、MemoHandleInteractorのコンストラクタに、NonAdPresenter を採用すると良さそうです。

interface_adapters/controllers/flask_controller.py

class FlaskController:
    def __init__(self, presenter):
        self.presenter = presenter

    def get(self, memo_id: int) -> str:
        return MemoHandleInteractor(self.presenter).get(memo_id)

    def save(self, memo_id: int, request: request) -> str:
        memo: str = request.json["memo"]
        memo_author: str = request.json["memo_author"]
        memo_data: MemoData = MemoData(memo_id, memo, memo_author)

        return MemoHandleInteractor(self.presenter).save(memo_data)

    def get_by_day_number(self) -> str:
        return MemoHandleInteractor(self.presenter).get_by_day_number()

frameworks_and_drivers/web/flask_router.py

@app.route('/memo/<int:memo_id>')
def get(memo_id):
    return jsonify(
        {
-           "message": FlaskController(AdPresenter()).get(memo_id)
+           "message": FlaskController(NonAdPresenter()).get(memo_id)
        }
    )

現状の設計で修正を加える場合の懸念点

しかし、現在の設計だと 2 つの懸念点があります。

  1. 新しい Presenter を作成する際、既存の Presenter が持つ method を持つよう実装する必要がある。

現在MemoHandleInteractor 内では、下記のように、AdPresenter が持つメソッド create_view_for_* を引き出しています。

application_business_rules/memo_handle_interactor.py

class MemoHandleInteractor:
    ...

    def get(self, memo_id):
        ...
        return self.presenter.create_view_for_get(result)

    ...

仮に NonAdPresenter 内のメソッドが、既存の AdPresenter を踏襲せず、create_ui_for_* として実装されていたとしましょう。

この場合、MemoHandleInteractor でのメソッド実行に失敗してしまいます。

interface_adapter/presenters/non_ad_presenter.py

from enterprise_business_rules.entity.memo_object import MemoObject


class NonAdPresenter:
    def create_ui_for_get(self, memo_object: MemoObject):
        return f'memo : [{memo_object.memo}]'

    def create_ui_for_save(self):
        return 'saved.'

このように、MemoHandleInteractor が、AdPresenter のメソッド呼び出しに依存してしまっているため、
新たな Presenter を生成する際は、既存の AdPresenter に依存して(模倣して)生成する必要があります。

今回はエンドポイントが 3 種類しかない簡素な API なので、既存の AdPresenter を見ながら、関数名や戻り値と引数を参考にして、新たな Presenter を記載することは簡単です。

しかし、実際の開発において、API は、よりたくさんのエンドポイントを持ちます。

なおかつ、今回のように単純なUIを描画する Presenter ではなく、複雑な UI を描画したり、メソッドごとに引数として与えられるオブジェクトの型が異なったりします。

この中で、既存の Presenter を模倣して新しいPresenterを作成しなければいけない という紳士協定を守りつつ実装を進めることは、とてもハードです。

Application の仕様ではなく、UI の変更によって、Application の仕様を管理する memo_handle_interactor を変更するのは避けたいです。

設計上の懸念点を再整理
  1. MemoHandleInteractor が、AdPresenter のメソッド呼び出しに依存してしまっている

    • そのために、新たな Presenter を生成・切り替えする際、 既存のAdPresenter に依存して(模倣して)新たな Presenter を作成する必要があり、変更のハードルが高い。
どのような設計なら、懸念点を回避して仕様変更できるか

ここで、CleanArchitecture における、Output Port を採用します。
Output Portとは、PresenterのInterfaceの役割を担います。

Output Port について: https://medium.com/@manakuro/clean-architecture-with-go-bce409427d31

これだけだとなぜ Output Portが、上記の課題を解決するかわかりにくいので、
コードを例示しつつ、Output Port を導入する利点を感じていきましょう。

懸念点を回避するための実装

Output Port を作成します。
今回は都度CleanArchitectureの全体図と比較できるよう OutputPort 名で Class を作成していますが、実態は Interface です。

from abc import ABC, abstractmethod

class MemoOutputPort(ABC):
    @abstractmethod
    def create_view_for_get(self, memo_data: MemoData) -> str:
        pass

    @abstractmethod
    def create_view_for_save(self, memo_data: MemoData) -> str:
        pass

    @abstractmethod
    def create_view_for_get_by_day(self, memo_data: MemoData) -> str:
        pass

まず、Presenter を作成するときは、この MemoOutputPort を継承するようにします。
これにより、Presenter は OutputPort に依存するため、既存のPresenterを考慮せず、新規のPresenterを追加することができます。


class AdPresenter(MemoOutputPort):
    def __init__(self):
        self.ad_message = "今なら70円引き!!XXXXマート!!"

    def create_view_for_get(self, memo_object: MemoObject):
        ...

この Interface を実装した Presenter を、MemoHandleInteractor のコンストラクタに指定します。



class MemoHandleInteractor:
    def __init__(self, presenter: MemoOutputPort):
        self.presenter = presenter

    def get(self, memo_id):
        ...

これにより、紳士協定的に、既存の Presenter を参考にして Presenter を作成せずとも、
Interface を継承することで、型に依存し Presenter を実装することができます。

また、現在は、Controller層のコンストラクタとしてPresenterを受け取り、
内部層でのPresenterの変更を行っていますが、

class FlaskController:
    def __init__(self, presenter: MemoOutputPort):
        self.presenter = presenter

    def get(self, memo_id: int) -> str:
        return MemoHandleInteractor(self.presenter).get(memo_id)
      
    ...

各処理ごとに、エンドポイントごとにPresenterを分けたいケースでは、
Controller層のコンストラクタとしてPresenterを受け取らず、内部で宣言するという方法でも良いかと思います。

class FlaskController:
-   def __init__(self, presenter: MemoOutputPort):
-       self.presenter = presenter

   def get(self, memo_id: int) -> str:
-       return MemoHandleInteractor(self.presenter).get(memo_id)
+       return MemoHandleInteractor(AdPresenter()).get(memo_id)
    ...

最終的なコード: https://github.com/y-tomimoto/CleanArchitecture/tree/master/part5

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

さて、Presenter の実装に加えて、OutputPort の実装も行いました。

これにより、UIを変更する際、既存のWebアプリケーションフレームワークや、ビジネスルールを考慮せず、UIのみ を独立して変更することが可能になりました。

このPresenterの導入により、CleanArchitecture のルール、UI独立が達成されています。

クリーンアーキテクチャ(The Clean Architecture翻訳) :https://blog.tai2.net/the_clean_architecture.html

UI独立。UIは、容易に変更できる。システムの残りの部分を変更する必要はない。たとえば、ウェブUIは、ビジネスルールの変更なしに、コンソールUIと置き換えられる。

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

5
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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?