考虑使用工厂的主要动机:
将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域 模型中的职责,但是依然是领域设计的一部分。工厂应该提供一个创建对象的接口,该 接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去引用那个实际被创 建的对象。对于聚合来说,我们应该一次性地创建整个聚合,并且确保它的不变条件得 到满足。[Evans, p. 138]
除了创建对象之外,工厂并不需要承担领域模型中的其他职责。一个只用于创 建某种聚合的对象并不会拥有其他的职责,甚至不会被看作是模型中的一等公民。
在本书的例子中,我们将更多地使用后一种方式。本书不例中大部分聚合的创 建过程都并不复杂。但是,我们必须考虑到创建过程中的一些重要细节,否则,所 创建的聚合将处于不正确状态。考虑一下,在多租户环境中,如果一个聚合被创建 在了一个错误的租户之下(即该聚合持有了错误的Tenamld),那么结果将是灾难 性的。我们需要将每个租户所持有的数据与其他租户分离开来。在聚合根中使用 适当的工厂方法可以保证这一点,同时也方便了客户端,因为此时客户端只需要传 入基本的参数——通常只是些值对象(6),这样我们也达到了向客户端隐藏创建 细节的目的。
另外,在聚合上使用工厂方法也有助于更好地表达通用语言,而这是使用构 造函数所不能达到的。
聚合根中的工厂方法
创建CalendarEntry实例
让我们先来看看对工厂的设计。在Calendar中,一个工厂方法用于创建CalendarEntry实例。CollabOvation团队向我们展示了该实现过程。
在上例中.scheduleCalendarEntry()方法需要9个参数。之后你还会发现,CalendarEntry的构造函数需要个参数。我们将在下文中讨论这种方式的好处。在创建好一个新的 CalendarEntry之后,客户端需要将其添加到资源中,否则,该CalendarEntry将被垃圾收集器所回收。
测试中的第一个断言语句验证所发布事件中的CalendarEmryld不能为null.这样可以表示事件的成功发送。在本例中,我们并不关心是否有客户端订阅了该事件,而是在于测试 CalendarEntryScheduled事件的确发布出去了。
新创建的CalendarEmry实例也不能为null。当然.我们还可以加入更多的断言,但是对于 工厂方法的设计和客户的使用来说,本例中的2个断言已经足够了。
接下来,让我们看看工厂方法的实现:
Calendar创建了一个新的聚合实例,即CalendarEntry。在 CalendarEntryScheduled事件发布之后,该实例将被返回给客户端(事件发布细节 对本例来说并不重要)。你会发现,在该工厂方法中,我们并没有提供守卫措施。对 于工厂方法来说,这也是没有必要的,因为所有值对象的构造函数、CalendarEntry 的构造函数,还有这些构造函数自委派的setter方法已经提供了这样的守卫措施 (更多有关自委派和守卫的知识,请参考实体(5))。当然,如果你想提供双重守 卫,也是可以的。
团队成员采用了能够表达通用语言的工厂方法名。这样, 领域专家和团队成员都可以使用相同的语言进行交流:
曰历计划曰历条目。
如果我们只是采用了 CalendarEntry的构造函数,那么这将 减弱模型的表达性,同时我们也无法对领域中的那部分通用 语言进行建模。在使用工厂方法时,聚合的构造函数对客户端 来说是隐藏的。我们将构造函数声明为了protected,这迫使客 户端只能通过Calendar的scheduleCalendarEntry ()工厂方法来 创建 CalendarEntry:
使用工厂方法的另一个好处在于,CalendarEntry构造函数所需的其中2个参数不用客户 端传入。该构造函数需要11个参数,但是客户端只需要传入9个参数,这样便减轻了客户端 的负担。此外.这9个参数中的多数参数都可以很容易地创建出来(需要承认的是,这里的 Invitee集合要复杂一些.但是这并不是工厂方法的错。团队成员们应该设计一种能够更方便 地创建该集合的方式,这有可能意味着创建一个单独的工厂类)。
另外,创建CalendarEntry所需的Tenant和Calendarld都由工厂方法提供。这样.我们可以 保证CalendarEntry实例是为正确的Tenant所创建的,并且关联了正确的Calendar。
让我们再看看协作上下文中的另一个例子。
创建Discussion实例
对于Fomm中的工厂方法来说,它和Calenda冲的工厂方法拥有相似的动机和 实现,因此,我们没有必要再深入讨论。但是,此处使用工厂方法还有一个额外的 好处。
考虑以下Form中的startDiscussion()方法:
除了创建Disscussion之外,如果一个Fomm处于关闭状态,那么该工厂方法将对这种情 况进行保护。Forum提供了Tenant和Forunnld。因此.在Discussion构造函数所需的5个参数中, 客户端只需要传入3个参数即可。
该工厂方法同时也表达出了协作上下文中的通用语言。Fomm中的startDiscussionO方法 很好地表达出了领域专家的意图:
作者启动论坛中的讨论。
对于客户端来说,便非常简单了:
的确简单,这也是领域建模者所追求的目标。
这里的工厂方法模式可以不断地重复使用。总的来说,它拥有以下好处:有效 地表达限界上下文中的通用语言;减轻客户端在创建新聚合实例时的负担;确保所 创建的实例处于正确的状态。
领域服务中的工厂
对于将领域服务作为工厂来说,由于它和集成限界上下文(13)相关,我将在 那章中做详细讨论,其中我的关注点主要集中在对防腐层(3)、发布语言(3)和开放主机服务(3)的集成上。这里,我所强调的是工厂本身以及如何将领域服务设 计成工厂。
SaaSOvation团队提供了协作上下文中的另一个例子—— CollaborationService,该工厂用于创建Collaborator实例:
该领域服务类将身份与访问上下文中的对象翻译成协作上下文中的对象。在限界上下文 ⑵中我们讲到,CollabOvation团队在讨论协作时.他们并不会触及到"用户"这个概念,而 是讨论不同的角色.比如作者、创建者、主持者、拥有者和参与者等。为了达到这样的目的. 团队需要和身份与访问上下文进行交互,并将其中的用户和角色对象相应地翻译成自己上下 文中的协作对象。
由于继承自抽象基类Collaborator的新对象都通过领域服务进行创建,此时的领域服务 实际上扮演了工厂的角色。以下是该领域服务的其中一个接口的实现:
由于这是一个技术上的实现.该类将被放置于基础设施层的模块(9)中。
在以上实现中,UserlnRoleAdapter把Tenant和一个标识-用户的名字-转换成了一个Autha实例。该适配器类[Ganmna et al.]将和身份与访问上下文的开放主机服务进行交互,以确认一 个给定的用户是否拥有Author角色。如果是,该适配器将委派给CollaboratorTranslator类,该 类把发布语言的返回结果翻译成本地模型中的Author类。这里的Author和其他Collaborate)「子 类都是简单的值对象:
在协作上下文中,用户名作为Collaborator的标识,即identity属性。另 外,emailAddress和name都是简单的String类型实例。在该模型中,团队决定尽可 能地保持这些概念的简单性。比如,对于用户名来说,他们决定使用单个String来 表示用户的全名。通过使用基于领域服务的工厂我们得以将两个限界上下文的生 命周期和概念术语进行分离。
在UserInRoleAdapter和CollaboratorTranslator中是存在一定复杂度的。简言 之,UserlnRoleAdapter只负责与外部上下文的通信,而CollaboratorTransIator则只负责翻译和创建新实例。更多细节,请参考集成限界上下文(13)。