包含演示者或返回数据的用例?

Posted

技术标签:

【中文标题】包含演示者或返回数据的用例?【英文标题】:Use case containing the presenter or returning data? 【发布时间】:2018-02-05 21:30:41 【问题描述】:

考虑到the Clean Architecture 的定义,尤其是描述控制器、用例交互器和演示者之间关系的小流程图,我不确定我是否正确理解“用例输出端口”应该是什么。

干净的架构,如端口/适配器架构,区分主要端口(方法)和次要端口(由适配器实现的接口)。按照通信流程,我希望“用例输入端口”是一个主要端口(因此,只是一个方法),而“用例输出端口”是一个要实现的接口,也许是一个构造函数参数,采用实际的适配器,以便交互者可以使用它。

做一个代码示例,这可能是控制器代码:

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 表示“继承”,而不是“实现”,但大概就是这个意思)。

但是,我对这种方法的问题是用例必须处理演示本身。现在,我看到Presenter 接口的目的是足够抽象以表示几种不同类型的演示者(GUI、Web、CLI 等),并且它实际上只是意味着“输出”,这是一种用例很可能有,但我仍然对它并不完全有信心。

现在,在网上寻找干净架构的应用程序时,我似乎只发现人们将输出端口解释为返回某些 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() 方法(这是我们的新输出端口)。此外,在我看来,我们在这里打破了“告诉,不问”的原则,因为我们要求交互者提供一些数据来处理它,而不是告诉它在第一名。

那么,这两种选择中的任何一种都是根据清洁架构对用例输出端口的“正确”解释吗?它们都可行吗?

在this answer to another question 中,Robert Martin 准确描述了一个用例,其中交互者根据读取请求调用演示者。没有提到MVC、MVVC等,所以我猜Clean Architecture和MVC的配合不是很好?

点击地图会导致调用placePinController。它收集点击的位置和任何其他上下文数据,构造一个 placePinRequest 数据结构并将其传递给 PlacePinInteractor,后者检查 pin 的位置,在必要时对其进行验证,创建一个 Place 实体来记录 pin,构造一个 EditPlaceReponse对象并将其传递给 EditPlacePresenter,它会调出地点编辑器屏幕。

一个可能的解释是,传统上会进入控制器的应用程序逻辑,在这里被移动到交互器,因为我们不希望任何应用程序逻辑泄漏到应用程序层之外。因此,这里的模型不是调用演示者,因为交互者不是模型,而是控制器的实际实现。模型只是被传递的数据结构。这似乎得到了证实:

这一层中的软件是一组适配器,可将数据从最适合用例和实体的格式转换为最适合某些外部机构(如数据库或 Web)的格式。

从原始文章中,谈论接口适配器。由于控制器必须只是将一种数据格式转换为另一种数据格式的瘦适配器,因此它不能包含任何应用程序逻辑,因此将其移至交互器。

【问题讨论】:

【参考方案1】:

符合CQS原则(命令查询分离原则)。

“执行”方法不应返回值,因为它执行命令。 因此,响应将提供给您的“execute”方法将接收的实例,然后从这里,您将调用此参数“presenter”,它实现了 UseCase 层中描述的适当接口。

所以从控制器,给定:

private IMyUseCase myUseCase; //Injected in Controller constructor

private IMyUseCaseResponseHandler presenter = new MyPresenter();

MyUseCaseRequest myUseCaseRequest = new MyUseCaseRequest();

不要这样做:

MyUseCaseResponse myUseCaseResponse = 
    this.myUseCase.handleRequest(this.myUseCaseRequest); //doesn't match CQS

this.presenter.present(myUseCaseResponse);

但是这样做:

this.myUseCase.handleRequest(this.myUseCaseRequest, this.presenter);

并且UseCase会调用名为“present”的IMyUseCaseResponseHandler方法, 将 MyUseCaseResponse 实例传递给它。

【讨论】:

【参考方案2】:

In a discussion related to your question,Bob 大叔在他的 Clean Architecture 中解释了演示者的目的:

