一、何为领域驱动设计
在启动一个软件项目时,我们应该关注软件涉及的领域。软件的最终目的是增进一个特定的领域。
- 如何理解你的领域
最佳的方式是让软件成为领域的一个映射。软件需要包含领域里重要的核心概念和元素,并精确实现他们之间的关系。也就是说,软件需要对领域进行建模。 - 构建领域知识
举个例子:飞机飞行控制系统-
每一个飞行器有一个出发机场和目的机场
img -
飞机飞行计划
img -
路线是有一些小的区间组成,可以看做一些方位点
img -
实际上飞行员会把这些点看做地球上的映射
img -
驾驶员离开前会接到详细的飞行计划,飞行计划包括:飞行路线、巡航高度、巡航速度和飞机的类型等等
img -
我们并不对飞行器的类型颜色等感兴趣,而是对“飞行”感兴趣,“飞行”才是系统的核心概念
img
-
二、通用语言
-
对公共语言的需要
在设计过程中,我们倾向于使用自己的方言,但是没有一种方言能成为一种公共的语言,因为他们都不能满足所有的需要。
通用语言:使用模型作为语言的主干。要求团队在进行所有的交流时都使用一致的语言,在代码中也是这样。在共享知识和推敲模型时,团队会使用演讲、文字和图形。
-
创建通用语言
我们可以使用文档来表现模型。每一张小图包含模型的一个子集。这些图会包含若干个类以及他们之间的关系。然后向图中添加文本,来表现图不能表现的行为和约束。
文档与模型必须保持同步。陈旧的文档使用了错误的语言,或不能如实的反馈模型都是没什么用的。
另外两种可用的通用语言:- UML 图:在原素较少时比较有帮助,系统较大时 UML 会难以阅读
- 代码:代码能够完成其功能,但不一定能表达清楚其所作的事情
三、模型驱动设计
我们应该如何完成从模型到代码的转换?
一个推荐的设计技术是创建分析模型,它被认为是与代码设计相互分离的、通常是
由不同的人完成的。
一种更好的方法是将领域建模和设计紧密关联起来。模型在构建时就考虑到软件实
现和设计。
-
模型驱动设计的基本构成要素
img -
分层架构
img
领域驱动设计的一个通用的架构解决方案包含了4 个概念层:
概念层 | 内容 **用户界面/展现层** | 负责向用户展现信息以及解释用户命令 **应用层** | 很薄的一层,用来协调应用的活动。它不包含业务逻辑。它也不保留业务对象的状态,但它保留有应用任务的进度状态 **领域层** | 本层包含关于领域的信息。这是业务软件的核心所在。在这里保留业务对象的状态。对业务对象和它们状态的持久化被委托给了基础设施层 **基础设施层** | 本层作为其他层的支撑库存在。它提供了层间的通信,实现对业务对象的持久化,包含对用户界面层的支持库等作用
-
实体
有一类对象看上去好像拥有标识符,它的标识符在历经软件的各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至能够超出软件系统的生命周期。我们把这样的对象称为实体。
当一个对象可以用其标识符而不是它的属性来区分时,可以将标识符作为在模型中该对象定义的主要部分。使类的定义保持简单并专注于生命周期的延续性和标识符。
实体是领域模型中非常重要的对象,并且它们应该在建模过程开始时就被考虑。 -
值对象
img
有的时候我们需要包含一个领域对象的某些属性。我们对它是哪一个对象并不感兴趣,而是只关心它所拥有的属性。用来描述领域的特定方面、并且没有标识符的一个对象,叫做值对象。
如果值对象是可共享的,那么它们应该是不可变的。值对象应该保持很小、很简单。
比如:
一个客户会跟其姓名、街道、城市、州相关。最好用一个单独的对象来包含这些地址信息,客户对象会包含一个对这个对象的引用。街道、城市、州应该归属于一个对象,因为它们在概念上属于一体的,而不应该作为客户对象分离的属性。 -
服务
服务的3 个特征:- 服务执行的操作代表了一个领域概念,这个领域概念无法自然地隶属于一个实体或者值对象。
- 被执行的操作涉及到领域中的其他的对象。
- 操作是无状态的。
当使用服务时,保持领域层的隔离非常重要。很容易弄混属于领域层的服务和属于基础设施层的服务。当我们在设计阶段建立模型时,我们需要确保将领域层与其他的层隔离开来。
-
模块
软件代码应该具有高层次的内聚性和低层次的耦合度。虽然内聚开始于类和方法级别,它也可以应用于模块级别。推荐的做法是将高关联度的类分组到一个模块,以提供尽可能大的内聚性。最常用到的两个内聚是通信性内聚(communicational cohesion)和功能性内聚(functional cohesion)。
-
聚合
img
聚合是针对数据变化可以考虑成一个单元的一组关联的对象。聚合使用边界将内部和外部的对象划分开来。每个聚合都有一个根。这个根是一个实体,并且它是外部可以访问的唯一的对象。
聚合的一个简单的例子如下图所示。客户(Customer)是聚合的根,并且其他所有的对象都是内部的。如果需要地址(Address),一个它的副本将被传递给外部对象。
-
工厂
为复杂对象和聚合创建实例的职责,应该转交给一个单独的对象。虽然这个对象本身在领域模型中没有职责,但它仍是领域设计的一部分。提供一个接口来封装所有复杂的组装过程,客户不需要引用正在初始化的对象所对应的具体类。将整个聚合当作一个单元来创建,强化它们的不变量。
有时工厂是不需要的,一个简单的构造器就足够了。在如下情况下应该使用构器:- 构造过程并不复杂。
- 一个对象的创建不涉及到其他对象的创建,可以将所有需要的属性传递给构造器。
- 客户对实现很感兴趣,可能希望选择使用策略(Strategy)模式。
- 类是特定的类型,不存在到层级,所以不用在一系列的具体实现中进行选择。
-
资源库
资源库扮演了一个全局可访问对象的存储地点
使用一个资源库,它的目的是封装所有获取对象引用所需的逻辑。领域对象不需处理基础设施,以得到领域中对其他对象的引用。只需要从资源库中获取它们,于是模型重获它应有的清晰和专注。
img
需要注意的是,资源库的实现可能会非常像是基础设施,然而资源库的接口却是纯粹的领域模型。
四、面向深层理解的重构
-
持续重构
重构是不改变应用的行为而重新设计代码使得它更好的过程。
重构通常是非常谨慎的,按照小幅且可控的步骤进行。 -
凸现关键概念
约束是一个很简单的表达不变量的方式。无论对象的数据如何变化,不变量都要得到保持。简单的实现方式是将不变量的逻辑放在一个约束中。
处理过程(process)通常在代码中被表达为procedure。从我们开始使用面向对象语言后我们就不再用一种过程化的方法,所以我们需要为处理过程选择一个对象,然后给它添加行为。最好的实现过程的方式是使用服务。
规约是用来测试一个对象是否满足特定条件的。规则应该被封装到其自身的一个对象中,这将成为客户的规约,并且被保留在领域层中。
五、保持模型的一致性
当存在多个团队,有不同的管理和协作时,我们会面对一系列不同的挑战。
img
- 界定上下文
主要的思想是定义模型的范围,定出它的上下文的边界,然后尽最大可能保持模型的统一。
明确定义模型所应用的上下文,根据以下因素来明确设置边界:团队的组织结构、应用的特定部分中的惯例、物理表现(例如代码库、数据库Schema)。 - 持续集成
我们需要这样一个集成的过程,以确保所有新增的元素和模型原有部分能够和谐相处,在代码中也被正确地实现。 - 上下文映射
上下文映射(Context Map)是描绘不同的界定上下文和它们之间关系的一份文档。它可以是像下面所展示的一个图表(diagram),也可以是其他任何形式的文档,细节层次可以有所不同。重要的是,要让每个在项目中工作的人都能够分享并理解它。 - 共享内核
img
需要指派两个团队同意共享的领域模型子集。这个明确被共享的东西有特殊的状态,在没有咨询另一个团队之前不能做修改。
共享内核的目的是减少重复,但是仍保持两个独立的上下文。 - 客户-供应商
有的时候两个子系统之间存在特殊的关系:一个子系统严重依赖另一个。两个子系统所在的上下文是不同的,并且一个系统的处理结果被作为另外一个的输入。它们没有共享的内核,因为有这样一个内核从概念上说是错误的,或者两个子系统要共享代码在技术上不可能实现。
这个时候,应在两个团队之间建立一个清晰的客户/供应商关系。在制定计划的过程中,让客户团队扮演和供应商团队打交道的客户角色。对满足客户需求的任务进行协商并做出预算,让每个人都理解相关的承诺和日程表。 - 顺从者
客户非常依赖于供应商,然而供应商却不依赖客户,这时客户-供应商关系就不可行了。最明显的做法是将它与供应商分离开,完全自力更生。 -
防崩溃层
img
防崩溃层也许包含多个服务。每一个服务都有一个相应的Facade,对每一个Facade我们为之添加一个适配器。我们不应该为所有的服务使用一个适配器,因为这样会将很多功能混在一起,从而导致杂乱无章。
我们必须再添加一个组件。适配器将外部系统的行为包装起来。我们还需要对象和数据转换(object and data conversion),可以使用一个转换器(translator)来完成这个任务。它可以是一个非常简单的对象,有很少的功能,满足数据转换的基本需要。如果外部系统有一个复杂的接口,最好在适配器和接口之间再添加一个额外的Facade。这会简化适配器的协议,将它和其他系统分离开来。 - 隔离通道
隔离通道模式适合于以下情况:一个企业应用可由几个较小的应用组成,而且从建模的角度来看彼此之间有很少或者没有公共之处。 - 开放主机
定义一个能以一组服务的形式访问你的子系统的协议。将这个协议开放出来,使得所有需要和你做集成的人都能使用它。 - 提炼
即使在我们改进和创建很多抽象之后,一个大的领域还是会有一个大的模型。就是在做了很多次重构之后,模型依然会很大。对于这样的情况,就需要做一次提炼了。其思路是定义一个代表领域本质的核心域(Core Domain)。提炼过程的副产品将是包含了领域中其他部分的普通子域(Generic Subdomain)。
初读《领域驱动设计》,回想做过的项目,感觉之前的设计还有很多不足之处在里面。希望读完可以边回顾边实践,再在文中加入一些自己的体会和感悟~~
网友评论