DDD入门

0、概述

本文主要从宏观角度,介绍下领域驱动模型。

领域驱动设计(DDD)作为一种软件开发方法,它可以帮助我们设计高质量的软件模型。

DDD同时提供了战略上的和战术上的建模工具来帮助我们设计高质量的软件模型。

DDD并不是关于技术的,而是关于讨论、聆听、理解、发现和业务价值的,而这些都是为了将知识集中起来。如果你了解公司的业务,那么你至少可以为DDD的通用语言(Ubiquitous Language)做出贡献。在实施DDD的过程中,你最好将那些不怎么使用技术语言的人加入自己的团队,此时你得仔细地聆听他们,还应该尊重他们的观点,并且相信他们比你了解得更多。

什么是领域模型?

领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并且表达了准确的业务含义。

1、为什么我们需要DDD

  • 使领域专家和开发者一起工作,这样开发出来的软件能够准确地传达业务规则。当然,对于领域专家和开发者来说,这并不表示单单地包容对方,而是将他们组成一个密切协作的团队。
  • “准确传达业务规则”的意思是说,此时的软件就像如果领域专家是编码人员时开发出来的一样。
  • 可以帮助业务人员自我提高。没有任何一个领域专家或者管理者敢说他对业务已经了如指掌了,业务知识也需要一个长期的学习过程。在DDD中,每个人都在学习,同时每个人又是知识的贡献者。
  • 关键在于对知识的集中,因为这样可以确保软件知识并不知识掌握在少数人手中。
  • 在领域专家、开发者和软件本身之间不存在“翻译”,意思是当大家都使用相同的语言进行交流时,每人都能听懂他人所说。
  • 设计就是代码,代码就是设计。设计时关于软件如何工作的,最好的编码设计来源于多次实验。这得益于敏捷的发现过程。
  • DDD同时提供了战略设计和战术设计两种模式。战略设计帮我们理解哪些投入是最重要的;哪些既有软件资产是可以重新拿来使用的;那些人应该被加入到团队中?战术设计则帮助我们创建DDD模型中各个部件。

开发能够传递真正业务价值的软件和开发普通的软件是不同的。具有真正业务价值的软件能够很好地符合业务战略,并且可以将竞争优势融合到解决方案当中。此时的软件并不是关于技术的,而是关于业务的。

2、DDD如何帮助我们

DDD作为一种软件开发方法,它主要关注以下三个方面:

  1. DDD将领域专家和开发人员聚集到一起,这样所开发的软件能够反映出领域专家的思维模型。这并不意味这我们将精力都花在了对“真实世界”的建模上,而是交付最具业务价值的软件。有时在实用和理想之间存在冲突,根据它们的互异程度,在DDD中我们将选择实用性。

    领域专家将和开发人员一起创建一套适用于领域建模的通用语言。通用语言必须在全队范围之内达成一致;所有成员都使用通用语言进行交流,通用语言也是对软件模型的直接反映。

  2. DDD关注业务战略。虽然说战略(Strategic)设计自然地包含了战术设计,但是战略设计关注更多的则是业务的战略方向。它帮助我们定义不同团队之间的组织关系,并在这些关系有可能导致项目失败的时候提供早期预警。DDD的战略设计用于清楚地界分不同的系统和业务关注点,这样可以保护每个业务层面的服务。更进一步,这将指引我们如何实现面向服务架构(service oriented architecture)或者业务驱动(business-driven architecture)架构。

  3. 通过使用战术设计建模工具,DDD满足了软件真正的技术需求。这些战术设计工具使开发人员能够按照领域专家的思维模型开发软件。同时,所开发出来的软件是可测试的,能够尽量避免错误,能执行服务层面协议(Service-Level Agreement,SLA),具有很好的伸缩性,并且允许分布式计算。DDD的最佳实践同时包含了高层的架构性实践和底层设计实践,关注业务规则和数据不变性,并且可以对业务规则起到保护作用。

通过这种方式开发软件,你和你的团队将能成功地交付真正的业务价值。

  • 处理领域复杂性

在使用DDD时,我们首先希望将它应用在最重要的业务场景下。对于那些可以轻易替换的软件来说,你是不会有所投入的。相反,值得你投入的是那些重要的、复杂的东西,因为这些东西将为你带来可观的回报。正因如此,我们将这样的模型命名为核心域(Core Domain),而那些相对次要的称为支撑子域(Supporting Subdomain)。那么现在,我们需要搞明白的是,“复杂”到底是什么意思?

  • DDD的作用是简化,而不是复杂化

在使用DDD时,我们应该采用最简单的方式对复杂领域进行建模,而不是使问题变得更加复杂。

不同的业务领域对于复杂的定义是不一样的。另外,不同的公司所面临的挑战不一样;成熟度不一样;软件开发能力也不一样。因此,与其去定义什么是复杂的,还不如定义什么是重要的。这时,你的团队和管理层应该做出决定:你们开发的软件系统是否值得做出DDD投入。

3、贫血症和失忆症

