web-dev-qa-db-ja.com

クリーンアーキテクチャ:プレゼンターを含む、またはデータを返すユースケース?

クリーンアーキテクチャ は、応答/表示を処理するために、ユースケースのインタラクターがプレゼンターの実際の実装(DIPの後に挿入される)を呼び出せるようにすることを提案しています。ただし、このアーキテクチャーを実装してインタラクターからの出力データを返し、コントローラー(アダプター層内)にそれを処理する方法を決定させる人々がいるのがわかります。 2番目のソリューションは、インタラクターへの入力ポートと出力ポートを明確に定義していないことに加えて、アプリケーションの責任をアプリケーション層から漏らしていますか?

入力および出力ポート

クリーンアーキテクチャ の定義、特にコントローラー、ユースケースのインタラクター、プレゼンターの間の関係を説明する小さなフロー図を考慮すると、「ユースケースの出力ポート」を正しく理解しているかどうかわかりません。 "する必要があります。

六角形のアーキテクチャのようなクリーンなアーキテクチャは、プライマリポート(メソッド)とセカンダリポート(アダプタによって実装されるインターフェイス)を区別します。通信フローに従って、「ユースケース入力ポート」がプライマリポート(したがって、単なるメソッド)であり、「ユースケース出力ポート」が実装されるインターフェース、おそらくコンストラクター引数が実際のアダプターを取ることを期待しています。インタラクターがそれを使用できるようにします。

コード例

コード例を作るために、これはコントローラーコードであるかもしれません:

_Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();
_

プレゼンターインターフェイス:

_// Use Case Output Port
interface Presenter
{
    public void present(Data data);
}
_

最後に、インタラクター自体:

_class UseCase
{
    private Repository repository;
    private Presenter presenter;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.repository = repository;
        this.presenter = presenter;
    }

    // Use Case Input Port
    public void doSomething()
    {
        Data data = this.repository.getData();
        this.presenter.present(data);
    }
}
_

インタラクターがプレゼンターを呼び出す

以前の解釈は、前述の図自体によって確認されているようです。コントローラーと入力ポートの関係は、「鋭い」ヘッド(「関連付け」のUML、つまり「ある」を意味します)のある実線の矢印で表されます。コントローラーは「使用例」があります)、プレゼンターと出力ポートの間の関係は、「白い」ヘッド(「継承」のUMLであり、「実装」のUMLではありませんが、おそらくとにかくその意味です)。

さらに、 別の質問に対するこの回答 で、Robert Martinは、インタラクターが読み取り要求でプレゼンターを呼び出すユースケースを正確に説明しています。

地図をクリックすると、placePinControllerが呼び出されます。クリックの場所とその他のコンテキストデータを収集し、placePinRequestデータ構造を作成して、PlacePinInteractorに渡します。PlacePinInteractorは、ピンの場所を確認し、必要に応じて検証し、Placeエンティティを作成してピンを記録し、EditPlaceReponseを作成します。オブジェクトを配置してEditPlacePresenterに渡し、プレイスエディター画面が表示されます。

これをMVCでうまく機能させるために、アプリケーションロジックがアプリケーションレイヤーの外に漏れないようにするため、従来はコントローラーに入るアプリケーションロジックをインタラクターに移動したと考えることができます。アダプター層のコントローラーはインタラクターを呼び出すだけであり、プロセスでいくつかのマイナーなデータ形式変換を行う可能性があります。

このレイヤーのソフトウェアは、ユースケースやエンティティに最も便利な形式から、データベースやWebなどの外部機関に最も便利な形式にデータを変換する一連のアダプターです。

元の記事から、インターフェースアダプターについて説明しています。

データを返すインタラクターについて

ただし、このアプローチの私の問題は、ユースケースがプレゼンテーション自体を処理する必要があることです。 Presenterインターフェースの目的は、いくつかの異なるタイプのプレゼンター(GUI、Web、CLIなど)を表すのに十分抽象的であること、そしてそれは単に「出力」を意味するだけであることがわかりました。ユースケースは非常によくあるかもしれませんが、それでも私は完全にそれを確信していません。

さて、クリーンアーキテクチャのアプリケーションをWebで見てみると、出力ポートをDTOを返すメソッドとして解釈している人しかいないようです。これは次のようになります。

_Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);

