软件设计首要面对的挑战是如何应对复杂多变的业务问题。而对于业务中台来说,这个问题变得尤为突出。一方面,数字化时代,高度不确定并且快速变化的商业环境必然要求企业的业务也能够及时快速的响应,业务复杂度随之也越来越高;另一方面,业务中台作为企业级能力承载与共享的中台,它是要把大部分业务能力积累沉淀为上层应用能够共享复用的能力池的。如何识别与隔离业务中的变与不变,保持复杂度总体可控,并使业务中台架构如实地与业务保持演进,是业务中台构建的关键。
应对这个挑战的方法在10几年前已经被提出来。2003年,Eric Evans提出了名为领域驱动设计(Domain Driven Design)的领域建模方法,其基本思想是把我们对软件架构设计的关注点拉回到业务上,以业务领域驱动架构设计,从而达到解决控制软件复杂度并保持软件架构随业务演进的问题。但是,由于当时的业务复杂度远低于现在,技术上中心化的单体架构已经足以应对,这种先进的方法论并没有在业界引起足够的关注度。随着最近几年微服务架构已成为软件技术架构设计的事实标准,DDD重新进入业界的视野并得到广泛使用,其天然能够很好分离业务复杂度的优点使它成为微服务划分甚至业务中台架构设计的最佳方法。
领域驱动设计的理念
架构设计的理念是分层、分治。事实上,领域驱动设计的核心理念恰恰也是分层、分治。它是在用分层、分治的思想解决分层、分治的问题。它天然地要求我们在做架构设计时,必须时刻区分此刻所处的是问题域空间还是解决方案域空间。对于软件系统来说,业务及其对应的业务架构是软件要解决的问题,所以业务架构处于问题域空间;而软件系统自然就是业务上的解决方案,所以软件架构处于解决方案域空间。这是DDD的分治理念。同时,DDD区分战术设计层与战略设计层。战术设计层是微观视角,描述领域模型的细节;战略设计层是宏观视角,把控领域模型的总体设计。这是DDD的分层理念。通过分层、分治,DDD使我们能够在保持关注点分离的同时可以直达问题本质,从而平滑地从问题过渡到解决方案。
领域驱动设计的原则
识别与聚焦核心域
在探索问题域空间时,在战略层会得到关于按照业务范围区分的子域(Subdomain)。而其中,有些子域体现了企业在业务上的核心竞争力与价值所在,这些子域是企业的核心域,而其余的子域属于非核心域(也可进一步分为通用域和支撑域,但二者往往难以区分且区分的价值不大)。识别与聚焦核心域是领域驱动设计首要原则。这是另一个层面上的分治。子域的目的一方面是要给产品后续开发提供投资策略依据,另一方面由于子域属于问题域空间,子域的明确有助于定义清晰的问题边界,从而有助于解决方案的验证。从业务中台的角度出发,子域可对应于业务中台的中心,但此时的中心仍然处于问题域空间,并没有明确的解决方案。我们只是把问题识别出来了。传统上来说,DDD只对核心域建模,但对于业务中台来说,非核心域也存在建模的价值,因为非核心域的解决方案可发展为中台的基础中心。
通过协同共创迭代式探索
分治的理念必然要求整个建模的过程是协同共创的。问题域空间的子域探索需要业务人员与领域专家提供输入并主导。他们需要向技术人员解释清楚业务流程,场景与问题,确保他们的业务输入是正确而完备的。这是影响最后模型质量的关键。解决方案域的领域建模与架构设计需要架构师与技术人员依据业务的输入进行深入的讨论、思考和抽象,并且需要向业务人员解释清楚建模的依据,以及验证模型是否具有足够的能力支撑业务。而由于业务的多变性,对模型的探索也不是一蹴而就的,它是一个迭代演进的过程,是需要花费一定的成本来维持的。如果没有持之以恒的动力坚持,模型最终只会沦为僵尸文档。
使用统一语言
由于整个探索过程是一个持续协同共创的过程,协同的各方需要形成统一的语言才能互相沟通无碍。事实上,在持续的协同过程中,协同的各方自然而然就会消除沟通中存在歧义的词汇,形成一套相互理解的语言,并最终反映到模型中。不用纠结什么是统一语言,也不用刻意去形成它,只要坚持协同共创的模式,你最终得到的模型就是最好的统一语言。
几个基本概念
实体(Entity)与值对象(Value Object)
这是DDD中最容易产生争议的术语。他们都是业务对象,实体拥有业务含义的全局唯一标识符,拥有生命周期,且标识符在经历软件系统的各种状态,生命周期后仍能保持一致;值对象一般来说不需要有业务含义的唯一标识符,因为我们并不关心它是谁。两个值对象,如果他们的值一致,我们就认为他们其实是同一个值对象的。但是两个实体,即使它们拥有的值完全相等,我们也不能认为他们是同一个实体。但是,复杂的地方在于,一个对象在一个场景下可能是实体,在另一个场景下可能就是值对象。例如纸币,在货币交换的场景下,我们一般不关心这张100块与另一张100块的区别,这个时候纸币就是值对象,因为对于购买者来说,同是100块钱的购买力是一样的;但是在货币收藏的场景下,不同的100块可能就意味着不一样的价值。这个时候,纸币上的编号变成了具有业务含义的唯一标识符。这个场景下的纸币就是实体。所以脱离了业务场景区分实体与值对象是不严谨的。
聚合(Aggregate)
聚合是一组可以被视为单个领域对象的集合。聚合通过定义清晰的所属关系和边界,并避免错综复杂的对象关系网来实现模型的内聚。聚合内保证业务不变性,聚合间耦合松散。通常来说,聚合就是由实体和值对象组成的集合。每个聚合都有一个聚合根,外部只能通过聚合根更新聚合内的对象。实际上,聚合根也是一个实体。
想象一辆处于正常驾驶场景下的汽车,可以认为它的转向系统就是一个典型的聚合。它由方向盘与轮胎等实体组成。在转向时,驾驶员旋转方向盘,实现转向。这个时候,方向盘就是转向系统的聚合根,驾驶员通过它来更新轮胎状态。
限界上下文(Bounded Context)
限界上下文可以分为限界和上下文两个词来理解。限界是指一个界限,具体的一个范围;上下文是指场景,环境。所以限界上下文是在某个场景或环境下的业务边界。该边界就是业务上的职责。可以认为,限界上下文是比聚合层次更高的抽象,是战略层的抽象。它封装了聚合,并处于解决方案空间,解决子域的业务问题。
可以认为汽车的加速系统和制动系统是典型的两个限界上下文,各自封装了以油门和刹车为聚合根的聚合,共同支撑起汽车速度控制这个子域。
事件风暴 – 充满乐趣的DDD实施方法
对于如何实施协同共创的DDD,有很多不同的探索。2015年11月,其中一种方法进入了ThoughtWorks的技术雷达,它就是由Alberto Brandolini发明的事件风暴(Event Storming)。顾名思义,它是关于事件的头脑风暴。实际上,它是一场充满乐趣的协同共创工作坊。在一个四面贴满大白纸的房间里面,业务人员与技术人员通力协作,共同探索复杂的业务领域。它会在大白纸上贴满不同颜色的便利贴,看上去就像是用便利贴“糊墙”,所以事件风暴的实施者又被戏称为“糊墙师”。
领域事件与架构设计的本质
事件风暴中的事件指的是领域事件。一个系统,如果要给外部的操作做出响应,或者本身实施了一个行为导致了一个后果,那么这种响应或者行为的后果,往往会产生一些痕迹。很多时候,这些痕迹会以数据的形式保留下来。从系统观察者的角度去看,产生这些数据或者痕迹的系统的响应或者行为后果就是领域事件,而外部触发领域事件的动作被成为决策。领域事件是系统行为的后果。按照时间相继发生的领域事件反映了系统的一系列行为。这些行为,从系统观察者的角度出发,就是系统应该具有的功能,而从系统本身出发,就反映了系统自身应该具有的能力。
功能或者能力,无论叫什么,它都来自于系统,必然会有具体的载体。而事件风暴微妙的地方在于,它不但按照时间线把系统的行为剖析开了,而且它把承载这个行为的载体也暴露在我们眼前。这些载体,以及它们空间上的关系,专业的术语就是我们所说的领域模型,或者,叫架构。这是事件风暴的理论基础。事实上,事件风暴之所以被证明可以快速探索复杂业务,不是因为它高效,也不是因为它充满乐趣,而是因为它触达了架构设计的本质——时空转换。本质上,体系结构,或者说架构,是空域的;而事件以及触发事件的动作,是时域的,伴随一个事件的发生会有时间的流逝。架构设计就是要设计系统空域结构,并且一旦赋予它时间维度后,它能实现预期的功能产生事件。面对简单的业务,架构师或许可以跳过时空转换很快就在头脑里面形成空间结构;一旦业务越复杂,尤其是要构建像业务中台这样庞大的系统,时域上挂载的事件越多(交互越多),人类的大脑不可能承载如此大的复杂度,这个时候就越应该按照类似于事件风暴的形式开展架构设计。
业务中台事件风暴步骤
通过事件风暴对业务中台建模的过程与一般的事件风暴过程大同小异。整个过程其实就是在战略层与战术层来回探索。稍微有点不一样的地方在于,识别出来的子域很可能会对应于业务中台中的中心,限界上下文变成了业务中心中的解决方案。最终,我们会拼凑完成出基于DDD的业务中台架构推演全景图。中台架构终于从问题域过渡到解决方案域。
如何实施
来到这里,理论上来说领域建模的过程就算完成了。我们已经得到业务中台逻辑上的系统架构,也就是领域模型,接下来是到了考虑如何落地实施的时候的。模型如何转变成代码,这也是很多人最关心的阶段,因为模型只有真正变成可以运行的代码了,才能体现其价值。
临门一脚 - 从限界上下文到微服务,从应用架构到技术架构
当我们遵循DDD的思路推演出业务中台的限界上下文时,我们得到的是中台的逻辑应用架构。在整个过程中,我们完全是从业务的视角去考虑,并且刻意弱化技术上的考虑以及具体落地时的约束。现在是到了要考虑实际情况的时候了。从业务边界的角度来说,一个限界上下文可以作为一个微服务。这也是我们辛辛苦苦识别出限界上下文的目的– 划分微服务。但是,限界上下文是逻辑的,而微服务最终是要部署的。应用架构是纸面上的,技术架构是要最终落地的。这意味着,从限界上下文到微服务,往往还要考虑一些实际约束。以下是一写可供参考的约束条件:
1. 需求变化频率– 需求变化相差比较大的两部分考虑划分为不同的微服务
2. 安全性– 有特殊安全性要求的部分考虑划分为单独的微服务
3. 性能– 对性能要求相差比较大的两部分考虑划分为不同的微服务
4. 技术异构– 技术栈相差比较大的两部分考虑划分为不同的微服务
5. 组织结构– 不同团队开发的两部分考虑划分为不同的微服务
从决策到接口
在限界上下文地图中,我们应该能够识别出哪些聚合具有哪些决策。聚合的决策可以给我们识别接口提供参考。那些外部角色或者外部限界上下文触发的决策可识别为微服务的接口,而那些限界上下文内的决策可识别为微服务内的方法。(应有图)当然,领域模型不能把所有的接口或者方法都识别出来,因为它不是详细设计。但是它应该能够把那些最核心的接口和方法识别出来,这些核心的接口和方法体现了模型之间的互操作性,是模型能够支撑业务的根本。
从领域模型到分层架构
领域模型的核心代码理应完全反映业务逻辑,它不应该被依赖污染。但是核心业务代码不可避免地要依赖于诸如数据库等基础设施。如何剪断这种直接的依赖关系呢?“计算机领域的任何问题都可以通过增加一个间接的中间层来解决。”这里再一次印证了这句话的正确性。具体的实现方式是领域层代码只保留基础设施的接口,接口的特定实现放在基础设施层。如此,双方都依赖于接口,领域模型不会直接感知到基础设施层的变更,二者脱耦了。同样的,对领域模型的外部调用(如Restful请求或消息)也不应该直接触达领域模型,而是应该由应用层服务隔离开。这也就形成了所谓的整洁架构。
最后的最后,从DDD到TDD
前面提到,DDD不能让你得到关于代码的详细设计,这不是它的职责所在。它顶多让你知道你应该有哪些微服务,每个微服务里面有哪些核心类(由聚合、实体、值对象识别得出),每个核心类有哪些核心方法(由限界上线文内决策得出),每个微服务有哪些核心接口(由限界上下文间决策得出)。但是它不能让你知道每一个具体的方法的每一行怎么写。不过,这可以通过TDD做到。TDD不是单纯的测试方法,本质上它是一种代码级微观设计方法。对单元测试的设计编写过程实际上是对代码的设计过程。你对代码的设计思路完全反映在你编写的单元测试中。如果真的纠结于必须得到代码级设计,不妨真的静下心来尝试实践一下TDD。
不忘初心
我们在构建业务中台的道路上已经走得足够远了。如果能坚持走到这里,我们的中台应该已经有一个雏形。它不是很完美,但能支撑一些业务,需要我们不断完善。但是,道路走远了,初心就容易被忘掉。共享与复用,这是我们建设中台的目的。它共享复用的本质注定它的构建一定是一个不断打磨沉淀的过程。中台建设从来都不是一蹴而就的,对其领域模型的维护也是一个需要长期投入的过程。不过,不需要再做全量的建模工作了,只需要针对新的需求做轻量级的事件风暴,然后分析其对现有模型的影响。如果不能把建模工作坚持下去,中台的架构就会慢慢腐化,最终和它的前辈——那些遗留系统一样,走向被重写,或被丢弃的命运。