プレゼンターはクリーンアーキテクチャで何を返す必要がありますか?

Dec 25 2020

Clean Architectureでは、合意された出力オブジェクトに基づいて、ユースケースがプレゼンターを呼び出します。プレゼンターが呼び出されると、ビューで使用されるViewModelが返されます。たとえば、CLIとWebの3つ以上のビューが表示されるまでは問題ありません。これらの2つのビューがある場合は、2つの異なるプレゼンターも必要です。ただし、ユースケースは両方のプレゼンターで同じです。ビューごとに異なるViewModelが必要になる可能性があるため、各プレゼンターは異なるデータを返す必要があります。

この問題は、各プレゼンターが異なるデータを返すときに発生します。ユースケースは、2つの異なるタイプを返す必要があります。しかし、これはJavaやC ++のような強く型付けされた言語では達成が困難です。

この関連する質問を見つけました。ユーザーはユースケースで使用する抽象的なプレゼンターを定義し、各プレゼンターは異なるビューモデルを返します。私が説明した問題が見つかるので、それを実装しようとするまで、その設計は問題ありません。

多分私はそれを考えすぎているか、クリーンなアーキテクチャについての十分な理解が不足しています。この問題をどのように解決すればよいですか?

回答

9 JKlen Dec 25 2020 at 16:01

まず、ボブおじさんのクリーンなアーキテクチャの解釈を使用していると仮定します。そのため、ここでソースを引用します。

たとえば、ユースケースでプレゼンターを呼び出す必要があるとします。ただし、依存関係ルールに違反するため、この呼び出しを直接行うことはできません。外側の円の名前を内側の円で指定することはできません。したがって、ユースケースで内側の円にインターフェイス(ここではユースケース出力ポートとして表示)を呼び出し、外側の円のプレゼンターに実装させます。

したがって、ユースケースは、プレゼンターごとに異なるタイプを返すことは絶対にできません。クリーンなアーキテクチャを壊すだけです。

ユースケースは、プレゼンテーション層の詳細(ボブおじさんが「インターフェースアダプター」と呼ぶもの)を気にせず、せいぜい、インターフェースが提示する必要のあるデータの種類を知っているだけです。したがって、それを消費する可能性のあるすべてのインターフェースに対して同じモデルを作成します。

次に、そのモデルはプレゼンターの抽象化に渡され、ユースケース側の承認なしに特定のプレゼンターに解決されます。

プレゼンターは、同じ汎用モデルを使用して、実際にインターフェイス固有のビューモデルを構築します

このバンドルPresenter+ViewModel+Viewは、多かれ少なかれ、WebであれCLIであれ、インターフェイスに固有のものですが、お互いについてできるだけ知らないようにする必要があります。ただし、それ自体はコアクリーンアーキテクチャの一部ではありません。

私は、ユースケースを定義することの全体的なポイントは、異なる...まあ...ユースケースを分離することであると主張します。プレゼンターが大きく異なるデータを返す必要があり、このすべてのデータをユースケースから渡された1つのモデル内に含めることが意味をなさない場合は、複数のデータを混合しているように見えるため、ユースケースを再定義する必要があります。それらの1つに。

3 candied_orange Dec 26 2020 at 01:02

いくつかの例でこれを明確にしましょう:

  • ユーザーが集中的な計算を要求すると、進行状況が表示されます

  • ユーザーが選択するとメニューが表示されます

これらは両方ともユースケースです。どちらもWebまたはCLIで実行できます。どちらも異なるユースケースインタラクターが必要です。ただし、CLIからWebに変更するだけでユースケースインターアクターを変更する必要がある場合は、プレゼンターの詳細をユースケースインターアクターにリークさせます。あなたはInteractorにPresentersの仕事の一部をさせています。

出力データを見て、進行状況インジケーターとメニューのどちらを見ているのかがわかるはずです。これらは完全に異なるクラス/データ構造である可能性があります。ただし、Webに表示されるのかCLIに表示されるのかはわかりません。それがモデルの表示ジョブです。

これは、@ JKlenが意味すると私が信じていることです。

Presenter + ViewModel + Viewのこのバンドルは、多かれ少なかれ、WebであれCLIであれ、インターフェイスに固有のものです。

