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

swa*_*nee 6 architecture hexagonal-architecture clean-architecture

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

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

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

Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();
Run Code Online (Sandbox Code Playgroud)

演示者界面:

// Use Case Output Port
interface Presenter
{
    public void present(Data data);
}
Run Code Online (Sandbox Code Playgroud)

最后,交互器本身:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这种解释似乎得到了前面提到的图表本身的证实,其中控制器和输入端口之间的关系由带有“锋利”头的实心箭头表示(UML 表示“关联”,意思是“有一个”,其中控制器“有一个”用例),而演示者和输出端口之间的关系由带有“白色”头的实心箭头表示(UML 表示“继承”,这不是“实现”的那个,但可能就是这样反正意思)。

但是,我对这种方法的问题是用例必须处理演示本身。现在,我看到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
Run Code Online (Sandbox Code Playgroud)

这很有吸引力,因为我们将“调用”表示的责任从用例中移出,因此用例不再关心知道如何处理数据,而只关心提供数据。此外,在这种情况下,我们仍然没有打破依赖规则,因为用例仍然不知道外层的任何信息。

然而,用例不再控制实际演示的执行时间(这可能很有用,例如在那个时候做额外的事情,比如日志记录,或者在必要时完全中止它)。另外,请注意我们丢失了用例输入端口,因为现在控制器只使用getData()方法(这是我们的新输出端口)。此外,在我看来,我们在这里打破了“告诉,不要问”的原则,因为我们要求交互者提供一些数据来用它做某事,而不是告诉它做实际的事情第一名。

那么,根据 Clean Architecture,这两个替代方案中的任何一个是对用例输出端口的“正确”解释吗?他们两个都可行吗?

这个对另一个问题的回答中,Robert Martin 准确地描述了一个用例,其中交互者根据读取请求调用演示者。没有提到MVC、MVVC等,所以我猜一般情况下Clean Architecture和MVC玩得不太好?

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

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

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

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

Jbo*_*aga 7

在与您的问题相关的讨论中,鲍勃叔叔在他的《清洁架构》中解释了演示者的目的:

鉴于此代码示例:

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()
        );
    }
}
Run Code Online (Sandbox Code Playgroud)

鲍勃叔叔是这样说的:

"演示者的目的是将用例与 UI 格式解耦。 在您的示例中, $response 变量由交互器创建,但由视图使用。这将交互器与视图耦合起来。例如,假设 $response 对象中的字段之一是日期。该字段将是一个二进制日期对象,可以以许多不同的日期格式呈现。需要一种非常特定的日期格式,可能是 DD/MM/YYYY。创建格式是谁的责任?如果交互器创建了该格式,那么它就对视图了解太多。但是如果视图采用二进制日期对象,那么它对交互器了解太多。

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

--- 鲍勃叔叔

鉴于鲍勃叔叔的回答,我认为我们是否执行选项#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);
    }
}
Run Code Online (Sandbox Code Playgroud)

...或者我们执行选项#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);
    }
}
Run Code Online (Sandbox Code Playgroud)

就我个人而言,我更喜欢选项#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);
    }
}
Run Code Online (Sandbox Code Playgroud)

...我希望能够执行这些与交互器if/else内部而不是外部的演示相关的操作。interactor

另一方面,如果我们执行选项#2,我们必须将错误消息存储在对象中responseresponseinteractor到返回该对象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;
    }
}
Run Code Online (Sandbox Code Playgroud)
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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我不喜欢解析response中的数据以查找错误controller,因为如果我们这样做,我们就是在做多余的工作——如果我们更改 中的某些内容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);
}
Run Code Online (Sandbox Code Playgroud)

如果我们使用选项#1,我们将if/else 只在一个地方有这个:interactor.


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

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

(请注意,我们需要public IActionResult Result在 ASP.NET MVC 应用程序的演示者中包含)

// 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);
}
Run Code Online (Sandbox Code Playgroud)
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);
    }
}
Run Code Online (Sandbox Code Playgroud)
// 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

(请注意,我们需要public IActionResult Result在 ASP.NET MVC 应用程序的演示者中包含)

如果我们决定为控制台创建另一个应用程序,我们可以重用上面的内容并为控制台UseCase创建 和ControllerPresenter

// 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);
    }
}
Run Code Online (Sandbox Code Playgroud)
// 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

public IActionResult Result(请注意,我们的控制台应用程序的演示者中没有)


k3b*_*k3b 0

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

[更新2017-08-29]

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