Action-Domain-Responder模式介绍

2020-09-18  本文已影响0人  许一沐

引用来源

出现的原因

MVC 诞生于 1979 年,它诞生于使用 CLI 用户界面的桌面应用上下文中,它暗示如果用户外部因素导致数据库变化,那么 UI 就应该自动地变化。TA是一种展现模式,是一种将模型、领域与用户界面分离的方法。


image.png

TA将代码拆分成了三个概念单元:

最初的 MVC 模式还有其它一些需要了解的的重要概念:

现在大众所熟知的 HTTP 请求响应范式并没有使用最初的 MVC 风格。这是因为,按照原始的设想,数据从 View 流向 Controller,这和我熟悉的一样,但另一边,数据直接从 Model 流向 View,并没有经过 Controller。

而且在现在的请求响应范式中,当数据库中的数据发生变化时,并不会触发浏览器中展示 View 的更新(尽管可以用 Web Socket 实现)。要看到更新后的数据,用户需要发起一次新的请求,而更新的数据总是会通过 Controller 返回。

因为大多数 Web 应用不会用 UI 变化来作为服务端发生的变化的后果,它们总是从 UI 发起对服务端的调用来更新界面。

为了改进MVC模式在web应用中的缺陷,有一些业内的大拿提出了一些改良方案.

如 Paul James 在 2008 年提出 Resource-Method-Representation模式(简称 RMR),它将 MVC 模式应用到了 REST API 上下文之中。其主要包括:

Resource

它的思想是将实体建模成 REST 资源(Resource,模式名称中的第一个 R),它只有和 HTTP 方法对应的公有方法:

<?php
// taken from http://www.peej.co.uk/articles/rmr-architecture.html
class Resource {
    private resourceData = [];
    method constructor(request, dataSource) {
        // load data from data source
    }
    method get(request) {
        return new Response(200, getRepresentation(request.url, resourceData));
    }
    method put(request) {
        return new Response(405);
    }
    method post(request) {
        return new Response(405);
    }
    method delete(request) {
        return new Response(405);
    }
}

Method

当向 API 发出一个请求时,它被路由给这些业务对象(即资源)中的一个,然后对应这个请求 HTTP 方法的资源中的一个方法(Method)被调用。该业务对象方法接下来将负责返回一个包含状态码和标头的完整HTTP响应。

Representation

以 API 或发起请求的客户端选择的格式表示的资源就是展现(Representation),这些格式有,JSON、XML 等等....展现就是由方法创建并返回给客户端的响应的内容,如果有任何需要返回的内容的话。

改进

RMR 告诉我们如何设计我们的业务对象和领域对象。不仅如此,它还告诉我们领域实体应该体现传达机制:HTTP 方法。

这就意味着它不只是展现模式,而是架构模式,因为它要影响应用的所有层次。它还意味着以这种模式构建的应用并非以领域为中心,而是以 HTTP 为中心。我们的实体最终拥有的是反应传达机制而非领域操作的方法。

缺陷

它将自己与 HTTP 紧密地耦合在一起,以至于很难将其映射到 CLI 或 GUI 界面。

Action-Domain-Responder(ADR)

ADR 模式由 Paul M. Jones 于 2014 年提出,其思想和 RMR 一致,就是将 MVC 应用于 Web REST API 上下文。

image.png

Action

是连接 Domain 和 Responder 的逻辑。它使用收集自 HTTP 请求的输入调用 Domain,然后使用构建 HTTP 响应所需的数据调用 Responder 。

<?php
namespace Pmjones\Adr\Web\Blog\Add;

use Pmjones\Adr\Domain\Blog\BlogService;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

class BlogAddAction
{
    protected $domain;
    protected $responder;

    public function __construct(
        BlogService $domain,
        BlogAddResponder $responder
    ) {
        $this->domain = $domain;
        $this->responder = $responder;
    }

    public function __invoke(Request $request) : Response
    {
        $payload = $this->domain->newPost();
        return $this->responder->__invoke($request, $payload);
    }
}

Domain

是组成应用核心的领域逻辑的入口,它根据需要修改状态并保存。 它可能是事务脚本、服务层、应用服务或者其它类似的概念。

<?php
namespace Pmjones\Adr\Domain\Blog;

use Pmjones\Adr\DataSource\Blog\BlogMapper;
use Pmjones\Adr\Domain\ApplicationService;
use Pmjones\Adr\Domain\Payload;
use Exception;

class BlogService extends ApplicationService
{
    protected $mapper;
    protected $filter;

    public function __construct(
        BlogMapper $mapper,
        BlogFilter $filter
    ) {
        $this->mapper = $mapper;
        $this->filter = $filter;
    }

    protected function fetchPage(int $page = 1, int $paging = 10) : Payload
    {
        $blogs = $this->mapper->selectAllByPage($page, $paging);

        return $this->newPayload(Payload::STATUS_FOUND, [
            'blogs' => $blogs,
        ]);
    }

    protected function fetchPost(int $id) : Payload
    {
        $blog = $this->mapper->selectOneById($id);

        if ($blog === null) {
            return $this->newPayload(Payload::STATUS_NOT_FOUND, [
                'id' => $id
            ]);
        }

        return $this->newPayload(Payload::STATUS_FOUND, [
            'blog' => $blog
        ]);
    }