@JKlenの答えを全面的に支持します。もう少し光を当てると思っただけです。

この問題は、各プレゼンターが異なるデータを返すときに発生します。ユースケースは、2つの異なるタイプを返す必要があります。しかし、これはJavaやC ++のような強く型付けされた言語では達成が困難です。

あなたが修正を知っていればそれは難しいことではありません。ユースケースインターアクターは、それがどのユースケースインターアクターであるかに基づいて「戻ります」(たとえば、進行状況またはメニュー)。一部のプレゼンター(すべてではない)がその特定のユースケースインタラクターの結果を処理する方法を知っているため、これは機能します。このオブジェクトグラフを作成するときは、それらを正しく一致させる必要があります。プログレスプレゼンターにメニューを送信すると問題が発生するためです。WebまたはCLI。

2 FilipMilovanović Dec 26 2020 at 09:50

少し違う視点で他の答えを補完してみましょう。

紛らわしいと思うのは、Clean Architectureには(一見)多くの「可動部分」があり、それが初めての場合、それらがどのように組み合わされているかは明らかではないということです。コンセプトの多くは、これまでに遭遇したことのないエキゾチックな何かについて話しているように見えますが、実際にはそうではありません。

それでは、これらの複雑さを取り除き、単一の関数について考えてみましょう。CRUDベースのアプリケーションに慣れている人にとっては簡単に感じるアプローチから始めて、そこからアーキテクチャを進化させる方法を見てみましょう。

プルベースのアプローチ

次のような関数があるとします。

    public ProcessingResult ProcessProducts(ProductCategory category) { ... }

したがって、この関数はいくつかのユースケースを実装します。を取り、ProductCategory内部で何かを実行して一連の製品に対して何らかの処理を実行し、ProcessingResult-操作に関する一般化された情報を含むオブジェクトと、処理された製品のリストを返します。とりあえず、そしてこの議論の目的のために、関数内で何が起こっているは気しません。正しく分離されているか、クリーンアーキテクチャに従うかどうかなどです。そのインターフェイス、つまりシグネチャ1に注目しましょう。関数の。


1わかりやすくするために、この回答では、署名とは関数の名前、パラメーターリストに表示される型、および戻り値の型(他のコードが関数を使用するときに依存するもの)を指します。一部の言語では、戻り値の型が署名の一部であると正式に見なされていません(戻り値の型をオーバーロードすることはできません)が、設計について説明する場合は役に立ちません。


ユースケースインタラクター(この簡略化された例では、オブジェクトではなく、この関数だけです)には、入力データ出力データ(別名、入力モデル出力モデル)があります。これらは単なる総称です。実際にアプリケーションでこれらの名前を使用することはありません。代わりに、より意味のある名前を選択します。

この場合、入力モデルは単なるProductCategoryクラスです。ユースケースに必要な製品カテゴリの特定の詳細を表すいくつかのプロパティがあります。それが「モデル」という言葉の意味です。モデルは何かを表したものです。同様に、ここでの出力モデルはProcessingResultクラスです。

OK。したがって、ProcessProducts関数の背後にあるすべての実装の詳細が「内部層」であると見なされているとしましょう(この内部層は内部に層を持つことができますが、今のところそれを無視しています)。関数自体とタイプProductCategory&はProcessingResult、この同じレイヤーに属しますが、レイヤーの境界にあるため特別です(必要に応じて、これらは内部レイヤーへのAPIです)。外層からのコードはこの関数を呼び出し、名前でこれらのタイプを参照します。言い換えると、外層からのコードは、この関数とその署名に表示されるタイプに直接依存します、関数の背後にあるコード(実装の詳細)については何も知りません-これにより、2つを変更できますこの関数のシグネチャを変更する必要がない限り、独立して。

外層の導入-ビューモデルなし

ここで、2つの異なるビューが必要だとします。これらに関連するコードは、外層に存在します。1つのビューはHTMLで、もう1つはCLIツールの出力として表示されるプレーンテキストです。

