《领域驱动设计之PHP实现》全书翻译 - 模块

 阅读约 37 分钟

模块

  1. 《领域驱动设计之PHP实现》全书翻译 - DDD入门
  2. 《领域驱动设计之PHP实现》全书翻译 - 架构风格
  3. 《领域驱动设计之PHP实现》全书翻译 - 值对象
  4. 《领域驱动设计之PHP实现》全书翻译 - 实体
  5. 《领域驱动设计之PHP实现》全书翻译 - 服务
  6. 《领域驱动设计之PHP实现》全书翻译 - 领域事件
  7. 《领域驱动设计之PHP实现》全书翻译 - 模块
  8. 《领域驱动设计之PHP实现》全书翻译 - 聚合
  9. 《领域驱动设计之PHP实现》全书翻译 - 工厂
  10. 《领域驱动设计之PHP实现》全书翻译 - 仓储
  11. 《领域驱动设计之PHP实现》全书翻译 - 应用服务
  12. 《领域驱动设计之PHP实现》全书翻译 - 集成上下文
  13. 《用PHP实现六边形架构》
当你把某些类一起放进一个模块,你正在告诉下一个要看你的设计的开发者要把他们作为一个整体进行思考。如果说模型是在讲述故事,那么模块就是其中的篇章。

《领域驱动设计-软件核心复杂性应对之道》 -Eric Evans

当遵循领域驱动设计构建应用程序的一个共识就是,把代码放到哪里。尤其当你使用 PHP 框架时,用推荐的方法来放置代码是很重要的,哪里应该放基础设施代码,以及模型中的不同概念应当怎样组织。

在领域驱动设计里,有一个为此的战术模式:模块。如今,每个人都用模块来组织代码。所有语言都有一些工具来组织类和语言定义。Java 有包 (packages)。Ruby 有模块 (modules)。而 PHP 则有命名空间(namespaces)。

领域驱动设计朝着将类打包和分组在一起又迈出了一步,并为这些构建块赋予了语义。实际上,它将模块 (modules) 视为模型 (model) 的一部分。作为模型的一部分,重要的是要找到最合适的命名,将彼此接近的领域对象组合在一起,并使不相关的领域对象保持解耦状态。模块不应被视为分离代码的方法,而应被视为分离模型中有意义的概念的方法。

总体概述

正如在第 1 章,DDD入门中所解释的,我们的领域在其内部组织为子域。每个子域都由一个限界上下文来建模和实现,但有时候需要多个。如果设计得当,每个限界上下文都是一个独立的系统,并由不同的团队开发和管理。我们的建议是用整个应用程序实现每个限界上下文。这意味着两个不同的限界上下文不会在同一个代码仓库中。这样,它们可以独立部署,具有 不同的开发周期,甚至可以使用不同的语言进行开发。在限界上下文中,你将使用模块来对相互之间具有密切关系的模块进行分组。

在 PHP 中使用模块

在 PHP 5.3 之前,还不能完全支持模块。但自从 PHP 5.3 引入以来,我们可以使用 PHP 命名空间来实现模块模式。由于历史原因,我们将介绍 PHP 5.3 以前如何使用命名空间,但你应该尽量使用支持命名空间的 PHP 版本。最好的办法就是始终选择最新的 PHP 稳定发行版。

一级命名空间

一个共识就是使用你公司名称来标识一级命名空间。这有助于避免与第三方库冲突。如果你使用 PSR-0,你得为命名空间准备一个文件夹;如果你使用 PSR-4,则没有必要。我们会很快对此深入讨论。不过首先,让我们来看看 PHP 命名空间的优点。

PEAR 风格命名空间

在 PHP 5.3 之前,由于缺少命名空间结构,使用的都是 PEAR 命名空间。PEAR 是 PHP Extension and Application Repository 的缩写,在过去,它是可重用组件的仓库。它现今仍然活跃,但不是很方便,使用它的人也不再那么多,特别是 Composer 和 Packagist 的引入。作为可重用组件的来源,PEAR 需要一种避免类名冲突的方法,因此贡献者开始使用类名加前缀的方法来表示命名空间。现在仍然有一些项目使用这种形式的命名空间 (PHPUnit 和 Zend FrameWork 1,仅举几例)。下面是一个 PEAR 风格的命名空间:

├── composer.json
├── composer.lock
└── src
    └── BuyIt
        └── Billing
            └── Domain
                └── Model
                    └── Bill
                        └── Bill.php