贫血症严重危害着人类健康,并且伴随着有危险的副作用。当贫血领域对象(Anemic Domain Object)被首次提出来的时候,它并不是一个博得赞美的词汇,它描述的是一个缺少内在行为的领域对象。奇怪的是,人们对于贫血领域对象的态度褒贬不一。问题在于,多数开发者认为这样的领域对象是正常的,他们并没有意识到这是一个严重的问题。

你是否想知道你所建模型的健康状况呢?如果你突然患上了技术上的“忧郁症”,这里你可以做个自我检查。你可能心情愉悦,也可能无比恐惧。

你的领域对象中是不是主要是些共有的 getter 和 setter 方法,并且几乎没有业务逻辑,或者完全没有业务逻辑-对象嘛,主要就是用来容纳属性值的?

软件组件经常使用的领域对象是否包含了系统主要的业务逻辑,并且多数情况下你需要调用那些 getter 和 setter?你可能会将这样的客户代码称为服务层(Service Layer)或者应用层(Application Layer)代码,也或者,如果这描述的是你的用户界面,请回答“Yes”,然后好好反省一下,告诫自己一定不要再这么做了。

如果你对以上两个问题的回答都是“No”,表明你的领域对象是健康的。如果都是“Yes”,表明你的领域对象已经病得不轻了,这便是贫血对象。

贫血领域对象是不好的,因为你花了很大的成本来开发领域对象,但是从中却获益甚少。比如,由于存在对象-关系阻抗失配(Object-Relational Impedance),开发者需要将很多时间花在对象和数据存储之间的映射上。这样的代价太大,而收益太小。我得说,你所说的领域对象根本就不是领域对象,而只是将关系型数据库中的模型映射到了对象上而已。这样的领域对象更像是活动记录(Active Record),此时你可以对架构做个简化,然后使用事务脚本(Transaction Script)进行开发。

贫血对象对模型做了些什么?

当在阅读一个贫血领域对象的示例代码时,比如应用服务中的中的事务脚本,

@Transactional
public void saveCustomer(
    String customerId,
    String customerFirstName, String customerLastName,
    String streetAddress1, String streetAddress2,
    String city, String stateOrProvince,
    String postalCode, String country,
    String homePhone, String mobilePhone,
    String primaryEmailAddress, String secondaryEmailAddress) {
 
    Customer customer = customerDao.readCustomer(customerId);
 
    if (customer == null) {
        customer = new Customer();
        customer.setCustomerId(customerId);
    }
 
    customer.setCustomerFirstName(customerFirstName);
    customer.setCustomerLastName(customerLastName);
    customer.setStreetAddress1(streetAddress1);
    customer.setStreetAddress2(streetAddress2);
    customer.setCity(city);
    customer.setStateOrProvince(stateOrProvince);
    customer.setPostalCode(postalCode);
    customer.setCountry(country);
    customer.setHomePhone(homePhone);
    customer.setMobilePhone(mobilePhone);

    customer.setPrimaryEmailAddress(primaryEmailAddress);
    customer.setSecondaryEmailAddress (secondaryEmailAddress);
 
    customerDao.saveCustomer(customer);
}

以上代码的功能是相当强大的。不管一个Customer是新建还是先前存在的,这段代码都会保存这个Customer。必须得承认,以上代码并不表示一个有趣的领域,但是却帮助我们看到了一个欠妥的设计,我们可以将其重构成更好的模型。

上面的saveCustomer()至少存在三大问题:
1.savecustomer()业务意图不明确;
2.方法的实现本身增加了潜在的复杂性;
3.Customer领域对象根本就不是对象,而只一个数据持有器(data holder)。

我们将这种情况称为“由贫血症导致的失忆症”。

4、如何DDD

DDD最具有威力的特性之一:通用语言。通用语言和限界上下文(Bounded Context)同时构成了DDD的两大支柱,并且它们是相辅相成的。

上下文术语: 就现在来说,可以将限界上下文看成是整个应用程序之内的一个概念性边界。这个边界之内的各种领域术语、词组或句子——也即通用语言,都有确定的上下文含义。在边界之外,这些术语可能表达不同的意思。

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

在开始的时候,通用语言可能只包含由领域专家使用的术语,但是随着时间推移,通用语言将不断壮大成长。例如,对于”注射流感疫苗“这个业务用例,我们不仅要完成建模,还应该让团队使用相同的通用语言。当团队讨论到业务模型时,他们会说:”护士给病人注射标准剂量的流感疫苗“。
在这里插入图片描述

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

根据上面的规则,重新设计saveCustomer()方法,使其能够反映出它应该支持的业务操作:
在这里插入图片描述
当然,以上的Customer并不是一个完美的模型,然而在实施DDD时,对设计的反思正是我们所期望的。

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

在这里插入图片描述
在这个例子中,我们只用一个应用层方法来修改Customer的姓名,除此之外,该方法别无其他业务功能。因此,在使用DDD的时,我们应该对着模型的修改相应地修改应用层。

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

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

5、使用DDD的业务价值

不管使用什么价值,目的都是提供业务价值。如果我们提供的技术方案比其他方案更能够产生业务价值,那么我们的业务能力也将增强。