さて、あなたがする必要があるのは、この関数を呼び出し、結果を取得し、それを適切な形式に変換することです。今はビューモデルを使用しないでください(すべてにビューモデルは必要ありません)。例えば:

    // In your web code:
    
    var result = ProcessProducts(category);   // controller invoking the use case

    // Presentation code 
    // (could be in the same function, but maybe it's in a separate function):

    // fill HTML elements with result.summary
    // create an <ul>
    // for each product in result.ProcessedProducts, create an <li>

または:

    // In your CLI code:
    
    var result = ProcessProducts(category);   // controller invoking the use case

    // Presentation code
    // (could be in the same function, but maybe it's in a separate function):
    Console.WriteLine(result.summary);
    foreach(var product in result.ProcessedProducts)
        Console.WriteLine(result.summary);

したがって、この時点で、これができました。コントローラーはユースケースを直接参照し、プレゼンテーションロジックを調整します。

モデルを表示

ビューに重要なロジックがあり、独自のビュー固有のデータを追加する場合、またはユースケースによって返されるデータを処理するのが不便な場合は、間接参照のレベルとしてビューモデルを導入すると、それに対処するのに役立ちます。

ビューモデルでは、ビューを直接作成しないことを除いて、コードは上記のコードとそれほど変わりません。代わりに、を取得してresult、そこからビューモデルを作成します。おそらく、それを返すか、ビューをレンダリングするものに渡します。または、そのいずれも実行しません。使用しているフレームワークがデータバインディングに依存している場合は、ビューモデルを更新するだけで、データバインディングメカニズムが接続されたビューを更新します。

プッシュベースのインターフェースに向けた再設計

さて、私が上で説明したのは「プルベース」のアプローチです-あなたは積極的に結果を求めます(「プル」)。あなたが気づいたと再設計する必要があるUI「ベースのプッシュ」に向けて2 -すなわち、あなたはProcessProducts関数を呼び出して、したいことは、それが処理を完了した後、いくつかのビューの更新を開始しますか?


2データをUIにプッシュする良いと言っているのではなく、それがオプションであるというだけです。私が得ようとしているのは CleanArchitectureがその要素を持っている理由です。


2つの非常に異なるビューをサポートする必要があるため、ユースケースのコードを具体的なビューを参照せずに記述したいことを忘れないでください。ビュー/プレゼンターを内部から直接呼び出すことはできません。そうしないと、依存関係のルールに違反します。さて、依存性逆転を使用してください。

依存性逆転

ProcessingResultをある出力場所にプッシュしたいが、関数にそれが何であるかを知られたくない。だから、あなたはある種の...ああ私は知らない...出力の抽象化が必要ですか?クリーンなアーキテクチャには、出力境界(別名出力ポート)の概念があります。これは、データをプッシュする必要があるものへの依存関係を抽象化するインターフェイスです。繰り返しになりますが、コードでは、より意味のある名前を付けます(ここで思いついた名前は素晴らしいものではありません、認めます)。この例では、このインターフェイスに必要なのはProcessingResult、パラメーターとして受け入れるメソッドだけです。

    public interface IProcessingOutputPresenter {
        void Show(ProcessingResult result);
    }

したがって、関数シグネチャを次のように再設計します。

    public void ProcessProducts(ProductCategory category, IProcessingOutputPresenter presenter) { 
        // stuff happens...
        ProcessingResult result = <something>; 
        presenter.Show(result);
    }

または多分それは長時間実行されている操作です:

    public async Task ProcessProductsAsync(ProductCategory category, IProcessingOutputPresenter presenter) { 
        // stuff happens...
        ProcessingResult result = await <something>; 

        presenter.Show(result);
    }

だから今、あなたはこれを行うことができます:

    // presenter class:
    public class WebPresenter : IProcessingOutputPresenter { ... }

    // In your web controller:    
    ProcessProducts(category, this.webPresenter);

または:

    // presenter class:
    public class CliPresenter : IProcessingOutputPresenter { ... }

    // In your CLI controller:
    ProcessProducts(category, this.cliPresenter);

または、あなたのテストでは

    // mock presenter:
    public class MockPresenter : IProcessingOutputPresenter { ... }

    // In your test:
    var presenter = new MockPresenter();
    ProcessProducts(category, mockPresenter);

これで、3つの異なるコンテキストでコードを再利用 できましたProcessProducts