// I'm omitting the changes to the classes, which are fairly obvious
_

プレゼンテーションを「呼び出す」という責任をユースケースの外に移すので、これは魅力的です。したがって、ユースケースは、データを提供するだけでなく、データをどう処理するかを知ることとは関係ありません。また、この場合でも、依存関係のルールに違反しているわけではありません。これは、ユースケースがまだ外側のレイヤーについて何も知らないためです。

ただし、ユースケースは、実際のプレゼンテーションが実行されるタイミングを制御しません(たとえば、ロギングのような追加のことを実行したり、必要に応じてそれを完全に中止したりするのに役立ちます)。また、コントローラーはgetData()メソッド(新しい出力ポート)のみを使用しているため、ユースケース入力ポートを失ったことに注意してください。さらに、インタラクターにデータで何かを実行するように指示するのではなく、インタラクターにデータで何かを実行するように要求しているため、ここでは「教えて、聞かないで」の原則を破っているように見えます最初の場所。

ポイントへ

では、これらの2つの選択肢のいずれかは、クリーンアーキテクチャによるユースケース出力ポートの「正しい」解釈ですか?どちらも実行可能ですか?

53
swahnee

Clean Architectureは、応答/表示を処理するために、ユースケースのインタラクターがプレゼンターの実際の実装(DIPの後に挿入される)を呼び出せるようにすることを提案しています。ただし、このアーキテクチャを実装し、インタラクターから出力データを返し、その後controller(アダプターレイヤー内)に処理方法を決定させる人々を目にしますそれ。

それは確かに CleanOnion 、または Hexagonal アーキテクチャではありません。つまり this

enter image description here

そうではありません [〜#〜] mvc [〜#〜] はそのように実行する必要があります

enter image description here

モジュール間で通信し、MVCと呼ぶためにさまざまな方法を使用できます 。 MVCを使用していると言っても、コンポーネントの通信方法はわかりません。それは標準化されていません。それが私に言うすべては、それらの3つの責任に焦点を合わせた少なくとも3つのコンポーネントがあるということです。

それらの方法のいくつかは 異なる名前enter image description here

そして、それらすべてを正当にMVCと呼ぶことができます。

とにかく、流行語のアーキテクチャ(クリーン、オニオン、および16進数)がすべてあなたに要求するものを実際にキャプチャしているものはありません。

enter image description here

振り回されているデータ構造を追加し(そして何らかの理由で上下を逆にします)、 あなたが得る

enter image description here

ここで明確にすべきことの1つは、応答モデルがコントローラーを通過しないことです。

イーグルアイドなら、流行語アーキテクチャのみが完全に 循環依存 を回避していることに気づいたかもしれません。重要なのは、コード変更の影響がコンポーネントを循環しても広がらないことです。気にしないコードにヒットすると、変更は停止します。

制御の流れが時計回りになるように、上下を逆にしたのではないでしょうか。その詳細、およびこれらの「白い」矢印は後で説明します。

2番目のソリューションは、インタラクターへの入力ポートと出力ポートを明確に定義していないことに加えて、アプリケーションの責任をアプリケーション層から漏らしていますか?

ControllerからPresenterへの通信はアプリケーションの「レイヤー」を経由することを目的としているため、コントローラーをPresentersジョブの一部にすることはおそらくリークです。これが VIPERアーキテクチャ に対する私の主な批判です。

これらの分離が非常に重要である理由は、おそらく Command Query Responsibility Segregation を研究することで最もよく理解できます。

入力および出力ポート

クリーンアーキテクチャの定義、特にコントローラー、ユースケースのインタラクター、プレゼンター間の関係を説明する小さなフロー図を考えると、「ユースケースの出力ポート」が何であるかを正しく理解しているかどうかはわかりません。

この特定のユースケースでは、出力を送信するためのAPIです。それだけです。この使用例のインタラクターは、出力がGUI、CLI、ログ、またはオーディオスピーカーに送られるかどうかを知る必要も、知りたくもありません。インタラクターが知る必要があるのは、作業の結果を報告できるようにする非常に単純なAPIだけです。

六角形のアーキテクチャのようなクリーンなアーキテクチャは、プライマリポート(メソッド)とセカンダリポート(アダプタによって実装されるインターフェイス)を区別します。通信フローに従って、「ユースケース入力ポート」がプライマリポート(したがって、単なるメソッド)であり、「ユースケース出力ポート」が実装されるインターフェイス、おそらくコンストラクター引数が実際のアダプターを取ることを期待しています。インタラクターがそれを使用できるようにします。

出力ポートが入力ポートと異なるのは、それが抽象化するレイヤーによって所有されてはならないためです。つまり、抽象化するレイヤーに変更を指示することを許可してはなりません。アプリケーション層とその作成者のみが、出力ポートを変更できると決定する必要があります。

これは、抽象化するレイヤーが所有する入力ポートとは対照的です。アプリケーションレイヤーの作成者のみが、入力ポートを変更するかどうかを決定する必要があります。

これらの規則に従うことで、アプリケーション層または任意の内部層は、外部層についてまったく何も知らないという考えが保持されます。


インタラクターがプレゼンターを呼び出す

以前の解釈は、前述の図自体によって確認されているようです。コントローラーと入力ポートの関係は、「鋭い」ヘッド(「関連付け」のUML、つまり「ある」を意味します)のある実線の矢印で表されます。コントローラは「使用例」があります)、プレゼンターと出力ポートの関係は、「白い」ヘッドのある実線の矢印で表されます(「継承」のUML、「実装」のUMLではありませんが、おそらくそれはとにかく意味です)。

