如何DDD

让我们暂时撇开关于实现细节的讨论,现在来看看DDD最具威力的特性之一:通用语言。通用语言和限界上下文(Bounded Context,2)同时构成了DDD的两大支柱,并且它们是相辅相成的。

上下文术语

就现在来说,可以将限界上下文看成是整个应用程序之内的一个概念性边界。这个边界之内的每种领域术语、词组或句子——也即通用语言,都有确定的上下文含义。在边界之外,这些术语可能表示不同的意思。我们将在第2章中对限界上下文做深入探讨。

通用语言

通用语言是团队共享的语言。领域专家和开发者使用相同的通用语言进行交流。事实上,团队中每个人都使用相同的通用语言。不管你在团队中的角色如何,只要你是团队的一员,你都将使用通用语言。

那么,你认为你已经知道了什么是通用语言了?

很明显,通用语言是一种业务语言。 抱歉,不是。 通用语言必须采用工业标准术语。 不完全是... 通用语言是领域专家专用的。 对不起,不是。 通用语言是团队自己创建的公用语言,团队中同时包含领域专家和软件开发人员。 对了。

自然地,领域专家对通用语言有很大的影响,因为他们最了解业务,并且深受工业标准的影响。但是,通用语言更多地是关于业务本身如何思考和运作的。此外,很多时候,不同领域专家会在概念和术语上产生分歧,他们甚至也会犯错,因为他们也无法了解每种业务用例。因此,当领域专家和开发者一起创建领域模型的时候,他们有时会达成一致,有时会做一些妥协,但最终目的都是为了创造最适合项目的通用语言。团队成员们妥协的绝对不应是通用语言的质量,而是概念.术语和含义。然而,最初的一致并不表示始终一致,就像其他语言一样,通用语言也会随着时间推移而不断演化改变。

要使开发者和领域专家一样了解业务没有什么窍门。通用语言也不是强加在开发者身上的晦涩业务术语。通用语言是由整个团队共同创建的一门语言,其中包括领域专家、开发者、业务分析员等。在开始的时候,通用语言可能只包含由领域专家使用的术语,但是随着时间推移,通用语言将不断壮大成长。

在表1.4中,对于“注射流感疫苗”这个业务用例,我们不仅要完成建模,还应该让团队使用相同的通用语言。当团队讨论到业务模型时,他们会说:“护士给病人注射标准剂量的流感疫苗。

由于通用语言最初只来自于领域专家,分歧是难免的。然而,这正是创建最佳通用语言的自然过程。在这个过程中,团队成员通过讨论、参考资料、引用标准、查阅词典等对通用语言进行改进。有时我们发现,有些我们曾经认为能很好表达业务的词汇不再适用了,而另外的一些词汇具有更好的效果。

那么,你该如何掌握通用语言呢?这里有一些试验性的方法:

  • 同时绘制物理模型图和概念模型图,并标以名字和行为。虽然这些图并不是正式的设计图,但它们却包含了软件建模的某些方面。即使你的团队在使用统一建模语言(UnifiedModelingLanguage,UML)来完成正式建模,也不要得意忘形,因为这样可能反而不利于团队的讨论,最终将阻碍通用语言的产生。

  • 创建一个包含简单定义的术语表。将你能想到的术语都罗列出来,包括好的和不好的,并注明好与不好的原因。在你给术语下定义时,你在不经意间就会创造出一些可重用的词汇,因为此时你使用的是领域中的通用语言。

  • 如果你不喜欢术语表,可以采用其他类型的文档,但记得将那些“不正式”的模型图也包含进去。同样,这里最终的目的也是发现通用语言中的术语和词组。

  • 由于团队中有些人工作在术语表上,还有些人工作在文档上,此时你需要找到团队的其他人员来检查你的成果。分歧肯定是有的,你应该对此有所准备。

以上是建立通用语言的一些理想化步骤,这样建立起来模型肯定不能直接用来指导开发,而只是建立通用语言的起步而已。此后,改进之后的通用语言将反映到系统的源代码中,比如Java、C#或者Scala等。以上的模型图和文档并未表明通用语言会随着时间而扩大。在通用语言开发早期,这些材料可能会对我们产生鼓舞,但是时间一久,它们也将变得过时。这也是为什么只有团队的交流和代码才能持续到最后的原因,也只有这两者才能实时地反映通用语言。