Bill 实体的类名,使用 PEAR 风格命名空间的话,就变成 BuyIt_Billing_Domain_Model_Bill_Bill。不过,这有点丑陋,它没有遵循领域驱动设计的主要原则:每个类名都应使用通用语言来命名。因此,我们强烈不建议使用它。

PSR-0 和 PSR-4 命名空间

引入 PHP 5.3 时,命名空间以及其它重要功能进入了舞台。这是一个重大转变,同时一些重要的框架组织与 PHP-FIG (PHP Framework Interop Group)一起出现,试图标准化和统一框架与库的创建。该组织发布的首个 PHP 标准建议书 (PSR) 是一种自动加载的标准,简而言之,它使用名称和 PHP 文件之间提出了一对一的关系。如今,PSR-4 (简化了 PSR-0,仍保持类与 PHP 物理文件之间的关系)是组织代码首选的和推荐的方法。我们认为这应该是在项目里实现模块的一个选择。

回到上一节中展示的文件夹结构,让我们看看 PSR-0 的变化。Bill 实体的类名,在使用命名空间和 PSR-0 之后,将简单的变成 Bill,而完全限定的类名就是 BuyIt\Billing\Domain\Model\Bill\Bill

如你所见,这使我们能够依据通用语言来命名领域对象,同时也是组织和结构化代码的更好途径。如果你正在使用 Composer,那么你需要在你的 composer.json 文件中设置一些自动加载的配置:

...
"autoload": {
    "psr-0": {
        "BuyIt\\": "src/BuyIt/"
    }
},
"autoload-dev": {
    "psr-0": {
        "BuyIt": "tests/BuyIt/"
    }
},
...

如果你没有使用 PSR-4,或者没有从 PSR-0 中迁移过来,我们强烈推荐你迁移。你可以摆脱一级命名空间文件夹,同时你的代码结构也能更好的匹配通用语言:

├── composer.json
├── composer.lock
└── src
    └── BuyIt
        └── Billing
            └── Domain
                └── Model
                    └── Bill
                        └── Bill.php

不过,为了避免与第三方库之间的冲突,这里仍然建议在 composer.json 文件里添加一级命名空间:

...
"autoload": {
    "psr-4": {
        "BuyIt\\": "src/"
    }
},
"autoload-dev": {
    "psr-4": {
        "BuyIt\\": "tests/"
    }
},

如果你希望在 PSR-4 使用一级命名空间,这里则需要做一些小的改动:

├── composer.json
├── composer.lock
└── src
    └── BuyIt
        └── Billing
            └── Domain
                └── Model
                    └── Bill
                        └── Bill.php
...
"autoload": {
    "psr-4": {
        "BuyIt\\": "src/BuyIt/"
    }
},
"autoload-dev": {
    "psr-4": {
        "BuyIt\\": "tests/BuyIt/"
    }
},

你可以在这个例子里注意到了,我们分离了 srctests 目录。这是为了优化由 Composer 生成的自动加载文件,同时可以降低在存储类映射时的内存需要。在你生成单元测试代码覆盖报告时,它也可以帮你设置与白名单与黑名单选项。如果你想了解更多关于 Composer 的自动加载配置,可以去看看它的文档。

PEAR 文件怎样?

可以使用它们,不过,我们不推荐这样做。作为一个练习,请列出一份使用 PEAR 文件构建模块时的利弊列表。

限界上下文和应用程序

如果我们以一家名为 BuyIt 的虚拟公司为例,该公司处理电子商务领域,则有可能为解决特定领域问题而为不同限界上下文创建不同的应用程序。

假设这些不同的限界上下文是订单管理,支付管理,产品目录管理,以及库存管理,我们推荐为每个都创建一个应用程序:

├── catalog
│   ├── composer.json
│   ├── composer.lock
│   ├── src
│   └── tests
├── inventory
│   ├── composer.json
│   ├── composer.lock
│   ├── src
│   └── tests
├── orders
│   ├── composer.json
│   ├── composer.lock
│   ├── src
│   └── tests
└── payments
    ├── composer.json
    ├── composer.lock
    ├── src
    └── tests

每个应用程序都会公开所需的任何交付机制集。随着微服务的发展趋势,越来越多的人建立限界上下文,最终暴露 REST API 给外界。然而,限界上下文不仅仅是一个 API。请记住 API 只是许多交付机制中的一种。限界上下文也可以提供与之交互的 web 界面。