基本的に、ProcessProductsビューを気にする必要はありません.Show(result)。を呼び出すことで「起動して忘れる」だけです。入力をビューに必要なものに変換するのはプレゼンターの仕事です(ビューモデルが変更されたときにビューの更新をトリガーするデータバインディングメカニズムも含まれていると仮定します)。

ここで重要なのは依存関係の構造であり、オブジェクトと関数のどちらを使用しているかではありません。実際、IProcessingOutputPresenterはシングルメソッドインターフェイスなので、ラムダを使用することもできます。それでも同じパターン、同じアーキテクチャのアイデアです。ラムダは出力ポートの役割を果たします。

    public ProcessProducts(ProductCategory category, Action<ProcessingResult> presenterAction);

    // then:
    ProcessProducts(category, (result) => presenter.Show(result));

それは同じことです。

この設定で得られるのは、ここで強調表示されている部分です。

インターフェイスを再設計して、複数の同時ビューを可能にすることもできます。

    public void ProcessProducts(ProductCategory category, IEnumerable<IProcessingOutputPresenter> presenters)
    {
        // stuff happens...
        // ProcessingResult result = <something> 
        foreach (var presenter in presenters)
            presenter.Show(result);
    }

関数だけでなくオブジェクトがある場合はどうなりますか?

これは基本に同じ基本的な考え方ですが、通常、プレゼンター(出力境界インターフェイスの実装)をユースケースのコンストラクターに渡す点が異なります。以前のようにコントローラーからプレゼンターを渡す代わりに、依存性注入コンテナーに、または手動で、コンポジションルート(たとえば、Main())にセットアップすることができます。

    var cliPresenter = new CliPresenter();
    var productRepository = new ProductRepository(/* ... */);
    var productProcessor = new ProductProcessor(cliPresenter, productRepository);  // <----
    var cliController = new CliController(productProcessor);
    RunCliApplication(cliController);
    
    // (or something of the sort)

データアクセスコードも同様の方法で挿入されていることに注意してください。

または、出力先を動的に変更できるようにする場合は、出力先をユースケースオブジェクトのメソッドパラメーターにすることができます(たとえば、異なる製品カテゴリの出力を2つの異なるビューに表示する必要があります)。同じアプリケーションで):

productProcessor.Process(trackedProducts, graphPresenter);
productProcessor.Process(untrackedProducts, listPresenter);

同じ考え方がレイヤーの境界を越えて適用されます

これと同じ基本的な考え方がアプリケーション全体に適用されます。つまり、内部層を直接呼び出すか、内部層で定義されたインターフェイスを実装して、コードがユーザーを認識していなくても、ユーザーを呼び出すことができるようにします。

このテクニックを慎重に適用する必要があるだけです。すべて同じデータ構造を繰り返す5層の抽象化は必要ありません(または必要ありません)。(経験があっても)間違えるので、手間がかかりすぎて再設計するのをためらうでしょう。はい、最初の分析からさまざまなアーキテクチャ要素が何であるかをある程度理解できますが、一般的には、単純なものから始めて、コードが複雑になるにつれて、あちこちで分解して再構築します-進行中にコードが絡まりすぎないようにします。実装の詳細はユースケースのインターフェースの背後に隠されているため、これを行うことができます。複雑さが増すにつれて、内層の内側を「再形成」することができます。

コードの保守性が低下し始めていることに気づき、それに対して何かを行うことで、コードを保守可能に保ちます。

ここでは、最初はプレゼンターの作業も行っていたコントローラーによって呼び出される単純な関数から始めました。いくつかのリファクタリングの後、さまざまなパーツを抽出したり、インターフェイスを定義したり、さまざまなサブコンポーネントの責任を分離したりできるようになります。最終的には、理想的なクリーンアーキテクチャに近いものに近づきます。

ここには2つのポイントがあります。まず、CAのコンテキスト外で使用されるこれらの手法を見たことがあるでしょう。CAは、根本的に新しいことや異なることは何もしません。CAについてはそれほど不思議なことは何もありません。それはあなたにこれらのことについて考える方法を与えるだけです。次に、アーキテクチャのすべての要素を一度に把握する必要はありません(実際、そうすることで過剰設計のリスクがあります)。代わりに、コードが何であるかがわかるまで、これらの決定の一部を延期する必要があります。