    protected function newPost(array $data = []) : Payload
    {
        return $this->newPayload(Payload::STATUS_NEW, [
            'blog' => $this->mapper->newRecord($data)
        ]);
    }

    protected function addPost(array $data) : Payload
    {
        // instantiate a new record
        $blog = $this->mapper->newRecord($data);

        // validate the record
        if (! $this->filter->forInsert($blog)) {
            return $this->newPayload(Payload::STATUS_NOT_VALID, [
                'blog' => $blog,
                'messages' => $this->filter->getMessages()
            ]);
        }

        // insert the record
        $this->mapper->insert($blog);
        return $this->newPayload(Payload::STATUS_CREATED, [
            'blog' => $blog,
        ]);
    }

    protected function editPost(int $id, array $data) : Payload
    {
        // fetch the record
        $blog = $this->mapper->selectOneById($id);
        if ($blog === null) {
            return $this->newPayload(Payload::STATUS_NOT_FOUND, [
                'id' => $id
            ]);
        }

        // set data in the record; do not overwrite existing $id
        unset($data['id']);
        $blog->setData($data);

        // validate the record
        if (! $this->filter->forUpdate($blog)) {
            return $this->newPayload(Payload::STATUS_NOT_VALID, [
                'blog' => $blog,
                'messages' => $this->filter->getMessages()
            ]);
        }

        // update the record
        $this->mapper->update($blog);
        return $this->newPayload(Payload::STATUS_UPDATED, [
            'blog' => $blog,
        ]);
    }

    protected function deletePost(int $id) : Payload
    {
        // fetch the record
        $blog = $this->mapper->selectOneById($id);
        if (! $blog) {
            return $this->newPayload(Payload::STATUS_NOT_FOUND, [
                'id' => $id
            ]);
        }

        // delete the record
        $this->mapper->delete($blog);
        return $this->newPayload(Payload::STATUS_DELETED, [
            'blog' => $blog,
        ]);
    }
}

Responder

是基于自 Action 接收的数据创建 HTTP 响应的展现逻辑。它处理状态码、标头与 Cookie、内容、格式与转换、模板与视图,等等。

<?php
namespace Pmjones\Adr\Web\Blog\Add;

use Pmjones\Adr\Web\Blog\BlogResponder;

class BlogAddResponder extends BlogResponder
{
    protected function new() : void
    {
        $this->renderTemplate('add');
    }
}

工作机制

Responder 基于对领域响应的解析和理解来构造 HTTP 响应,而领域响应又依赖操作方法的用例。这意味着每个操作方法都需要一个特定的 Responder。

如果我们将所有资源方法放到同一个控制器中,我们就需要实例化全部 Responder 并注入到控制器中,而我们在一次 HTTP 请求中只会使用一个 Responder,这显然不是最优的方案。解决方法是每个控制器只有一个方法,这种控制器和它唯一的操作方法就是 ADR 所说的 Action。

既然 Action 只有一个方法,方法名就可以使用通用的 run、execute、或是 PHP 中的 __invoke,让这个类变成可以调用的。然而,由于其思想是将 MVC 模式应用到 HTTP REST API 上下文,Action(控制器)名称会被映射为 HTTP 请求方法,因此我们将得到名为 Get、Post、Put、Delete...的 Action,清楚地表明了每个 HTTP 请求类型调用的控制器。作为一种组织形式,一个资源的所有 Action 应该被一起放以该资源命名的文件夹下。

与 RMR 的差异

有些人认为 ADR 与 RAR 是同样的模式,只是细节有所调整, 然而并非如此:

ADR 模式专为 REST API 设计,是 MVC 在 HTTP 请求/响应范式上的最佳应用,因为它清晰地将 HTTP 请求和响应对应到了 Domain 请求和响应,同时仍然保持了 Domain 和展现层之间完全的解耦。

HTTP 请求方法(期望对资源进行的操作)被明确地连接到接收 HTTP 请求的代码,因为每个 HTTP 方法都直接映射到一个控制方法的名字。

这样做还有一个额外的好处,那就是产生了清晰、明确和可预测的代码组织结构,而不是具有大量操作的控制器,这些操作通常是不相关的,命名糟糕,不可预测,而且常常执行非常类似的操作。换句话说,它避免了混乱的意大利面式的控制器和操作。

还有,它也非常好地解耦了交互自身的代码(调用领域)和理解交互结果(领域响应)并转换给客户端的代码。

因为TA是专为REST API 而设计的,那"如何将其完全应用于 HTML 界面"呢?

我们可以模拟一些 REST API 没有的额外的 HTTP 方法,专门处理 HTML 请求。例如,我们可以在一个 REST API 中使用 PUT 或 POST 来创建和/或更新资源,而这就是该资源所需的全部方法,可是对于 HTML 界面来说,我们在发送 PUT 或 POST 之前需要一个表单,但是没有 HTTP 方法专门供客户端请求创建资源或编辑的表单。我们可以使用一个带有 create 或 edit 标头的 GET 请求来模拟它,前端控制器可以解析该请求并转发给对应的名为 Create 或 Edit 的操作,然后这些操作将回复对应的 HTML 表单。

对比MVC/ADR

image.png
上一篇 下一篇

猜你喜欢

热点阅读