两个界限上下文能共存于同一个应用程序吗?有没有其它方法?

最佳的选项就是一个子域对应一个限界上下文及一个应用程序。如果我们有一个限界上下文是用两个应用程序实现的,那么维护和部署就会有些棘手。对于就一个应用程序实现两个限界上下文的情况,部署过程,运行测试的时间,以及合并问题都会降低开发速度。

注意,每个限界上下文名称在我们电子商务领域里都表示一个有意义的概念,并且它依据通用语言来命名:

  • Catalog (产品目录) 持有所有与产品描述,产品组合等等相关联的代码。
  • Inventory (库存) 持有所有与产品存货管理相关的代码。
  • Order (订单) 持有所有与订单进度系统相关的代码。它包含所有负责处理订单的有限状态机。
  • Payment (支付) 持有所有与支付,金额,运单相关的代码。

在模块中组织代码

让我们更深入挖掘其中一个限界上下文。以检查订单上下文结构细节为例。顾名思义,这个限界上下文负责表示订单通过的所有流程 - 从创建到交付到购买该订单的用户。此外,它是一个独立的应用程序,因为包含源代码文件夹和测试文件夹。源代码文件夹包含所有能使此限界上下文正常工作的代码:即领域代码,基础结构代码,以及应用程序层。

下图应该能说明这个组织结构:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           └── Infrastructure
└── tests

所有代码都有一个用 vendor 命名空间的前缀,依据组织结构名称(这里为 BuyIt),并且包含两个子文件夹:Domain 持有所有领域代码,Infrastructure 持有基础设施层,从而将所有领域逻辑从基础设施层剥离出来。按照这种结构,我们将明确要使用六边形架构作为基础架构。下面是可以使用的替换示例方案:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   ├── Model
│           │   └── Service
│           └── Infrastructure
└── tests

上面的结构形式使用了一个额外的子文件夹来存储领域模型中定义的服务(Services)。尽管这种组织可能更有意义,但我们不想在此处使用它,因为这种分离代码的方式侧重于体系结构元素,而不是模型中的相关概念。我们认为,这种风格很容易产生领域模型之上的某种服务层,尽管这不算坏事。不过请记住,领域服务是用来描述领域中不属于实体或值对象的操作。因此从现在开始,我们将继续 用之前的代码组织。

可以将代码直接放进 Domain/Model 子文件夹里,例如,习惯性将通知接口和服务(例如 DomainEventPublisherDomainEventSubscriber)放入其中。

如果我们要建模一个订单管理上下文,我们可能需要一个 Order 实体,以及它的仓储 (Repository) 和所有状态信息。所以我们首先尝试将这些元素全部放到 Domain/Model 子文件夹里。一眼看来,这可能就是最简单的方式:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           │       ├── OrderLine.php
│           │       ├── OrderLineWasAdded.php
│           │       ├── Order.php
│           │       ├── OrderRepository.php
│           │       └── OrderWasCreated.php
│           └── Infrastructure
└── tests

设计指导

考虑实现模块时要注意的一些基本规则和典型问题:

  • 命名空间应该依据通用语言命名。
  • 不要用模式名称或者构建块(值对象,服务,实体等待)来命名 namespace。
  • 创建命名空间以使内部与其它命名空间尽可能地松耦合。
  • 用重构代码同样的方法重构命名空间。移动,重命名,组合,分解等等。
  • 不要使用商品名称,因为他们可以改变。坚持使用通用语言。

我们已经有了 OrderOrderLine 实体,OrderLineWasAdded 以及 OrderWasCreated 事件,还有 OrderRepository 在同一个子文件夹 Domain/Model 里。这个结构可能刚好,但这是因为我们的模型还很简单。如果有 Bill 实体和它的 Repository 会怎样呢?或者 Waybill 实体以及它们各自的 Repository?让我们添加这些元素来看看实际的代码结构:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           │       ├── BillLine.php
│           │       ├── BillLineWasAdded.php
│           │       ├── Bill.php
│           │       ├── BillRepository.php
│           │       ├── BillWasCreated.php
│           │       ├── OrderLine.php
│           │       ├── OrderLineWasAdded.php
│           │       ├── Order.php
│           │       ├── OrderRepository.php
│           │       ├── OrderWasCreated.php
│           │       ├── WaybillLine.php
│           │       ├── WaybillLineWasAdded.php
│           │       ├── Waybill.php
│           │       ├── WaybillRepository.php
│           │       └── WaybillWasGenerated.php
│           └── Infrastructure
└── tests