由于团队交流和代码才是对通用语言的持续表达,你应该试着抛弃那些模型图、术语表和文档。虽然这并不是DDD所要求的,但是这样做的确很实用,因为我们很难将项目文档和软件系统保持同步。

有了以上认识,我们便可以重新设计saveCustomer()方法了。我们将修改 Customer,使其能够反映出它应该支持的业务操作:

public interface Customer {
    public void changePersonalName(
        String firstName, String lastName);
    public void postalAddress(PostalAddress postalAddress);
    public void relocateTo(PostalAddress changedPostalAddress);
    public void changeHomeTelephone(Telephone telephone);
    public void disconnectHomeTelephone();
    public void changeMobileTelephone(Telephone telephone);
    public void disconnectMobileTelephone();
    public void primaryEmailAddress(EmailAddress emailAddress);
    public void secondaryEmailAddress(EmailAddress emailAddress);
}

当然,以上的Customer并不是一个完美的模型,然而在实施DDD时,对设计的反思正是我们所期望的。作为一个团队,我们可以自由地讨论什么样的模型才是最好的,在对通用语言达成了一致之后,才开始着手开发。然而,即便我们可以对通用语言进行一遍又一遍地提炼,此时上面的例子已经能够反映出一个Customer应该支持的业务操作了。

另外,我们还应该知道,对领域模型的修改也将导致对应用层的修改。每一个应用层的方法都对应着一个单一的用例流:

@Transactional
public void changeCustomerPersonalName(
    String customerId,
    String customerFirstName,
    String customerLastName) {

    Customer customer = customerRepository.customerOfId(customerId);

    if (customer == null) {
        throw new IllegalStateException("Customer does not exist.");
    }

    customer.changePersonalName(customerFirstName, customerLastName);
}

这和最开始的saveCustomer()例子是不同的,在那个例子中,我们使用了同一个方法来处理多个用例流。在这个新的例子中,我们只用一个应用层方法来修改Customer的姓名,除此之外,该方法别无其他业务功能。因此,在使用DDD时,我们应该对照着模型的修改相应地修改应用层。同时,这也意味着用户界面所反映的用户操作也变得更加狭窄。但是无论如何,这个特定的应用层方法不再要求我们在用户姓名参数之后跟上10个null了。

对这个新例子你还算满意吧?通过阅读代码你便能理解它的业务意图。你还可以通过测试来保证它的功能,即只修改Customer的姓名。

因此,我们使用通用语言来捕捉特定核心业务领域中的概念和术语,它是一种团队模式。软件模型包含名词、形容词、动词和一些富有含义的语句等,团队成员便通过这些语言进行交流。软件实现和测试中也使用和团队语言一样的通用语言。

是通用,不是万能

我想,关于通用语言,有必要再做一点澄清。在理解通用语言时,我们必须牢牢记住以下几点:

  • 这里的“通用”意思是“普遍的”,或者“到处都存在的”。通用语言在团队范围内使用,并且只表达一个单一的领域模型。

  • "通用语言"并不表示全企业、全公司或者全球性的万能的领域语言。

  • 限界上下文和通用语言间存在一一的关系。

  • 限界上下文是一个相对较小的概念,通常比我们起初想象的要小。限界上下文刚好能够容纳下一个独立的业务领域所使用的通用语言。

  • 只有当团队工作在一个独立的限界上下文中时,通用语言才是“通用”的。

  • 虽然我们只工作在一个限界上下文中,但是通常我们还需要和其他限界上下文打交道,这时可以通过上下文映射图(3)对这些限界上下文进行集成。每个限界上下文都有自己的通用语言,而有时语言间的术语可能有重叠的地方。

  • 如果你试图将某个通用语言运用在整个企业范围之内,或者更大的、夸企业的范围内,你将失败。

当你开始一个项目,而该项目已经在使用DDD了,此时你需要将你正在开发的独立限界上下文识别出来,这样便在你的领域模型周围加上了一个显式的边界。此时,你应该在这个限界上下文中使用其专属的通用语言。对于那些不包含在通用语言中的概念,你应该拒绝使用

results matching ""

    No results matching ""