その「白い」矢印についての重要なことは、これを実行できることです。

enter image description here

制御フローを依存関係の反対方向に進めることができます!つまり、内層は外層について知っている必要はありませんが、内層に飛び込んで戻ってくることができます!

それを行うことは、「インターフェース」キーワードの使用とは何の関係もありません。抽象クラスでこれを行うことができます。それが拡張できる限り、(ick)具象クラスでそれを行うことができます。 Presenterが実装する必要のあるAPIの定義にのみ焦点を当てたものでそれを行うのは、単にいいことです。開いた矢印は、多型のみを求めています。どのようなあなた次第です。

なぜその依存関係の方向を逆にすることが非常に重要であるかは、 依存関係の逆転の原理 を研究することで知ることができます。その原理をこれらの図にマッピングしました here

データを返すインタラクターについて

ただし、このアプローチの私の問題は、ユースケースがプレゼンテーション自体を処理する必要があることです。現在、Presenterインターフェースの目的は、いくつかの異なるタイプのプレゼンター(GUI、Web、CLIなど)を表すのに十分抽象的であること、そして実際には「出力」を意味するだけであり、これはユースケースの1つです。非常によく持っているかもしれませんが、それでも私はそれに完全に自信がありません。

いいえ、それだけです。内側のレイヤーが外側のレイヤーを認識しないようにするためのポイントは、外側のレイヤーを削除、置換、またはリファクタリングできることです。そうすることで、内側のレイヤーが破損することはありません。彼らが知らないことは彼らを傷つけません。それができれば、外側のものを好きなように変更できます。

さて、クリーンアーキテクチャのアプリケーションをWebで見てみると、出力ポートをDTOを返すメソッドとして解釈している人しかいないようです。これは次のようになります。

Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);
// I'm omitting the changes to the classes, which are fairly obvious

プレゼンテーションを「呼び出す」という責任をユースケースの外に移すので、これは魅力的です。したがって、ユースケースは、データを提供するだけでなく、データをどう処理するかを知ることとは関係ありません。また、この場合でも、依存関係のルールに違反しているわけではありません。これは、ユースケースがまだ外側のレイヤーについて何も知らないためです。

ここでの問題は、データを要求する方法を知っているものは何でも、データを受け入れるものでなければならないということです。コントローラーがユースケースインタラクターを呼び出す前に、応答モデルがどのように見えるか、どこに行くべきか、そしてどのようにそれを提示するかを知らないうちに、幸いにも気付かない。

繰り返しますが、 Command Query Responsibility Segregation を調べて、なぜそれが重要なのかを確認してください。

ただし、ユースケースは、実際のプレゼンテーションが実行されるタイミングを制御しません(たとえば、ロギングのような追加のことを実行したり、必要に応じてそれを完全に中止したりするのに役立ちます)。また、コントローラーはgetData()メソッド(新しい出力ポート)のみを使用しているため、ユースケース入力ポートを失ったことに注意してください。さらに、インタラクターにデータで何かを実行するように指示するのではなく、インタラクターにデータで何かを実行するように要求しているため、ここでは「教えて、聞かないで」の原則を破っているように見えます最初の場所。