尽管这种类型的代码组织可能还行,但是从长远来看,它可能变得不切实际并且难以维护。每次我们迭代和添加新功能时,模型都会变得更大,并且子文件夹将吞噬更多的代码。我们需要一种使我们对模型一目了然的方式拆分代码。这不是技术问题,仅仅是领域问题。为此,我们可以使用通用语言,通过找到意义的概念来拆分模型,这些概念可帮助我们根据领域对元素进行逻辑分组。

为此,我们可以尝试下面的方法:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           │       ├── Bill
│           │       │   ├── BillLine.php
│           │       │   ├── BillLineWasAdded.php
│           │       │   ├── Bill.php
│           │       │   ├── BillRepository.php
│           │       │   └── BillWasCreated.php
│           │       ├── Order
│           │       │   ├── OrderLine.php
│           │       │   ├── OrderLineWasAdded.php
│           │       │   ├── Order.php
│           │       │   ├── OrderRepository.php
│           │       │   └── OrderWasCreated.php
│           │       └── Waybill
│           │           ├── WaybillLine.php
│           │           ├── WaybillLineWasAdded.php
│           │           ├── Waybill.php
│           │           ├── WaybillRepository.php
│           │           └── WaybillWasGenerated.php
│           └── Infrastructure
└── tests

这样,从概念上讲,代码变得更有条理。正如 Eric Evans 在其蓝皮书中指出的那样,模式是一种交流方式,因为它们为我们提供了领域模型的内部工作方式,并为我们增加了凝聚力并减少了概念之间的耦合。如果我们看前面的示例,可以看到 OrderOrderLine 概念紧密相关,所以它们位于同一模块中。另一方面,尽管 OrderWaybill 共享同一上下文,但它们是不同的概念,因此它们位于不同的模块中。模块不仅是在模型中对相关概念进行分组的一种方式,而且还是表达模型设计的一部分的途径。

我们应该把 Repositories, Factories, Domain Events,以及 Services 放到它们自己的子目录吗?

事实上,可以将它们放在自己的子文件夹中,但强烈反对这样做。这样做时,我们会把技术问题和领域问题混合在一起,请记住,该模块的主要目的是将相关概念与领域模型组合在一起,并将它们与不相关的代码分离。模块不分离代码,而是分离有意义的概念。

基础设施层模块

到目前为止,我们一直在讨论如何在领域层构造和组织代码,但是我们几乎没有说过。并且由于我们使用了六边形架构反转了领域层和基础设施层之间的依赖关系,我们需要一个地方,可以放置所有在领域层定义的接口的具体实现。回到计费(Bill)上下文的示例,我们需要一个地方来实现 BillRepositoryOrderRepositoryWaybillRepository

很明显它们应该被放到 Infrastructure 目录,不然放哪里?假设我们决定使用 Doctrine ORM 来实现持久层。我们该怎样把我们的仓储 (Repositories) 的 Doctrine 实现放到 Infrastructure 文件夹?让我们直接做看看:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           │       └── Bill
│           │           ├── BillLine.php
│           │           ├── BillLineWasAdded.php
│           │           ├── Bill.php
│           │           ├── BillRepository.php
│           │           └── BillWasCreated.php
│           └── Infrastructure
│               ├── DoctrineBillRepository.php
│               ├── DoctrineOrderRepository.php
│               └── DoctrineWaybillRepository.php
└── tests

我们可以保持原样,但是正如我们在领域层看到的那样,这种组织和结构很快就会腐烂,并在几次模型迭代后变得一团糟。每当模型增长时,它将可能需要更多基础设施,并且最终我们将混合各种技术问题,例如持久化,消息,日志等等。我们避免基础架构实现混乱的第一步就是为限界上下中的每个技术关注点定义一个模块:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           │       └── Bill
│           │           ├── BillLine.php
│           │           ├── BillLineWasAdded.php
│           │           ├── Bill.php
│           │           ├── BillRepository.php
│           │           └── BillWasCreated.php
│           └── Infrastructure
│               ├── Logging
│               ├── Messaging
│               └── Persistence
│                   ├── DoctrineBillRepository.php
│                   ├── DoctrineOrderRepository.php
│                   └── DoctrineWaybillRepository.php
└── tests

