译《领域驱动设计之PHP实现》架构风格(中)

视图层

视图层可以从模型层和/或者控制层接收数据,也能向其发送数据。它的主要目的是向用户UI层呈现模型,同时在模型每次更新后刷新UI的呈现形式。一般来说,视图层接收的对象 – 通常是一个数据传输对象(DTO)而不是模型层实例 – 从而收集被成功呈现的所有必需信息。对于 PHP,这已经有几种模板引擎可以帮助从模型本身和从控制层分离模型的表示。其中最流行的一个叫Twig。让我们看看使用Gwig的视图层是怎样的。

为什么是数据传输对象(DTO)而不是模型实例? 这是一个古老且有活力的话题。为什么要创建一个 DTO 而不是把模型实例直接交给视图层? 简短来说,还是关注点分离。让视图层方便直接使用模型实例将导致视图层与模型层间的紧耦合。事实上,模型层中的一个改变将可能破坏所有使用改变后的模型的所有视图。

{% extends "base.html.twig" %}
{% block content %}
{% if errormsg is defined %}<div class="alert error">{{ errormsg }}</div>{% else %}<div class="alert success">
   Bravo! Post was created successfully!</div>{% endif %}<table>
   <thead>
   <tr>
       <th>ID</th>
       <th>TITLE</th>
       <th>ACTIONS</th>
   </tr>
   </thead>
   <tbody>
   {% for post in posts %}    <tr>
       <td>{{ post.id }}</td>
       <td>{{ post.title }}</td>
       <td><a href="{{ editPostUrl(post.id) }}">Edit Post</a></td>
   </tr>
   {% endfor %}    </tbody></table>{% endblock %}

大多数时候,当模型触发一个状态改变,同时也会通知相关视图UI已经刷新了。在一个典型的web场景中,由于客户端-服务器这一约束,模型和它的表示之间的同步可能会有一点棘手。在这些情况下,通常要用一些 JavaScript 定义的交互方式来维护这些同步。由于这个原因,近年来 JavaScript MVC 框架开始变得广泛流行,正如下面这些框架: – AngularJS – Ember.js – Marionette.js – React

控制层

控制层主要负责组织和编排视图和模型。它接收来自视图层的消息和为了执行期望的动作而触发模型行为。此外,为了呈现模型的表示,它也发送消息给视图。被执行的动作也需要感谢应用层,即负责编排,组织和封装领域行为的这一层。 就一个 PHP 的 web 应用来说,控制层包括一组类,为了达到它们的目的,叫做“HTTP”。换句话说,它们接收一个 HTTP 请求,同时返回一个 HTTP 响应:

class PostsController{
   public function updateAction(Request $request)
   {
       if (
           $request->request->has('submit') &&
           Validator::validate($request->request->post)
       ) {
           $postService = new PostService();
           try {
               $postService->createPost(
                   $request->request->get('title'),
                   $request->request->get('content')
               );
               $this->addFlash(
                   'notice',
                   'Post has been created successfully!'
               );
           } catch (Exception $e) {
               $this->addFlash(
                   'error',
                   'Unable to create the post!'
               );
           }
       }
       return $this->render('posts/update-result.html.twig');
   }}

依赖倒置:六边形架构

依照分层架构的基本思想,当实现包含有关基础设施层的领域接口时,是存在风险的。 以 MVC 为例,先前例子中的PostRepository类应该放在领域模型当中。然而,把基础设施细节放在领域之中是违背关注点分离这一原则的.这是有问题的;它很难避免违背分层架构的基本思想,如果模型层有技术实现,这将会导致一种很难测试的代码类型出现。

依赖倒置原则(DIP)

我们可以怎样改进呢?由于领域模型层依赖基础设施的具体实现,依赖倒置原则(DIP),可以通过应将基础设施层重新放在其它三层之上来应用。

依赖倒置原则 高层次模型不应该依赖于低层次模型。它们都应该依赖于抽象。 抽象不应该依赖于细节,细节应该依赖于抽象。 –Robert C.Martin

通过使用依赖倒置原则,架构模式改变了,基础设施层 – 可以称为低层次模块 – 现在依赖于UI,应用层和模型层这些高层次模块。于是依赖被倒置了。

但什么是六边形架构呢?它是怎样适合这里面的所有问题呢?六边形架构(即端口与适配器)是 Alistair Cockburn 在他的书《六边形架构》中定义的。它将应用描述成一个六边形,每条边被表示为一个端口和多个适配器。端口是一个可插拔适配器的连接器件,适配器将外部输入转换为应用内部可理解的数据。就依赖倒置(DIP)来说,端口是高层次模块,适配器是低层次模块。此外,如果应用需要发送消息给外部,它可以用一个带适配器的端口来发送和转换可以被外部可理解的数据。正因为如此,六边形架构提出了应用里对称性的概念,这也是为什么架构模式发生变化的主要原因。它经常被表示为六边形,因为讨论顶层或者底层不再有任何意义。相反,六边形架构主要是外与内部间的对话。

如果你想要了解更多细节,Youtube 上有 Matthias Noback 关于六边形架构的非常好的视频

应用六边形架构