はい!尋ねるのではなく、伝えることは、このオブジェクトを手続き型ではなく指向に保つのに役立ちます。

ポイントへ

では、これらの2つの選択肢のいずれかは、クリーンアーキテクチャによるユースケース出力ポートの「正しい」解釈ですか?どちらも実行可能ですか?

機能するものはすべて実行可能です。しかし、あなたが提示した2番目のオプションがClean Architectureに忠実に従っているとは言えません。うまくいくかもしれません。しかし、それはClean Architectureが要求するものではありません。

58
candied_orange

あなたの質問に関連するディスカッションで 、ボブおじさんはクリーンアーキテクチャでのプレゼンターの目的を説明します。

このコードサンプルを考えると:

namespace Some\Controller;

class UserController extends Controller {
    public function registerAction() {
        // Build the Request object
        $request = new RegisterRequest();
        $request->name = $this->getRequest()->get('username');
        $request->pass = $this->getRequest()->get('password');

        // Build the Interactor
        $usecase = new RegisterUser();

        // Execute the Interactors method and retrieve the response
        $response = $usecase->register($request);

        // Pass the result to the view
        $this->render(
            '/user/registration/template.html.twig', 
            array('id' =>  $response->getId()
        );
    }
}

ボブおじさんはこう言った:

"プレゼンターの目的は、UIの形式からユースケースを切り離すことです。この例では、$ response変数はインタラクターによって作成されますが、ビューによって使用されます。これにより、ビューへのインタラクター。たとえば、$ responseオブジェクトのフィールドの1つが日付であるとします。そのフィールドは、さまざまな日付形式でレンダリングできるバイナリ日付オブジェクトになります。非常に具体的な日付形式が必要です。おそらくDD/MM/YYYYです。フォーマットを作成するのは誰の責任ですか?インタラクターがそのフォーマットを作成する場合、ビューについての知識が多すぎます。ただし、ビューがバイナリ日付オブジェクトを取る場合、インタラクターについての知識が多すぎます。

「プレゼンターの仕事は、応答オブジェクトからデータを取得し、それをビュー用にフォーマットすることです。-ビューもインタラクターも、互いのフォーマットを認識していません。 "

---ボブおじさん

(UPDATE:2019年5月31日)

ボブおじさんの答えを考えると、私はそれは問題ではないかどうかにかかわらずオプション#1(インタラクターにプレゼンターを使用させる)...

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

...または、次のようにしますオプション#2(インタラクターが応答を返し、コントローラー内にプレゼンターを作成して、応答をプレゼンターに渡します)...

class Controller
{
    public void ExecuteUseCase(Data data)
    {
        Request request = ...
        UseCase useCase = new UseCase(repository);
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        presenter.Show(response);
    }
}

個人的には、オプション#1を制御したいので、私はinteractorwhenの中で以下の例のように、データとエラーメッセージを表示します。

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

...インタラクターの外ではなくinteractor内のプレゼンテーションに関連するこれらのif/elseを実行できるようにしたいです。

一方、オプション#2を実行する場合、エラーメッセージをresponseオブジェクトに保存し、responseオブジェクトをinteractorからcontroller、およびcontrollerparseresponseオブジェクトにします...

class UseCase
{
    public Response Execute(Request request)
    {
        Response response = new Response();
        if (<invalid request>) 
        {
            response.AddError("...");
        }

        if (<there is another error>) 
        {
            response.AddError("another error...");
        }

        if (response.HasNoErrors)
        {
            response.Whatever = ...
        }

        ...
        return response;
    }
}
class Controller
{
    private UseCase useCase;