鉴于此代码示例:

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 对象中的一个字段是日期。该字段将是一个二进制日期对象,可以以许多不同的日期格式呈现。需要一个非常具体的日期格式,可能是 DD/MM/YYYY。创建格式是谁的责任?如果交互器创建该格式,那么它对视图了解太多。但是如果视图采用二进制日期对象,那么它对视图了解太多交互者。 “演示者的工作是从响应对象中获取数据并为视图设置格式。视图和交互者都不知道彼此的格式。” --- 鲍勃叔叔

鉴于 Bob 大叔的回答,我认为 我们是否执行 选项 #1(让交互者使用演示者)没有那么重要... p>

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,因为我希望能够控制 interactor 何时 显示数据和错误消息,比如下面这个例子:

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,并且使controller 解析 response 对象...

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);
        
    

我不喜欢在 controller 中解析 response 数据中的错误,因为如果我们这样做,我们正在做多余的工作 --- 如果我们更改 interactor 中的某些内容,我们还必须更改其中的某些内容controller

此外,如果我们稍后决定重用我们的 interactor 来使用控制台呈现数据,例如,我们必须记住将所有这些 if/else 复制粘贴到我们控制台应用程序的 controller 中。

// 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/elseinteractor


如果您使用的是 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

【讨论】:

投票,因为我也出于同样的原因使用选项#1。【参考方案3】:

我认为你很好地解释了你的问题和你的 cmets 中已经对 k3b 的答案的所有内容。

关键是:controller和presenter是同一个类吗?

如果你使用 Asp.Net MVC 作为 web 框架,例如控制器和演示者是同一个类。在这种情况下,就接口而言,不需要输出端口。控制器只是调用交互器上的一个方法并获取一些输出数据作为返回值。

如果控制器和演示者分开类,则需要一种“将结果传递给演示者”的方法。为此,需要输出端口。输出端口实际上是在用例圈中定义的接口,并在接口适配器圈中实现。

两种方法都可行

如果您对更详细的示例感兴趣,可以查看我的博客系列:https://plainionist.github.io/Implementing-Clean-Architecture-UseCases/

更新:我在此处添加了一篇博文以深入探讨此讨论:https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/

【讨论】:

感谢您的回复!我不确定让控制器完成演示者的工作是否真的有益......将两者分开意味着让控制器忘记响应模型,例如,这将有助于实现 CQRS。或者更简单地说,它将允许替换演示者,而控制器不受影响。当然,这对于许多用例来说可能过于复杂,但我认为这是 Clean Architecture 正在推动的方向。 总的来说我同意你的说法。确定单一职责模式是做出该决定时的强大驱动力。但与每种模式一样:应用它是否有意义取决于上下文。与架构模式相同。如果你有一个类似的简单应用程序并选择了像 asp.net MVC 这样的 MVC 框架,那么将控制器和演示者放在一个类中将是一个很好的折衷方案。即使在这种情况下,进一步遵循干净的架构也是有益的:依赖规则、无框架的 bizlogic ...【参考方案4】:

文章说用例独立于 gui(演示者),因此控制器的工作是与用例(也称为服务或工作流)和演示者对话

[2017-08-29 更新]

如果模型使用演示者接口,这不再是一个干净的 mvc、mvp 或 mvvm 架构,而是其他东西。

【讨论】:

是的,但是独立性也可以通过使用在用例层中定义的通用“演示者”接口来实现,实际演示者将实现该接口。因此,用例只知道会有某种“输出”服务,但不知道具体是哪一种。 我更新了这个问题,并参考了 Robert Martin 在这个特定问题上的更详细描述。在这一点上,我不确定在 Clean Architecture 中严格遵循 MVC 是否重要。 我自己刚刚开始处理这个问题。我很难找到将控制器用作演示者的任何问题。更多的外层,你得到更少的高水平,无论如何处理更多的细节。因此,用例返回响应,您的控制器将其格式化为视图模型。完毕。如果你必须改变它,你会的。我没有发现这是一个问题。即使您在这里使用了演示者,也必须为其命名,以便您现在拥有一个耦合

以上是关于包含演示者或返回数据的用例?的主要内容,如果未能解决你的问题,请参考以下文章

DataSet Spark 的用例是啥?

接口自动化用例设计

IEEE 754 中 Infinity 的用例是啥

接口测试&管理续集

在一个中等复杂的用例中,策略处理和使订阅的缓存数据失效

用例建模Use Case Modeling