我们继续博客应用的例子,首先我们需要的概念就是端口,即外部世界与应用程序对话的渠道。在这个例子中,我们使用一个 HTTP 端口及相应的适配器,外部通过端口发送消息给应用程序。博客例子使用数据库存储整个博客帖子集合,所以为了让应用程序从数据库中检索博客帖子数据,端口就是必须的:

interface PostRepository{
   public function byId(PostId $id);   public function add(Post $post);}

该接口暴露有关博客帖子的端口,应用程序通过它检索信息。它也被放置在领域层。现在,则需要这个端口的适配器。该适配器负责定义用特定技术检索博客帖子的方法:

class PDOPostRepository implements PostRepository{
   private $db;   public function __construct(PDO $db)
   {
       $this->db = $db;
   }   public function byId(PostId $id)
   {
       $stm = $this->db->prepare(
           'SELECT * FROM posts WHERE id = ?'
       );
       $stm->execute([$id->id()]);
       return recreateFrom($stm->fetch());
   }   public function add(Post $post)
   {
       $stm = $this->db->prepare(
           'INSERT INTO posts (title, content) VALUES (?, ?)'
       );
       $stm->execute([
           $post->title(),
           $post->content(),
       ]);
   }}

只要我们定义了端口及其适配器,最后就是重构PostService从而可以它们。这可以通过依赖注入(Dependency Injection)轻松实现:

class PostService{
   private $postRepository;   public function __construct(PostRepositor $postRepository)
   {
       $this->postRepository = $postRepository;
   }   public function createPost($title, $content)
   {
       $post = Post::writeNewFrom($title, $content);
       $this->postRepository->add($post);
       return $post;
   }}

这仅仅是六边形架构的一个简单例子,它是一个灵活的,类似分层,有利于关注点分离的架构。由于内部应用通过端口与外部通信,这也同时提升了对称性。从现在开始,这将作为基本架构来构建和解释 CQRS 及事件源模式。

想了解更多关于这种架构的例子,你可以去查看附录中的《Hexagonal Architecture with PHP》。对于一个更详细的例子,你可以跳到第11章-应用程序,此章介绍了一些高级主题,像事务性和其它交叉问题。

命令查询职责分离(CQRS)

六边形架构是一个很好的基础性架构,但它有一些限制。例如,复杂 UI 需要在不同的表单上显示聚合信息(第八章,聚合),或者它们可以从多个聚合获取数据。在这种场景下,我们可以在仓储里使用许多查找方法(可能和应用程序里存在的 UI 视图一样多)。或者,也许我们可以直接将这种复杂性转移到应用服务,使用复杂结构来从多个聚合里积累数据,这里有一个例子:

interface PostRepository{
   public function save(Post $post);   public function byId(PostId $id);   public function all();   public function byCategory(CategoryId $categoryId);   public function byTag(TagId $tagId);   public function withComments(PostId $id);   public function groupedByMonth();// ...}

当这些技术被滥用时,对 UI 视图层的构建将变得非常痛苦。我们应该权衡是该用应用服务返回领域实例还是某些 DTO 。后一种选择里,我们避免了领域模型与基础设施代码( web 控制器,CLI 控制器等等)间的紧耦合。

幸运的是,我们有另一种方法。如果需求有许多且独立的视图,我们可以将它们从领域模型中排除,把它们视为一种纯粹的基础设施问题。这种方法即基于一个设计原则,命令查询分离(CQS)。这个原则由 Bertrand Meyer 提出,然后,相应地,成长为一个全新的架构模式,叫作命令查询职责分离(CQRS),CQRS由 Greg Young 定义。

命令查询分离 提出一个问题不应该改变对应的答案 – Bertrand Meyer 这种设计原则提出每个方法应该要么是执行动作的命令,要么是返回数据给调用者的查询,而不是两者都是 – 维基百科

CQRS谋求一种更为激进的关注点分离,即将模型分为两部分: – 写模型: 同时也称为命令模型,它执行写入和负责真实的领域行为。 – 读模型: 它在应用内负责读取,并将这部分视为领域模型之外的内容。

每次只要触发一个命令给写模型,它就会执行渴求数据的存储写入。除此之外,它还会触发读模型的更新,保证在读模型上显示最后一次的更改。

这种严格的分离导致了另一个问题,最终一致性。读模型的一致性现在受写模型执行的命令的影响。换句话说,读模型是最终一致性的。也就是说,每次当写模型执行一个命令,它就会负责挂起一个进程,依照写模型上最后一次更改,来更新读模型。所以这里存在一个时间窗口,UI可能会向用户展示旧的信息。在 web 场景中,这种情况经常发生,因为我们受当前技术因素限制。

考虑一个 web 应用的缓存系统,每次用新信息数更新数据库时,缓存层的数据有可能是陈旧的,所以每当模型有更新时,也应该同时更新缓存系统。所以 缓存系统是最终一致性的。

这些处理过程,在CQRS术语中被称为写模型投影,或者就称作投影。即投影一个写模型到读模型上。这个过程可以是同步或者异步,取决于你的需要,同时它可以用另一种很有用的战术设计模式 – 领域事件(本书后面的章节会讲到)来实现。写模型投影的基本过程就是收集所有发布的领域事件,然后用事件中的信息来更新读模型。

本文分享自微信公众号 - PHP技术大全(phpgod)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-07-08

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券