    public Controller(UseCase useCase)
    {
        this.useCase = useCase;
    }

    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        if (response.ErrorMessages.Count > 0)
        {
            if (response.ErrorMessages.Contains(<invalid request>))
            {
                presenter.ShowError("...");
            }
            else if (response.ErrorMessages.Contains("another error")
            {
                presenter.ShowError("another error...");
            }
        }
        else
        {
            presenter.Show(response);
        }
    }
}

response内のエラーについてcontrollerデータを解析するのは好きではありません。これを行うと冗長な作業が行われるためです--- interactorで何かを変更すると、また、controllerで何かを変更する必要があります。

また、たとえば、後でコンソールを使用してinteractorを再利用してデータを表示する場合、controllerのすべてのif/elseをコピーして貼り付けることを忘れないでください。コンソールアプリ。

// in the controller for our console app
if (response.ErrorMessages.Count > 0)
{
    if (response.ErrorMessages.Contains(<invalid request>))
    {
        presenterForConsole.ShowError("...");
    }
    else if (response.ErrorMessages.Contains("another error")
    {
        presenterForConsole.ShowError("another error...");
    }
}
else
{
    presenterForConsole.Present(response);
}

オプション#1を使用する場合、これはif/else1か所のみinteractorになります。


ASP.NET MVC(または他の同様のMVCフレームワーク)を使用している場合、オプション#2はより簡単な方法です。

しかし、そのような環境でもオプション#1を実行できます。 ASP.NET MVCでオプション#1を実行する例を次に示します:

(ASP.NET MVCアプリのプレゼンターにpublic IActionResult Resultが必要であることに注意してください)

class UseCase
{
    private Repository repository;

    public UseCase(Repository repository)
    {
        this.repository = repository;
    }

    public void Execute(Request request, Presenter presenter)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {
            ...
        }
        this.presenter.Show(response);
    }
}
// controller for ASP.NET app

class AspNetController
{
    private UseCase useCase;

    public AspNetController(UseCase useCase)
    {
        this.useCase = useCase;
    }

    [HttpPost("dosomething")]
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new AspNetPresenter();
        useCase.Execute(request, presenter);
        return presenter.Result;
    }
}
// presenter for ASP.NET app

public class AspNetPresenter
{
    public IActionResult Result { get; private set; }

    public AspNetPresenter(...)
    {
    }

    public async void Show(Response response)
    {
        Result = new OkObjectResult(new { });
    }

    public void ShowError(string errorMessage)
    {
        Result = new BadRequestObjectResult(errorMessage);
    }
}

(ASP.NET MVCアプリのプレゼンターにpublic IActionResult Resultが必要であることに注意してください)

コンソール用に別のアプリを作成する場合は、上記のUseCaseを再利用して、コンソール用のControllerPresenterのみを作成できます。

// controller for console app

class ConsoleController
{    
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new ConsolePresenter();
        useCase.Execute(request, presenter);
    }
}
// presenter for console app

public class ConsolePresenter
{
    public ConsolePresenter(...)
    {
    }

    public async void Show(Response response)
    {
        // write response to console
    }

    public void ShowError(string errorMessage)
    {
        Console.WriteLine("Error: " + errorMessage);
    }
}

(コンソールアプリのプレゼンターにpublic IActionResult Resultがないことに注意してください)

11
Jboy Flaga

使用例には、プレゼンターまたは返されるデータを含めることができ、アプリケーションフローで何が必要かによって異なります。

さまざまなアプリケーションフローを理解する前に、いくつかの用語を理解しましょう。

  • ドメインオブジェクト:ドメインオブジェクトは、ビジネスロジック操作が実行されるドメインレイヤーのデータコンテナーです。
  • ビューモデル:ドメインオブジェクトは通常、アプリケーションレイヤーのビューモデルにマップされ、ユーザーインターフェイスと互換性があり、使いやすいものになります。
  • Presenter:通常、アプリケーションレイヤーのコントローラーはユースケースを呼び出しますが、ドメインを委任して、モデルマッピングロジックを表示して別のクラスに表示することをお勧めします(以下の単一の責任原則)。これは「プレゼンター」と呼ばれます。

戻りデータを含むユースケース

通常のケースでは、ユースケースはドメインオブジェクトをアプリケーションレイヤーに返すだけで、アプリケーションレイヤーでさらに処理してUIに表示しやすくすることができます。

コントローラーはユースケースを呼び出す責任があるため、この場合、レンダリングするビューに送信する前にモデルマッピングを表示するドメインを実行するそれぞれのプレゼンターの参照も含まれます。

簡略化したコードサンプルを次に示します。

namespace SimpleCleanArchitecture
{
    public class OutputDTO
    {
        //fields
    }

    public class Presenter 
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    public class Domain
    {
        //fields
    }

