? ? ? ? 为了保证软件实现简洁且与模型保持一致,不管实际情况如何复杂,必须运用建模和设计的最佳实践,即设计模式 GoF等。
? ? ? ? 领域驱动设计能够使模型和程序紧密结合一起,互相促进对方的效用。这种结合要求我们注意每个设计的细节。这种设计风格沿续了“职责驱动设计”的原则,也用利了其他面向对象的设计原则如“SOLID”原则等
? ? ? ? 为了使领域驱动设计过程更加灵活,开发人员需理解上述原则是如何支持Model-Driven Design的,以便在设计过程中做出正确的选择。按照标准“模式语言”,可组织一张导航图,描述领域驱动设计模式下的各种模式及这些模式彼此关联的方式。
? ? ? ? 共同这些标准模式可以使设计有序进行,也使项目团队成员能够更方便的了解彼此的工作内容。同时,标准模式也使Ubiquitous Language(通用语言)更加丰富,所有的项目团队成员都可以使用通用语言来讨论模型和设计决策。
? ? ? ? 模型中各个元素的实际设计和实现相对系统化,将领域设计与软件系统中的其他关注点分离会使设计与模型之间的关系非常清晰。根据不同的特征来定义模型元素则会使元素的意义更加鲜明。对每个元素使用已验证的模式有助于创建出更易于实现的模型。
? ? ? ? 只有在充分考虑这些基本原理之后,精心设计的模型才能化繁为简,创建出项目团队成员可以放心地进行组合的详细元素。
? ? ? ? 在软件中,用于解决领域问题的那部分代码通常占比很小,但非常重要。我们需要将领域相关对象在众多的系统对象中分离出来,避免与其他软件技术对象混淆而迷失了领域。
? ? ? ? 分离领域的复杂技术早已出现,要想创建出能够处理复杂任务的程序,需要做到关注点分离——使设计中的每个部分都得到单独的关注。在分离的同时,也需维持系统内部复杂的交互关系。
? ? ? ? 软件系统有各种各样的划分方式,但是根据软件行业的经验和惯例,普遍采用Layered Architecture(分层架构),特别是有几个层基本上已经成了标准层。
? ? ? ? 分层的价值在于第一层都只是代表程序的某一特定方面。这种限制使每个方面的设计都更具内聚性,更容易解释。
? ? ? ? 用户界面层:负责与外部交互,可以是人也可能是另一个系统
? ? ? ? 应用层:负责定义软件任务、作业,不包含业务规则或知识而是使用领域层对象来解决问问题
? ? ? ? 领域层(模型层):负责表达业务概念、信息、规则,本层是业务软件的核心
? ? ? ? 基础设施层:为上面各层提供通用的技术能力,为应用层传递消息,为领域层提供持久化机制,为界面层绘制屏幕组件等。还能够通过架构框架来支持四个层次间的交互模式。
????????
? ? ? ? 因此:
? ? ? ? 将复杂的应用程序划分层次,在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效地使用这些知识。
? ? ? ? 各层被设计成松散连接,也需协同工作。最早将各层相连的模式是MVC模式,或各层提供服务Service的形式,也可以利用架构框架来实现协同各层。
? ? ? ? 当前,大部分软件系统都采用了Layered Architecture,只是采用分层方案存在不同而已。然而,领域驱动设计只需要一个特定的层存在即可。
? ? ? ? 领域模型是一系列概念的集合。“领域层”则是领域模型以及所有与其直接相关的设计元素的表现,它由业务逻辑的设计和实现组成。在Model-Driven Design中,领域层的软件构造反映出了模型概念。
? ? ? ? 如果领域模型与程序中的其他关注点混在一起,就不可能实现这种一致性。将领域实现独立出来是领域驱动设计的前提。
? ? ? ? 有些简单项目,并不需要分层设计,需是反向而行,将所有功能混合在一起。但这样做在有些特殊项目中却是十分有利的。如单页面程序、小工具类软件等。
? ? ? ? 在用户界面中实现所有的业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储。使用自动化程度最高的用户界面创建工具和可用的可视化编程工具。
? ? ? ? 这里讨论Smart UI只是为了说明何时需要采用诸如Layered Architecture这样的模式来分离出领域层。
? ? ? ? 遗憾的是,除了基础设施和用户界面之外,还有一些其他的因素也会破坏你精心设计的领域模型。你必须要考虑那些没有完全集成到模型中的领域元素。你不得不与同一领域中使用不同模型的其他团队合作。还有其他的因素会让你的模型结构不再清晰,并且影响模型的使用效率。比如:Bounded Context(上下文边界)、Anti-corruption layer(防腐层)等。
? ? ? ? 非常复杂的领域模型本身就是难以使用的。毕竟,把领域隔离出来的最大的好处就是可以真正专注于领域设计,而不用考虑其他的方面。
? ? ? ? 领域模型中的元素有三种基本模式:Entity、Value Object和Service。从表面上来看,定义领域概念中的对象很容易,但要想确切地反映其真实含义却很困难,这要求我们明确区分各模型元素,并与设计实践结合起来,从而开发出合适的对象。
? ? ? ? Entity元素可以用来表示有连续性和唯一标识的事物,与软件设计中实体框架中的Entity类似。
? ? ? ? Value Object元素可以用来表示某种状态或属性,与软件设计中的Struct、Enum类似。
? ? ? ? Service元素可用来表示动作或操作(注:这里的操作不是指对象的行为或函数),也可以用来表示技术层中的Service,是应客户端请求来完成某事。与软件设计中的接口、Restful API等类似
? ? ? ? 关联可简单看成系统中实体与实体之间的关系。领域模型中对象之间的关联使得建模与实现之间的交互更为复杂。模型中每个可遍历的关联,软件中都要有同样的实现机制。
? ? ? ? 现实生活中有大量“多对多”关联,其中很多关联天生就是双向的。在模型构建的早期也会得到很多这样的关联。这些普通的关联会使软件的实现和维护变得很复杂。
????????在设计中,至少有三种方法可以使关联变得易于控制。
(1)规定一个遍历方向。
(2)添加一个限定符,以便有效地减少多重关联
(3)消除不必要的关联
? ? ? ? 对象建模时我们有可能把注意力集中到对象的属性上,但实体的概念是一种贯穿整个生命周期的抽象的连续性。一些对象主要不是由它们的属性定义的。它们实际上表示了一条“标识线”(A Thread Of Identity),这条跨越时间,而且常常经历多种不同的表示。有时,这样的对象必须与另一个有不同属性的对象相匹配。而有时一个对象必须与具有相同属性的另一个对象区分开。错误的标识可能会破坏数据。
? ? ? ? 主要由标识定义的对象称为Entity。“Entity”并不特定指某个事物,也有可能是一次交易、一张订单或服务等。
? ? ? ? 当对一个Entity和对象建模时,最基本的职责是确保其在生命周期中的连续性,以便使其行为更清楚且可预测。不要将注意力集中在属性或行为上,要抓住Entity对象定义的最基本特征,尤其是那些用于识别、查找或匹配对象的特征。只添加那些对概念至关重要的行为和这些行为所必需的属性。此外,应该将行为和属性转移到与核心实体关联的其他对象中。除了标识问题之外,Entity往往通过协调其关联对象的操作来完成自己的职责。
? ? ? ?每个Entity必须有一种建立标识的操作方式,以便与其他对象区分开,即使这些对象与它具有相同的描述属性。无论系统如何定义,都必须确保Entity标识的唯一性,包括在分布式系统中或已归档。?
? ? ? ? 有时会用某些属性的组合来表示其唯一性,但需注意其意外的变动情况。如用名称、城市和出版日期来标识日报,但要注意临时增刊和名称变更。
? ? ? ? 当对象属性无法形成真正唯一时,常用的的方法是是为实例附加一个在类中唯一的符号。
? ? ? ? ID通常是由系统自动生成的,生成算法必须确保其在系统中的唯一性,如GUID等。
? ? ? ? 当对象会在其他系统中流转时,需确保ID在多个系统中的唯一性。
? ? ? ? 很多对象没有概念上的标识,它们描述了一个事物的某种特征。如:颜色、地址、时区等。当我们只关心一个模型元素的属性时,应该把它归类为Value Object。我们应该使这个模型元素能够表示出其属性的意义,并为它提供功能,但它是不可变的,不用为它分配任何标识。大多数情况下,可以在软件中把它设计为Structure或Enumerate。
? ? ? ? 因为我们不关心使用的是Value Object的哪个实例,所以设计可以获得更大的自由,可以简化设计或优化性能。
? ? ? ? 例如,两个人同名并不意味着他们是同一个人,也不意味着他们是可互换的。但表示名字的对象是可以互换的。事实上两个Person对象可能不需要自己的名字实例,他们可以共享同一个Name对象(其中每个Person对象都有一个指向同一个名字实例的指针),而无需改变它们的行为或标识。
? ? ? ? Value Object为性能优化提供了更多的选择,定义Value Object并将其指定为不可变是一条一般规则,这样做是为了避免在模型中产生不必要的约束,从而让开发人员可以单纯地从技术上优化性能。若开发人员能显式定义重要约束,需确保不会在设计调整时无意更改重要的行为。
? ? ? ? 如果说Entity间的双向关联很难维护,那么两个Value Object间的双向关联则完全没有意义。由于没有标识,说一个对象指向的对象正是那个指向它的对象没有任何意义。因此,我们应尽量完全清除Value Object之间的双向关联。如果在模型中确实需要这种关联,那么首先应重新考虑下将对象声明为Value Object这个决定是否正确。或许它需拥有一个标识,而你没有注意到它。
? ? ? ? 有时,对象不是一个事物。
? ? ? ? 在某些情况下,最清楚、实用的设计会包含一些特殊的操作,这些操作从概念上讲不属于任何对象。与其把它们强制地归于哪一类,不如顺其自然地在模型中引入一种新的元素,这就是Service(服务)
? ? ? ? 当领域中的某个重要过程或转换操作不是Entity或Value Object的自然职责时,应该在模型中添加一个作为独立接口的操作,并将其声明为Service。定义接口时要使用模型语言,并确保操作名称是Ubiquitous Language中的术语。此外,因使Service成为无状态的。
? ? ? ? 好的Service有以下3个特征。
? ? ? ? (1)与领域概念相关的操作不是Entity或Value Object的一个自然组成部分。
? ? ? ? (2)接口是根据领域模型的其他元素定义的。
? ? ? ? (3)操作是无状态的
? ? ? ? 这种模式只重视那些在领域中具有重要意义的Service,但Service并不只是在领域层中使用。我们需要区分属于领域层的Service和其他层的Service,划分责任,以便将它们明确区分开。
? ? ? ? 具体实践中应用层Service和领域层Service可能很难区分,我们需关注很多领域层的Service是在Entity和Value Object的基础上建立起来的。Entity和Value Object往往由于粒度过细而无法提供对领域层功能的便捷访问,所以在这里要注意领域层和应用层之间很微妙的那根分界线。
? ? ? ? 将某个领域概念建模为Service的同时,Service还可以控制领域层中接口的粒度,并且避免客户端与Entity和Value Object的耦合。
? ? ? ? 在大型系统中,中等粒度、无状态的Service更容易被复用,因为他们在简单的接口背后封装了重要的功能。此外,细粒度的对象可能导致分布式系统的消息传递的效率低下。
? ? ? ? 由于应用层负责对领域层对象的行为进行协调,因此细粒度的领域对象可能会把领域层的知识泄漏到应用层中。明智地引入领域层服务有助于在应用层和领域层之间保持一条明确的界限。
? ? ? ? 这种模式有利于保持接口的简单性,便于客户端控制并提供了多样化的功能。它提供了一种在大型或分布式系统中便于对组件进行打包的中等粒度的功能。
? ? ? ? 像J2EE和CORBA这样的分布式系统架构提供了特殊的Service发布机制,这些发布机制具有一些使用上的惯例,并且增加了发布和访问功能。但是,并非所有项目都会使用这样的框架,即使在使用了它们的时候,如果只是为了在逻辑上实现关注点分离,那么它们也是大材小用了。
? ? ? ? 与分离特定职责的设计决策相比,提供对Service的访问机制的意义并不是十分重大。一个“操作”对象可能足以作为Service接口的实现。我们很容易编写一个简单的Singleton对象来实现对Service的访问。从编码惯例可以明显看出,这些对象只是Service接口的提供机制,而不是有意义的领域对象。只有当真正需要实现分布式系统或充分利用框架功能的情况下才应该使用复杂的架构。
? ? ? ? Module是一个传统的、较成熟的设计元素。虽然使用模块有一些技术上的原因,但主要原因却是“认知超载”。Module为人们提供了两种观察模型的方式,一是可以在Module中查看细节,而不会被整个模型淹没,二是观察Module之间的关系,而不考虑其内部细节。
? ? ? ? 领域层中的Module应该成为模型中有意义的部分,Module从更大的角度描述了领域。
? ? ? ? 低耦合高内聚作为通用的设计原则即适用于各种对象,也适用于Module,但Module作为一种更粗粒度的建模和设计元素,采用低耦合高内聚原则显得更为重要。Module并不仅仅是代码的划分,而且是概念的划分。将事物概念由大及小的拆分符合人的基本逻辑思维。
? ? ? ? 像领域驱动设计中的其他元素一样,Module是一种表达机制。Module的选择应该取决于被划分到模块中的对象的意义。当你将一些类放到Module中时,相当于告诉下一位看到你的设计的开发人员要把这些类放在一起考虑。如果说模型讲述了一个故事,那个Module就是这个故事的各个章节。
????????Module的名称应该是Ubiquitous Language中的术语。Module及其名称应反映出领域的深层知识。你可能会向一位业务专家说“现在让我们讨论一下‘客户’模块”,这就为你们接下来的对话设定了上下文。
? ? ? ? Module需要与模型的其他部分一同演变。这意味着Module的重构必须与模型和代码一起进行。但这种重构通常不会发生,更改Module可能需要大范围地更新代码。这些更改可能会妨碍团队沟通,甚至会妨碍开发工具的使用。因此,Module结构和名称往往反映了模型的较早形式,而类则不是这样。
? ? ? ? 在Module选择的早期,有些错误是不可避免的,这些错误导致了高耦合,从而使Module很难进行重构。而缺乏重构又会导致问题变得更加严重。克服这一问题的唯一方法是接受挑战,仔细地分析问题的要害所在,并据此重新组织Module。
? ? ? ? 技术框架对打包决策有着极大的影响,有些技术框架是有帮助的,有些则要坚决抵制。如分层打包可能会把实现概念对象的元素分得很零散,不利于清楚表示模型。
? ? ? ? 除非真正有必要将代码分布到不同的服务器上,否则就把实现单一概念对象的所有代码放在同一个模块中。
? ? ? ? 利用打包把领域层从其他代码中分离出来。否则,就尽可能让领域开发人员自由地决定领域对象的打包方式,以便支持他们的模型和设计选择。
? ? ? ? Model-Driven Design 要求使用一种与建模范式协调的实现技术。但实践中只有少数几种得到了广泛应用。目前主流的范式是面向对象设计,这种范式流行有许多原因,包括对象本身的固有因素、环境因素,以及广泛使用所带来的一些优势。
? ? ? ? 一些团队选择对象范式并不是出于技术上的原因,甚至也不是出于对象本身的原因,而是从一开始,对象建模就在简单性和复杂性之间实现了一个很好的平衡。
? ? ? ? 面向对象分析问题的思维方式与人们在现实世界中的思维方式相近,有些对象和实物有一一对应的关系。使得大部分人都比较容易理解面向对象设计的基本知识,即使是非专业人员也可以理解对象模型图。
? ? ? ? 现在面向对象技术已经相对成熟。业内已经提供了很多现成的解决方案,它们可以满足大部分常风的基础设施需要。这就是目前大部分采用Model-Driven Design项目很明智地使用面向对象技术作为系统核心的原因。
? ? ? ? 也有一些领域不适合用对象封装来建模,例如,涉及大量数学问题的领域或者受全局逻辑推理控制的领域就不适合使用面向对象的范式。
? ? ? ? 领域模型不一定是对象模型。例如,用Prolog语言实现的Model-Driven Design,它的模型是由逻辑规则和事实构成的。当领域的主要部分明显属于不同范式时,明智的做法是用适合各个部分的范式对其建模,并使用混合工具集来进行实现。当各个领域间依赖性较小时,可以把另一种范式用子系统封装起来。
? ? ? ? 将业务规则引擎或工作流引擎这样的非对象组件集成到对象系统中,这种混合使用不同范式的方法使得开发人员能够用最适合的风格对特殊概念进行建模。
? ? ? ? 在面向对象的应用程序开发项目中,有时会混用一些其他的技术,规则引擎就是一个常见的例子。但人们并不总是能够从规则引擎的使用中得到预期结果。重要的是在使用规则引擎的同时继续考虑模型。将各个部分紧密结合在一起的最有效的工具就是健壮的Ubiquitous Language,它是构成事个异构模型的基础。
? ? ? ? 虽然Model-Driven Design不一定是面向对象的,但它确实是一种富有表达力的模型结构实现。即使选择了其他开发工具,也没有必要放弃Model-Driven Design,坚持使用它是值得的。
? ? ? ? 当将非对象元素混合到以面向对象为主的系统中时,需要遵循以下4条经验规则。
DOMAIN-DRIVERN DESIGN
TACKLING COMPLEXITY IN THE HEART OF SOFTWARE
领域驱动设计
软件核心复杂性应对之道
【美】Eric Evans 著? ?赵俐 盛海艳 刘霞 等 译