简介&何为领域驱动设计&模型驱动设计
简介
软件是一种被创建用来帮助我们处理现代生活中复杂问题的工具,它只是到达目的的一种方法,而这个目的通常就是非常实际和真实的事情。
软件必须是实际和有用的,否则我们不会花那么多时间和资源去创建它。这就使它和我们生活的某个方面有非常密切的联系。
软件设计是一门艺术,像其他艺术一样,它不能通过定理和公式以一门精确科学的方式被教授和学习。通过软件创建的过程,我们可以发现有用的规律和技巧,但是我们也许永远不能提供一个准确的方法,以满足从现实世界映射到代码模型的需要。
软件产品既包括设计和开发它的那些人的个人劳动,也包括致力于它发端和成长的那些人的某些领导力和洞察力。
完成软件设计的方法多种多样。在过去的 20 年中,软件产业发现和使用了多种方法创建它的产品,每一个都有自己的优点和不足之处。
本书的目的是专注介绍一个出现和发展了 20 多年但近几年才有所成果的设计方法:领域驱动设计。
何为领域驱动设计
软件开发也是一样。我们不能直接坐下来敲代码。当然也可以这样做,在开发价值不大的软件时。但我们不能用这种方法开发复杂的软件。
为了创建一个好软件,你必须知道这个软件究竟是什么。在你充分了解金融业务是什么之前,你是做不出一个好的银行业软件系统的,你必须理解银行业的领域 。
业务领域就是我们永远的起始点。
在启动一个软件项目时,我们应该关注软件涉及的领域。
软件的最 终目的是增进一个特定的领域。为了达到这个目的,软件需要跟要 它服务的领域和谐相处,否则,它会给领域引入麻烦,产生障碍、 灾难甚至导致混乱等。
我们怎样才能让软件和领域和谐相处呢?
最佳的方式是让软件成为领域的反射(映射)。软件需要具现领域里重要的核心概念和元素,并精确实现它们之间的关系。软件需要对领域进行建模。
所以我们从领域开始着手。接下来要做什么呢?
领域是世界中的某 些事物,不要企图能轻而易举地捕获它们,以为敲几下键盘就能出 来代码。我们需要建立领域的抽象。当我们跟领域专家交流时,我 们会学到好多领域知识,但这些未加工的知识不能被容易地转换成 软件构造,除非我们为它建立一个抽象——在脑海中建立一个蓝 图。开始时,这个蓝图总是不完整的,但随着时间的推移,经过不断的努力,我们会让它越来越好,让它看上去越来越清晰。
这个抽象是什么?它是一个模型,一个关于领域的模型。
按照 Eric Evans 的观点,领域模型不是一幅具体的图,它是那幅图要极力去传达的 那个思想。它也不是一个领域专家头脑中的知识,而是一个经过严格组织并进行选择性抽象的知识。一幅图能够描绘和传达一个模 型,同样,经过精心编写的代码和一段英语句子都能达到这个目 的。
模型是软件设计中最基础的部分。我们需要它,是因为能够用它来 处理复杂问题。我们对领域的所有的思考过程被汇总到这个模型 中。
软件设计有不同的方法,
其中之一是瀑布设计方法。这种方法包含 了一些阶段。
业务专家提出一堆需求同业务分析人员进行交流,分 析人员基于那些需求来创建模型并作为结果传递给开发人员,开发人员根据他们收到的内容开始编码。在这个方法中,知识只有单一 的流向。虽然这种方法作为软件设计的一个传统方法,这么多年来 已经有了一定级别的成功应用,但它还是有它的缺点和局限。主要 问题是业务专家得不到分析人员的反馈信息,分析人员也得不到开 发人员的反馈信息。
另一个方法是敏捷方法学。
本书描述了领域驱动设计的原则,应用这些原则会增进对领域内复杂问题进行建模和实现的开发过程能力。领域驱动设计结合了设计和开发实践,展示了设计和开发如何协同工作以创建一个更好的解 决方案。优良的设计会加速开发的过程,而开发过程中的反馈也会 进一步优化设计。
构建领域知识
通过与领域专家的交谈,软件设计人员的分析型思维会帮助他挖掘出一些领域中的关键概念,并且帮助构建出可用于将来讨论用的结构,我们将在下一章中看到这种结构。
作为软件方面的专家(软件 架构师和开发人员)和领域专家,我们会在一起创建领域的模型, 这个模型会体现两个专业领域的交汇。这看上去是个很消耗时间的 过程,并且确实如此,但是它也应该被这样做,因为软件的最终目 的是去解决真实领域中的业务问题,所以它必须和领域完美结合。
通用语言
对通用语言的需要
通过前一章的案例,我们认识到由软件专家和领域专家通力合作开发出一个领域的模型是绝对需要的,但是,那种方法通常会由于一些 基础交流的障碍而存在难点。
为克服这种交流方式的不同,在建立模型时,我们必须通过沟通来交换对模型和模型中涉及到的元素的想法,应该如何连接它们,哪些是有关的,哪些不是?在这种层次上的交流对一个成功的项目而 言是极为重要的。如果一个人说了什么事情,其他的人不能理解, 或者更糟的是错误理解成其他事情,又有什么机会来保证项目成功 呢?
在讨论模型和定义模型时,我们确实需要讲同一种语言。
领域驱动设计的一个核心的原则是使用一种基于模型的语言。因为 模型是软件满足领域的共同点,它很适合作为这种通用语言的构造 基础。
使用模型作为语言的核心骨架。要求团队在进行所有的交流是都使 用一致的语言,在代码中也是这样。在共享知识和推敲模型时,团 队会使用演讲、文字和图形。这儿需要确保团队使用的语言在所有 的交流形式中看上去都是一致的。因为这个原因,这种语言被称为 “通用语言(Ubiquitous Language)”。
构建一个类似这样的语言会得到一个清晰的结果:模型和语言相互密切关联。一个对语言的变更会变成对模型的变更。
我们已经看到了语言是如何在整个团队中被共享的,也看到了它是如何帮助我们构建知识和创建模型的。我们应如何使用这些语言呢,只是语言交谈吗?
UML 很适合用来构建模型,它也真是一种很好的记录关键 概念(如类)和表现它们之间的关系的工具。
文档。一个明智的沟通模型的方式是创建一些小的 图,让每副小图包含模型的一个子集。
模型驱动设计
软件开发过程的重点:它必须以业务领域为中心。 我们说过让模型植根于领域、并精确反映出领域中的基础概念是建立模型的一个最重要的基础。
通用语言应该在建模过程中广泛尝试以推动软件专家和领域专家之间的沟通,以及发现要在模型中使用 的主要的领域概念。建模过程的目的是创建一个优良的模型,下一 步是将模型实现成代码。
一个更好的方法是紧密关联领域建模和设计。模型在构建时就考虑到软件和设计。开发人员会被加入到建模的过程中来。主要的想法 是选择一个能够恰当在软件中表现的模型,这样设计过程会很顺畅 并且基于模型。代码和其下的模型紧密关联会让代码更有意义并与 模型更相关。
为了紧密捆绑起实现和模型,通常需要支持建模范型的软件开发工具和语言,例如面向对象编程。
面向对象编程非常适合对模型的实现,因为它们基于同一个范型。 面向对象编程提供了对象的类和类之间的关联关系、对象实例、以 及对象实例之间的消息通信。面向对象编程语言让建立模型对象、对象关系与它们的编程副本之间的直接映射成为可能。
模型驱动设计的基本构成元素
分层架构
当我们创建一个软件应用时,这个应用的很大一部分是不能直接跟领域关联的,但它们是基础设施的一部分或者是为软件服务的。最好能让应用中的领域部分尽可能少地和其他的部分掺杂在一起,
因此,将一个复杂的程序切分成层。
开发每一个层中内聚的设计,让每个层仅依赖于它底下的那层。遵照标准的架构模式以提供层的低耦合。
将领域模型相关的代码集中到一个层中,把它从用户界面、应用和基础设施代码中分隔开来。
释放领域对象的显示自己、保存自己、管理应用任务等职责,让它专注于展现领域模型。这会让一个模型进一步富含知识,更清晰地捕获基础的业务知识,让它们正常工作。
一个通用的领域驱动设计的架构性解决方案包含4个概念层:
将应用划分成分离的层并建立层间的交换规则很重要。
如果代码没有被清晰隔离到某层中,它会迅即混乱,因为它变得非常难以管理 变更。在某处对代码的一个简单修改会对其他地方的代码造成不可估量的结果。
领域层应该关注核心的领域问题。它应该不涉及基础设施类的活动。
用户界面既不跟业务逻辑紧密捆绑也不包含通常属于基础设施层的任务。
在很多情况下应用层是必要的。它会成为业务逻辑之上的管理者,用来监督和协调应用的整个活动。
实体
有一类对象看上去好像拥有标识符,它的标识符在历经软件的各种状态后仍能保持一致。
对这些对象来讲这已经不再是它们关心的属性,这意味着能够跨越系统的生命周期甚至能超越软件系统的一系列的延续性和标识符。我们把这样的对象称为实体。
因此,在软件中实现实体意味着创建标识符。对一个人而言,其标 识符可能是属性的组合:名称,出生日期,出生地,父母名称、当 前地址。
实体是领域模型中非常重要的对象,并且它们应该在建模过程开始 时就被考虑。
值对象
实体 在领域模型中是必需的对象。我们应该将所有的对象视为实体吗? 每一个对象都应该有一个标识符吗?
将所有的对象视为实体也会 带来隐含的性能问题,因为需要对每个对象产生一个实例。如果 Customer 是一个实体对象,那么这个对象的一个实例标识一个特殊 的银行客户,不能被对应其他客户的账户操作所复用,造成的结果 是必须为每一个客户建立一个这样的实例。这会导致系统在处理成 千上万的实例时性能严重下降。
这是我们需要包含一个领域对象的某些属性时的例子。我们对某个对象是什么不感兴趣,只关心它拥有的属性。用来描述领域的特殊方面、且没有标识符的一个对象,叫做值对象。
只建议选择那些符合实 体定义的对象作为实体,将剩下的对象处理成值对象。这会简化设计,并且将会产生某些其他的积极的意义。
没有标识符,值对象就可以被轻易地创建或者丢弃。没有人关心创建一个标识符,在没有其他对象引用时,垃圾回收会处理这个对象。这极大简化了设计。
极力推荐值对象是不变的。它们由一个构造器创建,并且在它们的生命周期内永远不会被修改。
值对象可以包含其他的值对象,它们甚至还可以包含对实体对象的引用。
服务
当我们分析领域并试图定义构成模型的主要对象时,我们发现有些方面的领域很难映射成对象。对象要通常考虑的是拥有属性,对象会管理它的内部状态并暴露行为。在我们开发通用语言时,领域中的主要概念被引入到语言中,语言中的名词很容易被映射成对象。
语言中对应那些名词的动词变成那些对象的行为。但是有些领域中的动作,它们是一些动词,看上去却不属于任何对象。它们代表了领域中的一个重要的行为,所以不能忽略它们或者简单的把它们合并到某个实体或者值对象中。给一个对象增加这样的行为会破坏这个对象,让它看上去拥有了本该属于它的功能。
但是,要使用一种 面向对象语言,我们必须用到一个对象才行。我们不能只拥有一个单独的功能,它必须附属于某个对象。通常这种行为类的功能会跨越若干个对象,或许是不同的类。例如,为了从一个账户向另一个 账户转钱,这个功能应该放到转出的账户还是在接收的账户中?感觉放在这两个中的哪一个也不对劲。
当这样的行为从领域中被识别出来时,最佳实践是将它声明成一个服务。
这样的对象不再拥有内置的状态了,它的作用是为了简化所提供的领域功能。服务所能提供的协调作用是非常重要的,一个服 务可以将服务于实体和值对象的相关功能进行分组。最好显式声明 服务,因为它创建了领域中的一个清晰的特性,它封装了一个概 念。把这样的功能放入实体或者值对象都会导致混乱,因为那些对 象的立场将变得不清楚。
服务的 3 个特征:
1. 服务执行的操作涉及一个领域概念,这个领域概念通常不属于一 个实体或者值对象。
2. 被执行的操作涉及到领域中的其他的对象。
3.操作是无状态的。
不论是应用服务还是领域服务,通常都是建立在领域实体和值对象 的上层,以便直接为这些相关的对象提供所需的服务。
模块
对一个大型的复杂项目而言,模型趋向于越来越大。模型到达了一 个作为整体很难讨论的点,理解不同部件之间的关系和交互变得很 困难。
基于此原因,很有必要将模型组织进模块。模块被用来作为组织相关概念和任务以便降低复杂性的一种方法。
另一个使用模块的原因跟代码质量有关。普遍认为软件代码应该具 有高层次的内聚性和低层次的耦合度。虽然内聚开始于类和方法级 别,它也可以应用于模块级别。
强烈推荐将高关联度的类分组到一 个模块以提供尽可能大的内聚。
有很多类型的内聚。最常用到的两个是通信性内聚和功能性内聚。通信性内聚通常在模块的部件操作 相同的数据时使用。把它们分到一组很有意义,因为它们之间存在 很强的关联性。功能性内聚在模块中的部件协同工作以完成定义好 的任务时使用。这被认为是最佳的内聚类型。
给定的模块名称会成为通用语言的组成部分。模块和它们的名字应该能反映对领域的深层理解。
聚合
管理领域对象的生命周期自身就会遇到一个挑战,如果做得不恰当,就会对领域模型产生一个负面的影响。我们将引入 3 个模式来 帮助我们处理这个挑战。
聚合是一个用来定义对象所有权和边界的领域模式。工厂和资源库是另外的两个设计模式,用来帮助我们处 理对象的创建和存储问题。我们将从聚合开始讨论。
工厂
以帮助封装复杂的对象创建过程,它就是工厂 (Factory)。
工厂用来封装对象创建所必需的知识,它们对创建聚合特别有用。当聚合的根建立时,所有聚 合包含的对象将随之建立,所有的不变量得到了强化。
当创建一个极其复杂的对象或者创建的对象涉及到创建其他一系列的对象时(例如,创建一个聚合),需要大量的时间。隐藏聚合的内部构建所需要的任务可以用一个单独的工厂对象来完成。
有时工厂是不需要的,一个简单的构造函数就足够了。在如下情况 下使用构造函数:
构造过程不复杂;
对象的创建不涉及到其他对象的创建,所有的属性需要传递给构造函数。
资源库
因此,使用一个资源库,它的目的是封装所有获取对象引用所需的逻辑。领域对象不需处理基础设施,以得到领域中对其他对象的所需的引用。只需从资源库中获取它们,于是模型重获它应有的清晰和焦点。
资源库会保存对某些对象的引用。当一个对象被创建出来时,它可 以被保存到资源库中,然后以后使用时可从资源库中检索到。如果 客户程序从资源库中请求一个对象,而资源库中并没有它,就会从 存储介质中获取它。
工厂和资源库之间存在一定的关系。它们都是模型驱动设计中的模式,它们都能帮助我们关联领域对象的生命周期。
然而工厂关注的是对象的创建,而资源库关心的是已经存在的对象。资源库可能会在本地缓存对象,但更常见的情况是需要从一个持久化存储中检索 它们。对象可以用构造函数创建,也可以被传递给一个工厂来构建。
面向深层理解的重构
持续重构
代码设计应该 围绕模型展开,模型自身也会基于设计决定而有所增进。
脱离了模型的设计会导致软件不能反映它所服务的领域,甚至可能得不到期 望的行为。
建模如果得不到设计的反馈或者缺少了开发人员的参 与,会导致必须实现模型的人很难理解它,并且可能对所用的技术 不太适合。
在设计和开发过程中,我们需要一次次得停下来,查看代码。这意味着到了重构的时间了。重构是不改变应用行为而重新设计代码以让它更好的过程。
从传统意义上讲,重构描述的是从技术动机的代码转换。重构同样可以由对领域的深入理解,以及对模型及其代码表达进行相应的精化所推动。
凸现关键概念
重构是小幅度进行的,其结果也必然是一系列小的改进。有时,会 有很多次小的变更,每次给设计增加非常小的价值,有时,会有很 少的变更,但造成很大的差异。这就是突破。
保持模型一致性
本章涉及的是 需要若干个团队通力配合的大型项目;
不是试图保持一个迟早要四分五裂的大模型,我们应该做的是有意识地将大模型分解成数个较小的部分。只要遵守相绑定的契约,整合得好的小模型会越 来越有独立性。每个模型都应该有一个清晰的边界,模型之间的关 系也应该被精确地定义。
我们会提供一整套技术来保持模型的完整性。
界定的上下文
每一个模型都有一个上下文。
如何把一个大的模型分解成小的部分没有什么具体的公式。尽量把那些相关联的以及能形成一个自然概念的因素放在一个模型里。模型应该足够小,以便能分给一个团队去实现。