领域驱动设计

本文基于《领域驱动设计精简版》整理,侵删。感谢InfoQ在软件之路上的贡献!
0. Preface
1. 协同开发不要破坏软件的概念完整性(语义一致性)
2. 软件的本质是设计与领域,而非新的技术栈
3. 重构是在代码实现层面对抗腐烂变质,而 DDD 是在代码架构设计层面对抗腐烂变质(控制复杂性)
4. 关注精简的业务模型及实现的匹配
1. OOP中,哪些对象对系统有用?哪些对象对拟建系统没有用处?我们该怎么保证选取的对象模型恰好够用?
2. 如何保留对象中有用的关系,去除无用的关系,并限定变更范围以降低系统的复杂度?
3. 业务驱动设计是DDD的观点,如果DDD特别关注的是 MDD (模型驱动设计) MDA (模型驱动架构)中 M 以及其实现,那么这个M如何与架构、开发过程相融合?
4. 不同的团队模型会让一个模型实现更优秀或者更糟糕
5. 面向对象技术,或者说是提炼的模型如何在工程中使用?
6. 一个项目使用统一的模型,还是分解为不同的模型?
1. What is DDD ?
Information
软件开发通常是真实世界中已存在流程的自动化,或为真实业务问题提供的解决方案。软件的领域是需要自动化的业务或真实世界的问题。从一开始,我们就必须明白软件起源于领域并与其领域密切相关。
创建一个好的软件,必须要知道软件究竟是什么。在充分了解金融业务是什么之前,是做不出好的银行业软件系统的。
银行的从业者最清楚银行业业务,他们知道所有的细节、困难、可能出现的问题、业务规则。这些就是我们永远的起点:领域。软件的最终目的是增进一个特定的领域。为了达到这个目的,软件需要与它服务的领域和谐共处,否则就会导致很大的混乱。
如何使软件与领域和谐共处?最佳方式是让软件成为其领域的映射。软件需要包含领域里的重要核心概念和元素,并精准实现其关系。也就是说,软件需要对领域进行建模。对银行业业务不了解的人也可以阅读此代码学习大量知识。
对软件进行领域建模需要去产出一个领域模型,一个经过严格组织并选择性抽象的知识。
模型是对目标领域内部的展现方式,会贯穿设计与开发的全过程。我们需要组织信息并将其系统化、分割成小一点的信息块,再将信息块放入逻辑块中,进行分离关注。
模型需要更精准、更完整、没有二义性,我们需要模型来交流。
瀑布设计方法的缺点是分析人员与开发人员间信息的不可见,另一种方法叫做敏捷,深入了解客户领域及需求变化,持续产出客户需要的软件。但有过度工程、过度设计的可能性。
Construct domain knowledge
范例
在给定的时刻,空中会有成千上万的飞机。它们会朝着各自的目的地按照路线飞行,很重要的事是需要确保它们不会在空中碰撞。我们不会试图描述一个完整的交通系统,而只是关注其中的一个小小的子集:飞行监控系统
飞行监控系统会跟踪指定区域内的任意航班,判断班机是否遵照预定航线,以及是否可能碰撞。
分析
软件开发应从理解领域开始。
本例就是从空中监控系统开始。控制人员是这个领域的专家,但他们并不是软件领域的专家,你不能期望他们会给你提供一个关于他们问题的完整描述。
你听到的很可能是例如飞机起飞、着陆、碰撞的危险、着陆等杂乱无章的知识。
为了从看似杂乱无章的信息中寻找规律,我们需要从某个地方开始。
Step 1
控制人员和你都认为飞行器必须拥有出发机场和目的机场:
graph LR A(Departure) --> B(Aircraft) B --> C(Destination)
Step 2
那么,飞机在飞行中时,发生了什么?控制人员说,会给每架飞机指派一个飞行计划,描述飞行的全过程。飞行计划是飞机在空中必须遵循的路径,也就是路线(Route)。这是一个重要的概念,其将出发机场和目的机场关联起来。所以将路线关联而不是将飞机关联似乎更合理与自然:
graph LR A(Aircraft) --> B(Route) B --> C(Departure) B --> D(Destination)
Step 3
进一步交流后,你发现路线是由小的区间段组成的曲线,所以路线可以被考虑成一系列的方位点:
flowchart LR A(Aircraft) --> B(Route) B -- * --o C(Fix) C --> D(2DPoint)
本质
本质上,你和领域专家交谈时,你们在交换知识。你在交谈中学习、理解领域,通过正确的问题正确地处理得到的信息。这样的模型一开始不能保证正确,但它是个开始点。
通常软件领域与领域专家人员会进行很长时间的讨论来交换知识。建立早期模型并验证其是否可以工作,修改模型就不再是开发人员的单项联系,而是存在反馈。这帮助我们更清晰、准确地理解领域。这样的模型体现两个专业领域的交汇,很消耗时间,但软件的最终目的是解决真实领域的业务问题,所以它必须与领域完美结合。
2. The Ubiquitous language
Request for public language
两个领域合力开发领域模型带来的问题是语言不一致。
开发人员满脑子都是类、方法、算法、模式,总是想将实际生活中的概念和程序中的工 件做对应。他们希望看到要建立哪些对象类,要如何对对象类之间的关系建模。但领域专家一无所知,他们只了解他们特定领域的专业知识。克服此现象需要使用 两个领域的公共语言 去描述领域,若不能共享一个公共语言来讨论领域,项目会面临严峻问题。
领域驱动设计的一个核心的原则是使用一种基于模型的语言。因为模型是软件满足 领域的共同点,它很适合作为这种通用语言的构造基础。使用模型作为语言的主干,并要求团队在交流时使用一致的语言,在代码中也是这样。这就是通用语言(Ubiquitous Language)[1]
这种语言的形成并非一日之功,需要展开艰难的工作,确保发现语言的那些关键元素。我们需要发现定义领域和模型的关键概念,描述他们的适当用词,并开始使用它们。它们当中的一些概念很容易被发现,但另一些则不然。
构建这样的语言会获取清晰的结果:模型与语言相互密切关联。一个对语言的变更会导致模型的变更。
开发人员应留意领域专家试图呈现设计中的内容存在的二义性或不一致的部分。
Create public language
范例
flowchart LR A(Aircraft) --> B(Flight Plan) B --> C(Route) C -- * --o D(Fix) D --> E(2DPoint)
UML很适合构建模型,它也真的是一种很好的记录关键概念及其关系的工具。但UML图一单复杂,即使是软件专家也很难阅读。所以,我们可以使用项目文档,通过小的图对模型的沟通,每一张小图包含了模型的一个子集,就像是分离关注[2]。这些文档甚至可以手绘——因为它达到比较稳定的状态前会发生多次变化。
大图混乱,并不能传达比小图的集合更好理解,当然,也可以使用优秀的代码进行沟通(代码即注释),通过测试断言来描述行为。
3. Model-driven Design
通用语言应充分被应用于建模过程,以推动两领域间的交流。下一步是将模型实现为代码。这是非常重要的阶段。优良的模型为能成功转化成代码会得到有质量问题的软件。在这一个过程中,开发人员往往因为代码难以正确表达概念与关系而转以创建自己的设计。更多的类被添加进代码,进一步加大了原始模型与最终实现的差距。
违背软件设计原则的模型是不建议的。选择一个能被轻易和准确地转换成代码的模型是很重要的。
开发人员应参与并确保模型在软件中得到实现。如果其中某处有错误,尽早纠正。代码模型的一个变更其实就隐含对模型的变更。 若分析人员与开发人员分离,最终的结果是模型将不再实用。 软件系统中的模型需要确保能如实反映领域模型,让映射显而易见。甚至可以让让软件模型如你所愿反映出对领域更深层的理解。
Main constituent elements of Model-driven Design
接下来会展现模型驱动设计中要是用的最重要的模式。这些模式的作用是从领域驱动设计的角度展现一些对象建模软件设计的关键元素。
模式与模式间的总图:
flowchart LR SU(Smart UI)x-- 相互排斥的选择 --xMDD(Model-driven Design) MDD-- 表达模型 --> S(Services) MDD-- 表达模型 --> E(Entities) E-- 访问 --> R(Repositories) E-- 封装 --> F(Factories) E-- 扮演 or 保持完整性 --> A(Aggregates) A-- 访问 --> R A-- 封装 --> F MDD-- 表达模型 --> VO(Value Objects) MDD-- 领域隔离 --> LA(Layered Architecture) VO-- 封装 --> A VO-- 封装 --> F
分层架构
flowchart LR subgraph Infrastructure In1(Infrastructure 1) In2(Infrastructure 2) In3(Infrastructure 3) In4(Infrastructure 4) end subgraph Domain D1(Domain 1) D2(Domain 2) end subgraph Application A1(Application 1) A2(Application 2) end subgraph User Interface I1(Interface 1) I2(Interface 2) end I2-->A2 I1-->A1 I1-->In1 I1-->D1 A1-->D1 A2-->D2 D1-->In2 D1-->In4 A2-->In4
我们创建软件应用时,这个应用很大一部分没有直接与领域关联,但它们是基础设施的一部分或是为 软件本身提供服务的。最好能让应用中的领域部分与其他部分相比保持尽可能小(也不是掺杂在一起)。
在面向对象程序中,UI界面、数据库等支持性代码经常被直接写到业务对象中,附加的业务逻辑被嵌入到UI组件和数据库脚本的行为中,这样做可以让事情快速工作起来。但是,当领域相关的代码被混入其他层时,要阅读和思考这些代码也变得极其困难。表面上是对UI的修改,却变成了对业务逻辑的修改。对业务规则的变更又需要跟踪其他代码。实现都粘连在了一起,模型驱动对象[3] (model-driven objects) 于是变得不再可行,也很难展开自动化测试。所以对于所有活动中包含的全部技术和逻辑而言,程序必须保持简单,否则就会难以理解。
因此,将复杂的程序划分成很多层。为每个层开发一个内聚设计,让每个层仅依赖于它下方的层。遵照标准的架构模式实现与上面的层的低耦合。将领域模型相关的代码集中到一个层中 ,把它从用户界面、应用和基础设施代码中隔离开来。领域对象不必在承担显示自己、保存自己、管理应用任务的职责,而是专注于表达领域模型。这会让一个模型逐渐进化的足够丰满、清晰,以便于捕获最基本的业务知识,并能够正常工作。
领域驱动设计的架构的通用解决方案包含了4个概念层:
名称 作用
展现层 负责向用户展现信息以及解释用户命令
应用层 很薄的一层,用来协调应用的活动。
它不包含业务逻辑,也不保留业务对象的状态
但它保留有应用任务的进度状态
领域层 本层包含关于领域的信息。
这是业务软件的核心所在。
这里保留业务对象的状态。
对业务对象和它们状态的持久化被委托给了基础设施层
基础设施层 本层作为其他层的支撑库存在。
它提供了层间的通信,实现对业务对象的持久化,
包含对展现层的支持库等作用
建立层的边界与通信规则很重要。如果代码没有被清晰地隔离到一些层中,就会很快发生混乱。
领域层应该关注核心的领域问题,不应该包括基础设施方面的活动;用户界面不该与业务逻辑捆绑,也不该与属于基础设施层的任务紧紧捆绑;应用层在很多情况下是必要的,它会成为业务逻辑之上的管理者,用来 监督和协调应用内的一切活动。
Entities
有一类对象看上去好像拥有标识符,它的标识符在历经软件的各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至能够超出软件系统的生命周期。我们把这样的对象称为实体。
如果一个类可以创建多个完全相当却拥有不同引用的对象,那它们并不是实体。例如天气信息类,很容易产生不同的实例,但其拥有不同的引用。
例如银行会计系统,每个账户拥有其自己的编号。每个账户可以用编号来精确的标识,这个编号在系统的生命周期中保持不变,并保证延续性。当一个对象可以用其标志符而不是属性来区分时,可将标志符作为在模型中该对象定义的主要部分。使类的定义保持简单并专注于生命周期的延续性和标志符。 这个标志符可以确保对每个对象产生一个唯一结果集的操作。
实体是领域模型中非常重要的对象,定义一个对象是否是一个实体也很重要。
Value Objects
跟踪与创建标志符需要很大的成本,并且我们需要很多仔细的考虑来决定由什么构成一个标志符,跟踪也不是易事。成千上万的实体也会给系统性能带来严重的下降,这不是我们期望的。
有些时候我们需要包含一个领域对象的某些属性,但我们对它是哪个对象并不感兴趣,而只关心它拥有的属性。 用来描述领域的特定方面、并没有标识符的一个对象,叫做值对象。
值对象可以被轻易的创建和丢弃。极力推荐奖值对象实现为不可变的。它们由一个构造器创建,并且在它们的生命周期内永远不会修改。这是很必要的,不可变对象可以维持一致性。
如果值对象是可共享的,那么它们应该是不可变的。值对象应该保持很小、很简单。当其他参与方需要一个值对象时,可以简单地传递值,或者创建一个副本。制作一个值对象的副本是非常简单的,通常不会有什么副作用。如果没有标识符, 你可以按你所需创建若干副本,然后根据需要来销毁它们。
classDiagram class OriginalCustomer { customerId name street city state } NewCustomer <|-- Address NewCustomer: customerId NewCustomer: name NewCustomer: address class Address { street city state }
值对象可以包含任何值对象,或是对实体对象的引用。属性可以被分组到不同的对象中,被选择用来构成一个值对象的属性应该形成一个概念上的整体。
Service
有一些领域中的动作,看上去不属于任何对象。它们代表了领域中的重要行为,不能忽略它们或简单地把他们合并到某个实体或值对象中。给一个对象增加这样的行为会破坏这个对象,让这个对象拥有了本不该属于它的功能。这样的行为从领域中被识别出来时, 最佳实践是将其声明为一个服务。这样的对象不再有内置的状态,而仅仅是为领域提供相应的功能。
服务可以将服务于实体和值对象的相关功能进行分组。最好显示声明服务,因为它在领域中创建了一个清晰的区分,它封装了一个概念,这个概念相当于一个操作的接口。
服务也能被运用于领域层。一个服务不是与执行服务的对象有关,而是与操作所要执行的对象有关。这种情况下,服务通常变成了多个对象的一个连接点。这也是为什么行为应该很自然的隶属于一个服务而不是被包含在领域对象中的一个原因。这也是行为必须在服务下而不被包含在领域对象中的一个原因——这会导致高耦合度。
一个操作凸显为领域的重要概念时,就需要为它创建一个服务。以下是服务的三个特征:
1. 服务执行的操作代表了一个领域概念,这个无法自然的隶属于一个实体或值对象
2. 被执行的操作涉及到领域的其他对象
3. 操作是无状态的
当一个重要过程、操作应该是个服务时,根据模型的语言定义一个接口,并确保操作的名字是通用语言中的一部分。 使用服务时,保持领域层的隔离非常重要。我们在设计阶段建立模型时,需要确保将领域层与其他层隔离开。如果服务在概念上属于应用层, 则将服务放入应用层。如果服务关于领域对象,而且确实与领域有关的、为领域的需要服务,那么它就属于领域层。
Module
模块发展到某个规模后,将它作为整体来讨论很困难,理解不同部件的关系和交互变得很困难。所以,必须要将模块组织到模块中。 模块被用来作为组织相关概念和任务以便于降低复杂性的一种方法。 查看模块包含的内容以及其间的关系,就很容易掌握大型模型的情况。
软件普遍上应该具有高内聚和低耦合,它也可以被应用于模块级别。推荐的做法是将高关联度的类分组到一个模块,以提供尽大的内聚性。最常见的内聚是通信性内聚(Communicational cohesion)[4]和功能性内聚(Functional cohesion)[5]。在模块中的部件操作相同相同的数据时,可以得到通信性内聚。在模块中的部件协同工作以完成定义好的任务时,可以得到功能性内聚。功能性内聚是最佳的内聚模型。
flowchart LR subgraph 调方域 D1(Domain) -- 4 实现 --> G1 A1(Application) -- 3 --> D1 G1(Gateways) -- 2 --> A1 end subgraph 被调方域 D(Domain) -- 7 实现 --> G A(Application) -- 6 调用 --> D G(Gateways) end U(用户发送请求) -- 1 --> G1 A -.       Gateways依赖Application .-> G1 G1 -- 5 调用 --> A
模块应由在功能上或逻辑上属于一体的元素构成,以确保内聚性。模块应该具有定义好的接口并可以被其他模块访问。如果模块间仅有极少的连接,通过连接来执行定义好的功能,能让人更容易理解系统如何工作。
应该选择那些能够表达系统功能并且包含具有内聚性的一组概念的模块。这样做常常会降低模块间的耦合。 重新定义模型,直到能够按照高级别的领域概念将它区分开来,而且对应的代码也被很好地解耦。
模块名称会成为通用语言的组成部分。模块和它们的名称能够反映出对领域的深层理解。
模块的设计应灵活,允许随着项目进展而变化,并不应该被冻结。
Next
管理领域对象的生命周期自身就会遇到一个挑战,如果做得不恰当,就会对领域模型产生负面影响。下面三个模式帮助我们处理这个挑战。
聚合是用来定义对象所有权和边界的领域模式;工厂和资源库是另外的两种设计模式,用来帮助我们处理对象的创建与存储问题。
工厂和资源库之间存在一定的关系。它们都是模型驱动设计中的模式,它们都能帮助我们管理领域对象的生命周期。
然而工厂关注的是对象的创建,而资源库关注的是已经存在的对象。
资源库可能会在本地缓存对象,但更常见的情况是需要从一个持久化存储中检索它们。对象可以通过构造器创建,也可以通过一个工厂来构建。出于这个理由, 资源库也可以被看作是一个工厂,因为它会创建对象。然而它不是从无到有创建新的对象,而是重建已有的对象。我们不应该将资源库与工厂混合在一起。工厂应该用来创建 新的对象,而资源库应该用来发现已经创建的对象。当一个新对象被添加到资源库时, 它应该是先由工厂创建好的,然后它应该被传递给资源库,由资源库来保存它。
另外要注意的是工厂是“纯的领域”,而资源库会包含到基础设施的连接,例如数据库。
Aggregate
无论设计时有多少考虑,我们都会看见一个模型中很多对象会与其他对象发生关联,进而形成复杂的关系网。对模型中每一个可导航的关联而言,都应该有对应的软件机制加强它。来自模型的挑战常常不是让它们尽量完整,而是让它们尽量简单和容易理解。这就意味着,我们需要对模型中的关系进行消减和简化。
例如
1. 一对多关联关系通常可以被简单的转化为一个对象与其他对象的集合之间的关联
2. 多对多关联的简化很困难。关联的数字应该被尽可能的减小。
1. 要删除模型中非基本的关联关系。他们可能在领域中存在但在模型中不必要。
2. 通过添加约束减少多重性。如果很多对象满足一种关系,那么这个关系上加上正确的约束后,很可能只有一个对象会继续满足这种关系。
3. 很多时候双向关联可以转化为非双向关联。例如:每一辆汽车都有一台发动机,并且发动机在运转时,都会属于一辆汽车。这种关系是双向的,但是很容易将其简化为汽车拥有发动机,而不用考虑反向的。
减少关联后,我们仍然可能会获得很多关系。一个银行系统会保留并处理客户数据。这些数据包括客户的个人数据(例如姓名、地址、电话号码、工作描述等)和账户数据:账户、余额、执行的操作等。
当系统归档或者完全删除一个客户的信息时,必须要确保所有的引用都被删除了。如果许多对象持有这样的引用,则很难确保它们全被清除了。同样地,如果一个客户的某些数据发生了变化,系统必须确保在整个系统中执行了适当的更新,数据的一致性必须得到保证。这通常是在数据库层面进行处理的。通常会使用事务来确保数据的一致性。但是如果模型没有被仔细地设计过,会产生很大程度的数据库争夺,导致性能很差。当数据库事务在这样的操作中担负重要角色时,我们会期望直接在模型中解决跟数据一致性相关的一些问题。
所以,在模型拥有复杂关联的对象发生变化时,很难保证一致性。因此使用聚合。
聚合是针对数据变化可以考虑成一个单元的一组关联对象。聚合使用边界将内部与外部划分开来。
每个内聚都有一个根,这个根是一个实体,且是外部可以访问唯一的对象。 根对象可以持有对任意聚合对象的引用,其他的对象可以互相持有彼此的引用,但一个外部对象只能持有对根对象的引用。如果边界内还有其他的实体,拿着洗实体的标识符是本地化的,只有聚合内有意义。
聚合保证一致性的原因是其他对象只能访问根对象而不可以访问根对象聚合内的其他对象。其他对象所能做的只有请求根的变更。
根对象可以将内部对象的临时引用传递给外部对象,作为限制,当操作完成后,外部对象不能持有这个引用。一种简单的实现方式是向外部对象传递值对象的副本。在副本对象上发生了什么事情将不再重要,因为它不会影响聚合的一致性。
聚合的对象存储于数据库时,可以通过查询来获得的应该只有根对象。其他的对象只能通过根对象出发导航关联的对象来获得。
聚合内的对象可以被允许持有对其他聚合的根对象的引用。
根实体拥有全局的标识符,并且有责任维护不变量。内部的实体拥有内部的标识符。
将尸体和值对象据聚集在聚合之中,并定义各个聚合之间的边界。为每个聚合选择一个实体作为根,并通过控制根来控制所有边界内的对象的访问。允许外部对象持有对根的引用。这种安排使强化聚合内的对象的不变量变得可行。对于聚合而言,它在任何状态变更中都是一个整体。
classDiagram class ContactInfo { homePhoneNumber workPhoneNumber faxNumber emailAddress } class Address { street city state } Customer <|-- ContactInfo Customer <|-- Address Customer: customerId Customer: name
Factory
实体和聚合常常很大很复杂。过于复杂以至于难以通过根实体的构造器来创建。实际上通过构造器构建一个复杂的聚合并不是领域本身通常应该做的事情。在领域中,某些事物时由别的事物创建的。创建一个对象可以是它自身的主要操作,但是复杂的组装应由工厂来获得。
工厂被用来封装对象创建所必须的知识,他们对创建聚合特别有用。当聚合的根被创建后,所有聚合所包括的对象都应该随之创建,所有的不变量得到强化。
保持创建过程的原子性非常重要。
为复杂对象和聚合的创建实例的职责,应该转交给一个单独的对象。虽然这个对象本身在领域模型中没有职责,但其仍是领域设计的一部分。将整个聚合当做一个单元来创建,强化它们的不变量。
有的时候创建对象的逻辑会非常复杂,或者创建对象涉及到创建其他一系列对象时,可以使用单独的工厂对象来隐藏聚合的内部构造所需要的任务。
当创建一个工厂时,我们被迫违反一个对象的封装原则,这必须谨慎行事。每当对象中发生了某种变化时,会对构造规则或者某些不变量造成影响。我们需要确保工厂也可以更新并支持新的条件。工厂与他们将要创建的对象是紧密关联的。较为合适的做法是使用一个专用的工厂类来负责创建整个聚合,在这个工厂类中将包含应该为聚合强化的规则、约束和不变量。这个对象会保持简单并完成特定目的。
有些时候,工厂不需要,构造器就足够。在如下情况应该使用构造器:
1. 构造过程不复杂
2. 对象的创建不涉及到其他对象的创建,可以将所有需要的属性传递给构造器
3. 客户对实现很感兴趣,可能希望使用策略(Strategy)模式
4. 类是特定的类型,不存在层级
另一个观察角度是工厂需要从无到有创建一个新对象,也可能是反序列化。反序列化包含的过程与创建不同,重建的新对象不需要标识符,这个对象已经有一个标识符了,对不变量的违反也将区别对待。当从无到有创建新对象时,任何对不变量的违反都会异常。对反序列化,我们不能这么处理,否则会数据丢失。
Repository
创建对象完全是为了使用它们,我们必须持有对象的引用才能使用。为了获得这个引用,客户需要创建对象或是通过导航关联关系来获得它。在大型工程中,这会是一个问题:我们必须确保客户程序始终拥有所需要的对象的应用或导航。这会增加耦合性和一系列非真正需要的关联。
要使用一个对象,则说明它已经被创建完毕了。如果该对象是个根,则它是一个实体,会被保存为一个持久化的状态,可能是在数据库中。如果是个值对象,可以通过导航获取。实际上大量对象都可以从数据库中直接获取,这解决了获取对象引用的问题。当一个客户程序需要使用一个对象时,通过标识符访问数据库并检索使用。这看上去是简单的解决方案,但它会对设计产生负面影响。
数据库是基础设施的一部分,一个糟糕的解决方案是客户程序必须知道访问数据库所需的细节。例如,客户需要创建SQL查询语句来检索想要的数据。数据库查询可能会返回一组记录、甚至是更多内部的细节。当客户程序不得不直接从数据库创建对象时,会导致这样的代码扩散至整个模型而使领域模型遭受损害。它必须处理大量基础设施的细节而不是领域概念。如果对数据库的变更会导致所有扩散代码的变更。这样做会破坏封装、带来未知的结果。
因此,使用一个资源库,它的目的是封装所有获取对象引用所需的逻辑。领域对象不需处理基础设施,以得到领域中对其他对象的引用。只需要从资源库中获取它们,于是模型重获它应有的清晰和专注。
资源库会保存对某些对象的引用。当一个对象被创建后,它可以被保存在到资源库中,可以从资源库取出以备后续使用, 资源库扮演了全剧可访问对象的存储地点。 总体的结果时领域模型本身与需要保存对象或它们的引用、访问持久化基础设施实现了解藕。
对于需要全局访问的每种类型的对象,创建一个对象来提供该类型所有对象都在内存中的假象。通过一个众所周知的全局接口来设置访问途径。提供方法来添加或者删除对象,封装向数据存储中插入或者删除数据的实际操作。提供基于某些条件选择对象的方法,返回属性值符合条件的完全实例化的对象或对象集合,从而封装实际的存储和查询技术。仅仅为真正需要直接访问的聚合根提供资源库。让客户程序保持对模型的专注,将所有的对象存储和访问细节都委托给资源库。
资源库可以包含用来访问基础设施的细节信息,但它的接口应该非常简单。资源库应该拥有一组用来检索对象的方法。客户程序调用这样的方法,传递一个或者多个代表筛选条件的参数用来选择一个或者一组匹配的对象。可以通过传递实体的标识符来轻易指定一个实体。其他筛选条件可能由一组对象属性构成。资源库将所有的对象与这组条件来比较,并返回符合条件的那些对象。资源库接口可能还包含用来执行某些辅助计算 (例如获取特定类型对象的数量)的方法。
需要注意的是,资源库的实现可能会非常像是基础设施,然而资源库的接口却是纯粹的领域模型。
4. Refactoring for deeper understanding
Continuous Refactor
模型必须与源自的领域紧密相连。代码设计应该围绕模型展开并改善。脱离了模型的设计会导致软件无法真实表达它所服务的领域,很可能会得不到期望的行为。 建模如果得不到设计的反馈或者缺少了开发人员的参与,会导致必须实现模型的人很难理解它,并且对于所用的技术而言可能不太适合。
在设计和开发过程中,时不时停下来查看一下代码意味着到了重构的时间。重构通常是非常谨慎的,按照小幅且可控的步骤进行,这样我们就不会破坏功能或者引入一些 bug 了。 毕竟,重构的目的是让代码更好而不是更坏。自动化测试可以为我们提供很大帮助,确保我们没有破坏任何事情[6]
还有另一种类型的重构,与领域和它的模型相关。有时会对领域有新的理解,有些事物变得更加清晰,或者发现了两个元素间的关系。所有的这些会通过重构工作被包括到设计中。得到容易阅读和理解的、有表现力的代码是非常重要的。通过阅读代码,一个人应该不仅仅能够了解代码是做什么的,同时了解它为什么要这样做。只有这样才能让代码真正捕获模型的实质。
与一致的语言一道使用经过验证的基础构造块,这样做会使得开发工作在某种程度 上保持明智。这带来了一个挑战:如何发现一个深刻的模型(Incisive model),这个模型能够捕获到领域专家头脑中微妙的概念,并且以此来驱动实际的设计。一个忽略肤浅 的表面内容且捕捉到基本内容的模型是一个深层模型(Deep model)。这会让软件更加与领域专家的思路合拍,也更能满足用户的需要。
从传统意义上讲,重构描述的是出于技术动机的代码转换。重构的动机同样可以出于对领域的深入理解,以及对模型及其代码表达进行相应的改进。
除非使用迭代的重构过程,加上领域专家和开发人员一起密切关注对领域的学习, 否则一个复杂成熟的领域模式是很难开发出来的。
Highlight key concepts
我们会为模型添加新的概念和抽象, 然后对基于模型的设计做重构。每一次改进都会让设计更加清晰,从而带来突破。突破常包括思维上的变化,如同我们理解模型一样。它也是项目中取得巨大进展的源泉,然而它也有一些缺点。突破可能隐含了大量的重构。这意味着需要时间和资源,大量的重构也会在应用中引入行为上的变化。
为达到一次突破,我们需要将隐含的概念显现出来。当我们跟领域专家交谈时,我们交换了大量的想法和知识。某些概念成为了通用语言的一部分,但也有一些概念在起初未被重视。在改进设计的过程中,我们发现这些概念在设计中担任了重要的角色。因此我们需要将这些隐含的概念显现出来。我们应该为它们创建类和关系。当这种情况出现时,我们就拥有了突破的机会。
隐含的概念可能不会仅于此。如果它们是领域概念,它们应该被引入到模型和设计中。我们应该如何识别它们呢?
第一种发现隐含概念的方式是倾听用到的语言。我们在建模和设计过程中使用的语言中包含了大量关于领域的信息。起初可能不会很多,或者某些信息没有被正确地使用。某些概念可能无法被完全理解,甚至理解是完全错误的。 这是在学习一个新的领域所必须经历的一部分。但因为我们建造了我们的通用语言,关键概念会被加入其中。我们应该从那里开始查找隐含的概念。
有时设计的一些部分可能不会那么清晰,有一组关系让路径的计算变得难以进行, 或者其过程会复杂到难以理解。这些部分在设计中显得十分笨拙,但这也是寻找隐藏的概念的绝佳之所。可能我们错过了什么。如果某个关键概念在破解谜团时缺失了,其他的事物就不得不替代它完成它的功能。这会让某些对象变胖。设计的清晰度受到了损害。努力寻找是否有缺失的概念,如果找到一个,就将它显现出来。对设计做重构,让它更简单、更具灵活性。
当我们构建知识时很可能会遇到矛盾的情况,一个需求可能看上去与另一个需求矛盾。有一些矛盾其实不是真正的矛盾,只是因为看待同一事物的方式不同,或者只是因为在讲解时缺乏精确度造成的。我们应该努力去解决矛盾,有时这确实会帮助我们发现重要的概念。即使并没有发现它们,能够保持所有事物清晰也是很重要的。
挖掘模型概念的另一种明显的方式是使用领域文献。现在有众多为几乎任何可能的主题而编写的书,它们包含了大量关于特定领域的知识。这些书通常不包含所介绍领域 的模型,它们包含的信息需要进一步处理、提炼和改进。但是,在书中发现的信息是有价值的,会给我们提供对领域的深层视图。
在将概念显现出来时,还有其他一些非常有用的概念:约束、过程和规约。约束是一个很简单的表达不变量的方式。无论对象的数据如何变化,不变量都要得到保持。简单的实现方式是将不变量的逻辑放在一个约束中。
5. Keep consistency of modules
当多个团队开发一个项目时,代码开发是并行完成的,不同模块的开发可能对另一个模块的领域知识不理解。最终导致破坏领域、甚至应用的功能。这种情况很容易发生,因为没有人会花时间去完全理解整个模型。每个人都知道自己的后院里有什么,但对其他地方却并不是非常了解。
从一个良好的模型开始,发展到后来却变成了一个不一致的模型,这种情况很容易出现。模型的首要需求是:模型必须是一致的,保持不变的术语,并且没有矛盾。模型内部的一致性被称为“统一”(Unification)。
解决方案并非显而易见, 我们应该做的是有意识地将大模型分解成多个较小的模型。只要遵守它们所绑定的契约,良好整合的小模型能够独立进化。每个模型都应该有一个清晰的边界,模型之间的关系也应该被精确地定义。
我们将会介绍一组技术,用来维护模型的完整性。下面的图展示了这些技术,以及它们之间的关系。
flowchart LR BC(Bounded Content)-- 语言映射 -->UL(Ubiquitous Language) BC-- 保持模型统一 --> CI(Continous Integration) BC-- 评估/概述关系 --> CM(Content Map) BC-- 重叠上下文 --> SK(Shared Kernel) BC-- 关联上下文 --> QST(Customer/Supplier Teams) BC-- 单方面重叠 --> C(Conformist) BC-- 支持多客户 --> OS(Openhost Service) OS-- 形式化 --> PL(Published Language) BC-- 自由组队 --> SW(Separate Ways) BC-- 单方面隔离 --> ACL(Anti-Corruption Layer)
Bounded Content
每一个模型都有一个上下文。新的应用有自己的模型和上下文,独立于遗留模型及其上下文。它们无法被合并、混合或者混淆起来。所以当我们开发大的企业应用时,我们需要为每一个我们创建的模型定义上下文。
采用多个模型,对任何大型项目都能起到作用。如果将基于明显不同模型的代码合并在一起,软件就会变得有很多 bug、不可靠而且很难理解。团队成员之间的沟通会容易产生混淆。模型不应该被应用于哪些上下文,通常不是非常明确。
如何将一个大的模型分解成小的模型没有什么固定的准则。尽量把那些相关联的以及能形成一个自然概念的元素放在一个模型里。模型应该足够小,以便能分配给一个团队去实现。团队协作和沟通会更加流畅,这会有助于开发人员共同完成一个模型。模型的上下文是一些条件的集合,这些条件可以确保应用在模型里的术语都有一个明确的含义。
主要的思想是定义模型的范围,定出它的上下文的边界,然后尽最大可能保持模型的统一。在模型跨越整个企业项目时,要保持它的纯洁是很困难的。但是在它被限定到一个特定区域时,要保持它的纯洁就容易得多。明确界定上下文并根据以下因素明确设置边界:
1. 团队的组织结构
2. 应用的特定部分中的惯例
3. 物理表现(例如代码库、数据库Schema)
保持模型在这些边界里严格一致,不要因外界因素而产生干扰或混淆。
界定的上下文并不是模块。界定的上下文提供有模型在其中进化的逻辑框架。模块是被用来组织模型的元素,因此界定的上下文包含了模块。
要时刻意识到任何针对模型的变更都有可能破坏现 有的功能。当使用多个模型时,每个人在自己的模型之上可以自由地工作。我们都知道自己模型的界限,都恪守在这些边界里。我们需要确保模型的纯洁、一致和统一。每个模型应能使重构尽可能容易,而不会影响到其他的模型。设计能够被改进和提炼,以达到最高的纯洁性。
有多个模型时总是会付出些代价。我们需要定义不同模型间的边界和关系。这需要额外的工作和设计努力,可能还有不同模型间的翻译。我们不能在不同模型间传递任何对象,也不能像是没有边界一样自由地调用行为。但这并不是一个非常困难的任务,而且带来的好处证明克服这些困难是值得的。
Continuous Integration
一旦界定的上下文被定义好,我们就必须保持它的完整性。系统被破坏成更小的上下文后,最终会失去完整性和一致性的价值。
即使是只有一个团队工作于一个界定的上下文,也有犯错误的时候。在团队内部我们需要充分的沟通,以确保每个人都能理解模型中每个元素所扮演的角色。如果一个人不理解对象之间的关系,他就可能会以和原意完全相反的方式修改代码。如果我们不能百分之百地专注于模型的纯洁性,就会很容易犯这种错误。团队的某个成员可能会添加重复的代码,因为他不知道这些代码已经存在,或者因为担心破坏现有的功能而不去改变已有的代码,却选择了添加重复的代码。
模型不是一开始就被完全定义。而是先被创建,然后基于对领域新的理解和来自开发过程的反馈持续进化。
这意味着新的概念会进入模型,新的元素也会被添加到代码中。所有的这些需求都会被集成进一个统一的模型,进而用代码来实现。这也就是为什么持续集成在界定的上下文中如此必要的原因。
我们需要这样一个集成的过程,以确保所有新增的元素和模型原有部分能够和谐相处,在代码中也被正确地实现。我们需要有一个过程用来合并代码。合并得越早越好。对于单个小团队,推荐做每日合并。我们还需要有一个适当的构建过程(build process)。合并的代码需要自动地被构建,这样才能够被测试。另外一个必要的需求是执行自动化测试。如果团队有测试工具,并创建了一个测试套件,那么每次构建都可以运行测试,任何错误都可以被检测出来。而这时也可以 较容易地修改代码以修正报告的错误,因为它们被发现的很早,然后合并、构建、和测试过程会重新开始。
持续集成是基于模型中概念的集成,然后通过测试来实现。任何模型的不一致性在实现中都会被检测出来。持续集成应用于界定的上下文,不会被用来处理相邻上下文之间的关系。
Content Map
一个企业应用有多个模型,每个模型都有自己的界定的上下文。建议使用上下文作为团队组织的基础。在同一个团队里的人们能更容易地沟通,也能更好地将模型和实现集成。尽管每个团队都工作于自己的模型,最好让每个人都能了解总体的图景。上下文映射(Context Map)是描绘不同的界定上下文和它们之间关系的一份文档,细节层次可以有 所不同。重要的是,要让每个在项目中工作的人都能够分享并理解它。
只有独立的统一模型还不够,它们还必须被集成在一起,因为每个模型的功能都只是整个系统的一部分。在最后,单个的部分要被组装在一起,整个的系统必须能正确地 工作。如果上下文定义的不清晰,很有可能彼此之间互相覆盖。如果上下文之间的关系没有被描绘出来,在系统被集成的时候它们就有可能无法工作。
每个界定的上下文都应该有一个作为通用语言一部分的名字。当团队之间讨论整个系统的时候,这会对团队之间的沟通产生极大帮助。每个人也应该知道每个上下文的边界以及在上下文和代码之间的映射等。一个常用的做法是先定义上下文,然后为每个上下文创建模块,再用一个命名约定来指明每个模型所属的上下文。
Shared Kernel
当缺少功能集成时,持续集成的成本会显得过于高昂。尤其是在团队不具备相关的技能或者行政组织来维护持续集成,或者是单个团队又大又笨拙的时候。所以独立的界定上下文可能会由多个团队来定义和形成。
工作于紧密关联的应用程序上团队如果缺乏协调,有时会进展得很快,但他们的工作成果有可能会很难整合。他们在转换层(Translation layers)和改造(Retrofitting)上花费的时间比一开始就做持续集成会更多,做了许多重复劳动,失去了公共的通用语言带来的好处。
因此,需要指派两个团队同意共享的领域模型子集。当然除了包括模型的子集部分,还要包括模型相关联的代码或数据库设计的子集。这个明确被共享的东西有特殊的状态,在没有咨询另一个团队之前不能做修改。
要经常整合功能系统,但是可以不用像在团队内部进行持续集成那么频繁。在集成的时候,两个团队开发的测试都要运行。
共享内核的目的是减少重复,但是仍保持两个独立的上下文。对于共享内核的开发需要多加小心。两个开发团队都有可能修改内核的代码,还必须对所做的修改做集成。 如果团队用的是内核代码的副本,那么要尽可能早地合并代码,至少每周一次。还应该使用一个测试套件,这样每一个针对内核的修改都能快速地被测试。内核的任何改变都应该与另一个团队进行沟通,并且通知相关团队,使大家都能了解新增的功能。
Customer - Supplier Teams
有的时候两个子系统之间存在特殊的关系:一个子系统严重依赖另一个。两个子系统所在的上下文是不同的,并且一个系统的处理结果被作为另外一个的输入。它们没有共享的内核,因为有这样一个内核从概念上说是错误的,或者两个子系统要共享代码在技术上不可能实现。
当我们面对这样一个场景时,我们应该采取行动。让两者扮演客户 - 供应商角色。客户应该介绍它的需求而供应商应根据需求来制定计划。
需要精确定义两个子系统之间的接口。另外还要创建一个顺从的测试套件,在关注任何接口需求的时候用来做测试。供应商团队能够在他们的设计上大胆地工作,因为接口测试套件构成的保护网会在任何有问题的时候报警。
在两个团队之间建立一个清晰的客户/供应商关系。在制定计划的过程中,让客户团队扮演和供应商团队打交道的客户角色。
联合开发可以对预期接口做验证的自动化验收测试。将这些测试添加到供应商团队 的测试套件里,作为团队的持续集成过程的一部分运行。这个测试能使供应商团队放心地做修改,而不用担心会产生影响客户团队应用的副作用。
Conformist
在两个团队对彼此的关系都有兴趣时,客户-供应商关系是可行的。客户非常依赖于供应商,然而供应商却不依赖客户。如果有管理手段来保证合作的执行,供应商会给予客户需要的关注,并聆听客户的要求。如果管理手段没有清晰地界定在两个团队之间需要完成什么,或者管理很糟糕,或者缺乏管理,供应商慢慢地会更加关注它的模型和设计,对帮助客户不再感兴趣。
毕竟他们有自己的 deadline。即使他们是好人,愿意帮助其他团队,时间的压力却不允许他们这么做,客户团队会受到损害。在团队属于不同公司的情况下,这样的事情也会发生。沟通是困难的,供应商的公司也许没兴趣在维持关系上投资太多。他们要么提供零星的帮助,或者直接拒绝合作。结果是客户团队孤立无援,只能尽自己的努力摸索模型和设计。
客户团队没有多少选择。最明显的做法是将它与供应商分离开,完全自力更生。在后面的“隔离通道”模式中我们再对它做详细介绍。有时供应商子系统提供的好处不值得所付出的努力。创建一个独立的模型,并且在不必考虑供应商模型的情况下做设计也许更简单些。但这样做并不总是管用。
有时候供应商的模型会有一些价值,这时不得不维持一个连接。但是因为供应商团队不会帮助客户团队,所以后者不得不采取一些措施来保护自己,以防止前者对模型所做变更带来的影响。他们需要实现连接两个上下文的转换层。也有可能供应商团队的模型没有被很好地构思,导致其实现非常糟糕。虽然客户上下文仍然可以使用它,但是它应该通过使用一个我们后面要讨论的“防崩溃层”来保护自己。
如果客户不得不使用供应商团队的模型,而且这个模型做得很好,那么就需要顺从这个模型了。客户团队遵从供应商团队的模型,完全顺从它。这和共享内核很相似,但有一个重要的不同之处。客户团队不能对内核做更改。他们只能将它作为自己模型的一部分,可以在所提供的现有代码上完成构建。在很多情况下,这种方案是可行的。当有人提供一个丰富的组件,并提供了一个访问该组件的接口时,我们就可以建造包括了这个组件的模型,就好像这个组件是我们自己的东西。如果组件有一个小的接口,那么最好只为它简单地创建一个适配器,在我们的模型和组件模型之间做转换。这会将我们的模型隔离出来,可以有很高的自由度去开发它。
Anti-Corruption Layer
我们会经常遇到以下情况:所创建的新应用需要与遗留软件或者一个独立应用交互。在我们的模型和遗留模型之间就必须有一个集成层,这也是使用老旧应用的需求之一。
我们的客户端系统和一个外部系统交互有很多种方法。一种方法是通过网络连接,两个应用需要使用相同的网络通信协议,客户端需要遵从外部系统所使用的接口。另一种交互的方法是通过数据库。外部系统使用存储在数据库里的数据,客户端系统被假定访问同样的数据库。
在这两个案例中,我们所处理的都是在两个系统之间传输的原始数据。这看上去相当简单,然而事实是原始数据不包括任何关于模型的信息。我们不能将数据从数据库中取出来,全部作为原始数据来处理。在这些数据后面隐藏着大量的语义。一个包含有与原始数据相关的其他原始数据的数据库,构成了一个关系网。数据的语义非常重要,并且需要被充分考虑。客户端应用不能在不理解被使用数据含义的情况下就访问数据库并执行写操作。我们看到外部模型的一些部分被反映在数据库里,然后影响了我们的模型。
如果我们允许这样的事情发生,那么就会存在外部模型修改客户端模型的风险。我们应该在我们的客户端模型和外部模型之间建造一个 防崩溃层。从我们模型的观点来看,防崩溃层是模型天然的一部分,并不像一个外来的东西。它操作的是与我们的模型相似的概念和动作,但是防崩溃层使用外部语言与外部模型交流,而不是客户端语言。这个层在两个领域和语言之间扮演 双向转换器,它最大的好处在于可以使客户端模型保持纯洁和一致,不会受到外部模型的污染。
我们应该如何来实现防崩溃层?一个非常好的解决方案是将这个层看作来自客户端模型的一个服务。使用服务是非常简单的,因为它抽象了其他系统并让我们以自己的术语来定位它。服务会处理所需要的转换,所以我们的模型可以保持绝缘。防崩溃层最有可能还需要一个适配器(Adapter)。适配器可以使你将一个类的接口转换成客户端能够理解的另一个接口。在我们的这个案例中,适配器不需要一 定包装一个类,因为它的工作是在两个系统之间做转换。
flowchart LR subgraph External System C3(Class C3) I1(Interface I1) end subgraph Anticorruption Layer F1(Façade F1) A1(Adapter A1) T1(Translator T1) F3(Façade F3) T2(Translator T2) F2(Façade F2) A2(Adapter A2) end subgraph Client System CC1(Class C1) CC2(Class C2) end CC1-->F1 CC2-->F2 F1-->A1 A1-->T1 A1-->F3 A1-->T2 F2-->A2 A2-->F3 A2-->T2 F3-->C3 F3-->I1
我们必须再添加一个组件。适配器将外部系统的行为包装起来。我们还需要对象和数据转换,可以使用一个转换器(translator)来完成这个任务。它可以是一个非常简单的对象,有很少的功能,满足数据转换的基本需要。如果外部系统有一个复杂的接口,最好在适配器和接口之间再添加一个额外的 Facade。这会简化适配器的协议,将它和其他系统分离开来。
Isolation channel
隔离通道模式适合于以下情况:一个企业应用可由几个较小的应用组成,而且从建模的角度来看彼此之间有很少或者没有公共之处。它有一组自己的需求,从用户角度看这是一个应用,但是从建模和设计的观点来看,它可以由具有不同实现的独立模型来完 成。我们应该查看一下需求,思考一下它们是否可以被划分成两个或者多个几乎没有相通之处的部分。如果可以这样做,那么我们就创建独立的界定上下文(Bounded Context), 并独立建模。这样做的好处是可以自由地选择用来实现模型的技术。我们正创建的应用 可能会共享一个公共的瘦 GUI,作为带有链接或按钮的一个门户来访问每一个应用。相对于集成后端的模型,将应用组织在一起是一个较小的集成。
在采用隔离通道模式之前,我们需要确信我们将不会回到一个集成的系统。独立开发的模型是很难做集成的,它们的相通之处很少,不值得这样做。
Openhost Service
当我们试图集成两个子系统时,通常要在它们之间创建一个转换层。这个层在客户端子系统和我们想要集成的外部子系统之间扮演了缓冲的角色。这个层可以是始终如一的,这要看关系的复杂度和外部子系统是如何设计的。如果外部子系统不是被一个客户端子系统使用,而是被多个子系统使用的话,我们需要为所有的子系统创建转换层。所有的这些层都会重复相同的转换任务,也会包含相似的代码。
当一个子系统要和其他很多子系统集成时,为每一个子系统定制一个转换器会使整个团队陷入困境。会有越来越多的代码需要维护,当做出变更时,有越来越多的事情需要担心。
这个问题的解决方案是,将外部子系统看作服务提供者。如果我们能为这个系统封装一组服务,那么所有的其他子系统将会访问这些服务,我们也就不需要任何转换层。 难点在于每一个子系统也许需要以一种特殊的方式和外部子系统交互,那么要创建一组一致的服务可能会比较麻烦。
定义一个能以一组服务的形式访问你的子系统的协议。将这个协议开放出来,使得 所有需要和你做集成的人都能使用它。然后增强和扩展这个协议,使其能够处理新的集 成需求,但某团队有特殊需求时除外。对于特殊的需求,使用一个一次性的转换器来增强协议,从而使得共享的协议保持简单和一致。
Refinings
提炼是从一种混合物中分离出其组成物质的过程。提炼的目的是从混合物中提取出某种特殊的物质。在提炼的过程中,可能会得到某些副产品,它们可能也是有价值的。
即使在我们改进和创建很多抽象之后,一个大的领域还是会有一个大的模型。就是在做了很多次重构之后,模型依然会很大。对于这样的情况,就需要做一次提炼了。其思路是定义一个代表领域本质的核心域(Core Domain)。提炼过程的副产品将是包含了领域中其他部分的普通子域(Generic Subdomain)。
在设计一个大型系统时,有那么多分布式组件,所有的都是那么复杂而且绝对必须不出差错,而领域模型的基本内容,也就是真正的业务资产(business asset),却变得模糊不清和不受重视。
当我们处理一个大的模型时,应该尝试将基本概念与普通概念分离开来。一开始我们曾经举过一个关于飞机空中交通监控系统的例子。我们说飞行计划包含了飞机必须遵循的设计好的路线。在这个系统里,路线好像是一个无时不在的概念。实际上,这个概念是一个普通的概念,不是一个基本概念。在很多领域里都会用到路线概念,可以设计一个普通的模型来描述它。空中交通监控的基本内容是在其他地方。监控系统知道飞机应该遵循的路线,但是它还会接收来自对飞行中飞机做跟踪的雷达网络的输入。这个数据显示飞机真正遵循的飞行路线,而它经常和预先描述好的路线有些偏差。系统必须基于飞机当前的飞行参数、飞机的特性和天气情况来计算飞行轨道。这一轨道是一个能完全描述飞机当前飞行路线的四维路线,它可能会在接下来的两分钟里被计算出来,也可能是几十分钟,或者是两小时。每一个计算都有助于决策制定过程。计算飞机轨道的目的是看看是否有可能与其他飞机的飞行路线产生交叉。在机场附近,在飞机起飞或者降落时,有很多飞机在空中盘旋或者移动。如果一个飞机偏离了它的计划路线,很有可能会和其他飞机相撞。空中交通监控系统会计算飞机的轨道,在路线出现交叉的可能性时发出警报。空中交通控制人员需要快速做出决策,指挥飞机改变飞行路线,以避免相撞发生。飞机之间相距越远,计算轨道的时间就越长,做出反应的时间也越长。根据已有的数据合成飞机轨道的模块才是这个业务系统的核心。可以将这个模块标记为核心域。 而路线模型应该作为一个普通域。
系统的核心域是什么,取决于我们如何看待系统。一个简单的路线系统会将路线和与它相关的概念看作核心域,而空中交通监控系统却将路线看作普通子域。一个应用的核心域有可能会变成另一个应用的普通子域。正确标识核心域,以及它与模型其他部分之间的关系是非常重要的。
对模型做提炼,找到核心域,提供一种手段将核心域与支持模型和代码(mass of supporting model and code)容易地区分开来。对最有价值的和专业的概念加以强调。尽量使核心域小一些。
将你们的顶尖人才投入到核心域上,并且开展所需的招聘。在核心域上付出努力, 找到一种深层的模型,开发一个足够灵活的设计以实现系统的远景。根据其他部分如何支持提炼出的核心域,判断这些部分投资价值。
配最好的开发人员去承担实现核心域的任务是很重要的。开发人员经常沉溺于技术,喜欢学习最好的和最新的编程语言,相对于业务逻辑他们更关注基础设施。一个领域的业务逻辑在他们看来是令人厌倦的,也看不到什么回报。
但是领域的业务逻辑是业务的核心所在,这个核心的设计和实现中如果存在错误,将会导致整个项目遭到抛弃。如果核心业务逻辑不起作用,所有技术上的亮点都等于做了无用功。
核心域的创建无法在一步完成。这需要有一个改进的过程,在核心越来越清晰之前必须开展持续的重构。我们需要强化将核心作为设计工作的中心的地位,并划定它的边界。我们还需要根据其与新核心的关系来重新考虑模型的其他元素,也许它们也需要被重构,一些功能也许需要改变等。
模型的某些部分会增加复杂度,对于捕获和交流专业知识没有帮助。任何题外的事物都会使核心域难于辨认和被理解。因为广为人知的普遍原则(general principles everyone knows),或者那些并非是你首要关注而是扮演支持角色的细节都会影响模型。 但是这些其他元素对系统功能和模型的完整表达来说依然是必要的。
标识出并非是项目动机的的相关子域。找出那些子域的普通模型,并将它们放在分离的模块中。不要在这些模块中留下什么专业知识。
一旦它们被分离开,就将对它们做持续开发的优先级调得比核心域更低,而且注意:不要把你的核心开发人员分配到这些任务中(因为它们从中获取不了多少领域知识)。 另外要考虑现成的解决方案,或者那些已发布的针对普通子域的模型。
每个领域会用到其他领域所使用的概念。钱和与其相关的概念,例如货币和汇率, 可以被包扩在不同的系统里。图表是另外一个被广泛使用的概念,就它本身而言是非常复杂的,但是可以被用在很多应用中。
有下面几种方法可以实现普通子域:
1. 购买现成的解决方案。这个方法的好处是可以使用别人已经完成的全套解决方案。随之而来的是学习曲线的问题,而且这样的方案还会引入一些依赖。如果代码有很多 bug,你只得等待别人来解决。你还需要使用特定的编译器和类库版本。与自己实现的系统相比,将这样的方案与自己系统做集成也不是那么容易。
2. 外包。将设计和实现交给另外一个团队,有可能是其他公司的团队。这样做可以使你专注于核心域,不再承受处理另一个领域的负担。不便的地方是集成外包的代码。用来与子域通信的接口需要预先定义好,并且与其他团队进行沟通。
3. 已有模型。一个取巧的方案是使用一个已经创建的模型。市面上已经有一些关于分析模式的书,可以用来作为我们子域的灵感源。直接复制原有的模式不太现实,但确实有些模式只需要做少许改动就可以用了。
4. 自己实现。这个方案的好处是能够做到最好的集成,但这也意味着额外的付出, 包括维护的压力等。
返回首页