这看起来更好,并且在长期看来,比我们首次尝试组织时要有更多可维护性。不过,我们的命名空间缺少一些通用语言的关系。让我们考虑一个变种:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           │       └── Bill
│           │           ├── BillLine.php
│           │           ├── BillLineWasAdded.php
│           │           ├── Bill.php
│           │           ├── BillRepository.php
│           │           └── BillWasCreated.php
│           └── Infrastructure
│               └── Domain
│                   └── Model
│                       ├── Bill
│                       │    └── DoctrineBillRepository.php
│                       └── ...
└── tests

现在好多了。它正好匹配我们的领域模型组织结构,但是在基础设施层内部,似乎更容易找到所有内容。如果你事先知道始终只有一个持久化机制,则可以坚持使用这种结构和组织。因为相当简单且易于维护。

但是,当你不得不实现好几个持久化机制时会怎样呢?现今,有一个关系型持久化机制以及几种共享内存型持久化机制(例如 Redis 或者 Riak),或者具有某种本地内存实现以便能够测试代码,已经相当普遍了。让我们看看这如何适应实际情况:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           │       └── Bill
│           │           ├── BillLine.php
│           │           ├── BillLineWasAdded.php
│           │           ├── Bill.php
│           │           ├── BillRepository.php
│           │           └── BillWasCreated.php
│           └── Infrastructure
│               └── Domain
│                   └── Model
│                       ├── Bill
│                       │    ├── DoctrineBillRepository.php
│                       │    ├── InMemoryBillRepository.php
│                       │    └── RedisBillRepository.php
│                       └── ...
└── tests

我们推荐上面的做法。但是,所有仓储实现存在于同一模块。当拥有许多不同技术时,这似乎有点奇怪。如果你觉得有趣,可以创建一个附加模块,以便按底层技术对相关实现进行分组:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           ├── Domain
│           │   └── Model
│           │       └── Bill
│           │           ├── BillLine.php
│           │           ├── BillLineWasAdded.php
│           │           ├── Bill.php
│           │           ├── BillRepository.php
│           │           └── BillWasCreated.php
│           └── Infrastructure
│               └── Domain
│                   └── Model
│                       ├── Bill
│                       │    ├── Doctrine
│                       │    │   └── DoctrineBillRepository.php
│                       │    ├── InMemory
│                       │    │   └── InMemoryBillRepository.php
│                       │    └── Redis
│                       │        └── RedisBillRepository.php
│                       └── ...
└── tests

这种方式类似于单元测试组织结构。但是这还有类,配置,模板等等。这些无法匹配领域模型。这就是为什么你可能需要在基础设施中拥有与特定技术相关的额外模块的原因。

你应该把 Doctrine 映射文件或者 Twig 模板放到哪里?

└── Infrastructure
    ├── Domain
    │   └── ...
    ├── Logging
    ├── Messaging
    └── Persistence
        ├── BaseDoctrineRepository.php
        ├── Doctrine
        ├── EntityManagerFactory.php
        └── Mapping
            ├── BuyIt.Billing.Domain.Model.Bill.Bill.dcm.yml
            └── ...

正如你所见,为了使 Doctrine 正常工作,我们需要一个 EntityManagerFactory 和所有映射文件。我们也可能引入任何其它所需的基础对象作为基类。因为他们与我们的领域模型没有直接关联,这最好把这些资源放到不同的模块。同样的事情也发生在传递机制 (Delivery Mechanisms) 上(API, Web, Console Commands, 等等)。事实上,你可以为每个传递机制使用不同的 PHP 框架或者库:

└── Infrastructure
    ├── ...
    └── Delivery
        ├── API
        │   └── Laravel
        │       └── ...
        ├── Console
        │   └── Symfony
        │       └── ...
        └── Web
            ├── Silex
            │   └── ...
            └── Slim

在前面的例子中,我们使用 Laravel 框架来提供 API,使用 Symfony Console 组件作为命令行的入口点,并使用 Silex 和 Slim 作为 Web 的传递机制。关于用户界面,应将其旋转在每个传递机制中。但是,如果有机会在不同的传递机制间共享 UI,则可以在 Persistence 或者 Delivery 同一级创建一个名为 UI 的模块。通常,我们建议是在框架告诉你如何组织代码的过程中努力挣扎。。。框架应该服从你,而不是相反。

