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

时间:2017-08-28 14:57:16

标签: architecture clean-architecture hexagonal-architecture

考虑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);
    }
}

这种解释似乎可以通过上述图表本身得到证实,其中控制器和输入端口之间的关系由带有" sharp"的实线箭头表示。 head(UML用于"关联",意思"有一个",其中控制器"有一个"用例),而演示者和输出端口之间的关系用实线箭头表示"白色" head(UML for" inheritance",这不是"实现",但可能是这个意思)。

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

现在,在网上浏览干净架构的应用程序,我似乎只找到人们将输出端口解释为返回某些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等,所以我猜清洁架构一般不能很好地与MVC一起发挥作用?

  

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

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

  

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

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

4 个答案:

答案 0 :(得分:0)

文章说用例与gui(演示者)是独立的,因此控制器的工作是与用例(即服务或工作流程)和演示者交谈

[更新2017-08-29]

如果模型使用presenter-interface,那么这不再是一个干净的mvc,mvp或mvvm架构,而是其他东西。

答案 1 :(得分:0)

我想你很好地解释了你在问题中的所有内容以及对k3b答案的评论。

关键方面是:控制器和演示者是同一个类吗?

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

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

两种方法都是可行的

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

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

答案 2 :(得分:0)

In a discussion related to your question,鲍勃叔叔在其“干净的体系结构”中解释了演示者的目的:

给出以下代码示例:

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。创建格式的责任是谁?如果交互者创建了该格式,那么它对View的了解就太多了,但是如果视图采用了二进制date对象,那么它对它的了解就太多了交互者。   

  “演示者的工作是从响应对象中获取数据并将其格式化为视图。视图和交互器都不知道彼此的格式。”   

  -鲍勃叔叔

鉴于鲍勃叔叔的回答,我认为我们是否执行选项#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 ,因为我希望能够在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);
    }
}

...我希望能够在if/else内部而不是在交互器外部进行与演示相关的interactor

如果另一方面,我们执行选项2,则必须将错误消息存储在response对象中,然后将response对象从interactor返回到controller,并controller对象{strong}解析 ...

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); } } } 数据中的response内部错误,因为如果这样做,我们会做多余的工作---如果我们更改controller中的内容,我们会还必须更改interactor中的某些内容。

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

controller

如果我们使用选项#1,我们将// 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); } 仅放在一个地方:if/else


如果您使用的是ASP.NET MVC(或其他类似的MVC框架),则选择方法#2是更简单的方式。

但是我们仍然可以在这种环境中执行选项1。 以下是在ASP.NET MVC中执行选项#1的示例:

(请注意,我们的ASP.NET MVC应用的演示者必须拥有interactor

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

(请注意,我们的ASP.NET MVC应用的演示者必须拥有// 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); } }

如果我们决定为控制台创建另一个应用程序,则可以重复使用上面的public IActionResult Result并为控制台仅创建UseCaseController

Presenter
// 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); } }

答案 3 :(得分:0)

符合 CQS 原则(Command Query Separation 原则)。

“execute”方法不应返回值,因为它执行命令。 因此,响应将提供给您的“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 实例传递给它。