2.1 领域驱动设计
领域驱动设计是由Eric Evans最早提出的综合软件系统分析和设计的面向对象建模方法,如今已经发展为一种针对大型复杂系统的领域建模与分析方法。它完全改变了传统软件开发工程师针对数据库进行的建模方法,从而将要解决的业务概念和业务规则转换为软件系统中的类型及类型的属性与行为,通过合理运用面向对象的封装、继承、多态等设计要素,降低或隐藏整个系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。
领域驱动设计本质上是一种方法论(Methodology)。它建立了以领域为核心驱动力的设计体系,因而具有一定的开放性。在这个体系中,你可以使用不限于领域驱动设计提出的任何一种方法来解决这些问题。例如,我们可以使用用例(Use Case)、测试驱动开发(TDD)、用户故事(User Story)帮助我们对领域建立模型;我们可以引入整洁架构思想及六边形架构,帮助我们建立一个层次分明、结构清晰的系统架构;我们可以引入函数式编程思想,利用纯函数与抽象代数结构的不变性及函数的组合性来表达领域模型。这些实践方法与模型已经超越了Eric Evans最初提出的领域驱动设计范畴,但在体系上却是一脉相承的。这也是为什么在领域驱动设计社区,能够不断诞生诸如CQRS模式、事件溯源(Event Sourcing)模式与事件风暴(Event Storming)等新概念的原因。
2.1.1 领域驱动设计概览
作为一种软件设计方法论,领域驱动设计贯穿了整个软件开发的生命周期,包括对需求的分析、建模、架构和设计,甚至最终的编码实现、测试与重构。它尤为强调领域模型的重要性,并提倡通过模型驱动设计来保障领域模型与程序设计的一致。领域驱动设计认为:开发团队应该从业务需求中提炼出统一语言(Ubiquitous Language),再基于统一语言建立领域模型;这个领域模型会指导程序设计及编码实现;最后,又通过重构来发现隐式概念,并运用设计模式改进设计与开发质量。
然而,当我们面对规模庞大、业务复杂的软件系统时,如果从一开始就要深入每个功能点进行需求分析和领域建模,则可能会面临高复杂度的挑战。这是因为对于一个复杂的软件系统而言,我们要处理的问题域实在太庞大了。在为问题域寻求解决方案时,需要从宏观层次划分不同业务关注点的子领域,再深入子领域中从微观层次对领域进行建模。宏观层次是战略的层面,微观层次是战术的层面,因此,领域驱动设计将整个设计过程划分为两个阶段:战略设计阶段与战术设计阶段。
1.战略设计阶段
领域驱动设计的战略设计阶段是从两个方面来考量的。
● 问题域方面:引入核心领域(Core Domain)与子领域(SubDomain)来划分问题域,然后通过限界上下文(Bounded Context)和上下文映射(Context Map)给出这些问题域的解决方案,在减小领域模型规模的同时,维持领域概念的一致性。
● 架构方面:通过分层架构来隔离关注点,尤其是将领域独立出来,可以更利于领域模型的单一性与稳定性;引入六边形架构清晰地表达领域与技术基础设施的边界;CQRS模式则分离了查询场景和命令场景,针对不同场景选择使用同步或异步操作,提高架构的低延迟性与高并发能力。
Eric Evans提出战略设计的初衷是要保持模型的完整性。限界上下文的边界可以保护上下文内部和其他上下文之间的领域概念互不冲突。然而,如果我们将领域驱动设计的战略设计模式引入架构过程,就会发现限界上下文不仅限于对领域模型的控制,还在于分离关注点之后,使得整个上下文可以成为独立部署的设计单元,这就是“微服务”的概念,上下文映射的诸多模式则对应了微服务之间的协作。因此在战略设计阶段,微服务扩展了领域驱动设计的内容,反过来领域驱动设计又能够保证良好的微服务设计。
一旦确立了限界上下文的边界,尤其是作为物理边界,分层架构就不再针对整个软件系统,而是针对粒度更小的限界上下文。此时,限界上下文定义了技术实现的边界,对当前上下文的领域与技术实现进行了封装,我们只需要关心对外暴露的接口与集成方式即可。显然,限界上下文是整个战略设计阶段的核心要素。
2.战术设计阶段
整个软件系统被分解为多个限界上下文后,我们就可以分而治之,对每个限界上下文进行战术设计。领域驱动设计提倡用领域模型来表达复杂的领域知识,构成模型的要素包括:
● 值对象(Value Object);
● 实体(Entity);
● 领域服务(Domain Service);
● 领域事件(Domain Event);
● 资源库(Repository);
● 工厂(Factory);
● 聚合(Aggregate);
● 应用服务(Application Service)。
Eric Evans通过图2-1勾勒了战术设计诸要素之间的关系。
图2-1
领域驱动设计围绕领域模型进行设计,通过分层架构(Layered Architecture)将领域独立出来。表示领域模型的对象包括实体、值对象、领域服务和领域事件。领域逻辑都应该封装在这些对象中。这一严格的设计原则可以避免业务逻辑渗透到领域层之外,导致技术实现与业务逻辑的混淆。
聚合是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。在聚合中,至少包含一个实体,且只有实体才能作为聚合根(Aggregate Root)。注意,在领域驱动设计中,聚合代表的是边界概念,而非领域概念。极端情况下,一个聚合可能有且只有一个实体。
工厂和资源库都是对领域对象生命周期的管理。前者负责领域对象的创建,往往用于封装复杂或可能变化的创建逻辑。后者则负责从存放资源的位置(数据库、内存或其他Web资源)获取、添加、删除或修改领域对象。领域模型中的资源库不应该暴露访问领域对象的技术实现细节。
3.演进的领域驱动设计过程
战略设计会控制和分解战术设计的边界与粒度,战术设计则以实证角度验证领域模型的有效性、完整性与一致性,进而以演进的方式对之前的战略设计阶段进行迭代,从而形成一种螺旋式上升的迭代设计过程,如图2-2所示。
图2-2
面对客户的业务需求,由领域专家与开发团队展开充分的交流,经过需求分析与知识提炼,获得清晰的问题域。通过对问题域进行分析和建模,识别限界上下文,利用它划分相对独立的领域,再通过上下文映射建立它们之间的关系,辅以分层架构与六边形架构划分系统的逻辑边界与物理边界,界定领域与技术之间的界限。之后,进入战术设计阶段,深入限界上下文内对领域进行建模,并以领域模型指导程序设计与编码实现。在实现过程中,若发现领域模型存在重复、错位或缺失,就需要对已有模型进行重构,甚至重新划分限界上下文。
两个不同阶段的设计目标是保持一致的,它们是一个连贯的过程,彼此之间又相互指导与规范,并最终保证一个有效的领域模型和一个富有表达力的实现同时演进。
2.1.2 问题域与解决方案域
领域驱动设计的整个过程,其实就是从问题域到解决方案域的过程。问题域属于需求分析阶段,重点是明确这个系统要解决什么问题,能够提供什么价值,也就是关注系统的What与Why。解决方案域属于系统设计阶段,针对识别出来的问题域,寻求合理的解决方案,也就是关注系统的How。在领域驱动设计中,核心领域(Core Domain)与子领域(Sub Domain)属于问题域的范畴,限界上下文(Bounded Context)则属于解决方案域的范畴,它们之间的关系如图2-3所示。
图2-3
很多人总是困惑于核心领域/子领域与限界上下文之间的关系:一对一、一对多,或者多对多?然而就图2-3所示,由于二者出现在不同阶段,关注的重心也不尽相同,因此准确地说,它们之间并没有所谓的映射关系。前者关注于系统的价值与功能,因而对它们的识别,只限于从业务上对它们的分解。之所以要区分核心领域与子领域,不过是为后续的解决方案域提供实现成本的考量。对于核心领域,我们应付出更多的开发成本,组建更好的团队为其建立稳定正确的领域模型;至于子领域,就可以降低设计与开发要求,甚至可以引入外包团队对其进行开发,或者购买提供通用功能的组件或服务。
从问题域到解决方案域,实际上就是从需求分析到设计的过程,也是我们逐步识别限界上下文的过程。限界上下文是解决方案域的架构基石。一些开发人员在接触领域驱动设计时,常常会疑惑限界上下文究竟是什么?他们往往会结合自身的开发经验,想要将限界上下文视为模块、组件、包或服务。这实际违背了Eric Evans引入限界上下文的初衷。在从问题域推演至解决方案域时,限界上下文仅仅是一种业务边界的划分,在这个时候,根本就不应该考虑它究竟是模块、组件、包还是服务,因为这会导致技术决策对领域分析的干扰。只有在初步确定了限界上下文之后,进入实现阶段时,才开始考虑它究竟该映射为模块、组件、包还是服务,这一决策会直接影响整个系统的架构。
2.1.3 限界上下文
领域驱动设计对微服务设计的辅助(或者说推动)作用,主要体现在战略设计阶段。其中,限界上下文(Bounded Context)扮演了最关键的角色,是推动整个微服务设计的“核心引擎”。那么,什么才是限界上下文呢?
让我们来读一个句子:
wǒ yǒu kuài dì
到底是什么意思?
我们能确定到底是哪个意思呢?确定不了!! !我们必须结合说话人的语气与语境理解。例如:
● wǒ yǒu kuài dì, zǔ shàng liú xià lái de→我有块地,祖上留下来的。
● wǒ yǒu kuài dì, shùn fēng de→我有快递,顺丰的。
在日常对话中,说话的语气与语境就是帮助我们理解对话含义的上下文(Context)。当我们在理解系统的领域需求时,同样需要借助这样的上下文。而限界上下文的含义就是用一个清晰可见的边界(Bounded)将这个上下文勾勒出来。如此就能在自己的边界内维持领域模型的一致性与完整性。Eric Evans用细胞来形容限界上下文,因为“细胞之所以能够存在,是因为细胞膜限定了什么在细胞内、什么在细胞外,并且确定了什么物质可以通过细胞膜。”这里的细胞代表上下文,而细胞膜代表了包裹上下文的边界。
分析限界上下文的本质,就是对边界的控制。观察角度的不同,限界上下文划定的边界也有所不同。大体可以分为如下三个方面。
● 领域逻辑层面:限界上下文确定了领域模型的业务边界,维护了模型的完整性与一致性,从而降低系统的业务复杂度。
● 团队合作层面:限界上下文确定了开发团队的工作边界,建立了团队之间的合作模式,避免团队之间的沟通变得混乱,从而降低系统的管理复杂度。
● 技术实现层面:限界上下文确定了系统架构的应用边界,保证了系统层和上下文领域层各自的一致性,建立了上下文之间的集成方式,从而降低了系统的技术复杂度。
这三种边界体现了限界上下文对不同边界的控制力。业务边界是对领域模型的控制,工作边界是对开发协作的控制,应用边界是对技术风险的控制。引入限界上下文的目的,其实不在于如何划分边界,而在于如何控制边界。
1.业务边界
限界上下文首先分离了业务边界,用以约束不同上下文的领域模型。这种对领域模型的划分符合架构设计的基本原则,即从更加宏观和抽象的层次去分析问题域,如此既可以避免分析者迷失在纷繁复杂的业务细节知识中,又可以保证领域概念在自己的上下文中的一致性与完整性。
例如在电商系统中,产品实体Product在不同的限界上下文具有不同的含义,关注的属性与行为也不尽相同。在采购上下文中,需要关注产品的进价、最小起订量与供货周期;在市场上下文中,则关心产品的品质、售价,以及用于促销的精美图片和销售类型;在仓储上下文中,仓库工作人员更关心产品放在仓库的哪个位置,产品的重量与体积,是否是易碎品,以及订购产品的数量;在推荐上下文中,系统关注的是产品的类别、销量、收藏数、正面评价数、负面评价数。在引入限界上下文之后,每个限界上下文都拥有自己的Product领域模型,该领域模型仅仅满足符合当前上下文需要的产品唯一表示,如图2-4所示。这其实是领域驱动设计引入限界上下文的主要目的。
图2-4
虽然不同的限界上下文都存在相同的Product领域模型,但由于有了限界上下文作为边界,使得我们在理解领域模型时,是基于当前所在的上下文作为概念语境的。这样的设计既保证了限界上下文之间的松散耦合,又能够维持限界上下文各自领域模型的一致性。此时的限界上下文成为保障领域模型不受污染的边界屏障。
2.工作边界
结合领域驱动设计的需求,我们应该考虑在保持团队规模足够小的前提下,按照软件的特性(feature)而非组件(component)来组织软件开发团队,这就是所谓“特性团队”与“组件团队”之分。
传统的“组件团队”强调的是专业技能与功能重用,例如熟练掌握数据库开发技能的成员组建一个数据库团队,深谙前端框架的成员组建一个前端开发团队。这种遵循“专业的事情交给专业的人去做”原则的团队组建模式,可以更好地发挥每个人的技能特长,然而牺牲的却是团队成员业务知识的缺失,对客户价值的漠视。这种团队组建模式也加大了团队之间的沟通成本,导致系统的整体功能无法持续和频繁地集成。例如,由于业务变更需要针对该业务特性修改用户描述的一个字段,就需要从数据存储开始考虑到业务模块、服务功能,最后到前端设计。一个小小的修改就需要横跨多个组件团队,这种交流的浪费是多么不必要啊!
特性团队可以规避这些不必要的沟通,消除知识壁垒。所谓“特性团队”,就是一个端对端的开发垂直细分领域的跨职能团队,它将需求分析、架构设计、开发测试等多个角色糅合在一起,专注于领域逻辑,实现该领域特性的完整的端对端开发。特性团队专注的领域特性,与领域驱动设计中限界上下文对应的领域是相对应的。当我们确定了限界上下文时,等同于确定了特性团队的工作边界,确定了限界上下文之间的关系,也就意味着确定了特性团队之间的合作模式;反之亦然。之所以如此,则是因为康威定律(Conway's Law)为我们提供了理论支持。
康威定律认为:“任何组织在设计一套系统(广义概念上的系统)时,所交付的设计方案在结构上都与该组织的沟通结构保持一致。”在康威定律中起到关键杠杆作用的是沟通成本。如果同一个限界上下文的工作交给了两个不同的团队分工完成,为了合力解决问题,就必然需要这两个团队进行密切的沟通。然而,团队间的沟通成本显然要高于团队内的沟通成本,为了降低日趋增高的成本,就需要重新划分团队。反过来,如果让同一个团队分头做两个限界上下文的工作,则会因为工作的弱相关性带来自然而然的团队隔离。
3.应用边界
架构师在划分限界上下文时,不能只满足于业务边界的确立,还得从控制技术复杂度的角度考虑技术实现,从而做出对系统质量属性的响应与承诺。这种技术因素影响限界上下文划分的例子可谓不胜枚举。
高并发
一个外卖系统的订单业务与门店、支付等领域存在业务相关性,然而考虑外卖业务的特殊性,它往往会在某个特定的时间段(如中午11时到13时)达到订单量的高峰值。系统面临高并发压力,同时还需要快速地处理每一笔外卖订单,与电商系统的订单业务不同,外卖订单的特点是周期短,必须在规定较短的时间内走完下订单、支付、门店接单、配送等整个流程。如果我们将订单业务从整个系统中剥离出来,作为一个单独的限界上下文对其进行设计,就可以从物理架构上保证它的独立性,在资源分配上做到高优先级地扩展,在针对领域进行设计时,尽可能引入异步化与并行化,提高服务的响应能力。
功能重用
对于一个面向企业雇员的国际报税系统,报税业务、旅游业务与Visa业务都需要账户功能的支撑。系统对用户的注册与登录有较为复杂的业务处理流程。对于一个新用户而言,系统会向客户企业的雇员发送邀请信,收到邀请信的用户只有通过问题验证才能成为合法的注册用户,否则该用户的账户就会被锁定,称之为Registration Locked。在用户使用期间,若违背了系统要求的验证条件,则可能会根据不同的条件锁定账户,分别称之为Soft Locked和Hard Locked。只有用户提供了可以证明其合法身份的材料,其账户才能被解锁。
账户管理并非系统的核心领域,但与账户相关的业务逻辑却相对复杂。从功能重用的角度考虑,我们应该将账户管理作为一个单独的限界上下文,以满足不同核心领域对这一功能的重用,避免了重复开发和重复代码。
实时性
在电商系统中,商品自然是核心,而价格(Price)则是商品概念的一个重要属性。倘若仅从业务的角度考虑,在进行领域建模时,价格仅仅是一个普通的领域值对象。倘若该电商系统的商品数量达到数十亿种,每天获取商品信息的调用量在峰值达到数亿乃至数百亿次时,价格就不再是业务问题,而变成了技术问题。对价格的每一次变更都需要及时同步,真实地反馈给电商客户。
为了保证这种在高并发情况下的实时性,我们就需要专门针对价格领域提供特定的技术方案。例如通过读写分离、引入Redis缓存、异步数据同步等设计方法。此时,价格领域将作为一个独立的限界上下文,形成自己与众不同的架构方案。同时,为价格限界上下文提供专门的资源,并在服务设计上保证无状态,从而满足快速扩容的架构约束。
第三方服务集成
一个电商系统需要支持多种常见的支付渠道,如微信、支付宝、中国银联及各大主要银行的支付。买家在购买商品及进行退货业务时,可以选择适合自己的支付渠道完成支付。电商系统需要与这些第三方支付系统进行集成。不同的支付系统公开的API并不相同,安全、加密及支付流程对支付的要求也不相同。
在技术实现上,一方面我们希望为支付服务的客户端提供完全统一的支付接口,以保证调用上的便利性与一致性;另一方面我们希望能解除第三方支付服务与电商系统内部模块之间的耦合,避免引起“供应商锁定(Vender Lock)”,也能更好地应对第三方支付服务的变化。因此,我们需要将这种集成划分为一个单独的限界上下文。
遗留系统
当我们在运用领域驱动设计对北美医疗内容管理系统提出的新需求进行设计与开发时,这个系统的已有功能已经运行了数年时间。我们的任务是在现有系统中增加一个全新的Find &Replace模块,其目的是为系统中的医疗内容提供针对医疗术语、药品及药品成分的查询与替换。这个系统已经定义了自己的领域模型。这些领域模型与新增模块的领域有相似之处。但是,为了避免已有模型对新开发模块的影响,我们应该将这些已有功能视为具有技术债的遗留系统,并将该遗留系统整体视为一个限界上下文。
通过这个遗留系统限界上下文的边界保护,就可以避免我们在开发过程中陷入遗留系统庞大代码库的泥沼。由于新增需求与原有系统在业务上存在交叉功能,因而可能失去了部分代码的重用机会,却能让我们甩开遗留系统的束缚,放开双手运用领域驱动设计的思想建立自己的领域模型与架构。只有在需要调用遗留系统的时候,作为调用者站在遗留系统限界上下文之外,去思考我们需要的服务,然后酌情地考虑模型对象之间的转换及服务接口的提取。
如上的诸多案例都是从技术层面而非业务层面为系统划分了应用边界,这种边界是由限界上下文完成的,通过它形成了对技术实现的隔离,避免不同的技术方案选择互相干扰导致架构的混乱。
综上所述,限界上下文是“分而治之”架构原则的体现,我们引入它的目的其实为了控制软件的复杂度。它并非某种固定的设计单元,例如模块、服务或组件,在识别限界上下文时我们甚至要忘记这些概念,将它看作一个由业务进行驱动的抽象单元,并通过它帮助我们做出高内聚低耦合的设计。一旦确定了限界上下文,我们再来思考它的边界及它们之间的协作关系,才需要进一步确认它究竟是模块、服务还是组件。
2.1.4 上下文映射
领域驱动设计通过上下文映射(Context Map)来表达限界上下文之间的协作关系。上下文映射是一种设计手段,Eric Evans总结了诸如共享内核(Shared Kernel)、防腐层(Anticorruption Layer)、开放主机服务(Open Host Service)等多种模式。由于上下文映射本质上是与限界上下文一脉相承的,所以要掌握这些协作模式,就应该从限界上下文的角度进行理解,着眼点还是在于“边界”。领域驱动设计认为:上下文映射是用于将限界上下文边界变得更清晰的重要工具。所以当我们正在为一些限界上下文的边界划分左右为难时,不妨先放一放,在定下初步的限界上下文后,通过绘制上下文映射来检验,或许会有意外收获。
两个限界上下文之间的关系是有方向的。领域驱动设计使用两个专门的术语表述它们:“上游(upstream)”和“下游(downstream)”。在上下文映射图中,以U代表上游、D代表下游。理解它们之间的关系,正如理解该术语隐喻的河流,自然是上游产生的变化会影响下游,反之则不然。故而从上游到下游的关系方向,代表了影响产生的作用力。影响作用力的方向与程序员惯常理解的依赖方向恰恰相反,上游影响了下游,意味着下游依赖于上游。二者之间的关系如图2-5所示。
图2-5
为了将上下文映射运用到领域驱动的战略设计阶段,Eric Evans总结了常用的上下文映射模式。为了更好地理解这些模式,结合限界上下文对边界的控制力,再根据这些模式的本质,我将这些上下文映射模式分为了两大类:团队协作模式与通信集成模式。前者对应的其实是团队合作的工作边界,后者则从应用边界的角度分析了限界上下文之间该如何进行通信才能提升设计质量。针对通信集成模式,结合领域驱动设计社区的技术发展,在原有上下文映射模式基础上,增加了发布/订阅事件模式。
1.上下文映射的团队协作模式
领域驱动设计根据团队协作的方式与紧密程度,定义了五种团队协作模式。
● 合作关系(Partnership):合作关系代表了工作在两个限界上下文之间的团队存在一种一起成功或一起失败的“同生共死”关系。这种关系代表的固然是良好的合作,却也说明二者可能存在强耦合关系,甚至是糟糕的双向依赖。对于限界上下文的边界而言,即使是逻辑边界,出现双向依赖也是不可饶恕的错误。倘若我们视限界上下文为微服务,则这种“确保这些功能在同一个发布中完成”的要求,无疑抵消了许多微服务带来的好处,负面影响不言而喻。
● 共享内核(Shared Kernel):共享内核是两个或多个团队都同意共享的一个子集。从设计层面看,共享内核是解除不必要依赖实现重用的重要手段。当我们发现属于共享内核的限界上下文后,需要确定它的团队归属。注意,共享内核仍然属于领域的一部分,它不是横切关注点,也不是公共的基础设施。分离出来的共享内核属于上游团队的职责,因而需要处理好它与下游团队的协作。
● 客户方-供应方开发(Customer-Supplier Development):正常情况下,这是团队合作中最常见的合作模式,体现的是上游(供应方)与下游(客户方)的合作关系。
这种合作需要两个团队共同协商:
◦ 下游团队对上游团队提出的领域需求;
◦ 上游团队提供的服务采用什么样的协议与调用方式;
◦ 下游团队针对上游服务的测试策略;
◦ 上游团队给下游团队承诺的交付日期;
◦ 当上游服务的协议或调用方式发生变更时,该如何控制变更。
● 遵奉者(Conformist):可以从两个角度来理解遵奉者模式,即需求的控制权与对领域模型的依赖。一个正常的客户方-供应方开发模式,是上游团队满足下游团队提出的领域需求;但当需求的控制权发生了逆转,由上游团队来决定是响应还是拒绝下游团队提出的请求时,所谓的“遵奉者”模式就产生了。从这个角度来看,我们可以将遵奉者模式视为一种“反模式”。遵奉者还有一层意思是下游限界上下文对上游限界上下文模型的追随。当我们选择对上游限界上下文的模型进行“追随”时,就意味着下游上下文可以直接重用上游上下文的模型,这样既减少了模型的重复定义,也可以减少两个限界上下文之间模型的转换成本,伴随而来的是下游限界上下文对上游产生的强依赖。在重用与解耦两者之间,我们需要做出设计权衡。
● 分离方式(Separate Ways):分离方式的合作模式就是指两个限界上下文之间没有哪怕一丁点儿的关系。这种“无关系”仍然是一种关系,而且是一种最好的关系。这意味着我们无须考虑它们之间的集成与依赖,它们可以独立变化而互相不产生影响。
2.上下文映射的通信集成模式
无论采用何种设计,限界上下文之间的协作都是不可避免的。应用边界的上下文映射模式以更加积极的态度应对这种不可避免的协作。从设计的角度讲,就是不遗余力地降低限界上下文之间的耦合关系。领域驱动设计根据限界上下文之间的协作方式提出了如下模式。
● 防腐层(Anti-Corruption Layer):防腐层其实是设计思想“间接”的一种体现。在架构层面,通过引入一个间接的层,就可以有效隔离限界上下文之间的耦合。这个间接的防腐层可以扮演“适配器”的角色、“调停者”的角色、“外观”的角色。防腐层往往属于下游限界上下文,用以隔绝上游限界上下文可能发生的变化。因为不管是遵奉者模式,还是客户方-供应方模式,下游团队终究可能面临不可掌控的上游变化。在防腐层中定义一个映射上游限界上下文的服务接口,就可以将掌控权控制在下游团队中,即使上游发生了变化,影响的也仅仅是防腐层中的单一变化点,只要防腐层的接口不变,下游限界上下文的其他实现就不会受到影响。在绘制上下文映射图时,我们往往用ACL缩写来代表防腐层。
● 开放主机服务(Open Host Service):设计开放主机服务,就是定义公开服务的协议,包括通信的方式、传递消息的格式(协议)。同时,也可视为是一种承诺,保证开放的服务不会轻易做出变化。开放主机服务常常与发布语言(Published Language)模式结合起来使用。当然,在定义这样的公开服务时,为了被更多调用者使用,需要力求语言的标准化。在分布式系统中,通常采用RPC(Protocol Buffer)、WebService或RESTful。若使用消息队列中间件,则需要事先定义消息的格式。在绘制上下文映射图时,我们往往用OHS缩写代表开放主机服务。
● 发布者-订阅者:一个限界上下文作为事件的发布者,另外的多个限界上下文作为事件的订阅者,二者的协作通过经由消息中间件进行传递的事件消息来完成。当确定了消息中间件后,发布方与订阅方唯一存在的耦合点就是事件,准确地说,是事件持有的数据。由于业务场景通常较为稳定,我们只要保证事件持有的业务数据尽可能满足业务场景即可。这时,发布者不需要知道究竟有哪些限界上下文需要订阅该事件,它只需要按照自己的心意,随着一个业务命令的完成发布事件即可。订阅者也不用关心它所订阅的事件究竟来自何方,它要么通过“拉”的方式主动去拉取存于消息中间件的事件消息,要么等着消息中间件将来自上游的事件消息根据事先设定的路由推送给它。通过消息中间件,发布者与订阅者完全隔离了。发布/订阅事件模式是低耦合的,但它有特定的适用范围,通常用于异步非实时的业务场景。当然,它的非阻塞特性也使得整个架构具有更强的响应能力,因而常用于业务相对复杂却没有同步要求的命令(Command)场景。这种协作模式往往用于事件驱动架构或CQRS(Command Query Responsibility Segregation,命令查询职责分离)架构模式中。
2.1.5 领域架构
将微服务的复杂度分为技术与业务两个维度,实际体现了软件复杂度的两个方面,即业务复杂度与技术复杂度。在一个软件系统中,此二者并非完全独立。正如两种不同性质的元素混合在一起,可能会产生未知的化合作用一般,技术与业务的混杂会让系统的复杂度变得不可预期,难以掌控。同时,技术的变化维度与业务的变化维度并不相同,产生变化的原因也不一致,倘若未能很好地界定二者之间的关系,系统架构缺乏清晰边界,会变得难以梳理。复杂度一旦增加,团队规模也将随之扩大,再加上严峻的交付周期、人员流动等诸多因素,就好似将各种不稳定的易燃易爆气体混合在一个不可逃逸的密闭容器中一般,随时都可能爆炸。图2-6说明了这种混合的复杂度。
图2-6
要避免两者的混淆,就需要确定业务逻辑与技术实现的边界,从而隔离各自的复杂度。理想状态下,我们应该保证业务规则与技术实现是正交的。无论是否实现为微服务架构风格,都需要遵循“关注点分离”的普适性架构原则。在领域驱动的战略设计阶段,通过引入分层架构与六边形架构来确保业务逻辑与技术实现的隔离。这是解决软件复杂度的第一步。
解决了混淆的复杂度,还需要解决业务与技术自身的复杂度。领域驱动设计的方法通过限界上下文来降低整个系统的规模,同时维护好各自的业务边界、工作边界和应用边界。这是系统的“分”。有分就有合,领域驱动设计利用上下文映射来标记限界上下文彼此之间的关系。取决于模式的不同,协作关系也有非常明显的区别。如果在限界上下文之间采用“事件”作为基础的通信单元,就可以改变传统协作的上下游关系。这种面向“事件”的架构风格则称之为事件驱动架构。
如果说分层是关注点的横向划分,限界上下文是业务领域的纵向划分,那么在面对资源的操作时,我们还可以基于查询与命令本质上的不同,通过引入CQRS架构来分离查询与命令,从而降低整个系统的技术复杂度。
1.分层架构
分层架构遵循了“关注点分离”原则,将属于业务逻辑的关注点放到领域层(Domain Layer)中,而将支撑业务逻辑的技术实现放到基础设施层(Infrastructure Layer)中。同时,领域驱动设计又颇具创见地引入了应用层(Application Layer)。应用层扮演了双重角色。一方面它作为业务逻辑的外观(Facade),暴露了能够体现业务用例的应用服务接口;另一方面它又是业务逻辑与技术实现的黏合剂,实现二者之间的协作。
图2-7展现的就是一个典型的领域驱动设计分层架构。应用层与领域层中的内容与业务逻辑有关,基础设施层的内容与技术实现有关,二者泾渭分明,然后汇合在应用层。应用层确定了业务逻辑与技术实现的边界,通过直接依赖或者依赖注入(Dependency Injection, DI)的方式将二者结合起来。
图2-7
2.六边形架构
由Cockburn提出的六边形架构则以“内外分离”的方式,更加清晰地勾勒出业务逻辑与技术实现的边界,且将业务逻辑放在了架构的核心位置。这种架构模式改变了我们观察系统架构的视角,如图2-8所示。
图2-8
体现业务逻辑的应用层与领域层处于六边形架构的内核,并通过内部的六边形边界与基础设施的模块隔离开。当我们在进行软件开发时,只要恪守架构上的六边形边界,就不会让技术实现的复杂度污染业务逻辑,保证了领域的整洁。边界还隔离了变化产生的影响。如果我们在领域层或应用层抽象了技术实现的接口,再通过依赖注入将控制的方向倒转,业务内核就会变得更加稳定,不会因为技术选型或其他决策的变化而导致领域代码的修改。
六边形架构又叫“端口-适配器”模式,图2-8中列出的REST Services、Repositories、Cache等模块都扮演了适配器的角色,这些适配器将通过端口与外部资源进行通信。端口与适配器部分都属于架构中技术实现的内容。
3.事件驱动架构
分析业务时,如果认为业务对象之间的协作关系以“事件”的方式进行,就会改变领域建模的本质,从围绕“领域概念”为核心的建模方式转换为围绕“状态迁移”为核心的建模方式。每个“事件”就是每次状态迁移时产生的事实(Fact)。倘若在架构设计时,皆以事件为媒介驱动架构的设计,并利用事件来解耦两个协作者之间的协作时,就可以认为是事件驱动架构(Event Driven Architecture, EDA)。如果限界上下文之间采用事件进行协作,则采用的上下文映射模式就是发布者/订阅者模式。结合前面提到的六边形架构,传递事件的就是六边形的端口,适配器负责发布/订阅事件。Vaughn Vernon在《实现领域驱动设计》一书中使用六边形架构形象地展现了这一架构风格,如图2-9所示。
图2-9
如果我们将限界上下文视为一个微服务,那么事件驱动架构会将编排(orchestration)方式的微服务协作改为协同(choreography)方式的微服务协作。比起服务的编排,这种围绕事件进行协同的方式会显著降低微服务之间的耦合度,同时还可以利用事件的异步本质来提高整个系统的响应速度。
4. CQRS架构
CQRS即Command Query Responsibility Seperation(命令查询职责分离),其设计思想来源于Mayer提出的CQS(Command Query Seperation)。之所以采用这种职责分离方式,是因为命令与查询操作有着诸多的差异。
● 副作用:查询操作不会造成数据的修改,是无副作用的;命令操作会修改数据,有副作用。
● 数据一致性:由于查询操作不会导致数据的变更,因而不会对数据一致性造成影响;命令操作则恰好相反。
● 协作方式:查询操作常常需要同步请求,实时返回结果,属于request-response协作模式;命令操作不要求一定返回结果,可以采用fire-and-forget模式。
● 复杂度:查询操作的业务逻辑通常比较简单,只需返回符合条件的数据即可,不会牵涉太多业务规则和逻辑;命令操作的业务逻辑通常比较复杂,牵涉诸多业务流程和业务规则的约束。
● 操作频率:发起查询操作的频率通常要远远高于命令操作。
既然查询操作与命令操作存在这么多的差异,就有必要分别对待它们,给出完全不同的架构设计方案,这就催生了如图2-10所示的CQRS架构模式。
图2-10
我们往往会将CQRS模式的C端与事件驱动架构结合起来。C端的核心概念是命令(Command)与事件(Event)。命令是系统中引起状态变化的活动,通常是一种命令语气,例如注册会议RegisterToConference。事件则描述了某种事件的发生,通常是命令的结果,例如订单确认事件OrderConfirmed。如果我们将所有事件都记录下来,就可以通过事件进行溯源,满足审计和业务追溯的需求。
命令和事件都有对应的处理器。它们具有一个共同的特征,即支持异步处理方式。这也是为何在CQRS架构中引入命令总线和事件总线的原因。在UI端执行命令请求,事实上就是将命令发送到命令总线中。设计时,可以运用设计模式中的命令模式,为不同的命令定义一个命令对象。在对命令对象进行命名时,应遵循统一语言的要求,使其能够体现业务的意图。命令总线更像是一个调停者(Mediator),在收到命令时,会将其路由到准确的命令处理器。事件的处理方式与命令相似,但它们代表的业务含义并不相同。针对事件,还有必要引入事件存储(Event Store),以支持事件溯源(Event Sourcing)。
在领域驱动设计中,我们通常需要引入实体、值对象及聚合来表达领域模型。在CQRS模式中,命令处理通常与聚合根对象进行通信。但对于查询操作而言,就可以简化领域逻辑的处理过程,甚至可以抛弃领域驱动战术设计的推荐做法,直接使用一个薄的数据访问层封装访问数据库的逻辑。这正是该模式具有实证主义的体现,即不拘泥于领域驱动设计模式,而是根据具体场景确定不同的实现模式。