一、架构设计是为了解决系统复杂性
整个软件技术发展的历史,其实就是一部与“复杂性”斗争的历史。架构也是为了应对软件系统复杂性而提出的一个解决方案,其主要目的是为了解决软件系统复杂性带来的问题。
这里包括两个名词:系统和复杂性,下面分别对其进行解析
1.1 复杂性的定义
复杂性这个名词很复杂,麻省理工学院的物理学家塞思·劳埃德统计了复杂性的定义数量,至少有45种:
信息 ,熵 ,算法复杂性 ,算法信息量 ,费希尔信息 , Renyi 熵 ,自描述代码长度 ,矫错代码长度 ,Chernoff 信息 ,最小描述 长度 ,参量个数或自由度或维数 ,Lempel - Ziv 复杂性 ,共有 信息或通道容量 ,演算共有信息 ,相关性 ,储存信息 ,条件信 息 ,条件演算信息量 ,计量熵 ,分形维 ,自相似 ,随机复杂性 , 混和 ,拓扑机器容量 ,有效或理想的复杂性 ,分层复杂性 ,树 形多样性 ,同源复杂性 ,时间计算复杂性 ,空间计算复杂性 , 基于信息的复杂性 ,逻辑深度 ,热力学深度 ,语法复杂性 , Kullbach - Liebler 信息 ,区别性 ,费希尔距离 ,分辨力 ,信息距 离 ,演算信息距离 ,Hamming 距离 ,长幅序 ,自组织 ,复杂适应 系统 ,混沌边缘
这也太多了,我们找几个有用的定义进行解析
1.1.1 复杂性就是任何使得软件难于理解和修改的因素。
John Ousterhout教授在 A Philosophy of Software Design 书中提到,复杂性就是任何使得软件难于理解和修改的因素。
复杂的系统有一些非常明显的特征,John教授将它抽象为变更放大(Change amplification)、认知负荷(Cognitive load)与未知的未知(Unknown unknowns)这3类。
变更放大(Change amplification)指得是看似简单的变更需要在许多不同地方进行代码修改。系统开发者之前没有及时重构代码,提取公共逻辑,而是省时间Ctrl-C,Ctrl-V式代码开发(这样做不会影响已有的稳定模块,不需要做比较多的回归测试,上线风险小)。当需求变化时,需要改动多处代码。
认知负荷(Cognitive load)是指系统的学习与理解成本高,开发人员的研发效率大大降低。
未知的未知(Unknown unknowns)是指不知道修改哪些代码才能使系统功能正确的运行,也不知道这行代码的改动是否会引发线上问题。这一项是复杂性中最糟糕的一个表现形式。
1.1.2 复杂性可以定义为系统表明自身方式数目的对数 ,或是系统可能状态数目的对数
德国学者弗里德里希·克拉默在其著作《混沌与秩序—生物系统的复杂结构》中,以系统为定义的对象 ,给出了一个复杂性的定义 ,他认为“复杂性可以定义为系统表明自身方式数目的对数 ,或是系统可能状态数目的对数,其中 K=logN ,式中K是复杂性 ,N 是不同的可能状态数。这个定义借自于信息论。系统越复杂 ,它所携带的信息越多。
这个对数我不是很认同,信息论中信息的大小需要用该信息可能出现概率的倒数取个对数,这样能够达成信息的累加性,但在个人实践中,复杂性是与状态数目成正比的,不需要取对数。
1.1.3 凡是不能用还原论方法处理或不宜用还原论方法处理的问题 ,都是复杂性问题
中国著名学者钱学森认为复杂性是系统的属性,他从研究方法上 ,在简单性与复杂性问题之间划了一道比较模糊的绝对界线 ,他认为“凡是不能用还原论方法处理或不宜用还原论方法处理的问题 ,都是复杂性问题。
何谓“还原论”? 笛卡尔认为,如果一件事物过于复杂,以至于一下子难以解决,那么就可以将它分解成一些足够小的问题,分别加以分析,然后再将它们组合在一起,就能获得对复杂事物的完整、准确的认识──这是还原论的通俗表达。
1.1.4 复杂性取决于认识主体 与 事物的结构
弗鲁德(Robert L.Flood)和卡生( Ewart R. Carson)从《韦伯第三国际词典 (Webster’s Third International Dic2 tionary) 》关于复杂 (complex) 的释义中选择两个作为复杂性 (complexity) 的常识定义,这两个释义为:
1. 有许多相关的部分、模式或要素,以致难于充分理解;
2.表现为包括许多部分、方面、细节和观念,因此为了理解和处理需进行充分的研究和分析。
这个复杂性的常识定义涉及两个方面:一方面是认识主体,即认识者;另一方面是事物的复合结构,它由许多相联系的部分或要素构成,即系统。因此,他们把复杂性分解为下图
1.1.5 总结
从上面的这些定义我们大概可以总结出系统复杂性有什么特征:系统组成多、可能存在的状态多、难以认知从而难以维护。
也找到了一个降低系统复杂性的可行方法:将系统分解为一些足够小的要素,分别加以分析,然后再将它们组合在一起,就能获得对复杂事物的完整、准确的认识,从而降低复杂性。
1.2 从系统论的视角研究软件系统
1.2.1 系统的定义
中国著名学者钱学森认为:系统是由相互作用相互依赖的若干组成部分结合而成的,具有特定功能的有机整体,而且这个有机整体又是它从属的更大系统的组成部分。
《一般系统论的原理及方法》将系统建模为:
其中Q1,Q2...Qn是组成系统的要素,任何一个要素Qi的变化都是其他要素Q1,Q2...Qn的函数
这里可以看出描述系统的两个关键点:组成部分及它们的关系
-
组成部分:可以视为在某个维度下不可分的要素,它具有特定的功能
-
关系:某个要素的变化会导致其他要素的变化
这个变化的范围有点广,既有实现层面的:一个facade接口修改字段后其他的系统就需要修改代码;也有功能层面的:一个要素处理逻辑变化后输出会不同,那么另一个要素使用该要素的输入,其输出也会变化,整体的功能就会不同。
我们再来看子系统的定义。
子系统也是由一群有关联的要素所组成的系统,多半会是更大系统中的一部分。
其实子系统的定义和系统定义是一样的,只是观察的角度有差异,一个系统可能是另外一个更大系统的子系统。
还原论把系统的各个组成部分当作是相互独立的个体,这是最易于大脑去理解的一种方式,而系统论多了一个“关系”,这是指系统各个要素对彼此的影响,1.1.1 中变更放大与未知的未知是不是就是没有管控好要素之间的关系导致的?随着发展,系统的规模和状态数不可避免增大,我们可以采用一些手段去延缓,比如抽象共同逻辑,但无法根治问题,我们能做的就是明确要素的边界、并管控好这些要素之间的关系,使某一要素变化时对其他要素产生的影响达到最小。我们常用的设计模式是不是将各种要素之间的关系管理玩出了花?
1.2.2 软件系统是什么子系统组成的?
要解答这个问题,我们首先总结一下软件开发人员每天的工作:使用计算机技术将现实世界映射为计算机语言。
这里有两个重点,一个是计算机技术,比如数据库、高级语言、操作系统等等;另一个是映射,将现实世界的各种概念用计算机语言来描述,比如各种类、对象等等就是对现实世界的映射。那么软件系统的子系统就为两种:技术系统与业务系统,他们相互作用相互依赖共同为业务目标服务。
1.2.2.1 技术系统
对于技术系统来说,计算机技术是边界清晰的,各个技术之间的关系也非常清楚,比如数据库与服务器之间的交互方式就那么几种,每种的优缺点都很清晰,是可穷尽的。我们只需要了解每种方案选型的优缺点并针对性的选出最优解即可。
1.2.2.2 业务系统
对于业务系统来说,各个组成要素的边界是不清晰的,关系也是不明确的,我们需要使用一些方法论来帮助我们明确要素边界及管理它们之间的关系。
二、技术系统复杂性的六个来源及通用解法
由1.2.2.1可知,技术系统是边界清晰且可穷尽的,业内有现成的方案来处理各种情况,下面是李运华大佬总结的方案,我直接抄过来。本段参考5.2
2.1 高性能
软件系统中高性能带来的复杂性主要体现在两方面:
-
一方面是单台计算机 内部为了高性能带来的复杂性;
-
另一方面是多台计算机 集群为了高性能带来的复杂性。
2.1.1 单机复杂性
计算机内部复杂性最关键的地方就是操作系统,计算机性能的发展本质上是由硬件发展驱动的,尤其是 CPU 的性能发展。而将硬件性能充分发挥出来的关键就是操作系统,所以操作系统本身也是随硬件的发展而发展的,操作系统是软件系统的运行环境,操作系统的复杂性直接决定了软件系统的复杂性。
操作系统和性能最相关的就是进程和线程。
-
进程:用进程来对应一个操作系统执行的任务,每个任务都有自己独立的内存空间,进程间互不相关,由操作系统来进行调度。
-
多进程:为了达到多进程并行运行的目的,采取了分时的方式,即把 CPU 的时间分成很多片段,每个片段只能执行某个进程中的指令。
-
进程间通信:为了解决进程在运行时相互通信的问题,人们设计了各种进程间通信,包括管道、消息队列、信号量、共享存储等。
-
多线程:多进程让多任务能够并行处理任务,但本身还有缺点,单个进程内部只能串行处理,而实际上很多进程内部的子任务并不要求是严格按照时间顺序来执行的,也需要并行处理。为了解决这个问题发明了线程,线程是进程内部的子任务,但这些子任务都共享同一份进程数据。为了保证数据的正确性,又发明了互斥锁机制。有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。
操作系统发展到现在,如果要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术并不是最新的就是最好的,也不是非此即彼的选择。
在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合,这个过程同样很复杂。例如,下面的系统都实现了高性能,但是内部实现差异很大:
-
Nginx 可以用多进程也可以用多线程
-
JBoss 采用的是多线程
-
Redis 采用的是单进程
-
Memcache 采用的是多线程
2.1.2 集群复杂性
让多台机器配合起来达到高性能的目的,是一个复杂的任务,常见的方式有:
任务可以指完整的业务处理,也可以指某个具体的任务。
-
任务分配:每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。
-
任务分解:业务越来越复杂,单台机器处理的性能会越来越低。为了能够继续提升性能,采用任务分解。
2.1.2.1 任务分配
-
增加一个任务分配器,可以是硬件(F5、交换机)、软件(LVS)、负载均衡软件(Nginx、HAProxy)、自己开发的系统。
-
任务分配器与业务服务器之间的连接和交互。
-
任务分配器增加分配算法(轮询、权重、负载)。
业务量继续提升,需要增加任务分配器的数量。
-
任务分配器增加为多台,这样需要将不同的用户请求分配到不同的任务分配器上(DNS轮询、智能DNS、CDN、GSLB全局负载均衡)。
-
任务分配器和业务服务器之间从一对多变成多对多的网状结构。
-
业务服务器继续扩增,状态管理和故障处理复杂性更大。
2.1.2.2 任务分解
微服务架构就采用了这种思路,通过任务分配的方式,能够突破单台机器处理性能的瓶颈,通过增加更多的机器来满足业务的性能需求,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。
通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),任务分解能够提升性能的主要原因是:
-
简单的系统更容易做到高性能:系统的功能越简单,影响性能的点就越少,就更加容易进行有针对性的优化。
-
可以针对单个任务进行扩展:当各个逻辑任务分解到独立的子系统后,整个系统的性能瓶颈更加容易发现,而且发现后只需要针对有瓶颈的子系统进行性能优化或者提升,不需要改动整个系统,风险会小很多。
最终决定业务处理性能的还是业务逻辑本身,业务逻辑本身没有发生大的变化下,理论上的性能是有一个上限的,系统拆分能够让性能逼近这个极限,但无法突破这个极限。
2.2 高可用
系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
本质上都是通过“冗余”来实现高可用。高可用的“冗余”解决方案,单纯从形式上来看,和高性能是一样的,都是通过增加更多机器来达到目的,但其实本质上是有根本区别的:高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元。
通过冗余增强了可用性,但同时也带来了复杂性。
2.2.1 计算高可用
计算的特点是无论从哪台机器上进行计算,同样的算法和输入数据,产出的结果都是一样的,所以将计算从一台机器迁移到另一台对业务没有影响。
-
需要增加一个任务分配器
-
任务分配器和真正的业务服务器之间有连接和交互
-
任务分配器需要增加分配算法(主备【冷备、温备、热备】、主主、多主多倍【2主2备、4主0备】)
2.2.2 存储高可用
对于需要存储数据的系统而言,整个系统的高可用设计的难点和关键点在于“存储高可用”。存储和计算的本质区别在于将数据从一台机器搬移到另一台机器时需要通过线路进行传输,而线路传输是存在延迟的,速度在毫秒级别,距离越远,延迟越高。加之各种异常情况(如传输中断、丢包、拥塞),会导致延迟更高。对于高可用系统来说,在某个时间点通信中断就意味着整个系统的数据不一致。按照“数据 + 逻辑 = 业务”的公式,数据不一致将导致最终业务表现不同。如果不做冗余备份,系统的整体高可用性无法保证。因此,存储高可用的难点不在于如何备份数据,而在于如何减少或规避数据不一致对业务造成的影响。
分布式领域内著名的 CAP 定理从理论上证实了存储高可用的复杂性。存储高可用不可能同时满足“一致性、可用性、分区容忍性”,最多只能满足其中两个。因此,在进行架构设计时需要结合业务进行取舍。
2.3 可扩展性
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题;设计模式更是将可扩展性做到了极致。
设计具备良好可扩展性的系统,有两个基本条件:
-
正确预测变化
-
完美封装变化
2.3.1 预测变化
“唯一不变的是变化”,按照这个标准衡量,架构师每个设计方案都要考虑可扩展性。预测变化的复杂性在于:
-
不能每个设计点都考虑可扩展性
-
不能完全不考虑扩展性
-
所有的预测都存在出错的可能性
如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准,更多是靠经验、直觉。
2.3.2 应对变化
预测变化是一回事,采取什么方案来应对变化,又是另外一个复杂的事情。即使预测很准确,如果方案不合适,则系统扩展一样很麻烦。
微服务架构中的各层进行封装和隔离也是一种应对变化的解决方式。
2.3.2.1 变化层VS稳定层
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
-
系统需要拆分出变化层和稳定层(如何拆分)
-
需要设计变化层和稳定层之间的接口(稳定层接口越稳定越好,变化层接口从差异中找到共同点)
2.3.2.2 抽象层VS实现层
第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。
抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是策略模式。
2.4 低成本
在设计高性能、高可用的架构方案时,如果涉及到数百、数千甚至数万台服务器,成本就会成为一个非常重要的考虑点。为了控制成本,需要减少服务器的数量,这与增加更多服务器来提升性能和可用性的通用做法相冲突。因此,低成本往往不是架构设计的首要目标,而是一个附加约束。为了解决这个问题,需要先设定一个成本目标,然后根据高性能和高可用的要求设计方案,并评估是否能够满足成本目标。如果不能,就需要重新设计架构;如果无论如何都无法设计出满足成本要求的方案,那只能找老板调整成本目标了。 低成本给架构设计带来的主要复杂性体现在,往往只有"创新"才能达到低成本目标。"创新"的含义是开创一个全新的技术领域,或者引入新技术来解决问题。如果没有找到能够解决自己问题的新技术,那么就需要自己创造新技术了。例如,NoSQL(如Memcache、Redis等)是为了解决关系型数据库无法应对高并发访问带来的访问压力;全文搜索引擎(如Sphinx、Elasticsearch、Solr)是为了解决关系型数据库like搜索的低效问题;Hadoop则是为了解决传统文件系统无法应对海量数据存储和计算的问题。创造新技术的主要复杂性在于需要创造全新的理念和技术,并且新技术需要与旧技术相比有质的飞跃。
2.5 安全
从技术的角度来讲,安全可以分为两类:
-
一类是 功能 上的安全,
-
一类是 架构 上的安全。
2.5.1 功能安全
常见的 XSS 攻击、CSRF 攻击、SQL 注入、Windows 漏洞、密码破解等,本质上是因为系统实现有漏洞,黑客有了可乘之机,功能安全其实就是“防小偷”。
从实现的角度来看,功能安全更多地是和具体的编码相关,与架构关系不大。开发框架会内嵌常见的安全功能,但是开发框架本身也可能存在安全漏洞和风险。
所以功能安全是一个逐步完善的过程,而且往往都是在问题出现后才能有针对性的提出解决方案,我们永远无法预测系统下一个漏洞在哪里,也不敢说自己的系统肯定没有任何问题。
换句话讲,功能安全其实也是一个“攻”与“防”的矛盾,只能在这种攻防大战中逐步完善,不可能在系统架构设计的时候一劳永逸地解决。
2.5.2 架构安全
如果说功能安全是“防小偷”,那么架构安全就是“防强盗”。
架构设计时需要特别关注架构安全,尤其是互联网时代,理论上来说系统部署在互联网上时,全球任何地方都可以发起攻击。
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。
防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域应用较多。但在互联网领域,防火墙的应用场景并不多。
互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
2.6 规模
规模带来复杂性的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂性会发生质的变化。常见的规模带来的复杂性有:
1.功能越来越多,系统复杂性指数级上升
2.数据越来越多,系统复杂性发生质变
2.6.1 功能越来越多,系统复杂性指数级上升
例如,某个系统开始只有 3 大功能,后来不断增加到 8 大功能,虽然还是同一个系统,但复杂性已经相差很大了,具体相差多大呢?我以一个简单的抽象模型来计算一下,假设系统间的功能都是两两相关的,系统的复杂度 = 功能数量 + 功能之间的连接数量,通过计算我们可以看出:3个功能的系统复杂度为3+3=6
8个功能的系统复杂性为8+(7+0)*8/2=36 可以看出,具备8个功能的系统的复杂性不是比具备 3 个功能的系统的复杂度多5,而是多了30,基本是指数级增长的,主要原因在于随着系统功能数量增多,功能之间的连接呈指数级增长。下图形象地展示了功能数量的增多带来了复杂度。
2.6.2 数据越来越多,系统复杂性发生质变
随着数据量的不断增长,传统的数据处理和管理方式已经无法适应,因此“大数据”这一概念应运而生。大数据的诞生主要是为了解决数据规模变得越来越大时,传统的数据收集、存储、分析等方式无法胜任的问题。Google的三篇技术论文,即Google File System、Google Bigtable和Google MapReduce则分别开创了大数据文件存储、列式数据存储和大数据运算的技术领域。即便数据规模没有达到大数据的水平,数据增长仍然可能会给系统带来复杂性。例如,在使用关系数据库存储数据时,当单表数据达到一定规模时,就会导致添加索引、修改表结构等操作变得很慢,可能需要几个小时,这就会对业务造成不良影响。因此,必须考虑将单表拆分为多表来解决这个问题,但这个过程也会引入更多的复杂性。
2.7 简单的复杂性分析案例
我们来分析一个简单的案例,一起来看看如何将“架构设计的真正目的是为了解决软件系统复杂性带来的问题”这个指导思想应用到实践中。
当我们设计一个大学的学生管理系统时,我们需要考虑该系统的复杂性以及如何解决这些复杂性带来的问题。首先,我们可以将该系统的复杂性分为以下几个方面:
性能 该系统的访问频率并不高,因此性能并不是一个很大的问题。我们可以使用MySQL作为存储,Nginx作为Web服务器,无需考虑缓存。
可扩展性 该系统的功能比较稳定,可扩展的空间并不大,因此可扩展性方面也不是一个很大的问题。
高可用 数据丢失是不可接受的,故该系统的高可用性方面需要考虑多种异常情况,如机器故障、机房故障等。为此,我们需要设计MySQL同机房主备方案和MySQL跨机房同步方案。
安全性 该系统存储的信息涉及到学生的隐私,因此需要考虑安全性。我们可以使用Nginx提供的ACL控制、用户账号密码管理和数据库访问权限控制来保证系统的安全性。
成本 由于该系统比较简单,基本上几台服务器就可以搞定,因此成本方面并不需要太多关注。
规模 同上,规模复杂性无需过度关注。
总体来说,我们需要在架构设计中充分考虑系统的复杂性,同时根据不同问题选择合适的解决方案,以提高系统的可靠性和安全性。
2.8 总结
第二节总结了技术系统复杂性的来源及通用的解法,当我们设计技术架构时,只需要照着这六个来源按图索骥即可。
三、使用DDD作为方法论建立业务系统
由1.2.2.2可知,对于业务系统来说,各个组成要素的边界是不清晰的,关系也是不明确的,我们需要使用一些方法论来帮助我们明确要素边界及管理它们之间的关系。
DDD 是一种处理高度复杂领域的设计思想 ,它试图分离技术实现的复杂性,并围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演进的问题。DDD 是一种架构设计方法论 ,它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现架构演进。
DDD 包括战略设计和战术设计两部分。
-
战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。 战略设计完成了子系统的初步划分。
-
战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。 战术设计则对子系统进行了进一步的划分,并采用领域服务、应用服务和资源库等对要素(子系统)的关系进行了管控。
3.1 使用DDD确定系统要素
3.1.1 方法介绍
我们不妨来看看 DDD 是如何进行战略设计的。
DDD战略设计会建立领域模型,领域模型可以用于指导微服务的设计和拆分。事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。
我们可以用三步来划定领域模型的边界。
-
第一步:在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
-
第二步:根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。
-
第三步:根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。但实质上,不过是子系统的划分而已,较大的子系统定为微服务实例,较小的子系统则在其中运行,当某个子系统由于业务发展变得庞大起来,我们可以将它作为一个微服务实例独立出来。
3.1.2 举个例子
下面我们以保险的投保业务场景为例,看一下聚合的构建过程主要都包括哪些步骤。
**第 1 步:**采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。
**第 2 步:**从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一 ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。
**第 3 步:**根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出 1 个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。
**第 4 步:**在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。这里我需要说明一下:投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体。
**第 5 步:**多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。
3.2 使用DDD管理要素关系
DDD管理要素关系的方案是不全面的,其实我想建立一套管理要素之间关系的方法论,首先明确要素之间的关系有哪些,现有管控关系的方法有哪些,然后后面按图索骥即可,就像这样:
但是我积累实在太浅,工作时长不到一年,之前又是转专业来计算机的,所以现在很难去构建出这一套体系来,如果有人能够搭建出来,欢迎交流分享。
那就看看DDD是如何管理要素之间关系的吧。
3.2.1 管理业务要素之间的关系
主要有两个:领域服务和应用服务,我们来一起看看吧。
3.2.1.1 领域服务
在战术建模当中,并非所有模型都是事物。有些模型是对领域中的一些行为操作进行建模。此类模型我们称之为领域服务。当一些重要的领域操作无法放到实体、值对象或者聚合时,他们本质是行为而不是事物。如果我们不寻找一些对象来封装这些领域行为的话,又会演变成之前过程式的编程方式。我们希望在领域设计当中统一用模型对象进行交互。此时领域服务使用细粒度的领域对象如实体或者值对象进行交互,在服务内部描述领域知识得出结果并将其返回。领域服务本质是将领域模型内部的字段关系建模为了对象。
3.2.1.2 应用服务
应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。关于领域模型之间的关系,并没有给出解决方案,或许是设计时应用层就很薄,无需特别处理。在这里要明晰一个概念,只有复杂的系统才需要用到本文的方法论,简单的系统随便写写就好了。
3.2.2 管理业务要素与系统要素之间关系
和我们熟知的mvc架构类似,DDD也搞了个分层架构,包括Web层、Interface层、Application层、Domain层、Infrastructure层,这主要。
Web层:是领域层的输入。包含面向用户的web接口Controller,主要处理输入解析、验证、转换、序列化/反序列化、用户登录权限校验等,不包含业务逻辑。包含Facade,负责web层的controller接口的具体实现,通过调用多个Application原子行为接口进行组合。
Interface层: 也是领域层的输入。包括对外提供的API的具体实现, MQ监听器,任务调度执行入口类。
Application层:原子行为服务层,定义原子业务行为能力,主要负责处理跨域业务流程的组合编排、事务处理等,可以调用领域层,也可以直接调用基础设施层。
Domain层:领域层,定义核心领域实体,使用领域模型和领域服务实现业务规则。另外在领域层定义repository和adapter的SPI接口,用于持久化或者调用外部依赖的服务,但领域层不关心具体实现,repository和adapter SPI接口在Infrastructure层进行 实现(依赖倒置,Infrastructure层依赖domain层, domain层成为内核,保持业务领域的稳定)。
Infrastructure层:基础设施层,封装持久化操作和外部依赖,包括DB、Tair、外部HSF接口等。
这样架构隔离了变化的传导,从而对关系进行了管理。
3.2.3 总结
DDD给出了两种管理关系的方案:将复杂关系建模为对象、通过防腐层隔离变化的传导。
四、总结
本文通过对系统和复杂度的解析,引出了解决系统复杂度的一般方法:明确要素的边界、并管控好这些要素之间的关系,使某一要素变化时对其他要素产生的影响达到最小。后提出软件系统的两个子系统:计算系统和业务系统,并对如何架构这两个系统提出了自己的思考。
五、参考
5.1 复杂性是什么:https://www.sinoss.net/uploadfile/2010/1130/6171.pdf
5.2 从零开始学架构:https://grow.alibaba-inc.com/course/4800010540010032/section/1800010540005583
5.3 架构实战案例解析:https://time.geekbang.org/column/intro/100046301?tab=catalog
六、致谢
感谢升麻对我视野上的扩充及对本文的建议,感谢善宁师兄教导我需要提高技术视野及输出。