DDD的业务价值总结大致为以下几点:

  • (1)你将获得了一个非常有用的领域模型;

DDD强调将精力花在对业务最有价值的东西上。我们不过度建模,而是关注业务的核心域。有些模型是用来支撑核心域的,它们同样重要。但是,这些支撑作用的模型在优先级上没有核心域高。

当我们将关注点放在自己的业务和别人业务的区别上时,我们便更好地理解自己的任务所在,同时我们将更具竞争优势。

  • (2)你的业务得到了更准确的定义和理解;

业务人士能够更好地理解业务本身。在团队讨论过程中,一些业务细节被不断地暴露出来,这些细节有助于掌握业务价值。

  • (3)领域专家可以为软件设计做出贡献;

当人们对自己的核心业务有了更深的了解时,业务价值自然就出来了。开发者和领域专家共享一套交流语言,领域专家将知识传递给开发者。当开发者离开或岗位变动时,领域专家、剩下的开发者可以继续使用通用语言进行交流。

  • (4)更好的用户体验;

用户体验可以更好地反映出领域模型的好坏。
当用户体验按照领域专家心中的模型来设计时,会让用户更容易理解软件,而不需要业务人员来培训。效率提高了,这就是业务价值。

  • (5)清晰的模型边界;

并不鼓励技术团队将精力单纯地放在编码和算法上,而是期望能面向技术。明确的目标产生更高效的解决方案,而要达到这样的目的往往需要更好地理解项目的限界上下文。

  • (6)更好的企业架构;

上下文之间的边界和关系是清晰的。当不同上下文之间的模型间存在依赖关系时,我们将使用上下文映射图来集成不同的限界上下文,而这有助于我们全面的了解整个企业的架构。

  • (7)敏捷、迭代式和持续建模;

DDD并不是画模型图,而是将领域专家的思维模型转化成有用的业务模型。DDD不是创建一个真实世界的模型,而是模仿现实。

  • (8)使用战略和战术新工具。

限界上下文为团队创建了一个建模边界,成员在边界内部为特定的业务领域创建解决方案。在单个限界上下文中团队成员共享一套通用语言。不同的团队有时各自负责一个限界上下文,此时可以使用上下文映射图在战略层面上对限界上下文进行界分和集成。在某一个建模边界内部,团队将使用战术建模工具:聚合(Aggregate)、实体(entity)、值对象(value object)、领域服务(Domain Service)和领域事件(Domain Event)等。

6、实施DDD所面临的挑战

(1)为创建通用语言腾出时间和精力;
(2)持续地将领域专家引入项目;
(3)改变开发者对领域的思考方式。

7、为领域建模正名

通常来说,战术建模比战略建模复杂。因此,如果你打算采用DDD的战术模式(聚合、领域服务、值对象和领域事件等)来建立领域模型的话,需要更仔细的思考和更大的投入。那么,为什么还依然要采用战术建模呢,又拿什么标准来衡量在DDD上的投入是值得的呢?

  • 如果一个限界上下文被当成核心域来开发,那么从战略上来说,这个限界上下文对业务的成功是极其重要的。核心模型是不易理解的,需要不断地尝试和重构,通过持续改进可以延长它的效用生命。它是值得使用战术模式的。

  • 一个子域,对于消费方来说,可能是通用子域或者支撑子域,但是却有可能成为你自己的核心域。如果你正在开发的限界上下文是你主要的业务,那么它便是你的核心域,也可以使用战术模式。

  • 如果你正在开发一个支撑子域,但是由于种种原因,该支撑子域不能从第三方的通用子域直接获得,那么此时战术模式将帮上大忙。如果此时的模型增加了特定的业务价值,那么该模型就可以认为是创新的。如果团队有能力实施战术设计,这个支撑子域又是创新的,并且持续存在很长时间,那么此时便是采用战术设计的好时机。尽管如此,该子域也不能称为核心域,因为在业务人士眼中,这样的领域只是支撑性的。

8、DDD并不笨重

DDD绝非是充满繁文缛节的笨重开发过程。事实上,DDD能够很好地与敏捷项目框架结合起来。比如Scrum。DDD也倾向于“测试先行,逐步改进”的设计思路。在你开发一个新的领域对象时,比如实体或值对象,可以按照以下步骤:
(1)编写测试代码以模拟客户代码是如何使用该领域对象的。
(2)创建该领域对象以使测试代码能够编译通过。
(3)同时对测试和领域对象进行重构,直到测试代码能够正确地模拟客户代码,同时领域对象拥有能够表明业务行为的方法签名。
(4)实现领域对象的行为,直到测试通过为止,再对实现代码进行重构。
(5)向你的团队成员展示代码,包括领域专家,以保证领域对象能够正确地反映通用语言。

以上的开发步骤将不断重复,直到领域模型满足本次迭代的任务为止,这种方法是敏捷的。

总而言之,DDD的合理使用将带来巨大的业务价值。

©️2020 CSDN 皮肤主题: 黑客帝国 设计师:上身试试 返回首页