结合不同技术

在大型关键业务应用程序中,多种技术的结合非常普遍。例如,在读密集型 Web 应用中,通常会有某种非规范化的数据源 (Solr, Elasticsearch, Sphinx 等等)提供应用程序的所有读取资源,而传统的 RDBMS (如 MySQL 或 Postgres)主要负责所有写入。发生这种情况时,通常会出现的问题之一是,我们是否可以将搜索操作与搜索引擎一起使用,而写操作与 RDBMS 数据源一起使用。我们这里一般建议是,这种情况有点 CQRS 的味道,因为我们需要独立衡量应用程序的读写。因此,如果你可以使用 CQRS,那可能是最佳选择。

但是如果由于某种原因你不能使用 CQRS,那么需要一种替换方法。在这种情况下,使用 GoF 四人帮的代理模式会非常方便。我们可以根据代理模式来定义仓储实现:

namespace BuyIt\Billing\Infrastructure\FullTextSearching\Elastica;

use BuyIt\Billing\Domain\Model\Order\OrderRepository;
use BuyIt\Billing\Infrastructure\Domain\Model\Order\Doctrine\
DoctrineOrderRepository;

class ElasticaOrderRepository implements OrderRepository
{
    private $client;
    private $baseOrderRepository;

    public function __construct(
        Client $client,
        DoctrineOrderRepository $baseOrderRepository
    )
    {
        $this->client = $client;
        $this->baseOrderRepository = $baseOrderRepository;
    }

    public function find($id)
    {
        return $this->baseOrderRepository->find($id);
    }

    public function findBy(array $criteria)
    {
        $search = new \Elastica\Search($this->client);
// ...
        return $this->toOrder($search->search());
    }

    public function add($anOrder)
    {
// First we attempt to add it to the Elastic index
        $ordersIndex = $this->client->getIndex('orders');
        $orderType = $ordersIndex->getType('order');
        $orderType->addDocument(
            new \ElasticaDocument(
                $anOrder->id(),
                $this->toArray($anOrder)
            )
        );
        $ordersIndex->refresh();
// When it is done, we attempt to add it to the RDBMS store
        $this->baseOrderRepository->add($anOrder);
    }
}

这个例子提供了使用 DoctrineOrderRepository 和 Elastica 客户端(一个与 Elasticsearch 服务端交互的客户端)的简单实现。请注意,对于某种操作,我们使用的是 RDBMS 数据源,对于其他操作,我们使用的是 Elastica 客户端。还要注意添加操作由两部分组成。第一个尝试将 Order 存储到 Elasticsearch 索引,第二个尝试将 Order 存储到关系数据库,并将操作委托给 Doctrine 实现。考虑到这只是一个示例和一种实现方法。它可以改进 - 例如,现在整个添加操作都是同步的。我们可以将操作放到某种消息中间件里,该中间件将 Order 存储到 Elasticsearch 中。根据你的需求,这还有很多可能性和改进。

应用层模块

我们已经看到了领域和基础设施模块,因此现在让我们看看应用层。在领域驱动设计里,我们建议使用应用服务作为一种将客户端与领域模型以及与之交互的必要知识分离的方法。正如你将在第 11 章,应用程序 中看到的那样,应用服务是由其依赖项构建的,通过 DTO 请求执行,并返回 DTO 响应。

它同样可以使用独立输出来返回结果:

├── composer.json
├── composer.lock
├── src
│   └── BuyIt
│       └── Billing
│           ├── Application
│           │   └── PlaceAnOrder
│           │       ├── PlaceAnOrder.php
│           │       ├── PlaceAnOrderRequest.php
│           │       └── PlaceAnOrderResponse.php
│           ├── Domain
│           │   └── ...
│           └── Infrastructure
│               └── ...
└── tests

我们建议应用服务创建模块。每个模块保留它的请求和回复。如果你将 Data Transformer 当作输出依赖项,请像使用 UI 一样遵循基础设施方法。

小结

模块是组织和分离我们应用程序概念的一种方法。模块应该依据通用语言命名。我们应该牢记,模块是传达高层次概念的方法,而这有助于我们做到低耦合高内聚。我们已经看到,即使在旧版本的 PHP 中,也可以使用前缀来创建有意义的模块。如今,遵循 PSR-0 和 PSR-4 命名空间惯例来构建我们的模块更容易。

阅读 630更新于 1月14日

推荐阅读
目录