    public class UseCaseInteractor
    {
        public Domain Process(Domain domain)
        {
            // additional processing takes place here
            return domain;
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            UseCaseInteractor userCase = new UseCaseInteractor();
            var domain = userCase.Process(new Domain());//passing dummy domain(for demonstration purpose) to process
            var presenter = new Presenter();//presenter might be initiated via dependency injection.

            return new View(presenter.Present(domain));
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

プレゼンターを含むユースケース

一般的ではありませんが、ユースケースでプレゼンターを呼び出す必要がある場合があります。その場合、プレゼンターの具体的な参照を保持する代わりに、インターフェイス(または抽象クラス)を参照ポイント(依存関係の注入によって実行時に初期化する必要がある)と見なすことをお勧めします。

(コントローラー内ではなく)別のクラスでモデルマッピングロジックを表示するドメインを使用すると、コントローラーとユースケースの間の循環依存関係も解除されます(ユースケースクラスでマッピングロジックへの参照が必要な場合)。

enter image description here

以下は、元の記事に示されているように、制御フローの簡略化された実装であり、それがどのように実行できるかを示しています。図に示すのとは異なり、簡単にするためにUseCaseInteractorは具象クラスであることに注意してください。

namespace CleanArchitectureWithPresenterInUseCase
{
    public class Domain
    {
        //fields
    }

    public class OutputDTO
    {
        //fields
    }

    // Use Case Output Port
    public interface IPresenter
    {
        OutputDTO Present(Domain domain);
    }

    public class Presenter: IPresenter
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    // Use Case Input Port / Interactor   
    public class UseCaseInteractor
    {
        IPresenter _presenter;
        public UseCaseInteractor (IPresenter presenter)
        {
            _presenter = presenter;
        }

        public OutputDTO Process(Domain domain)
        {
            return _presenter.Present(domain);
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            IPresenter presenter = new Presenter();//presenter might be initiated via dependency injection.
            UseCaseInteractor userCase = new UseCaseInteractor(presenter);
            var outputDTO = userCase.Process(new Domain());//passing dummy domain (for demonstration purpose) to process
            return new View(outputDTO);
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}
2
Ashraf

プレゼンターを含む、またはデータを返すユースケース?

では、これらの2つの選択肢のいずれかは、クリーンアーキテクチャによるユースケース出力ポートの「正しい」解釈ですか?どちらも実行可能ですか?


要するに

はい、両方のアプローチがビジネスレイヤーと配信メカニズムの間の制御Inversion Of Controlを考慮している限り、どちらも実行可能です。 2番目のアプローチでは、IOC=オブザーバー、メディエーター、他のいくつかの設計パターンを利用することで、まだ導入できます...

彼のClean Architectureを使用して、Uncle Bobの試みは、既知のアーキテクチャの束を合成して、OOPに広く準拠するための重要な概念とコンポーネントを明らかにすることです原則。

彼のUMLクラス図(下図)をユニークなClean Architecture設計と見なすことは逆効果です。この図は、具体的な例…のために描いた可能性がありますが、通常のアーキテクチャ表現よりも抽象性がはるかに低いため、具体的な選択を行わなければなりませんでした。 実装の詳細のみであるインタラクター出力ポートの設計

Uncle Bob's UML class diagram of Clean Architecture


私の2セント

私がUseCaseResponseを返すことを好む主な理由は、このアプローチがユースケースflexibleを保持し、両方のcompositionそれらとgenericitygeneralizationand特定の世代)。基本的な例:

// A generic "entity type agnostic" use case encapsulating the interaction logic itself.
class UpdateUseCase implements UpdateUseCaseInterface
{
    function __construct(EntityGatewayInterface $entityGateway, GetUseCaseInterface $getUseCase)
    {
        $this->entityGateway = $entityGateway;
        $this->getUseCase = $getUseCase;
    }

    public function execute(UpdateUseCaseRequestInterface $request) : UpdateUseCaseResponseInterface
    {
        $getUseCaseResponse = $this->getUseCase->execute($request);

        // Update the entity and build the response...

        return $response;
    }
}

// "entity type aware" use cases encapsulating the interaction logic WITH the specific entity type.
final class UpdatePostUseCase extends UpdateUseCase;
final class UpdateProductUseCase extends UpdateUseCase;

同じように、UMLのユースケースinclude/extendingに類似しており、異なるサブジェクト(エンティティ)でreusableとして定義されています。 。


データを返すインタラクターについて

ただし、ユースケースは、実際のプレゼンテーションが実行されるタイミングを制御しません(たとえば、ロギングのような追加のことを実行したり、必要に応じてそれを完全に中止したりするのに役立ちます)。

これが何を意味するのかわからない場合、プレゼンテーションの実行を「制御」する必要があるのはなぜですか?ユースケースレスポンスを返さない限り、制御しませんか?

ユースケースは、応答中にステータスコードを返して、操作中に正確に何が起こったかをクライアント層に知らせることができます。 HTTP応答ステータスコードは、ユースケースの操作ステータスを記述するのに特に適しています…

1
ClemC

私は一般的に@CandiedOrangeからの回答に同意しますが、インタラクターがコントローラーからプレゼンターに渡されるデータを再実行するというアプローチにもメリットがあります。

これは、たとえば、Asp.Net MVCのコンテキストでクリーンアーキテクチャ(依存関係ルール)のアイデアを使用する簡単な方法です。

私はこのディスカッションをより深く掘り下げるためにブログ投稿を書きました: https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/

1
plainionist

プレゼンターを使用する主な理由は、単一の責任/懸念の分離です。最新のフレームワークがコンテンツのネゴシエーションを行い、ワイヤー形式(JSONとXMLなど)を処理するため、Web APIの状況は少しあいまいです。

前述のように、ユースケースの応答モデルが「コントローラーを経由しない」ことを意味するため、私はプレゼンターを直接呼び出すユースケースを強く支持しています。ただし、コントローラーがプレゼンターと交通警官を演じたり、プレゼンターの外でヤンク状態をしたりするなどのアプローチは不便です。

プレゼンターをデリゲートにすると、少しすっきりした見栄えになりますが、少なくともこのデモコードでは、フレームワークによって提供されるコンテンツネゴシエーションが失われています。 OkResultを直接拡張して、コンテンツネゴシエーションに戻ることでできることは他にもあると思います。

[HttpGet]
public IActionResult List()
{
    return new GridEntriesPresenter(presenter =>
       _listGridEntriesUseCase.ListAsync(presenter));
}

次に、GridEntriesPresenterは拡張されるものです

public class ActionResultPresenter<T> : IActionResult
{
    private readonly Func<Func<T, Task>, Task> _handler;

    public ActionResultPresenter(Func<Func<T, Task>, Task> handler)
    {
        _handler = handler;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        await _handler(async responseModel =>
        {
            context.HttpContext.Response.ContentType = "application/json";
            context.HttpContext.Response.StatusCode = 200;
            await context.HttpContext.Response.StartAsync();

            await SerializeAsync(context.HttpContext.Response.Body, responseModel);

            await context.HttpContext.Response.CompleteAsync();
        });
    }

    ... 
}
public class GridEntriesPresenter : ActionResultPresenter<IEnumerable<GridEntryResponseModel>>
{
    public GridEntriesPresenter(Func<Func<IEnumerable<GridEntryResponseModel>, Task>, Task> handler) : base(handler)
    {
    }

    protected override Task SerializeAsync(Stream stream, IEnumerable<GridEntryResponseModel> responseModel)
    {
        ...
        return SerializeJsonAsync(stream, new {items, allItems, totalCount, pageCount, page, pageSize});
    }
}

そして、あなたのユースケースは次のようになります:

public class ListGridEntriesUseCase : IListGridEntriesUseCase
{
    private readonly IActivityRollups _activityRollups;

    public ListGridEntriesUseCase(IActivityRollups activityRollups)
    {
        _activityRollups = activityRollups;
    }

    public async Task ListAsync(int skip, int take, Func<IEnumerable<GridEntryResponseModel>, Task> presentAsync)
    {
        var activityRollups = await _activityRollups.ListAsync(skip, take);
        var gridEntries = activityRollups.Select(x => new GridEntryResponseModel
        {
            ...
        });
        await presentAsync(gridEntries);
    }
}

しかし、今では厄介なFunc<T>構文を使用しています(ただし、IDEサポートは、少なくともRiderを使用している場合はここで役立ちます)。タイプを明示的に宣言することは、おそらく不正行為です。

0
kayjtea