first commit

This commit is contained in:
张乾 2024-10-15 23:23:37 +08:00
parent 9327d43695
commit ac7d1ed7bc
41 changed files with 6327 additions and 0 deletions

View File

@ -0,0 +1,106 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 学好了DDD你能做什么
你好,我是欧创新,人保高级架构师,一名奋斗在软件架构一线十余年的技术人。
目前热衷于采用领域驱动设计DDD实现中台业务建模专注基于 DDD 的微服务设计和开发等。另外,我也正在深入探索传统企业中台数字化转型的技术和方法体系。很高兴在这个专栏和你见面!
我与 DDD
说起 DDD 的实践那就不得不提微服务了。2015 年,我刚开始接触微服务,那时候和别人去介绍微服务的设计理念,接受度并不高,毕竟大家普遍采用的还是集中式架构。
但即便是在四年前,业务的日渐复杂也是可以预见的,微服务的价值确确实实存在。我就从那个时候开始深入研究,作为公司的高级架构师,我也一直处于公司中台转型和微服务建设的一线。
这个过程中,我和我的技术团队踩过不少坑,最尖锐的一个问题是:“微服务到底怎么拆分和设计才算合理,拆多小才叫微服务?”而微服务的边界历来也是最容易产生争议的地方。
紧接着,阿里巴巴成功完成了中台战略转型。于是,很多大型公司也开启了中台数字化战略转型,中型公司也根据自身需求跃跃欲试。但也有很多公司由于历史原因,存在着大量系统重复建设的问题。
作为中台,需要将通用的可复用的业务能力沉淀到中台业务模型,实现企业级能力复用。因此中台面临的首要问题就是中台领域模型的重构。而中台落地时,依然会面临微服务设计和拆分的问题。这两个问题一前一后,放在任何一家公司,我想都是一个不小的挑战。
这也是我一直在探索和解决的问题。这两年,中台越来越火,微服务越来越热,参与的人越来越多。那是否有好的方法来指导中台和微服务的设计呢?
一次偶然的机会我接触到了 DDD深入研究后我发现运用 DDD 设计思想实现的微服务边界确实清晰很多,业务领域划分也十分合理。后来,我和我的伙伴们用 DDD 做了很多的微服务实践。
关于专栏
有了这样一个基础,我开始尝试将我对 DDD 的理解和一些实践经验沉淀,并写了几篇文章发表在了 InfoQ 上。在读者的留言中,我发现很多人对 DDD 是有一定的了解的,但由于 DDD 的知识点多且较为抽象,体系庞大,又缺少实践经验和案例指导,很多人对 DDD 的应用都有这样那样的困惑,哪怕知道 DDD 的好处,但是也感到无从下手。
收到极客时间的邀请后,我就开始全力打造课程大纲,力求干货充盈,理论与实践并重。这做起来并不是一件轻松的事儿,专栏从确定主题到和你见面,已经耗时三个多月了。
DDD 虽然历史很久了,但它与微服务和中台设计的结合,却是一片很新的领域。早在 2003 年就诞生的 DDD怎么来指导“迟到”近 20 年才大热的微服务设计呢?
我认为,要想应用 DDD首要任务就是要吃透 DDD 的核心设计思想,搞清楚 DDD、微服务和中台之间的关系。中台本质是业务模型微服务是业务模型的系统落地DDD 是一种设计思想它可以同时指导中台业务建模和微服务设计它们之间就是这样的一个铁三角关系。DDD 强调领域模型和微服务设计的一体性,先有领域模型然后才有微服务,而不是脱离领域模型来谈微服务设计。
其次,就是通过战略设计,建立领域模型,划分微服务边界。这步是关键,你可以借助专栏中的一些经验。
最后,通过战术设计,我们会从领域模型转向微服务设计和落地。此时,边界清晰、可持续演进的微服务架构雏形就在你面前了。
遵循以上过程,这门课的设计思路也就诞生了。
关于课程设计
如果你以往对 DDD 的了解并不深入,甚至是第一次接触,你一定会觉得 DDD 的术语非常多,且非常陌生,这些术语之间的关系都算是个“拦路虎”。
搞懂这些之后呢,怎么应用它们,从何下手来设计领域模型等等这些问题又接踵而至。
如果你对 DDD 有过研究,在学会怎么用之后,你可能还会反过来想:“我费了这么大劲儿去搞懂它,那它到底会让我的系统变成什么样呢?可以解决什么具体问题?是不是真有大家说得那么好?”
这些都将是这个专栏要交付给你的内容。总结一下的话,我希望这个专栏能带给你这样几点收获:
用浅显易懂的案例带你了解 DDD 必知必会的 10 大核心概念,深入设计思想,厘清各知识域之间的关系;
用 DDD 分层架构带你弄懂微服务架构各层之间的关系,并完成微服务分层和代码模型设计;
用 DDD 战略设计和事件风暴带你完成领域建模和企业级中台业务建模;
用一个典型的案例带你完整走一遍 DDD 战略设计和战术设计的全流程,学习 DDD 在领域模型和微服务设计过程中的技术要点;
带你深化微服务架构设计原则和注意事项,建立适应你公司技术能力和文化的微服务,建立演进式的微服务架构。
希望这些收获能够给正在从事或者有兴趣深入了解微服务设计和中台的你,提供一些实质性的帮助。
在具体的课程设计上,我将内容分为了三大部分:基础篇、进阶篇和实战篇。下面我来逐一介绍一下。
基础篇
基础篇主要讲解 DDD 的核心知识体系,具体包括:领域、子域、核心域、通用域、支撑域、限界上下文、实体、值对象、聚合和聚合根等概念。我会用浅显易懂的案例带你理解它们以及它们之间的合作、依赖关系。
进阶篇
进阶篇主要讲解领域事件、DDD 分层架构、几种常见的微服务架构模型以及中台设计思想等内容。具体包括:
如何通过领域事件实现微服务解耦?
怎样进行微服务分层设计?
如何实现层与层之间的服务协作?
通过几种微服务架构模型的对比分析,让你了解领域模型和微服务分层的作用和价值。
另外,我还会介绍中台设计的核心思想,和你探讨如何实现前中后台的协同和融合?如何利用 DDD 进行中台设计?
实战篇
实战篇是我们专栏课程的重点,我准备了多个实战小项目。
中台和领域建模的实战:带你了解如何用 DDD 设计思想构建企业级可复用的中台业务模型,了解事件风暴以及用事件风暴构建领域模型的过程。
微服务设计实战:带你了解如何用 DDD 设计微服务代码模型,如何从领域模型完成微服务设计,建立领域模型与微服务代码模型的映射关系,如何完成微服务的架构演进等。
然后我会用一个典型的案例将 DDD 所有的知识点串联在一起,带你深入了解如何用 DDD 的设计思想来完成领域建模和微服务设计的全流程。
最后,我还会补充分享一个前端的最新设计思想,带你了解如何借鉴微服务的设计思想来设计前端应用,实现前端应用的解耦。同时,我还为你总结了微服务设计原则以及分布式架构设计的关键注意事项。
最后我想说DDD 看似复杂,可学习起来并不困难,多动手参与几次 DDD 事件风暴工作坊,你就能很快理解 DDD 的核心设计思想和设计过程,成功进阶了。
如果你的公司尚不能给你提供实战的机会,你可以在这里小试牛刀。
相信这个专栏会帮助你掌握一套完整而系统的基于 DDD 的微服务设计和拆分方法,明确从战略设计到战术设计的微服务标准设计过程,使得你的微服务设计思路能够更加清晰,设计过程更加规范,让你的中台和微服务落地如虎添翼。

View File

@ -0,0 +1,68 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 领域驱动设计微服务设计为什么要选择DDD
你好,我是欧创新。
我们知道,微服务设计过程中往往会面临边界如何划定的问题,我经常看到项目团队为微服务到底应该拆多小而争得面红耳赤。不同的人会根据自己对微服务的理解而拆分出不同的微服务,于是大家各执一词,谁也说服不了谁,都觉得自己很有道理。
那在实际落地过程中,我也确实见过不少项目在面临这种微服务设计困惑时,是靠拍脑袋硬完成的,上线后运维的压力就可想而知了。那是否有合适的理论或设计方法来指导微服务设计呢?当你看到这一讲的题目时,我想你已经知道答案了。
没错,就是 DDD。那么今天我就给你详细讲解下“微服务设计为什么要选择领域驱动设计
软件架构模式的演进
在进入今天的主题之前,我们先来了解下背景。
我们知道,这些年来随着设备和新技术的发展,软件的架构模式发生了很大的变化。软件架构模式大体来说经历了从单机、集中式到分布式微服务架构三个阶段的演进。随着分布式技术的快速兴起,我们已经进入到了微服务架构时代。
我们可以用三步来划定领域模型和微服务的边界。
第一步:在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
第二步:根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。
第三步:根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。
有了这两层边界,微服务的设计就不是什么难事了。
在战略设计中我们建立了领域模型,划定了业务领域的边界,建立了通用语言和限界上下文,确定了领域模型中各个领域对象的关系。到这儿,业务端领域模型的设计工作基本就完成了,这个过程同时也基本确定了应用端的微服务边界。
在从业务模型向微服务落地的过程中,也就是从战略设计向战术设计的实施过程中,我们会将领域模型中的领域对象与代码模型中的代码对象建立映射关系,将业务架构和系统架构进行绑定。当我们去响应业务变化调整业务架构和领域模型时,系统架构也会同时发生调整,并同步建立新的映射关系。
DDD 与微服务的关系
有了上面的讲解,现在我们不妨再次总结下 DDD 与微服务的关系。
DDD 是一种架构设计方法,微服务是一种架构风格,两者从本质上都是为了追求高响应力,而从业务视角去分离应用系统建设复杂度的手段。两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构。
DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。
微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。
总结
今天我们主要讨论了微服务设计和拆分的难题。通过 DDD 战略设计可以建立领域模型,划定领域边界,解决微服务设计过程中,边界难以划定的难题。如果你的业务焦点在领域和领域逻辑,那么你就可以选择 DDD 作为微服务的设计方法!
更关键的一点是DDD 不仅可以用于微服务设计还可以很好地应用于企业中台的设计。如果你的企业正在做中台转型DDD 将会是一把利器,它可以帮你建立一个非常好的企业级中台业务模型。有关这点你还会在后面的文章中见到详解。
除此之外DDD 战术设计对设计和开发人员的要求相对较高,实现起来相对复杂。不同企业的研发管理能力和个人开发水平可能会存在差异。尤其对于传统企业而言,在战术设计落地的过程中,可能会存在一定挑战和困难,我建议你和你的公司如果有这方面的想法,就一定要谨慎评估自己的能力,选择最合适的方法落地 DDD。
也不妨根据收获权衡一下总体来说DDD 可以给你带来以下收获:
DDD 是一套完整而系统的设计方法,它能带给你从战略设计到战术设计的标准设计过程,使得你的设计思路能够更加清晰,设计过程更加规范。
DDD 善于处理与领域相关的拥有高复杂度业务的产品开发,通过它可以建立一个核心而稳定的领域模型,有利于领域知识的传递与传承。
DDD 强调团队与领域专家的合作,能够帮助你的团队建立一个沟通良好的氛围,构建一致的架构体系。
DDD 的设计思想、原则与模式有助于提高你的架构设计能力。
无论是在新项目中设计微服务,还是将系统从单体架构演进到微服务,都可以遵循 DDD 的架构原则。
DDD 不仅适用于微服务,也适用于传统的单体应用。

View File

@ -0,0 +1,93 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 领域、子域、核心域、通用域和支撑域:傻傻分不清?
你好,我是欧创新。
DDD 的知识体系提出了很多的名词,像:领域、子域、核心域、通用域、支撑域、限界上下文、聚合、聚合根、实体、值对象等等,非常多。这些名词,都是关键概念,但它们实在有些晦涩难懂,可能导致你还没开始实践 DDD 就打起了退堂鼓。因此,在基础篇中,我希望能带着你一起做好实践前的准备工作。
除此之外,我想说的是,这些名词在你的微服务设计和开发过程中不一定都用得上,但它可以帮你理解 DDD 的核心设计思想和理念。而这些思想和理念,在 IT 战略设计、业务建模和微服务设计中都是可以借鉴的。
那么,从这讲开始,我就会围绕以上这些 DDD 关键概念进行讲解,帮助你彻底理清它们与微服务的关系,了解它们在微服务设计中的作用。今天我们重点了解 DDD 的领域、子域、核心域、通用域和支撑域等重要概念。
如何理解领域和子域?
我们先看一下汉语词典中对领域的解释:“领域是从事一种专门活动或事业的范围、部类或部门。”百度百科对领域的解释:“领域具体指一种特定的范围或区域。”
两个解释有一个共同点——范围。对了!领域就是用来确定范围的,范围即边界,这也是 DDD 在设计中不断强调边界的原因。
在研究和解决业务问题时DDD 会按照一定的规则将业务领域进行细分当领域细分到一定的程度后DDD 会将问题范围限定在特定的边界内在这个边界内建立领域模型进而用代码实现该领域模型解决相应的业务问题。简言之DDD 的领域就是这个边界内要解决的业务问题域。
既然领域是用来限定业务边界和范围的,那么就会有大小之分,领域越大,业务范围就越大,反之则相反。
领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
我们知道DDD 是一种处理高度复杂领域的设计思想它试图分离技术实现的复杂度。那么面对错综复杂的业务领域DDD 是如何使业务从复杂变得简单,更容易让人理解,技术实现更容易呢?
其实很好理解DDD 的研究方法与自然科学的研究方法类似。当人们在自然科学研究中遇到复杂问题时,通常的做法就是将问题一步一步地细分,再针对细分出来的问题域,逐个深入研究,探索和建立所有子域的知识体系。当所有问题子域完成研究时,我们就建立了全部领域的完整知识体系了。
我们来看一下上面这张图。这个例子是在讲如何给桃树建立一个完整的生物学知识体系。初中生物课其实早就告诉我们研究方法了。它的研究过程是这样的。
第一步:确定研究对象,即研究领域,这里是一棵桃树。
第二步:对研究对象进行细分,将桃树细分为器官,器官又分为营养器官和生殖器官两种。其中营养器官包括根、茎和叶,生殖器官包括花、果实和种子。桃树的知识体系是我们已经确定要研究的问题域,对应 DDD 的领域。根、茎、叶、花、果实和种子等器官则是细分后的问题子域。这个过程就是 DDD 将领域细分为多个子域的过程。
第三步:对器官进行细分,将器官细分为组织。比如,叶子器官可细分为保护组织、营养组织和输导组织等。这个过程就是 DDD 将子域进一步细分为多个子域的过程。
第四步:对组织进行细分,将组织细分为细胞,细胞成为我们研究的最小单元。细胞之间的细胞壁确定了单元的边界,也确定了研究的最小边界。
这里先剧透一点聚合、聚合根、实体以及值对象的内容,我还会在 [第 04 讲] 和 [第 05 讲] 中详细讲解。
我们知道细胞核、线粒体、细胞膜等物质共同构成细胞,这些物质一起协作让细胞具有这类细胞特定的生物功能。在这里你可以把细胞理解为 DDD 的聚合,细胞内的这些物质就可以理解为聚合里面的聚合根、实体以及值对象等,在聚合内这些实体一起协作完成特定的业务功能。这个过程类似 DDD 设计时,确定微服务内功能要素和边界的过程。
这里总结一下,就是说每一个细分的领域都会有一个知识体系,也就是 DDD 的领域模型。在所有子域的研究完成后,我们就建立了全域的知识体系了,也就建立了全域的领域模型。
上面我们用自然科学研究的方法,说明了领域可以通过细分为子域的方法,来降低研究的复杂度。现在我们把这个话题再切换到业务领域,对比验证下,二者的细分过程是否是一致的。这里以我从事的保险行业为例。
保险是个比较大的领域,很早以前的保险核心系统把所有的功能都放在一个系统里来实现,这个系统就是我们常说的单体系统。后来单体系统开始无法适应保险业务的发展,因此保险公司开始了中台转型,引入分布式微服务架构来替换原来的单体系统。而分布式微服务架构就需要划分业务领域边界,建立领域模型,并实现微服务落地了。
为实现保险领域建模和微服务建设,我们可以根据业务关联度以及流程边界将保险领域细分为:承保、收付、再保以及理赔等子域,而承保子域还可以继续细分为投保、保全(寿险)、批改(财险)等子子域。
在投保这个限界上下文内可以建立投保的领域模型,投保的领域模型最后映射到系统就是投保微服务。这就是一个保险领域的细分和微服务的建设过程。
那么你可能会说,我不是保险行业的人,我怎么理解这个过程呢?我认为,不同行业的业务模型可能会不一样,但领域建模和微服务建设的过程和方法基本类似,其核心思想就是将问题域逐步分解,降低业务理解和系统实现的复杂度。
如何理解核心域、通用域和支撑域?
在领域不断划分的过程中,领域会细分为不同的子域,子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域。
决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。
这三类子域相较之下,核心域是最重要的,我们下面讲目的的时候还会以核心域为例详细介绍。通用域和支撑域如果对应到企业系统,举例来说的话,通用域则是你需要用到的通用系统,比如认证、权限等等,这类应用很容易买到,没有企业特点限制,不需要做太多的定制化。而支撑域则具有企业特性,但不具有通用性,例如数据代码类的数据字典等系统。
那为什么要划分核心域、通用域和支撑域,主要目的是什么呢?
还是拿上图的桃树来说吧。我们将桃树细分为了根、茎、叶、花、果实和种子等六个子域,那桃树是否有核心域?有的话,到底哪个是核心域呢?
不同的人对桃树的理解是不同的。如果这棵桃树生长在公园里,在园丁的眼里,他喜欢的是“人面桃花相映红”的阳春三月,这时花就是桃树的核心域。但如果这棵桃树生长在果园里,对果农来说,他则是希望在丰收的季节收获硕果累累的桃子,这时果实就是桃树的核心域。
在不同的场景下,不同的人对桃树核心域的理解是不同的,因此对桃树的处理方式也会不一样。园丁更关注桃树花期的营养,而果农则更关注桃树落果期的营养,有时为了保证果实的营养供给,还会裁剪掉疯长的茎和叶(通用域或支撑域)。
同样的道理,公司在 IT 系统建设过程中,由于预算和资源有限,对不同类型的子域应有不同的关注度和资源投入策略,记住好钢要用在刀刃上。
很多公司的业务,表面看上去相似,但商业模式和战略方向是存在很大差异的,因此公司的关注点会不一样,在划分核心域、通用域和支撑域时,其结果也会出现非常大的差异。
比如同样都是电商平台的淘宝、天猫、京东和苏宁易购,他们的商业模式是不同的。淘宝是 C2C 网站,个人卖家对个人买家,而天猫、京东和苏宁易购则是 B2C 网站,是公司卖家对个人买家。即便是苏宁易购与京东都是 B2C 的模式,他们的商业模式也是不一样的,苏宁易购是典型的传统线下卖场转型成为电商,京东则是直营加部分平台模式。
商业模式的不同会导致核心域划分结果的不同。有的公司核心域可能在客户服务,有的可能在产品质量,有的可能在物流。在公司领域细分、建立领域模型和系统建设时,我们就要结合公司战略重点和商业模式,找到核心域了,且重点关注核心域。
如果你的公司刚好有意向转型微服务架构的话,我建议你和你的技术团队要将核心域的建设排在首位,最好是有绝对的掌控能力和自主研发能力,如果资源实在有限的话,可以在支撑域或者通用域上想想办法,暂时采用外购的方式也未尝不可。
总结
领域的核心思想就是将问题域逐级细分,来降低业务理解和系统实现的复杂度。通过领域细分,逐步缩小微服务需要解决的问题域,构建合适的领域模型,而领域模型映射成系统就是微服务了。
核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略,其关注度也会不一样。

View File

@ -0,0 +1,129 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 限界上下文:定义领域边界的利器
你好,我是欧创新。今天我们重点学习“限界上下文”。
在 DDD 领域建模和系统建设过程中,有很多的参与者,包括领域专家、产品经理、项目经理、架构师、开发经理和测试经理等。对同样的领域知识,不同的参与角色可能会有不同的理解,那大家交流起来就会有障碍,怎么办呢?因此,在 DDD 中就出现了“通用语言”和“限界上下文”这两个重要的概念。
这两者相辅相成,通用语言定义上下文含义,限界上下文则定义领域边界,以确保每个上下文含义在它特定的边界内都具有唯一的含义,领域模型则存在于这个边界之内。你是不是感觉这么描述很抽象?没关系,接下来我会给你一一详细讲解。
在这之前,我想请你先看这样两个问题,这也是今天内容的核心。
为什么要提出限界上下文的概念(也就是说除了解决交流障碍这个广义的原因,还有更具体的吗)?
限界上下文在微服务设计中的作用和意义是什么?
什么是通用语言?
为了更好地理解限界上下文,回答这两个问题,我们先从通用语言讲起。
怎么理解通用语言这个概念呢?在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。也就是说,通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。
那么,通用语言的价值也就很明了了,它可以解决交流障碍这个问题,使领域专家和开发人员能够协同合作,从而确保业务需求的正确表达。
但是,对这个概念的理解,到这里还不够。
通用语言包含术语和用例场景,并且能够直接反映在代码中。通用语言中的名词可以给领域对象命名,如商品、订单等,对应实体对象;而动词则表示一个动作或事件,如商品已下单、订单已付款等,对应领域事件或者命令。
通用语言贯穿 DDD 的整个设计过程。作为项目团队沟通和协商形成的统一语言,基于它,你就能够开发出可读性更好的代码,将业务需求准确转化为代码设计。
下面我带你看一张图,这张图描述了从事件风暴建立通用语言到领域对象设计和代码落地的完整过程。
在事件风暴的过程中,领域专家会和设计、开发人员一起建立领域模型,在领域建模的过程中会形成通用的业务术语和用户故事。事件风暴也是一个项目团队统一语言的过程。
通过用户故事分析会形成一个个的领域对象,这些领域对象对应领域模型的业务对象,每一个业务对象和领域对象都有通用的名词术语,并且一一映射。
微服务代码模型来源于领域模型,每个代码模型的代码对象跟领域对象一一对应。
这里我再给你分享一条经验,我自己经常用,特别有效。设计过程中我们可以用一些表格,来记录事件风暴和微服务设计过程中产生的领域对象及其属性。比如,领域对象在 DDD 分层架构中的位置、属性、依赖关系以及与代码模型对象的映射关系等。
下面是一个微服务设计实例的部分数据表格中的这些名词术语就是项目团队在事件风暴过程中达成一致、可用于团队内部交流的通用语言。在这个表格里面我们可以看到DDD 分析过程中所有的领域对象以及它们的属性都被记录下来了,除了 DDD 的领域对象,我们还记录了在微服务设计过程中领域对象所对应的代码对象,并将它们一一映射。
到这里我要再强调一次。DDD 分析和设计过程中的每一个环节都需要保证限界上下文内术语的统一,在代码模型设计的时侯就要建立领域对象和代码对象的一一映射,从而保证业务模型和代码模型的一致,实现业务语言与代码语言的统一。
如果你做到了这一点,也就是建立了领域对象和代码对象的映射关系,那就可以指导软件开发人员准确无误地按照设计文档完成微服务开发了。即使是不熟悉代码的业务人员,也可以很快找到代码的位置。
什么是限界上下文?
那刚刚提到的限界上下文又是用来做什么的呢?
我们知道语言都有它的语义环境同样通用语言也有它的上下文环境。为了避免同样的概念或语义在不同的上下文环境中产生歧义DDD 在战略设计上提出了“限界上下文”这个概念,用来确定语义所在的领域边界。
我们可以将限界上下文拆解为两个词:限界和上下文。限界就是领域的边界,而上下文则是语义环境。通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流。
综合一下,我认为限界上下文的定义就是:用来封装通用语言和领域对象,提供上下文环境,保证在领域之内的一些术语、业务相关对象等(通用语言)有一个确切的含义,没有二义性。这个边界定义了模型的适用范围,使团队所有成员能够明确地知道什么应该在模型中实现,什么不应该在模型中实现。
进一步理解限界上下文
我们可以通过一些例子进一步理解一下这个概念,不要小看它,彻底弄懂会给你后面实践 DDD 打下一个坚实的基础。
都说中文这门语言非常丰富,在不同的时空和背景下,同样的一句话会有不同的涵义。有一个例子你应该听说过。
在一个明媚的早晨,孩子起床问妈妈:“今天应该穿几件衣服呀?”妈妈回答:“能穿多少就穿多少!”
那到底是穿多还是穿少呢?
如果没有具体的语义环境,还真不太好理解。但是,如果你已经知道了这句话的语义环境,比如是寒冬腊月或者是炎炎夏日,那理解这句话的涵义就会很容易了。
所以语言离不开它的语义环境。
而业务的通用语言就有它的业务边界,我们不大可能用一个简单的术语没有歧义地去描述一个复杂的业务领域。限界上下文就是用来细分领域,从而定义通用语言所在的边界。
现在我们用一个保险领域的例子来说明下术语的边界。保险业务领域有投保单、保单、批单、赔案等保险术语,它们分别应用于保险的不同业务流程。
客户投保时,业务人员记录投保信息,系统对应有投保单实体对象。
缴费完成后,业务人员将投保单转为保单,系统对应有保单实体对象,保单实体与投保单实体关联。
如客户需要修改保单信息,保单变为批单,系统对应有批单实体对象,批单实体与保单实体关联。
如果客户发生理赔,生成赔案,系统对应有报案实体对象,报案实体对象与保单或者批单实体关联。
投保单、保单、批单、赔案等,这些术语虽然都跟保单有关,但不能将保单这个术语作用在保险全业务领域。因为术语有它的边界,超出了边界理解上就会出现问题。
如果你对我从事的保险业不大了解也没关系,电商肯定再熟悉不过了吧?
正如电商领域的商品一样,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。看到这,我想你应该非常清楚了,领域边界就是通过限界上下文来定义的。
限界上下文和微服务的关系
接下来,我们对这个概念做进一步的延伸。看看限界上下文和微服务具体存在怎样的关系。
我想你买过车险吧,或者听过吧。车险承保的流程包含了投保、缴费、出单等几个主要流程。如果出险了还会有报案、查勘、定损、理算等理赔流程。
保险领域还是很复杂的,在这里我用一个简化的保险模型来说明下限界上下文和微服务的关系。这里还会用到我们在 [第 02 讲] 学到一些基础知识,比如领域和子域。
首先,领域可以拆分为多个子领域。一个领域相当于一个问题域,领域拆分为子域的过程就是大问题拆分为小问题的过程。在这个图里面保险领域被拆分为:投保、支付、保单管理和理赔四个子域。
子域还可根据需要进一步拆分为子子域,比如,支付子域可继续拆分为收款和付款子子域。拆到一定程度后,有些子子域的领域边界就可能变成限界上下文的边界了。
子域可能会包含多个限界上下文,如理赔子域就包括报案、查勘和定损等多个限界上下文(限界上下文与理赔的子子域领域边界重合)。也有可能子域本身的边界就是限界上下文边界,如投保子域。
每个领域模型都有它对应的限界上下文,团队在限界上下文内用通用语言交流。领域内所有限界上下文的领域模型构成整个领域的领域模型。
理论上限界上下文就是微服务的边界。我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案。
可以说,限界上下文是微服务设计和拆分的主要依据。在领域模型中,如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
不过,这里还是要提示一下:除了理论,微服务的拆分还是有很多限制因素的,在设计中不宜过度拆分。那这个度怎么把握好呢?有关微服务设计和具体的拆分方法,我会在实战篇中详细讲解。
总结
通用语言确定了项目团队内部交流的统一语言,而这个语言所在的语义环境则是由限界上下文来限定的,以确保语义的唯一性。
而领域专家、架构师和开发人员的主要工作就是通过事件风暴来划分限界上下文。限界上下文确定了微服务的设计和拆分方向,是微服务设计和拆分的主要依据。如果不考虑技术异构、团队沟通等其它外部因素,一个限界上下文理论上就可以设计为一个微服务。
可以说,限界上下文在微服务设计中具有很重要的意义,如果限界上下文的方向偏离,那微服务的设计结果也就可想而知了。因此,我们只有理解了限界上下文的真正涵义以及它在微服务设计中的作用,才能真正发挥 DDD 的价值,这是基础也是前提。

View File

@ -0,0 +1,149 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 实体和值对象:从领域模型的基础单元看系统设计
你好,我是欧创新。今天我们来学习 DDD 战术设计中的两个重要概念:实体和值对象。
这两个概念都是领域模型中的领域对象。它们在领域模型中起什么作用,战术设计时如何将它们映射到代码和数据模型中去?就是我们这一讲重点要关注的问题。
另外,在战略设计向战术设计过渡的这个过程中,理解和区分实体和值对象在不同阶段的形态是很重要的,毕竟阶段不同,它们的形态也会发生变化,这与我们的设计和代码实现密切相关。
接下来,我们就分别看看实体和值对象的这些问题,从中找找答案。
实体
我们先来看一下实体是什么东西?
在 DDD 中有这样一类对象,它们拥有唯一标识符,且标识符在历经各种状态变更后仍能保持一致。对这些对象而言,重要的不是其属性,而是其延续性和标识,对象的延续性和标识会跨越甚至超出软件的生命周期。我们把这样的对象称为实体。没理解?没关系!请继续阅读。
1. 实体的业务形态
在 DDD 不同的设计过程中,实体的形态是不同的。在战略设计时,实体是领域模型的一个重要对象。领域模型中的实体是多个属性、操作或行为的载体。在事件风暴中,我们可以根据命令、操作或者事件,找出产生这些行为的业务实体对象,进而按照一定的业务规则将依存度高和业务关联紧密的多个实体对象和值对象进行聚类,形成聚合。你可以这么理解,实体和值对象是组成领域模型的基础单元。
2. 实体的代码形态
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。在 DDD 里,这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
3. 实体的运行形态
实体以 DO领域对象的形式存在每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改修改后的数据和原来的数据可能会大不相同。但是由于它们拥有相同的 ID它们依然是同一个实体。比如商品是商品上下文的一个实体通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。
4. 实体的数据库形态
与传统数据模型设计优先不同DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。
在领域模型映射到数据模型时,一个实体可能对应 0 个、1 个或者多个数据库持久化对象。大多数情况下实体与持久化对象是一对一。在某些场景中,有些实体只是暂驻静态内存的一个运行态实体,它不需要持久化。比如,基于多个价格配置数据计算后生成的折扣实体。
而在有些复杂场景下,实体与持久化对象则可能是一对多或者多对一的关系。比如,用户 user 与角色 role 两个持久化对象可生成权限实体,一个实体对应两个持久化对象,这是一对多的场景。再比如,有些场景为了避免数据库的联表查询,提升系统性能,会将客户信息 customer 和账户信息 account 两类数据保存到同一张数据库表中,客户和账户两个实体可根据需要从一个持久化对象中生成,这就是多对一的场景。
值对象
值对象相对实体来说,会更加抽象一些,概念上我们会结合例子来讲。
我们先看一下《实现领域驱动设计》一书中对值对象的定义:通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在 DDD 中用来描述领域的特定方面,并且是一个没有标识符的对象,叫作值对象。
也就说,值对象描述了领域中的一件东西,这个东西是不可变的,它将不同的相关属性组合成了一个概念整体。当度量和描述改变时,可以用另外一个值对象予以替换。它可以和其它值对象进行相等性比较,且不会对协作对象造成副作用。这部分在后面讲“值对象的运行形态”时还会有例子。
上面这两段对于定义的阐述,如果你还是觉得有些晦涩,我们不妨“翻译”一下,用更通俗的语言把定义讲清楚。
简单来说,值对象本质上就是一个集。那这个集合里面有什么呢?若干个用于描述目的、具有整体概念和不可修改的属性。那这个集合存在的意义又是什么?在领域建模的过程中,值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。
这里我举个简单的例子,请看下面这张图:
人员实体原本包括:姓名、年龄、性别以及人员所在的省、市、县和街道等属性。这样显示地址相关的属性就很零碎了对不对?现在,我们可以将“省、市、县和街道等属性”拿出来构成一个“地址属性集合”,这个集合就是值对象了。
1. 值对象的业务形态
值对象是 DDD 领域模型中的一个基础对象,它跟实体一样都来源于事件风暴所构建的领域模型,都包含了若干个属性,它与实体一起构成聚合。
我们不妨对照实体,来看值对象的业务形态,这样更好理解。本质上,实体是看得到、摸得着的实实在在的业务对象,实体具有业务属性、业务行为和业务逻辑。而值对象只是若干个属性的集合,只有数据初始化操作和有限的不涉及修改数据的行为,基本不包含业务逻辑。值对象的属性集虽然在物理上独立出来了,但在逻辑上它仍然是实体属性的一部分,用于描述实体的特征。
在值对象中也有部分共享的标准类型的值对象,它们有自己的限界上下文,有自己的持久化对象,可以建立共享的数据类微服务,比如数据字典。
2. 值对象的代码形态
值对象在代码中有这样两种形态。如果值对象是单一属性,则直接定义为实体类的属性;如果值对象是属性集合,则把它设计为 Class 类Class 将具有整体概念的多个属性归集到属性集合,这样的值对象没有 ID会被实体整体引用。
我们看一下下面这段代码person 这个实体有若干个单一属性的值对象,比如 Id、name 等属性;同时它也包含多个属性的值对象,比如地址 address。
3. 值对象的运行形态
实体实例化后的 DO 对象的业务属性和业务行为非常丰富,但值对象实例化的对象则相对简单和乏味。除了值对象数据初始化和整体替换的行为外,其它业务行为就很少了。
值对象嵌入到实体的话,有这样两种不同的数据格式,也可以说是两种方式,分别是属性嵌入的方式和序列化大对象的方式。
引用单一属性的值对象或只有一条记录的多属性值对象的实体,可以采用属性嵌入的方式嵌入。引用一条或多条记录的多属性值对象的实体,可以采用序列化大对象的方式嵌入。比如,人员实体可以有多个通讯地址,多个地址序列化后可以嵌入人员的地址属性。值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。
如果你对这两种方式不够了解,可以看看下面的例子。
案例 1以属性嵌入的方式形成的人员实体对象地址值对象直接以属性值嵌入人员实体中。
案例 2以序列化大对象的方式形成的人员实体对象地址值对象被序列化成大对象 Json 串后,嵌入人员实体中。
4. 值对象的数据库形态
DDD 引入值对象是希望实现从“数据建模为中心”向“领域建模为中心”转变,减少数据库表的数量和表与表之间复杂的依赖关系,尽可能地简化数据库设计,提升数据库性能。
如何理解用值对象来简化数据库设计呢?
传统的数据建模大多是根据数据库范式设计的,每一个数据库表对应一个实体,每一个实体的属性值用单独的一列来存储,一个实体主表会对应 N 个实体从表。而值对象在数据库持久化方面简化了设计,它的数据库设计大多采用非数据库范式,值对象的属性值和实体对象的属性值保存在同一个数据库实体表中。
举个例子,还是基于上述人员和地址那个场景,实体和数据模型设计通常有两种解决方案:第一是把地址值对象的所有属性都放到人员实体表中,创建人员实体,创建人员数据表;第二是创建人员和地址两个实体,同时创建人员和地址两张表。
第一个方案会破坏地址的业务涵义和概念完整性,第二个方案增加了不必要的实体和表,需要处理多个实体和表的关系,从而增加了数据库设计的复杂性。
那到底应该怎样设计,才能让业务含义清楚,同时又不让数据库变得复杂呢?
我们可以综合这两个方案的优势,扬长避短。在领域建模时,我们可以把地址作为值对象,人员作为实体,这样就可以保留地址的业务涵义和概念完整性。而在数据建模时,我们可以将地址的属性值嵌入人员实体数据库表中,只创建人员数据库表。这样既可以兼顾业务含义和表达,又不增加数据库的复杂度。
值对象就是通过这种方式,简化了数据库设计,总结一下就是:在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
另外,也有 DDD 专家认为,要想发挥对象的威力,就需要优先做领域建模,弱化数据库的作用,只把数据库作为一个保存数据的仓库即可。即使违反数据库设计原则,也不用大惊小怪,只要业务能够顺利运行,就没什么关系。
5. 值对象的优势和局限
值对象是一把双刃剑,它的优势是可以简化数据库设计,提升数据库性能。但如果值对象使用不当,它的优势就会很快变成劣势。“知彼知己,方能百战不殆”,你需要理解值对象真正适合的场景。
值对象采用序列化大对象的方法简化了数据库设计,减少了实体表的数量,可以简单、清晰地表达业务概念。这种设计方式虽然降低了数据库设计的复杂度,但却无法满足基于值对象的快速查询,会导致搜索值对象属性值变得异常困难。
值对象采用属性嵌入的方法提升了数据库的性能,但如果实体引用的值对象过多,则会导致实体堆积一堆缺乏概念完整性的属性,这样值对象就会失去业务涵义,操作起来也不方便。
所以,你可以对照着以上这些优劣势,结合你的业务场景,好好想一想了。那如果在你的业务场景中,值对象的这些劣势都可以避免掉,那就请放心大胆地使用值对象吧。
实体和值对象的关系
实体和值对象是微服务底层的最基础的对象,一起实现实体最基本的核心领域逻辑。
值对象和实体在某些场景下可以互换,很多 DDD 专家在这些场景下,其实也很难判断到底将领域对象设计成实体还是值对象?可以说,值对象在某些场景下有很好的价值,但是并不是所有的场景都适合值对象。你需要根据团队的设计和开发习惯,以及上面的优势和局限分析,选择最适合的方法。
关于值对象我还要多说几句。其实DDD 引入值对象还有一个重要的原因,就是到底领域建模优先还是数据建模优先?
DDD 提倡从领域模型设计出发,而不是先设计数据模型。前面讲过了,传统的数据模型设计通常是一个表对应一个实体,一个主表关联多个从表,当实体表太多的时候就很容易陷入无穷无尽的复杂的数据库设计,领域模型就很容易被数据模型绑架。可以说,值对象的诞生,在一定程度上,和实体是互补的。
我们还是以前面的图示为例:
在领域模型中人员是实体,地址是值对象,地址值对象被人员实体引用。在数据模型设计时,地址值对象可以作为一个属性集整体嵌入人员实体中,组合形成上图这样的数据模型;也可以以序列化大对象的形式加入到人员的地址属性中,前面表格有展示。
从这个例子中,我们可以看出,同样的对象在不同的场景下,可能会设计出不同的结果。有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候你就可以将地址设计为值对象,比如收货地址。而在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。
总结
今天我们主要学习了实体和值对象在 DDD 不同设计阶段的形态,以及它们从战略设计向战术设计演进过程中的设计方法。
这个过程是从业务模型向系统模型落地的过程,比较复杂,很考验你的设计能力,很多时候我们都要结合自己的业务场景,选择合适的方法来进行微服务设计。强调一点,我们不避讳传统的设计方法,毕竟适合自己的才是最好的。希望你能充分理解实体和值对象的概念和应用,将学到的知识复用,最终将适合自己业务的 DDD 设计方法纳入到架构体系,实现落地。

View File

@ -0,0 +1,95 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 聚合和聚合根:怎样设计聚合?
你好我是欧创新。今天我们来学习聚合Aggregate和聚合根AggregateRoot
我们先回顾下上一讲在事件风暴中我们会根据一些业务操作和行为找出实体Entity或值对象ValueObject进而将业务关联紧密的实体和值对象进行组合构成聚合再根据业务语义将多个聚合划定到同一个限界上下文Bounded Context并在限界上下文内完成领域建模。
那你知道为什么要在限界上下文和实体之间增加聚合和聚合根这两个概念吗?它们的作用是什么?怎么设计聚合?这就是我们这一讲重点要关注的问题。
聚合
在 DDD 中,实体和值对象是很基础的领域对象。实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述。但实体和值对象都只是个体化的对象,它们的行为表现出来的是个体的能力。
那聚合在其中起什么作用呢?
举个例子。社会是由一个个的个体组成的,象征着我们每一个人。随着社会的发展,慢慢出现了社团、机构、部门等组织,我们开始从个人变成了组织的一员,大家可以协同一致的工作,朝着一个最大的目标前进,发挥出更大的力量。
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
你可以这么理解,聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
聚合有一个聚合根和上下文边界,这个边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。按照这种方式设计出来的微服务很自然就是“高内聚、低耦合”的。
聚合在 DDD 分层架构里属于领域层,领域层包含了多个聚合,共同实现核心业务逻辑。聚合内实体以充血模型实现个体业务能力,以及业务逻辑的高内聚。跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现;而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成,这时你就可以用应用服务来组合这两个服务。
聚合根
聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。
传统数据模型中的每一个实体都是对等的,如果任由实体进行无控制地调用和数据修改,很可能会导致实体之间数据逻辑的不一致。而如果采用锁的方式则会增加软件的复杂度,也会降低系统的性能。
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。
怎样设计聚合?
DDD 领域建模通常采用事件风暴,它通常采用用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合。
下面我们以保险的投保业务场景为例,看一下聚合的构建过程主要都包括哪些步骤。
第 1 步:采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。
第 2 步:从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一 ID是否可以创建或修改其它对象是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。
第 3 步:根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出 1 个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。
第 4 步:在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。这里我需要说明一下:投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体。
第 5 步:多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。
这就是一个聚合诞生的完整过程了。
聚合的一些设计原则
我们不妨先看一下《实现领域驱动设计》一书中对聚合设计原则的描述,原文是有点不太好理解的,我来给你解释一下。
1. 在一致性边界内建模真正的不变条件。聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。
2. 设计小聚合。如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。
3. 通过唯一标识引用其它聚合。聚合之间是通过关联外部聚合根 ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。
4. 在边界之外使用最终一致性。聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦(相关内容我会在领域事件部分详解)。
5. 通过应用层实现跨聚合的服务调用。为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。
上面的这些原则是 DDD 的一些通用的设计原则,还是那句话:“适合自己的才是最好的。”在系统设计过程时,你一定要考虑项目的具体情况,如果面临使用的便利性、高性能要求、技术能力缺失和全局事务管理等影响因素,这些原则也并不是不能突破的,总之一切以解决实际问题为出发点。
总结
[第 04 讲] 和 [第 05 讲] 的内容,其实是有强关联的。我们不妨在这里总结下聚合、聚合根、实体和值对象它们之间的联系和区别。
聚合的特点:高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位,但我不建议你对微服务过度拆分。但在对性能有极致要求的场景中,聚合可以独立作为一个微服务,以满足版本的高频发布和极致的弹性伸缩能力。
一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。有了这个逻辑边界,在微服务架构演进时就可以以聚合为单位进行拆分和组合了,微服务的架构演进也就不再是一件难事了。
聚合根的特点:聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。
实体的特点:有 ID 标识,通过 ID 判断相等性ID 在聚合内唯一即可。状态可变,它依附于聚合根,其生命周期由聚合根管理。实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。
值对象的特点:无 ID不可变无生命周期用完即扔。值对象之间通过属性值判断相等性。它的核心本质是值是一组概念完整的属性组成的集合用于描述实体的状态和特征。值对象尽量只引用值对象。

View File

@ -0,0 +1,151 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 领域事件:解耦微服务的关键
你好我是欧创新。今天我们来聊一聊“领域事件Domain Event”。
在事件风暴Event Storming我们发现除了命令和操作等业务行为以外还有一种非常重要的事件这种事件发生后通常会导致进一步的业务操作在 DDD 中这种事件被称为领域事件。
这只是最简单的定义,并不能让我们真正理解它。那到底什么是领域事件?领域事件的技术实现机制是怎样的?这一讲,我们就重点解决这两个大的问题。
领域事件
领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。
举例来说的话,领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可能是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发发送缴费邮件通知操作;或者一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。
那如何识别领域事件呢?
很简单,和刚才讲的定义是强关联的。在做用户旅程或者场景分析时,我们要捕捉业务、需求人员或领域专家口中的关键词:“如果发生……,则……”“当做完……的时候,请通知……”“发生……时,则……”等。在这些场景中,如果发生某种事件后,会触发进一步的操作,那么这个事件很可能就是领域事件。
那领域事件为什么要用最终一致性,而不是传统 SOA 的直接调用的方式呢?
我们一起回顾一下 [第 05 讲] 讲到的聚合的一个设计原则:在边界之外使用最终一致性。一次事务最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的最终一致性。
领域事件驱动设计可以切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,这样可以实现领域模型的解耦,维护领域模型的独立性和数据的一致性。在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求强一致性,而是基于事件的最终一致性。
回到具体的业务场景,我们发现有的领域事件发生在微服务内的聚合之间,有的则发生在微服务之间,还有两者皆有的场景,一般来说跨微服务的领域事件处理居多。在微服务设计时不同领域事件的处理方式会不一样。
1. 微服务内的领域事件
当领域事件发生在微服务内的聚合之间,领域事件发生后完成事件实体构建和事件数据持久化,发布方聚合将事件发布到事件总线,订阅方接收事件数据完成后续业务操作。
微服务内大部分事件的集成,都发生在同一个进程内,进程自身可以很好地控制事务,因此不一定需要引入消息中间件。但一个事件如果同时更新多个聚合,按照 DDD“一次事务只更新一个聚合”的原则你就要考虑是否引入事件总线。但微服务内的事件总线可能会增加开发的复杂度因此你需要结合应用复杂度和收益进行综合考虑。
微服务内应用服务,可以通过跨聚合的服务编排和组合,以服务调用的方式完成跨聚合的访问,这种方式通常应用于实时性和数据一致性要求高的场景。这个过程会用到分布式事务,以保证发布方和订阅方的数据同时更新成功。
2. 微服务之间的领域事件
跨微服务的领域事件会在不同的限界上下文或领域模型之间实现业务协作,其主要目的是实现微服务解耦,减轻微服务之间实时服务访问的压力。
领域事件发生在微服务之间的场景比较多,事件处理的机制也更加复杂。跨微服务的事件可以推动业务流程或者数据在不同的子域或微服务间直接流转。
跨微服务的事件机制要总体考虑事件构建、发布和订阅、事件数据持久化、消息中间件,甚至事件数据持久化时还可能需要考虑引入分布式事务机制等。
微服务之间的访问也可以采用应用服务直接调用的方式,实现数据和服务的实时访问,弊端就是跨微服务的数据同时变更需要引入分布式事务,以确保数据的一致性。分布式事务机制会影响系统性能,增加微服务之间的耦合,所以我们还是要尽量避免使用分布式事务。
领域事件相关案例
我来给你介绍一个保险承保业务过程中有关领域事件的案例。
一个保单的生成,经历了很多子域、业务状态变更和跨微服务业务数据的传递。这个过程会产生很多的领域事件,这些领域事件促成了保险业务数据、对象在不同的微服务和子域之间的流转和角色转换。
在下面这张图中,我列出了几个关键流程,用来说明如何用领域事件驱动设计来驱动承保业务流程。
事件起点:客户购买保险 - 业务人员完成保单录入 - 生成投保单 - 启动缴费动作。
\1. 投保微服务生成缴费通知单,发布第一个事件:缴费通知单已生成,将缴费通知单数据发布到消息中间件。收款微服务订阅缴费通知单事件,完成缴费操作。缴费通知单已生成,领域事件结束。
\2. 收款微服务缴费完成后,发布第二个领域事件:缴费已完成,将缴费数据发布到消息中间件。原来的订阅方收款微服务这时则变成了发布方。原来的事件发布方投保微服务转换为订阅方。投保微服务在收到缴费信息并确认缴费完成后,完成投保单转成保单的操作。缴费已完成,领域事件结束。
\3. 投保微服务在投保单转保单完成后,发布第三个领域事件:保单已生成,将保单数据发布到消息中间件。保单微服务接收到保单数据后,完成保单数据保存操作。保单已生成,领域事件结束。
\4. 保单微服务完成保单数据保存后,后面还会发生一系列的领域事件,以并发的方式将保单数据通过消息中间件发送到佣金、收付费和再保等微服务,一直到财务,完后保单后续所有业务流程。这里就不详细说了。
总之,通过领域事件驱动的异步化机制,可以推动业务流程和数据在各个不同微服务之间的流转,实现微服务的解耦,减轻微服务之间服务调用的压力,提升用户体验。
领域事件总体架构
领域事件的执行需要一系列的组件和技术来支撑。我们来看一下这个领域事件总体技术架构图,领域事件处理包括:事件构建和发布、事件数据持久化、事件总线、消息中间件、事件接收和处理等。下面我们逐一讲一下。
1. 事件构建和发布
事件基本属性至少包括:事件唯一标识、发生时间、事件类型和事件源,其中事件唯一标识应该是全局唯一的,以便事件能够无歧义地在多个限界上下文中传递。事件基本属性主要记录事件自身以及事件发生背景的数据。
另外事件中还有一项更重要,那就是业务属性,用于记录事件发生那一刻的业务数据,这些数据会随事件传输到订阅方,以开展下一步的业务操作。
事件基本属性和业务属性一起构成事件实体,事件实体依赖聚合根。领域事件发生后,事件中的业务数据不再修改,因此业务数据可以以序列化值对象的形式保存,这种存储格式在消息中间件中也比较容易解析和获取。
为了保证事件结构的统一,我们还会创建事件基类 DomainEvent参考下图子类可以扩充属性和方法。由于事件没有太多的业务行为实现方法一般比较简单。
事件发布之前需要先构建事件实体并持久化。事件发布的方式有很多种,你可以通过应用服务或者领域服务发布到事件总线或者消息中间件,也可以从事件表中利用定时程序或数据库日志捕获技术获取增量事件数据,发布到消息中间件。
2. 事件数据持久化
事件数据持久化可用于系统之间的数据对账,或者实现发布方和订阅方事件数据的审计。当遇到消息中间件、订阅方系统宕机或者网络中断,在问题解决后仍可继续后续业务流转,保证数据的一致性。
事件数据持久化有两种方案,在实施过程中你可以根据自己的业务场景进行选择。
持久化到本地业务数据库的事件表中,利用本地事务保证业务和事件数据的一致性。
持久化到共享的事件数据库中。这里需要注意的是:业务数据库和事件数据库不在一个数据库中,它们的数据持久化操作会跨数据库,因此需要分布式事务机制来保证业务和事件数据的强一致性,结果就是会对系统性能造成一定的影响。
3. 事件总线 (EventBus)
事件总线是实现微服务内聚合之间领域事件的重要组件,它提供事件分发和接收等服务。事件总线是进程内模型,它会在微服务内聚合之间遍历订阅者列表,采取同步或异步的模式传递数据。事件分发流程大致如下:
如果是微服务内的订阅者(其它聚合),则直接分发到指定订阅者;
如果是微服务外的订阅者,将事件数据保存到事件库(表)并异步发送到消息中间件;
如果同时存在微服务内和外订阅者,则先分发到内部订阅者,将事件消息保存到事件库(表),再异步发送到消息中间件。
4. 消息中间件
跨微服务的领域事件大多会用到消息中间件,实现跨微服务的事件发布和订阅。消息中间件的产品非常成熟,市场上可选的技术也非常多,比如 KafkaRabbitMQ 等。
5. 事件接收和处理
微服务订阅方在应用层采用监听机制,接收消息队列中的事件数据,完成事件数据的持久化后,就可以开始进一步的业务处理。领域事件处理可在领域服务中实现。
领域事件运行机制相关案例
这里我用承保业务流程的缴费通知单事件,来给你解释一下领域事件的运行机制。这个领域事件发生在投保和收款微服务之间。发生的领域事件是:缴费通知单已生成。下一步的业务操作是:缴费。
事件起点:出单员生成投保单,核保通过后,发起生成缴费通知单的操作。
投保微服务应用服务,调用聚合中的领域服务 createPaymentNotice 和 createPaymentNoticeEvent分别创建缴费通知单、缴费通知单事件。其中缴费通知单事件类 PaymentNoticeEvent 继承基类 DomainEvent。
\2. 利用仓储服务持久化缴费通知单相关的业务和事件数据。为了避免分布式事务,这些业务和事件数据都持久化到本地投保微服务数据库中。
\3. 通过数据库日志捕获技术或者定时程序,从数据库事件表中获取事件增量数据,发布到消息中间件。这里说明:事件发布也可以通过应用服务或者领域服务完成发布。
\4. 收款微服务在应用层从消息中间件订阅缴费通知单事件消息主题,监听并获取事件数据后,应用服务调用领域层的领域服务将事件数据持久化到本地数据库中。
\5. 收款微服务调用领域层的领域服务 PayPremium完成缴费。
\6. 事件结束。
提示:缴费完成后,后续流程的微服务还会产生很多新的领域事件,比如缴费已完成、保单已保存等等。这些后续的事件处理基本上跟 16 的处理机制类似。
总结
今天我们主要讲了领域事件以及领域事件的处理机制。领域事件驱动是很成熟的技术,在很多分布式架构中得到了大量的使用。领域事件是 DDD 的一个重要概念,在设计时我们要重点关注领域事件,用领域事件来驱动业务的流转,尽量采用基于事件的最终一致,降低微服务之间直接访问的压力,实现微服务之间的解耦,维护领域模型的独立性和数据一致性。
除此之外,领域事件驱动机制可以实现一个发布方 N 个订阅方的模式,这在传统的直接服务调用设计中基本是不可能做到的。

View File

@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 DDD分层架构有效降低层与层之间的依赖
你好,我是欧创新。前面我们讲了 DDD 的一些重要概念以及领域模型的设计理念。今天我们来聊聊“DDD 分层架构”。
微服务架构模型有好多种例如整洁架构、CQRS 和六边形架构等等。每种架构模式虽然提出的时代和背景不同,但其核心理念都是为了设计出“高内聚低耦合”的架构,轻松实现架构演进。而 DDD 分层架构的出现,使架构边界变得越来越清晰,它在微服务架构模型中,占有非常重要的位置。
那 DDD 分层架构到底长什么样DDD 分层架构如何推动架构演进?我们该怎么转向 DDD 分层架构?这就是我们这一讲重点要解决的问题。
什么是 DDD 分层架构?
DDD 的分层架构在不断发展。最早是传统的四层架构后来四层架构有了进一步的优化实现了各层对基础层的解耦再后来领域层和应用层之间增加了上下文环境Context五层架构DCI就此形成了。
我们看一下上面这张图在最早的传统四层架构中基础层是被其它层依赖的它位于最核心的位置那按照分层架构的思想它应该就是核心但实际上领域层才是软件的核心所以这种依赖是有问题的。后来我们采用了依赖倒置Dependency inversion principle,DIP的设计优化了传统的四层架构实现了各层对基础层的解耦。
我们今天讲的 DDD 分层架构就是优化后的四层架构。在下面这张图中,从上到下依次是:用户接口层、应用层、领域层和基础层。那 DDD 各层的主要职责是什么呢?下面我来逐一介绍一下。
1.用户接口层
用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是:用户、程序、自动化测试和批处理脚本等等。
2.应用层
应用层是很薄的一层,理论上不应该有业务规则或逻辑,主要面向用例和流程相关的操作。但应用层又位于领域层之上,因为领域层包含多个聚合,所以它可以协调多个聚合的服务和领域对象完成服务编排和组合,协作完成业务操作。
此外,应用层也是微服务之间交互的通道,它可以调用其它微服务的应用服务,完成微服务之间的服务组合和编排。
这里我要提醒你一下:在设计和开发时,不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦,时间一长你的微服务就会演化为传统的三层架构,业务逻辑会变得混乱。
另外,应用服务是在应用层的,它负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,以粗粒度的服务通过 API 网关向前端发布。还有,应用服务还可以进行安全认证、权限校验、事务控制、发送或订阅领域事件等。
3.领域层
领域层的作用是实现企业核心业务逻辑,通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力,它用来表达业务概念、业务状态和业务规则。
领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象。
这里我要特别解释一下其中几个领域对象的关系,以便你在设计领域层的时候能更加清楚。首先,领域模型的业务逻辑主要是由实体和领域服务来实现的,其中实体会采用充血模型来实现所有与之相关的业务功能。其次,你要知道,实体和领域对象在实现业务逻辑上不是同级的,当领域中的某些功能,单一实体(或者值对象)不能实现时,领域服务就会出马,它可以组合聚合内的多个实体(或者值对象),实现复杂的业务逻辑。
4.基础层
基础层是贯穿所有层的,它的作用就是为其它各层提供通用的技术和基础服务,包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。
基础层包含基础服务,它采用依赖倒置设计,封装基础资源服务,实现应用层、领域层与基础层的解耦,降低外部资源变化对应用的影响。
比如说,在传统架构设计中,由于上层应用对数据库的强耦合,很多公司在架构演进中最担忧的可能就是换数据库了,因为一旦更换数据库,就可能需要重写大部分的代码,这对应用来说是致命的。那采用依赖倒置的设计以后,应用层就可以通过解耦来保持独立的核心业务逻辑。当数据库变更时,我们只需要更换数据库基础服务就可以了,这样就将资源变更对应用的影响降到了最低。
DDD 分层架构最重要的原则是什么?
在《实现领域驱动设计》一书中DDD 分层架构有一个重要的原则:每层只能与位于其下方的层发生耦合。
而架构根据耦合的紧密程度又可以分为两种:严格分层架构和松散分层架构。优化后的 DDD 分层架构模型就属于严格分层架构,任何层只能对位于其直接下方的层产生依赖。而传统的 DDD 分层架构则属于松散分层架构,它允许某层与其任意下方的层发生依赖。
那我们怎么选呢?综合我的经验,为了服务的可管理,我建议你采用严格分层架构。
在严格分层架构中,领域服务只能被应用服务调用,而应用服务只能被用户接口层调用,服务是逐层对外封装或组合的,依赖关系清晰。而在松散分层架构中,领域服务可以同时被应用层或用户接口层调用,服务的依赖关系比较复杂且难管理,甚至容易使核心业务逻辑外泄。
试想下,如果领域层中的某个服务发生了重大变更,那该如何通知所有调用方同步调整和升级呢?但在严格分层架构中,你只需要逐层通知上层服务就可以了。
DDD 分层架构如何推动架构演进?
领域模型不是一成不变的,因为业务的变化会影响领域模型,而领域模型的变化则会影响微服务的功能和边界。那我们该如何实现领域模型和微服务的同步演进呢?
1.微服务架构的演进
通过基础篇的讲解,我们知道:领域模型中对象的层次从内到外依次是:值对象、实体、聚合和限界上下文。
实体或值对象的简单变更,一般不会让领域模型和微服务发生大的变化。但聚合的重组或拆分却可以。这是因为聚合内业务功能内聚,能独立完成特定的业务逻辑。那聚合的重组或拆分,势必就会引起业务模块和系统功能的变化了。
这里我们可以以聚合为基础单元,完成领域模型和微服务架构的演进。聚合可以作为一个整体,在不同的领域模型之间重组或者拆分,或者直接将一个聚合独立为微服务。
我们结合上图,以微服务 1 为例,讲解下微服务架构的演进过程:
当你发现微服务 1 中聚合 a 的功能经常被高频访问,以致拖累整个微服务 1 的性能时,我们可以把聚合 a 的代码,从微服务 1 中剥离出来,独立为微服务 2。这样微服务 2 就可轻松应对高性能场景。
在业务发展到一定程度以后,你会发现微服务 2 的领域模型有了变化,聚合 d 会更适合放到微服务 1 的领域模型中。这时你就可以将聚合 d 的代码整体搬迁到微服务 1 中。如果你在设计时已经定义好了聚合之间的代码边界,这个过程不会太复杂,也不会花太多时间。
最后我们发现,在经历模型和架构演进后,微服务 1 已经从最初包含聚合 a、b、c演进为包含聚合 b、c、d 的新领域模型和微服务了。
你看,好的聚合和代码模型的边界设计,可以让你快速应对业务变化,轻松实现领域模型和微服务架构的演进。你可能还会想,那怎么实现聚合代码快速重组呢?别急,后面实战篇会详细讲解,这里我们先感知下大的实现流程。
2.微服务内服务的演进
在微服务内部,实体的方法被领域服务组合和封装,领域服务又被应用服务组合和封装。在服务逐层组合和封装的过程中,你会发现这样一个有趣的现象。
我们看下上面这张图。在服务设计时,你并不一定能完整预测有哪些下层服务会被多少个上层服务组装,因此领域层通常只提供一些原子服务,比如领域服务 a、b、c。但随着系统功能增强和外部接入越来越多应用服务会不断丰富。有一天你会发现领域服务 b 和 c 同时多次被多个应用服务调用了,执行顺序也基本一致。这时你可以考虑将 b 和 c 合并,再将应用服务中 b、c 的功能下沉到领域层演进为新的领域服务b+c。这样既减少了服务的数量也减轻了上层服务组合和编排的复杂度。
你看,这就是服务演进的过程,它是随着你的系统发展的,最后你会发现你的领域模型会越来越精炼,越来越能适应需求的快速变化。
三层架构如何演进到 DDD 分层架构?
综合前面的讲解,相信 DDD 分层架构的优势,你心里也有个谱了。我们不妨总结一下最最重要两点。
首先由于层间松耦合我们可以专注于本层的设计而不必关心其它层也不必担心自己的设计会影响其它层。可以说DDD 成功地降低了层与层之间的依赖。
其次,分层架构使得程序结构变得清晰,升级和维护更加容易。我们修改某层代码时,只要本层的接口参数不变,其它层可以不必修改。即使本层的接口发生变化,也只影响相邻的上层,修改工作量小且错误可以控制,不会带来意外的风险。
那我们该怎样转向 DDD 分层架构呢?不妨看看下面这个过程。
传统企业应用大多是单体架构,而单体架构则大多是三层架构。三层架构解决了程序内代码间调用复杂、代码职责不清的问题,但这种分层是逻辑概念,在物理上它是中心化的集中式架构,并不适合分布式微服务架构。
DDD 分层架构中的要素其实和三层架构类似,只是在 DDD 分层架构中,这些要素被重新归类,重新划分了层,确定了层与层之间的交互规则和职责边界。
我们看一下上面这张图,分析一下从三层架构向 DDD 分层架构演进的过程。
首先,你要清楚,三层架构向 DDD 分层架构演进,主要发生在业务逻辑层和数据访问层。
DDD 分层架构在用户接口层引入了 DTO给前端提供了更多的可使用数据和更高的展示灵活性。
DDD 分层架构对三层架构的业务逻辑层进行了更清晰的划分改善了三层架构核心业务逻辑混乱代码改动相互影响大的情况。DDD 分层架构将业务逻辑层的服务拆分到了应用层和领域层。应用层快速响应前端的变化,领域层实现领域模型的能力。
另外一个重要的变化发生在数据访问层和基础层之间。三层架构数据访问采用 DAO 方式DDD 分层架构的数据库等基础资源访问采用了仓储Repository设计模式通过依赖倒置实现各层对基础资源的解耦。
仓储又分为两部分仓储接口和仓储实现。仓储接口放在领域层中仓储实现放在基础层。原来三层架构通用的第三方工具包、驱动、Common、Utility、Config 等通用的公共的资源类统一放到了基础层。
最后,我想说,传统三层架构向 DDD 分层架构的演进,体现的正是领域驱动设计思想的演进。希望你也感受到了,并尝试将其应用在自己的架构设计中。
总结
今天我们主要讲了 DDD 的分层架构,它作为微服务的核心框架,我想怎么强调其重要性都是不过分的。
DDD 分层架构包含用户接口层、应用层、领域层和基础层。通过这些层次划分,我们可以明确微服务各层的职能,划定各领域对象的边界,确定各领域对象的协作方式。这种架构既体现了微服务设计和架构演进的需求,又很好地融入了领域模型的概念,二者无缝结合,相信会给你的微服务设计带来不一样的感觉。

View File

@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 微服务架构模型:几种常见模型的对比和分析
你好,我是欧创新。
在上一讲中我重点介绍了 DDD 分层架构,同时我也提到了微服务架构模型其实还有好多种,不知道你注意到了没?这些架构模型在我们的实际应用中都具有很高的借鉴价值。
那么今天我们就把 DDD 分层架构(详情介绍如有遗忘可回看 [第 07 讲] )、整洁架构、六边形架构这三种架构模型放到一起,对比分析,看看如何利用好它们,帮助我们设计出高内聚低耦合的中台以及微服务架构。
整洁架构
整洁架构又名“洋葱架构”。为什么叫它洋葱架构?看看下面这张图你就明白了。整洁架构的层就像洋葱片一样,它体现了分层的设计思想。
在整洁架构里,同心圆代表应用软件的不同部分,从里到外依次是领域模型、领域服务、应用服务和最外围的容易变化的内容,比如用户界面和基础设施。
整洁架构最主要的原则是依赖原则,它定义了各层的依赖关系,越往里依赖越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的任何情况。
在洋葱架构中,各层的职能是这样划分的:
领域模型实现领域内核心业务逻辑,它封装了企业级的业务规则。领域模型的主体是实体,一个实体可以是一个带方法的对象,也可以是一个数据结构和方法集合。
领域服务实现涉及多个实体的复杂业务逻辑。
应用服务实现与用户操作相关的服务组合与编排,它包含了应用特有的业务流程规则,封装和实现了系统所有用例。
最外层主要提供适配的能力,适配能力分为主动适配和被动适配。主动适配主要实现外部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。
红圈内的领域模型、领域服务和应用服务一起组成软件核心业务能力。
六边形架构
六边形架构又名“端口适配器架构”。追溯微服务架构的渊源,一般都会涉及到六边形架构。
六边形架构的核心理念是:应用是通过端口与外部进行交互的。我想这也是微服务架构下 API 网关盛行的主要原因吧。
也就是说,在下图的六边形架构中,红圈内的核心业务逻辑(应用程序和领域模型)与外部资源(包括 APP、Web 应用以及数据库资源等)完全隔离,仅通过适配器进行交互。它解决了业务逻辑与用户界面的代码交错问题,很好地实现了前后端分离。六边形架构各层的依赖关系与整洁架构一样,都是由外向内依赖。
六边形架构将系统分为内六边形和外六边形两层,这两层的职能划分如下:
红圈内的六边形实现应用的核心业务逻辑;
外六边形完成外部应用、驱动和基础资源等的交互和访问,对前端应用以 API 主动适配的方式提供服务,对基础资源以依赖倒置被动适配的方式实现资源访问。
六边形架构的一个端口可能对应多个外部系统,不同的外部系统也可能会使用不同的适配器,由适配器负责协议转换。这就使得应用程序能够以一致的方式被用户、程序、自动化测试和批处理脚本使用。
三种微服务架构模型的对比和分析
虽然 DDD 分层架构、整洁架构、六边形架构的架构模型表现形式不一样,但你不要被它们的表象所迷惑,这三种架构模型的设计思想正是微服务架构高内聚低耦合原则的完美体现,而它们身上闪耀的正是以领域模型为中心的设计思想。
我们看下上面这张图,结合图示对这三种架构模型做一个分析。
请你重点关注图中的红色线框,它们是非常重要的分界线,这三种架构里面都有,它的作用就是将核心业务逻辑与外部应用、基础资源进行隔离。
红色框内部主要实现核心业务逻辑,但核心业务逻辑也是有差异的,有的业务逻辑属于领域模型的能力,有的则属于面向用户的用例和流程编排能力。按照这种功能的差异,我们在这三种架构中划分了应用层和领域层,来承担不同的业务逻辑。
领域层实现面向领域模型,实现领域模型的核心业务逻辑,属于原子模型,它需要保持领域模型和业务逻辑的稳定,对外提供稳定的细粒度的领域服务,所以它处于架构的核心位置。
应用层实现面向用户操作相关的用例和流程,对外提供粗粒度的 API 服务。它就像一个齿轮一样进行前台应用和领域层的适配,接收前台需求,随时做出响应和调整,尽量避免将前台需求传导到领域层。应用层作为配速齿轮则位于前台应用和领域层之间。
可以说,这三种架构都考虑了前端需求的变与领域模型的不变。需求变幻无穷,但变化总是有矩可循的,用户体验、操作习惯、市场环境以及管理流程的变化,往往会导致界面逻辑和流程的多变。但总体来说,不管前端如何变化,在企业没有大的变革的情况下,核心领域逻辑基本不会大变,所以领域模型相对稳定,而用例和流程则会随着外部应用需求而随时调整。把握好这个规律,我们就知道该如何设计应用层和领域层了。
架构模型通过分层的方式来控制需求变化从外到里对系统的影响,从外向里受需求影响逐步减小。面向用户的前端可以快速响应外部需求进行调整和发布,灵活多变,应用层通过服务组合和编排来实现业务流程的快速适配上线,减少传导到领域层的需求,使领域层保持长期稳定。
这样设计的好处很明显了,就是可以保证领域层的核心业务逻辑不会因为外部需求和流程的变动而调整,对于建立前台灵活、中台稳固的架构很有帮助。
看到这里,你是不是已经猜出中台和微服务设计的关键了呢?我给出的答案是:领域模型和微服务的合理分层设计。那么你的答案呢?
从三种架构模型看中台和微服务设计
结合这三种微服务架构模型的共性,下面我来谈谈中台和微服务设计的一些心得体会。
中台本质上是领域的子域,它可能是核心域,也可能是通用域或支撑域。通常大家认为阿里的中台对应 DDD 的通用域,将通用的公共能力沉淀为中台,对外提供通用共享服务。
中台作为子域还可以继续分解为子子域在子域分解到合适大小通过事件风暴划分限界上下文以后就可以定义微服务了微服务用来实现中台的能力。表面上看DDD、中台、微服务这三者之间似乎没什么关联实际上它们的关系是非常紧密的组合在一起可以作为一个理论体系用于你的中台和微服务设计。
1. 中台建设要聚焦领域模型
中台需要站在全企业的高度考虑能力的共享和复用。
中台设计时我们需要建立中台内所有限界上下文的领域模型DDD 建模过程中会考虑架构演进和功能的重新组合。领域模型建立的过程会对业务和应用进行清晰的逻辑和物理边界(微服务)划分。领域模型的结果会影响到后续的系统模型、架构模型和代码模型,最终影响到微服务的拆分和项目落地。
因此,在中台设计中我们首先要聚焦领域模型,将它放在核心位置。
2. 微服务要有合理的架构分层
微服务设计要有分层的设计思想,让各层各司其职,建立松耦合的层间关系。
不要把与领域无关的逻辑放在领域层实现,保证领域层的纯洁和领域逻辑的稳定,避免污染领域模型。也不要把领域模型的业务逻辑放在应用层,这样会导致应用层过于庞大,最终领域模型会失焦。如果实在无法避免,我们可以引入防腐层,进行新老系统的适配和转换,过渡期完成后,可以直接将防腐层代码抛弃。
微服务内部的分层方式我们已经清楚了,那微服务之间是否也有层次依赖关系呢?如何实现微服务之间的服务集成?
有的微服务可以与前端应用集成,一起完成特定的业务,这是项目级微服务。而有的则是某个职责单一的中台微服务,企业级的业务流程需要将多个这样的微服务组合起来才能完成,这是企业级中台微服务。两类微服务由于复杂度不一样,集成方式也会有差异。
项目级微服务
项目级微服务的内部遵循分层架构模型就可以了。领域模型的核心逻辑在领域层实现,服务的组合和编排在应用层实现,通过 API 网关为前台应用提供服务,实现前后端分离。但项目级的微服务可能会调用其它微服务,你看在下面这张图中,比如某个项目级微服务 B 调用认证微服务 A完成登录和权限认证。
通常项目级微服务之间的集成,发生在微服务的应用层,由应用服务调用其它微服务发布在 API 网关上的应用服务。你看下图中微服务 B 中红色框内的应用服务 B它除了可以组合和编排自己的领域服务外还可以组合和编排外部微服务的应用服务。它只要将编排后的服务发布到 API 网关供前端调用,这样前端就可以直接访问自己的微服务了。
企业级中台微服务
企业级的业务流程往往是多个中台微服务一起协作完成的,那跨中台的微服务如何实现集成呢?
企业级中台微服务的集成不能像项目级微服务一样,在某一个微服务内完成跨微服务的服务组合和编排。
我们可以在中台微服务之上增加一层,你看下面这张图,增加的这一层就位于红色框内,它的主要职能就是处理跨中台微服务的服务组合和编排,以及微服务之间的协调,它还可以完成前端不同渠道应用的适配。如果再将它的业务范围扩大一些,我可以将它做成一个面向不同行业和渠道的服务平台。
我们不妨借用 BFF服务于前端的后端Backend for Frontends这个词暂且称它为 BFF 微服务。BFF 微服务与其它微服务存在较大的差异就是它没有领域模型因此这个微服务内也不会有领域层。BFF 微服务可以承担应用层和用户接口层的主要职能,完成各个中台微服务的服务组合和编排,可以适配不同前端和渠道的要求。
3. 应用和资源的解耦与适配
传统以数据为中心的设计模式,应用会对数据库、缓存、文件系统等基础资源产生严重依赖。
正是由于它们之间的这种强依赖的关系,我们一旦更换基础资源就会对应用产生很大的影响,因此需要为应用和资源解耦。
在微服务架构中,应用层、领域层和基础层解耦是通过仓储模式,采用依赖倒置的设计方法来实现的。在应用设计中,我们会同步考虑和基础资源的代码适配,那么一旦基础设施资源出现变更(比如换数据库),就可以屏蔽资源变更对业务代码的影响,切断业务逻辑对基础资源的依赖,最终降低资源变更对应用的影响。
总结
今天我们详细讲解了整洁架构和六边形架构,并对包括 DDD 分层架构在内的三种微服务架构模进行对比分析,总结出了它们的共同特征,并从共性出发,梳理出了中台建模和微服务架构设计的几个要点,我们后面还会有更加详细的有关设计落地的讲述。
那从今天的内容中我们不难看出DDD 分层架构、整洁架构、六边形架构都是以领域模型为核心,实行分层架构,内部核心业务逻辑与外部应用、资源隔离并解耦。请务必记好这个设计思想,今后会有大用处。

View File

@ -0,0 +1,141 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 中台:数字转型后到底应该共享什么?
你好,我是欧创新。
在上一讲中我们了解了分层架构的设计思想,并提到了这种设计思想对中台建设十分有利,那么今天我就来讲一讲中台。
中台是数字化转型的一个热门话题。继阿里提出中台概念后,很多人又提出了各种各样的中台。今天我们主要讨论业务中台和数据中台。作为企业数字化中台转型的整体,我也会顺带聊一聊前台和后台的一些设计思路。
不少企业其实在很多年前就有了建大平台的实践经验,那在中台被热议时,我相信你一定听过很多质疑声。比如,有人说:“中台就是个怪名词,它不就是已经做了好多年的平台吗?”确实,中台源于平台,但它的战略高度要比平台高很多。
学完这一讲,你就会清楚地知道平台与中台的差异在什么地方?中台到底是什么?传统企业的中台建设方式是否应该和阿里一样…
平台到底是不是中台?
阿里提出中台战略后,很多企业开始拿着自己的系统与阿里的中台对标。有的企业在十多年前就完成了大一统的集中式系统拆分,实现了从传统大单体应用向大平台的演进,他们将公共能力和核心能力分开建设,解决了公共模块重复投入和重复建设的问题。
那这是不是阿里所说的中台呢?在回答这个问题之前,我们不妨先了解一下阿里的中台到底是什么样的。
阿里业务中台的前身是共享平台,而原来的共享平台更多的被当作资源团队,他们承接各业务方的需求,并为业务方在基础服务上做定制开发。 阿里业务中台的目标是把核心服务链路(会员、商品、交易、营销、店铺、资金结算等)整体当作一个平台产品来做,为前端业务提供的是业务解决方案,而不是彼此独立的系统。
下面我们分析一下传统企业大平台战略和阿里中台战略的差异。
平台只是将部分通用的公共能力独立为共享平台。虽然可以通过 API 或者数据对外提供公共共享服务,解决系统重复建设的问题,但这类平台并没有和企业内的其它平台或应用,实现页面、业务流程和数据从前端到后端的全面融合,并且没有将核心业务服务链路作为一个整体方案考虑,各平台仍然是分离且独立的。
平台解决了公共能力复用的问题,但离中台的目标显然还有一段差距!
中台到底是什么?
“一千个读者就有一千个哈姆雷特”,这句话形容技术圈对中台的定义再合适不过了,说法很多。
先看一下阿里自己人对中台的定义:“中台是一个基础的理念和架构,我们要把所有的基础服务用中台的思路建设,进行联通,共同支持上端的业务。业务中台更多的是支持在线业务,数据中台提供了基础数据处理能力和很多的数据产品给所有业务方去用。业务中台、数据中台、算法中台等等一起提供对上层业务的支撑。”
再看一下思特沃克对中台的定义:“中台是企业级能力复用平台。”
综上,我们可以提炼出几个关于中台的关键词:共享、联通、融合和创新。联通是前台以及中台之间的联通,融合是前台流程和数据的融合,并以共享的方式支持前端一线业务的发展和创新。
我认为,中台首先体现的是一种企业级的能力,它提供的是一套企业级的整体解决方案,解决小到企业、集团,大到生态圈的能力共享、联通和融合问题,支持业务和商业模式创新。通过平台联通和数据融合为用户提供一致的体验,更敏捷地支撑前台一线业务。
中台来源于平台,但中台和平台相比,它更多体现的是一种理念的转变,它主要体现在这三个关键能力上:对前台业务的快速响应能力;企业级复用能力;从前台、中台到后台的设计、研发、页面操作、流程服务和数据的无缝联通、融合能力。
其中最关键的是快速响应能力和企业级的无缝联通和融合能力,尤其是对于跨业经营的超大型企业来说至关重要。
数字化转型中台应该共享什么?
相对互联网企业而言,传统企业的渠道应用更多样化,有面向内部人员的门店类应用、面向外部用户的互联网电商以及移动 APP 类应用。这些应用面向的用户和场景可能不同,但其功能类似,基本涵盖了核心业务能力。此外,传统企业也会将部分核心应用的页面或 API 服务能力开放给生态圈第三方,相互借力发展。
为了适应不同业务和渠道的发展,过去很多企业的做法是开发很多独立的应用或 APP。但由于 IT 系统建设初期并没有企业级的整体规划,平台之间融合不好,就导致了用户体验不好,最关键的是用户并不想装那么多 APP。
为了提升用户体验,实现统一运营,很多企业开始缩减 APP 的数量,开始通过一个 APP 集成企业内的所有能力,联通前台所有的核心业务链路。
由于传统企业的商业模式和 IT 系统建设发展的历程与互联网企业不是完全一样的,因此传统企业的中台建设策略与阿里中台战略也应该有所差异,需要共享的内容也不一样。
由于渠道多样化,传统企业不仅要将通用能力中台化,以实现通用能力的沉淀、共享和复用,这里的通用能力对应 DDD 的通用域或支撑域;传统企业还需要将核心能力中台化,以满足不同渠道的核心业务能力共享和复用的需求,避免传统核心和互联网不同渠道应用出现“后端双核心、前端两张皮”的问题,这里的核心能力对应 DDD 的核心域。
这就属于业务中台的范畴了,我们需要解决核心业务链路的联通和不同渠道服务共享的问题。除此之外,我们还需要解决系统微服务拆分后的数据孤岛、数据融合和业务创新等问题,这就属于数据中台的范畴了,尤其是当我们采用分布式架构以后,我们就更应该关注微服务拆分后的数据融合和共享问题了。
综上,在中台设计和规划时,我们需要整体考虑企业内前台、中台以及后台应用的协同,实现不同渠道应用的前端页面、流程和服务的共享,还有核心业务链路的联通以及前台流程和数据的融合、共享,支持业务和商业模式的创新。
如何实现前中后台的协同?
企业级能力往往是前中后台协同作战能力的体现。
如果把业务中台比作陆军、火箭军和空军等专业军种的话,它主要发挥战术专业能力。前台就是作战部队,它需要根据前线的战场需求,对业务中台的能力进行调度,实现能力融合和效率最大化。而数据中台就是信息情报中心和联合作战总指挥部,它能够汇集各种数据、完成分析,制定战略和战术计划。后台就是后勤部队,提供技术支持。下面我们分别来说说。
1. 前台
传统企业的早期系统有不少是基于业务领域或组织架构来建设的,每个系统都有自己的前端,相互独立,用户操作是竖井式,需要登录多个系统才能完成完整的业务流程。
中台后的前台建设要有一套综合考虑业务边界、流程和平台的整体解决方案,以实现各不同中台前端操作、流程和界面的联通、融合。不管后端有多少个中台,前端用户感受到的就是只有一个前台。
在前台设计中我们可以借鉴微前端的设计思想,在企业内不仅实现前端解耦和复用,还可以根据核心链路和业务流程,通过对微前端页面的动态组合和流程编排,实现前台业务的融合。
前端页面可以很自然地融合到不同的终端和渠道应用核心业务链路中,实现前端页面、流程和功能复用。
2. 中台
传统企业的核心业务大多是基于集中式架构开发的,而单体系统存在扩展性和弹性伸缩能力差的问题,因此无法适应忽高忽低的互联网业务场景。而数据类应用也多数通过 ETL 工具抽取数据实现数据建模、统计和报表分析功能,但由于数据时效和融合能力不够,再加上传统数据类应用本来就不是为前端而生的,因此难以快速响应前端一线业务。
业务中台的建设可采用领域驱动设计方法,通过领域建模,将可复用的公共能力从各个单体剥离,沉淀并组合,采用微服务架构模式,建设成为可共享的通用能力中台。
同样的,我们可以将核心能力用微服务架构模式,建设成为可面向不同渠道和场景的可复用的核心能力中台。 业务中台向前台、第三方和其它中台提供 API 服务,实现通用能力和核心能力的复用。
但你需要记住这一点:在将传统集中式单体按业务职责和能力细分为微服务,建设中台的过程中,会产生越来越多的独立部署的微服务。这样做虽然提升了应用弹性和高可用能力,但由于微服务的物理隔离,原来一些系统内的调用会变成跨微服务调用,再加上前后端分离,微服务拆分会导致数据进一步分离,增加企业级应用集成的难度。
如果没有合适的设计和指导思想,处理不好前台、中台和后台的关系,将会进一步加剧前台流程和数据的孤岛化、碎片化。
数据中台的主要目标是打通数据孤岛,实现业务融合和创新,包括三大主要职能:
一是完成企业全域数据的采集与存储,实现各不同业务类别中台数据的汇总和集中管理。
二是按照标准的数据规范或数据模型,将数据按照不同主题域或场景进行加工和处理,形成面向不同主题和场景的数据应用,比如客户视图、代理人视图、渠道视图、机构视图等不同数据体系。
三是建立业务需求驱动的数据体系,基于各个维度的数据,深度萃取数据价值,支持业务和商业模式的创新。
相应的,数据中台的建设就可分为三步走:
第一步实现各中台业务数据的汇集,解决数据孤岛和初级数据共享问题。
第二步实现企业级实时或非实时全维度数据的深度融合、加工和共享。
第三步萃取数据价值,支持业务创新,加速从数据转换为业务价值的过程。
数据中台不仅限于分析型场景,也适用于交易型场景。它可以建立在数据仓库或数据平台之上,将数据服务化之后提供给业务系统。基于数据库日志捕获的技术,使数据的时效性大大提升,这样就可以为交易型场景提供很好的支撑。
综上,数据中台主要完成数据的融合和加工,萃取数据业务价值,支持业务创新,对外提供数据共享服务。
3. 后台
很多人提到中台时自然会问:“既然有前台和中台,那是否有后台,后台的职责又是什么?”
我们来看一下阿里对前台、中台和后台的定位。
前台主要面向客户以及终端销售者,实现营销推广以及交易转化;中台主要面向运营人员,完成运营支撑;后台主要面向后台管理人员,实现流程审核、内部管理以及后勤支撑,比如采购、人力、财务和 OA 等系统。
那对于后台,为了实现内部的管理要求,很多人习惯性将这些管理要求嵌入到核心业务流程中。而一般来说这类内控管理需求对权限、管控规则和流程等要求都比较高,但是大部分管理人员只是参与了某个局部业务环节的审核。这类复杂的管理需求,会凭空增加不同渠道应用前台界面和核心流程的融合难度以及软件开发的复杂度。
在设计流程审核和管理类功能的时候,我们可以考虑按角色或岗位进行功能聚合,将复杂的管理需求从通用的核心业务链路中剥离,参考小程序的建设模式,通过特定程序入口嵌入前台 APP 或应用中。
管理需求从前台核心业务链路剥离后,前台应用将具有更好的通用性,它可以更加容易地实现各渠道前台界面和流程的融合。一个前台应用或 APP 可以无差别地同时面向外部互联网用户和内部业务人员,从而促进传统渠道与互联网渠道应用前台的融合。
总结
今天我们主要讨论了中台建设的一些思路。企业的中台转型不只是中台的工作,我们需要整体考虑前台、中台和后台的协同、共享、联通和融合。
前台通过页面和流程共享实现不同渠道应用之间的前台融合,中台通过 API 实现服务共享。而前台、业务中台和数据中台的融合可以实现传统应用与互联网应用的融合,从而解决“后端双核心、前端两张皮”的问题。能力复用了,前台流程和数据融合了,才能更好地支持业务的融合和商业模式的创新。

View File

@ -0,0 +1,103 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 DDD、中台和微服务它们是如何协作的
你好,我是欧创新。今天我一起来聊聊 DDD、中台和微服务的关系。
DDD 和微服务来源于西方而中台诞生于中国的阿里巴巴。DDD 在二十多年前提出后一直默默前行中台和微服务的理念近几年才出现提出后就非常火爆。这三者看似风马牛不相及实则缘分匪浅。中台是抽象出来的业务模型微服务是业务模型的系统实现DDD 作为方法论可以同时指导中台业务建模和微服务建设,三者相辅相成,完美结合。
你可能会问:凭什么 DDD 可以指导中台和微服务建设,究竟起到了什么作用呢?
DDD 有两把利器,那就是它的战略设计和战术设计方法。
中台在企业架构上更多偏向业务模型,形成中台的过程实际上也是业务领域不断细分的过程。在这个过程中我们会将同类通用的业务能力进行聚合和业务重构,再根据限界上下文和业务内聚的原则建立领域模型。而 DDD 的战略设计最擅长的就是领域建模。
那在中台完成领域建模后我们就需要通过微服务来完成系统建设。此时DDD 的战术设计又恰好可以与微服务的设计完美结合。可以说,中台和微服务正是 DDD 实战的最佳场景。
DDD 的本质
我们先简单回顾一下 DDD 领域、子域、核心域、通用域和支撑域等概念,后面会用到。
在研究和解决业务问题时DDD 会按照一定的规则将业务领域进行细分领域细分到一定的程度后DDD 会将问题范围限定在特定的边界内,并在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。领域可分解为子域,子域可继续分为子子域,一直到你认为适合建立领域模型为止。
子域还会根据自身重要性和功能属性划分为三类子域,它们分别是核心域、支撑域和通用域。关于这三类子域更为详细的讲解,你可以回看[第 02 讲]。
接下来我们一起看下上面这张图,我选择了保险的几个重要领域,进行了高阶的领域划分。当然每个企业的领域定位和职责会有些不一样,那在核心域的划分上肯定会有一定差异。因此,当你去做领域划分的时候,请务必结合企业战略,这恰恰也体现了 DDD 领域建模的重要性。
通过领域划分和进一步的子域划分,我们就可以区分不同子域在企业内的功能属性和重要性,进而采取不同的资源投入和建设策略,这在企业 IT 系统的建设过程中十分重要,并且这样的划分还可以帮助企业进行中台设计。
中台的本质
中台来源于阿里的中台战略(详见《企业 IT 架构转型之道阿里巴巴中台战略思想与架构实战》钟华编著。2015 年年底,阿里巴巴集团对外宣布全面启动中台战略,构建符合数字时代的更具创新性、灵活性的“大中台、小前台”组织机制和业务机制,即作为前台的一线业务会更敏捷、更快速地适应瞬息万变的市场,而中台将集合整个集团的运营数据能力、产品技术能力,对各前台业务形成强力支撑。
中台的本质其实就是提炼各个业务板块的共同需求,进行业务和系统抽象,形成通用的可复用的业务模型,打造成组件化产品,供前台部门使用。前台要做什么业务,需要什么资源,可以直接找中台,不需要每次都去改动自己的底层。
DDD、中台和微服务的协作模式
我们在 [第 09 讲] 已经说过了传统企业和阿里中台战略的差异,那实际上更多的企业还是会聚焦在传统企业中台建设的模式,也就是将通用能力与核心能力全部中台化,以满足不同渠道核心业务能力的复用,那么接下来我们就还是把重点放在传统企业上。
传统企业可以将需要共享的公共能力进行领域建模,建设可共享的通用中台。除此之外,传统企业还会将核心能力进行领域建模,建设面向不同渠道的可复用的核心中台。
而这里的通用中台和核心中台都属于我们上一讲讲到的业务中台的范畴。
DDD 的子域分为核心域、通用域和支撑域。划分这几个子域的主要目的是为了确定战略资源的投入,一般来说战略投入的重点是核心域,因此后面我们就可以暂时不严格区分支撑域和通用域了。
领域、中台以及微服务虽然属于不同层面的东西,但我们还是可以将他们分解对照,整理出来它们之间的关系。你看下面这张图,我是从 DDD 领域建模和中台建设这两个不同的视角对同一个企业的业务架构进行分析。
如果将企业内整个业务域作为一个问题域的话,企业内的所有业务就是一个领域。在进行领域细分时,从 DDD 视角来看,子域可分为核心域、通用域和支撑域。从中台建设的视角来看,业务域细分后的业务中台,可分为核心中台和通用中台。
从领域功能属性和重要性对照来看,通用中台对应 DDD 的通用域和支撑域,核心中台对应 DDD 的核心域。从领域的功能范围来看,子域与中台是一致的。领域模型所在的限界上下文对应微服务。建立了这个映射关系,我们就可以用 DDD 来进行中台业务建模了。
我们这里还是以保险领域为例。保险域的业务中台分为两类:第一类是提供保险核心业务能力的核心中台(比如营销、承保和理赔等业务);第二类是支撑核心业务流程完成保险全流程的通用中台(比如订单、支付、客户和用户等)。
这里我要提醒你一下:根据 DDD 首先要建立通用语言的原则,在将 DDD 的方法引入中台设计时,我们要先建立中台和 DDD 的通用语言。这里的子域与中台是一致的,那我们就可以将子域统一为中台。
中台通过事件风暴可以进一步细分,最终完成业务领域建模。中台业务领域的功能不同,限界上下文的数量和大小就会不一样,领域模型也会不一样。
当完成业务建模后,我们就可以采用 DDD 战术设计,设计出聚合、实体、领域事件、领域服务以及应用服务等领域对象,再利用分层架构模型完成微服务的设计。
以上就是 DDD、中台和微服务在应用过程中的协作模式。
中台如何建模?
看完了三者的协作模式,我们就顺着上面的话题,接着来聊聊中台如何建模。
中台业务抽象的过程就是业务建模的过程,对应 DDD 的战略设计。系统抽象的过程就是微服务的建设过程,对应 DDD 的战术设计。下面我们就结合 DDD 领域建模的方法,讲一下中台业务建模的过程。
第一步:按照业务流程(通常适用于核心域)或者功能属性、集合(通常适用于通用域或支撑域),将业务域细分为多个中台,再根据功能属性或重要性归类到核心中台或通用中台。核心中台设计时要考虑核心竞争力,通用中台要站在企业高度考虑共享和复用能力。
第二步:选取中台,根据用例、业务场景或用户旅程完成事件风暴,找出实体、聚合和限界上下文。依次进行领域分解,建立领域模型。
由于不同中台独立建模,某些领域对象或功能可能会重复出现在其它领域模型中,也有可能本该是同一个聚合的领域对象或功能,却分散在其它的中台里,这样会导致领域模型不完整或者业务不内聚。这里先不要着急,这一步我们只需要初步确定主领域模型就可以了,在第三步中我们还会提炼并重组这些领域对象。
第三步:以主领域模型为基础,扫描其它中台领域模型,检查并确定是否存在重复或者需要重组的领域对象、功能,提炼并重构主领域模型,完成最终的领域模型设计。
第四步:选择其它主领域模型重复第三步,直到所有主领域模型完成比对和重构。
第五步:基于领域模型完成微服务设计,完成系统落地。
结合上面这张图,你可以大致了解到 DDD 中台设计的过程。DDD 战略设计包括上述的第一步到第四步主要为业务域分解为中台对中台归类完成领域建模建立中台业务模型。DDD 战术设计是第五步,领域模型映射为微服务,完成中台建设。
那么如果还是以保险领域为例的话,完成领域建模后,里面的数据我们就可以填上了。这里我选取了通用中台的用户、客户和订单三个中台来做示例。客户中台提炼出了两个领域模型:客户信息和客户视图模型。用户中台提炼出了三个领域模型:用户管理、登录认证和权限模型。订单中台提炼出了订单模型。
这就是中台建模的全流程,当然看似简单的背后,若是遇上复杂的业务总会出现各种各样的问题,不然应用起来也不会有那么多的困难。如果你在按照以上流程实施的过程中遇到什么问题,欢迎在留言区和我讨论。
总结
今天我们主要讨论了传统企业中台建设的一些思路,梳理了 DDD、中台和微服务的关系。DDD 的战略设计可用于中台业务建模,战术设计可指导中台微服务设计。相信 DDD 与中台的完美结合,可以让你的中台建设如虎添翼!
另外,这一讲只是开一个头,在下一讲中我还会以一个传统核心业务的中台建设案例,详细讲解中台的设计过程。

View File

@ -0,0 +1,139 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 DDD实践如何用DDD重构中台业务模型
你好,我是欧创新。
进入两千年后,随着互联网应用的快速发展,很多传统企业开始触网,建设自己的互联网电商平台。后来又随着微信和 App 等移动互联应用的兴起,又形成了新一轮的移动应用热潮。这些移动互联应用大多面向个人或者第三方,市场和需求变化快,需要以更敏捷的速度适应市场变化,为了保持快速响应能力和频繁发版的要求,很多时候这些移动互联网应用是独立于传统核心系统建设的,但两者承载的业务大部分又都是同质的,因此很容易出现业务能力重叠的问题。
阿里巴巴过去带动了传统企业向互联网电商转型。而如今又到了一个新的历史时期,在阿里巴巴提出中台战略后,很多企业又紧跟它的步伐,高举中台大旗,轰轰烈烈地开始了数字化转型之路。
那么传统企业在中台转型时,该如何从错综复杂的业务中构建中台业务模型呢?今天我就用一个传统企业中台建模的案例,带你一起用 DDD 的设计思想来构建中台业务模型。
传统企业应用分析
互联网电商平台和传统核心应用,两者面向的渠道和客户不一样,但销售的产品却很相似,它们之间的业务模型既有相同的地方,又有不同的地方。
现在我拿保险行业的互联网电商和传统核心应用来做个对比分析。我们看一下下面这张图,这两者在业务功能上会有很多相似和差异,这种相似和差异主要体现在四个方面。
1. 核心能力的重复建设。由于销售同质保险产品,二者在核心业务流程和功能上必然相似,因此在核心业务能力上存在功能重叠是不可避免的。传统保险核心应用有报价、投保、核保和出单功能,同样在互联网电商平台也有。这就是核心能力的重复建设。
2. 通用能力的重复建设。传统核心应用的通用平台大而全,通常会比较重。而互联网电商平台离不开这些通用能力的支撑,但为了保持敏捷性,一般会自己建设缩小版的通用功能,比如用户、客户等。这是通用能力的重复建设。
3. 业务职能的分离建设。有一类业务功能,在互联网电商平台中建设了一部分,在传统核心应用中也建设了一部分,二者功能不重叠而且还互补,组合在一起是一个完整的业务职能。比如缴费功能,互联网电商平台主要面向个人客户,于是采用了支付宝和微信支付的方式。而传统核心应用主要是柜台操作,仍在采用移动 POS 机的缴费方式。二者都是缴费,为了保证业务模型的完整性,在构建中台业务模型时,我们可以考虑将这两部分模型重组为一个完整的业务模型。
4. 互联网电商平台和传统核心功能前后完全独立建设。传统核心应用主要面向柜台,不需要互联网电商平台的在线客户、话务、订单和购物车等功能。而互联网电商平台主要面向个人客户,它不需要后端比较重的再保、佣金、打印等功能。在构建中台业务模型时,对这种情况应区别对待,将面向后端业务管理的应用沉淀到后台,将前端能力构建为面向互联网渠道的通用中台,比如订单等。
如何避免重复造轮子?
要避免重复建设,就要理解中台的理念和思想。前面说了“中台是企业级能力复用平台”,“复用”用白话说就是重复使用,就是要避免重复造轮子的事情。
中台的设计思想与“高内聚、低耦合”的设计原则是高度一致的。高内聚是把相关的业务行为聚集在一起,把不相关的行为放在其它地方,如果你要修改某个业务行为,只需要修改一处。对了!中台就是要这样做,按照“高内聚、松耦合”的原则,实现企业级的能力复用!
那如果你的企业遇到了重复造轮子的情况,应该怎么处理?
你需要站在企业高度,将重复的需要共享的通用能力、核心能力沉淀到中台,将分离的业务能力重组为完整的业务板块,构建可复用的中台业务模型。前端个性能力归前端,后端管理能力归后台。建立前、中、后台边界清晰,融合协作的企业级可复用的业务模型。
如何构建中台业务模型?
我们可以用 DDD 领域建模的方法来构建中台业务模型。你可以选择两种建模策略:自顶向下和自底向上的策略。具体采用哪种策略,你需要结合公司的具体情况来分析,下面我就来介绍一下这两种策略。
1. 自顶向下的策略
第一种策略是自顶向下。这种策略是先做顶层设计,从最高领域逐级分解为中台,分别建立领域模型,根据业务属性分为通用中台或核心中台。领域建模过程主要基于业务现状,暂时不考虑系统现状。自顶向下的策略适用于全新的应用系统建设,或旧系统推倒重建的情况。
由于这种策略不必受限于现有系统,你可以用 DDD 领域逐级分解的领域建模方法。从下面这张图我们可以看出它的主要步骤:第一步是将领域分解为子域,子域可以分为核心域、通用域和支撑域;第二步是对子域建模,划分领域边界,建立领域模型和限界上下文;第三步则是根据限界上下文进行微服务设计。
2. 自底向上的策略
第二种策略是自底向上。这种策略是基于业务和系统现状完成领域建模。首先分别完成系统所在业务域的领域建模;然后对齐业务域,找出具有同类或相似业务功能的领域模型,对比分析领域模型的差异,重组领域对象,重构领域模型。这个过程会沉淀公共和复用的业务能力,会将分散的业务模型整合。自底向上策略适用于遗留系统业务模型的演进式重构。
下面我以互联网电商和传统核心应用的几个典型业务域为例,带你了解具体如何采用自底向上的策略来构建中台业务模型,主要分为这样三个步骤。
第一步:锁定系统所在业务域,构建领域模型。
锁定系统所在的业务域,采用事件风暴,找出领域对象,构建聚合,划分限界上下文,建立领域模型。看一下下面这张图,我们选取了传统核心应用的用户、客户、传统收付和承保四个业务域以及互联网电商业务域,共计五个业务域来完成领域建模。
从上面这张图中,我们可以看到传统核心共构建了八个领域模型。其中用户域构建了用户认证和权限两个领域模型,客户域构建了个人和团体两个领域模型,传统收付构建了 POS 刷卡领域模型,承保域构建了定报价、投保和保单管理三个领域模型。
互联网电商构建了报价、投保、订单、客户、用户认证和移动收付六个领域模型。
在这些领域模型的清单里,我们可以看到二者之间有很多名称相似的领域模型。深入分析后你会发现,这些名称相似的领域模型存在业务能力重复,或者业务职能分散(比如移动支付和传统支付)的问题。那在构建中台业务模型时,你就需要重点关注它们,将这些不同领域模型中重复的业务能力沉淀到中台业务模型中,将分散的领域模型整合到统一的中台业务模型中,对外提供统一的共享的中台服务。
第二步:对齐业务域,构建中台业务模型。
在下面这张图里,你可以看到右侧的传统核心领域模型明显多于左侧的互联网电商,那我们是不是就可以得出一个初步的结论:传统核心面向企业内大部分应用,大而全,领域模型相对完备,而互联网电商面向单一渠道,领域模型相对单一。
这个结论也给我们指明了一个方向:首先我们可以将传统核心的领域模型作为主领域模型,将互联网电商领域模型作为辅助模型来构建中台业务模型。然后再将互联网电商中重复的能力沉淀到传统核心的领域模型中,只保留自己的个性能力,比如订单。中台业务建模时,既要关注领域模型的完备性,也要关注不同渠道敏捷响应市场的要求。
有了上述这样一个思路,我们就可以开始构建中台业务模型了。
我们从互联网电商和传统核心的领域模型中,归纳并分离出能覆盖两个域的所有业务子域。通过分析,我们找到了用户、客户、承保、收付和订单五个业务域,它们是可以用于领域模型对比分析的基准域。
下面我以客户为例,来给你讲一下客户中台业务模型的构建过程。
互联网电商客户主要面向个人客户,除了有个人客户信息管理功能外,基于营销目的它还有客户积分功能,因此它的领域模型有个人和积分两个聚合。
而传统核心客户除了支持个人客户外,还有单位和组织机构等团体客户,它有个人和团体两个领域模型。其中个人领域模型中除了个人客户信息管理功能外,还有个人客户的评级、重复客户的归并和客户的统一视图等功能,因此它的领域模型有个人、视图、评级和归并四个聚合。
构建多业务域的中台业务模型的过程,就是找出同一业务域内所有同类业务的领域模型,对比分析域内领域模型和聚合的差异和共同点,打破原有的模型,完成新的中台业务模型重组或归并的过程。
我们将互联网电商和传统核心的领域模型分解后,我们找到了五个与个人客户领域相关的聚合,包括:个人、积分、评级、归并和视图。这五个聚合原来分别分散在互联网电商和传统核心的领域模型中,我们需要打破原有的领域模型,进行功能沉淀和聚合的重组,重新找出这些聚合的限界上下文,重构领域模型。
最终个人客户的领域模型重构为:个人、归并和视图三个聚合重构为个人领域模型(客户信息管理),评级和积分两个聚合重构为评级积分领域模型(面向个人客户)。到这里我们就完成了个人客户领域模型的构建了。
好像还漏掉点什么东西呢?对了,还有团队客户领域模型!其实团体客户很简单。由于它只在传统核心中出现,我们将它在传统核心中的领域模型直接拿过来用就行了。
至此我们就完成了客户中台业务模型的构建了,客户中台构建了个人、团体和评级积分三个领域模型。
通过客户中台业务模型的构建,你是否 get 到构建中台业务模型的要点了呢?总结成一句话就是:“分域建模型,找准基准域,划定上下文,聚合重归类。”
其它业务域其实也是一样的过程,在这里我就不一一讲述了,你可以自己练习一下,作为课后作业。完成后你可以对照下面这张图看一下,这就是其它业务域重构后的中台业务模型。
第三步:中台归类,根据领域模型设计微服务。
完成中台业务建模后,我们就有了下面这张图。从这张图中我们可以看到总共构建了多少个中台,中台下面有哪些领域模型,哪些中台是通用中台,哪些中台是核心中台,中台的基本信息等等,都一目了然。你根据中台下的领域模型就可以设计微服务了。
重构过程中的领域对象
上面主要是从聚合的角度来描述中台业务模型的重组,是相对高阶的业务模块的重构。业务模型重构和聚合重组,往往会带来领域对象和业务行为的变化。下面我带你了解一下,在领域模型重组过程中,发生在更底层的领域对象的活动。
我们还是以客户为例来讲述。由于对象过多,我只选取了部分领域对象和业务行为。
传统核心客户领域模型重构之前,包含个人、团体和评级三个聚合,每个聚合内部都有自己的聚合根、实体、方法和领域服务等。
互联网电商客户领域模型重构前包含个人和积分两个聚合,每个聚合包含了自己的领域对象、方法和领域服务等。
传统核心和互联网电商客户领域模型重构成客户中台后,建立了个人、团体和评级积分三个领域模型。其中个人领域模型有个人聚合,团体领域模型有团体聚合,评级积分领域模型有评级和积分两个聚合。这些领域模型的领域对象来自原来的领域模型,但积分评级是重组后的领域模型,它们原来的聚合会带着各自的领域对象,加入到新的领域模型中。
这里还要注意:部分领域对象可能会根据新的业务要求,从原来的聚合中分离,重组到其它聚合。新领域模型的领域对象,比如实体、领域服务等,在重组后可能还会根据新的业务场景和需求进行代码重构。
总结
今天我们一起讨论了传统企业中台数字化转型,在面对多个不同渠道应用重复建设时,如何用 DDD 领域建模的思想来构建中台业务模型。中台业务建模有自顶向下和自底向上两种策略,这两种策略有自己的适用场景,你需要结合自己公司的情况选择合适的策略。
其实呢,中台业务模型的重构过程,也是微服务架构演进的过程。业务边界即微服务边界,业务边界做好了,微服务的边界自然就会很好。

View File

@ -0,0 +1,129 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 领域建模:如何用事件风暴构建领域模型?
你好,我是欧创新。
还记得我在 [第 01 讲] 中说过,微服务设计为什么要选择 DDD 吗?其中有一个非常重要的原因,就是采用 DDD 方法建立的领域模型,可以清晰地划分微服务的逻辑边界和物理边界。可以说,在 DDD 的实践中,好的领域模型直接关乎微服务的设计水平。因此,我认为 DDD 的战略设计是比战术设计更为重要的,也正是这个原因,我们的内容会更侧重于战略设计。
那么我们该采用什么样的方法,才能从错综复杂的业务领域中分析并构建领域模型呢?
它就是我在前面多次提到的事件风暴。事件风暴是一项团队活动,领域专家与项目团队通过头脑风暴的形式,罗列出领域中所有的领域事件,整合之后形成最终的领域事件集合,然后对每一个事件,标注出导致该事件的命令,再为每一个事件标注出命令发起方的角色。命令可以是用户发起,也可以是第三方系统调用或者定时器触发等,最后对事件进行分类,整理出实体、聚合、聚合根以及限界上下文。而事件风暴正是 DDD 战略设计中经常使用的一种方法,它可以快速分析和分解复杂的业务领域,完成领域建模。
那到底怎么做事件风暴呢?事件风暴需要提前准备些什么?又如何用事件风暴来构建领域模型呢?今天我们就来重点解决这些问题,深入了解事件风暴的全过程。
事件风暴需要准备些什么?
1. 事件风暴的参与者
事件风暴采用工作坊的方式,将项目团队和领域专家聚集在一起,通过可视化、高互动的方式一步一步将领域模型设计出来。领域专家是事件风暴中必不可少的核心参与者。很多公司可能并没有这个角色,那我们该寻找什么样的人来担当领域专家呢?
领域专家就是对业务或问题域有深刻见解的主题专家,他们非常了解业务和系统是怎么做的,同时也深刻理解为什么要这样设计。如果你的公司里并没有这个角色,那也没关系,你可以从业务人员、需求分析人员、产品经理或者在这个领域有多年经验的开发人员里,按照这个标准去选择合适的人选。
除了领域专家,事件风暴的其他参与者可以是 DDD 专家、架构师、产品经理、项目经理、开发人员和测试人员等项目团队成员。
领域建模是统一团队语言的过程,因此项目团队应尽早地参与到领域建模中,这样才能高效建立起团队的通用语言。到了微服务建设时,领域模型也更容易和系统架构保持一致。
2. 事件风暴要准备的材料
事件风暴参与者会将自己的想法和意见写在即时贴上,并将贴纸贴在墙上的合适位置,我们戏称这个过程是“刷墙”。所以即时贴和水笔是必备材料,另外,你还可以准备一些胶带或者磁扣,以便贴纸随时能更换位置。
值得提醒一下的是,在这个过程中,我们要用不同颜色的贴纸区分领域行为。如下图,我们可以用蓝色表示命令,用绿色表示实体,橙色表示领域事件,黄色表示补充信息等。补充信息主要用来说明注意事项,比如外部依赖等。颜色并不固定,这只是我的习惯,团队内统一才是重点。
3. 事件风暴的场地
什么样的场地适合做事件风暴呢?是不是需要跟组织会议一样,准备会议室、投影,还有椅子?这些都不需要!你只需要一堵足够长的墙和足够大的空间就可以了。墙是用来贴纸的,大空间可以让人四处走动,方便合作。撤掉会议桌和椅子的事件风暴,你会发现参与者们的效率更高。
事件风暴的发明者曾经建议要准备八米长的墙,这样设计就不会受到空间的限制了。当然,这个不是必要条件,看各自的现实条件吧,不要让思维受限就好。
4. 事件风暴分析的关注点
在领域建模的过程中,我们需要重点关注这类业务的语言和行为。比如某些业务动作或行为(事件)是否会触发下一个业务动作,这个动作(事件)的输入和输出是什么?是谁(实体)发出的什么动作(命令),触发了这个动作(事件)…我们可以从这些暗藏的词汇中,分析出领域模型中的事件、命令和实体等领域对象。
如何用事件风暴构建领域模型?
领域建模的过程主要包括产品愿景、业务场景分析、领域建模和微服务拆分与设计这几个重要阶段。下面我以用户中台为例,介绍一下如何用事件风暴构建领域模型。
1. 产品愿景
产品愿景的主要目的是对产品顶层价值的设计,使产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。
产品愿景的参与角色:领域专家、业务需求方、产品经理、项目经理和开发经理。
在建模之前,项目团队要思考这样两点:
用户中台到底能够做什么?
它的业务范围、目标用户、核心价值和愿景,与其它同类产品的差异和优势在哪里?
这个过程也是明确用户中台建设方向和统一团队思想的过程。参与者要对每一个点(下图最左侧列的内容)发表意见,用水笔写在贴纸上,贴在黄色贴纸的位置。这个过程会让参与者充分发表意见,最后会将发散的意见统一为通用语言,建立如下图的产品愿景墙。如果你的团队的产品愿景和目标已经很清晰了,那这个步骤你可以忽略。
2. 业务场景分析
场景分析是从用户视角出发的,根据业务流程或用户旅程,采用用例和场景分析,探索领域中的典型场景,找出领域事件、实体和命令等领域对象,支撑领域建模。事件风暴参与者要尽可能地遍历所有业务细节,充分发表意见,不要遗漏业务要点。
场景分析的参与角色:领域专家、产品经理、需求分析人员、架构师、项目经理、开发经理和测试经理。
用户中台有这样三个典型的业务场景:
第一个是系统和岗位设置,设置系统中岗位的菜单权限;
第二个是用户权限配置,为用户建立账户和密码,设置用户岗位;
第三个是用户登录系统和权限校验,生成用户登录和操作日志。
我们可以按照业务流程,一步一步搜寻用户业务流程中的关键领域事件,比如岗位已创建,用户已创建等事件。再找出什么行为会引起这些领域事件,这些行为可能是一个或若干个命令组合在一起产生的,比如创建用户时,第一个命令是从公司 HR 系统中获取用户信息,第二个命令是根据 HR 的员工信息在用户中台创建用户,创建完用户后就会产生用户已创建的领域事件。当然这个领域事件可能会触发下一步的操作,比如发布到邮件系统通知用户已创建,但也可能到此就结束了,你需要根据具体情况来分析是否还有下一步的操作。
场景分析时会产生很多的命令和领域事件。我用蓝色来表示命令,用橙色表示领域事件,用黄色表示补充信息,比如用户信息数据来源于 HR 系统的说明。
3. 领域建模
领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。领域模型利用限界上下文向上可以指导微服务设计,通过聚合向下可以指导聚合根、实体和值对象的设计。
领域建模的参与角色:领域专家、产品经理、需求分析人员、架构师、项目经理、开发经理和测试经理。
具体可以分为这样三步。
第一步:从命令和事件中提取产生这些行为的实体。用绿色贴纸表示实体。通过分析用户中台的命令和事件等行为数据,提取了产生这些行为的用户、账户、认证票据、系统、菜单、岗位和用户日志七个实体。
第二步:根据聚合根的管理性质从七个实体中找出聚合根,比如,用户管理用户相关实体以及值对象,系统可以管理与系统相关的菜单等实体等,可以找出用户和系统等聚合根。然后根据业务依赖和业务内聚原则,将聚合根以及它关联的实体和值对象组合为聚合,比如系统和菜单实体可以组合为“系统功能”聚合。按照上述方法,用户中台就有了系统功能、岗位、用户信息、用户日志、账户和认证票据六个聚合。
第三步:划定限界上下文,根据上下文语义将聚合归类。根据用户域的上下文语境,用户基本信息和用户日志信息这两个聚合共同构成用户信息域,分别管理用户基本信息、用户登录和操作日志。认证票据和账户这两个聚合共同构成认证域,分别实现不同方式的登录和认证。系统功能和岗位这两个聚合共同构成权限域,分别实现系统和菜单管理以及系统的岗位配置。根据业务边界,我们可以将用户中台划分为三个限界上下文:用户信息、认证和权限。
到这里我们就完成了用户中台领域模型的构建了。那由于领域建模的过程中产生的领域对象实在太多了,我们可以借助表格来记录。
4. 微服务拆分与设计
我们在基础篇讲过,原则上一个领域模型就可以设计为一个微服务,但由于领域建模时只考虑了业务因素,没有考虑微服务落地时的技术、团队以及运行环境等非业务因素,因此在微服务拆分与设计时,我们不能简单地将领域模型作为拆分微服务的唯一标准,它只能作为微服务拆分的一个重要依据。
微服务的设计还需要考虑服务的粒度、分层、边界划分、依赖关系和集成关系。除了考虑业务职责单一外,我们还需要考虑将敏态与稳态业务的分离、非功能性需求(如弹性伸缩要求、安全性等要求)、团队组织和沟通效率、软件包大小以及技术异构等非业务因素。
微服务设计建议参与的角色:领域专家、产品经理、需求分析人员、架构师、项目经理、开发经理和测试经理。
用户中台微服务设计如果不考虑非业务因素,我们完全可以按照领域模型与微服务一对一的关系来设计,将用户中台设计为:用户、认证和权限三个微服务。但如果用户日志数据量巨大,大到需要采用大数据技术来实现,这时用户信息聚合与用户日志聚合就会有技术异构。虽然在领域建模时,我们将他们放在一个了领域模型内,但如果考虑技术异构,这两个聚合就不适合放到同一个微服务里了。我们可以以聚合作为拆分单位,将用户基本信息管理和用户日志管理拆分为两个技术异构的微服务,分别用不同的技术来实现它们。
总结
今天我们讲了事件风暴的设计方法以及如何用事件风暴来构建领域模型。事件风暴是一种不同于传统需求分析和系统设计的方法,最好的学习方法就是找几个业务场景多做几次。
综合我的经验,一般来说一个中型规模的项目,领域建模的时间大概在两周左右,这与我们传统的需求分析和系统设计的时间基本差不多。但是如果在领域建模的过程中,团队成员全员参与,在项目开发之前就建立了共同语言,这对于后续的微服务设计与开发是很有帮助的,时间成本也可以视情况降低。
其实我也了解到了,很多开发人员在初次学习 DDD 时,似乎并不太关心领域建模,而只是想学学 DDD 的战术设计思想,快速上手,开发微服务。我想这是对 DDD 的一个误解,这已经偏离了 DDD 的核心设计思想,即先有边界清晰的领域模型,才能设计出清晰的微服务边界,这两个阶段一前一后是刚需,我们不能忽略。

View File

@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 代码模型如何使用DDD设计微服务代码模型
你好,我是欧创新。
上一讲我们完成了领域模型的设计,接下来我们就要开始微服务的设计和落地了。那微服务落地时首先要确定的就是微服务的代码结构,也就是我今天要讲的微服务代码模型。
只有建立了标准的微服务代码模型和代码规范后,我们才可以将领域对象所对应的代码对象放在合适的软件包的目录结构中。标准的代码模型可以让项目团队成员更好地理解代码,根据代码规范实现团队协作;还可以让微服务各层的逻辑互不干扰、分工协作、各据其位、各司其职,避免不必要的代码混淆。另外,标准的代码模型还可以让你在微服务架构演进时,轻松完成代码重构。
那在 DDD 里,微服务的代码结构长什么样子呢?我们又是依据什么来建立微服务代码模型?这就是我们今天重点要解决的两个问题。
DDD 分层架构与微服务代码模型
我们参考 DDD 分层架构模型来设计微服务代码模型。没错!微服务代码模型就是依据 DDD 分层架构模型设计出来的。那为什么是 DDD 分层架构模型呢?
我们先简单回顾一下 [第 07 讲] 介绍过的 DDD 分层架构模型。它包括用户接口层、应用层、领域层和基础层,分层架构各层的职责边界非常清晰,又能有条不紊地分层协作。
用户接口层:面向前端提供服务适配,面向资源层提供资源适配。这一层聚集了接口适配相关的功能。
应用层职责:实现服务组合和编排,适应业务流程快速变化的需求。这一层聚集了应用服务和事件相关的功能。
领域层:实现领域的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和事件等领域对象,以及它们组合所形成的业务能力。
基础层:贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力。
业务逻辑从领域层、应用层到用户接口层逐层封装和协作对外提供灵活的服务既实现了各层的分工又实现了各层的协作。因此毋庸置疑DDD 分层架构模型就是设计微服务代码模型的最佳依据。
微服务代码模型
现在,我们来看一下,按照 DDD 分层架构模型设计出来的微服务代码模型到底长什么样子呢?
其实DDD 并没有给出标准的代码模型,不同的人可能会有不同理解。下面要说的这个微服务代码模型是我经过思考和实践后建立起来的,主要考虑的是微服务的边界、分层以及架构演进。
微服务一级目录结构
微服务一级目录是按照 DDD 分层架构的分层职责来定义的。从下面这张图中,我们可以看到,在代码模型里分别为用户接口层、应用层、领域层和基础层,建立了 interfaces、application、domain 和 infrastructure 四个一级代码目录。
这些目录的职能和代码形态是这样的。
Interfaces用户接口层它主要存放用户接口层与前端交互、展现数据相关的代码。前端应用通过这一层的接口向应用服务获取展现所需的数据。这一层主要用来处理用户发送的 Restful 请求,解析用户输入的配置文件,并将数据传递给 Application 层。数据的组装、数据传输格式以及 Facade 接口等代码都会放在这一层目录里。
Application应用层它主要存放应用层服务组合和编排相关的代码。应用服务向下基于微服务内的领域服务或外部微服务的应用服务完成服务的编排和组合向上为用户接口层提供各种应用数据展现支持服务。应用服务和事件等代码会放在这一层目录里。
Domain领域层它主要存放领域层核心业务逻辑相关的代码。领域层可以包含多个聚合代码包它们共同实现领域模型的核心业务逻辑。聚合以及聚合内的实体、方法、领域服务和事件等代码会放在这一层目录里。
Infrastructure基础层它主要存放基础资源服务相关的代码为其它各层提供的通用技术能力、三方软件包、数据库服务、配置和基础资源服务的代码都会放在这一层目录里。
各层目录结构
1. 用户接口层
Interfaces 的代码目录结构有assembler、dto 和 façade 三类。
Assembler实现 DTO 与领域对象之间的相互转换和数据交换。一般来说 Assembler 与 DTO 总是一同出现。
Dto它是数据传输的载体内部不存在任何业务逻辑我们可以通过 DTO 把内部的领域对象与外界隔离。
Facade提供较粗粒度的调用接口将用户请求委派给一个或多个应用服务进行处理。
2. 应用层
Application 的代码目录结构有event 和 service。
Event事件这层目录主要存放事件相关的代码。它包括两个子目录publish 和 subscribe。前者主要存放事件发布相关代码后者主要存放事件订阅相关代码事件处理相关的核心业务逻辑在领域层实现
这里提示一下:虽然应用层和领域层都可以进行事件的发布和处理,但为了实现事件的统一管理,我建议你将微服务内所有事件的发布和订阅的处理都统一放到应用层,事件相关的核心业务逻辑实现放在领域层。通过应用层调用领域层服务,来实现完整的事件发布和订阅处理流程。
Service应用服务这层的服务是应用服务。应用服务会对多个领域服务或外部应用服务进行封装、编排和组合对外提供粗粒度的服务。应用服务主要实现服务组合和编排是一段独立的业务逻辑。你可以将所有应用服务放在一个应用服务类里也可以把一个应用服务设计为一个应用服务类以防应用服务类代码量过大。
3. 领域层
Domain 是由一个或多个聚合包构成共同实现领域模型的核心业务逻辑。聚合内的代码模型是标准和统一的包括entity、event、repository 和 service 四个子目录。
而领域层聚合内部的代码目录结构是这样的。
Aggregate聚合它是聚合软件包的根目录可以根据实际项目的聚合名称命名比如权限聚合。在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。聚合内实现高内聚的业务逻辑它的代码可以独立拆分为微服务。
以聚合为单位的代码放在一个包里的主要目的是为了业务内聚,而更大的目的是为了以后微服务之间聚合的重组。聚合之间清晰的代码边界,可以让你轻松地实现以聚合为单位的微服务重组,在微服务架构演进中有着很重要的作用。
Entity实体它存放聚合根、实体、值对象以及工厂模式Factory相关代码。实体类采用充血模型同一实体相关的业务逻辑都在实体类代码中实现。跨实体的业务逻辑代码在领域服务中实现。
Event事件它存放事件实体以及与事件活动相关的业务逻辑代码。
Service领域服务它存放领域服务代码。一个领域服务是多个实体组合出来的一段业务逻辑。你可以将聚合内所有领域服务都放在一个领域服务类中你也可以把每一个领域服务设计为一个类。如果领域服务内的业务逻辑相对复杂我建议你将一个领域服务设计为一个领域服务类避免由于所有领域服务代码都放在一个领域服务类中而出现代码臃肿的问题。领域服务封装多个实体或方法后向上层提供应用服务调用。
Repository仓储它存放所在聚合的查询或持久化领域对象的代码通常包括仓储接口和仓储实现方法。为了方便聚合的拆分和组合我们设定了一个原则一个聚合对应一个仓储。
特别说明:按照 DDD 分层架构,仓储实现本应该属于基础层代码,但为了在微服务架构演进时,保证代码拆分和重组的便利性,我是把聚合仓储实现的代码放到了聚合包内。这样,如果需求或者设计发生变化导致聚合需要拆分或重组时,我们就可以将包括核心业务逻辑和仓储代码的聚合包整体迁移,轻松实现微服务架构演进。
4. 基础层
Infrastructure 的代码目录结构有config 和 util 两个子目录。
Config主要存放配置相关代码。
Util主要存放平台、开发框架、消息、数据库、缓存、文件、总线、网关、第三方类库、通用算法等基础代码你可以为不同的资源类别建立不同的子目录。
代码模型总目录结构
在完成一级和二级代码模型设计后,你就可以看到下图这样的微服务代码模型的总目录结构了。
总结
今天我们根据 DDD 分层架构模型建立了标准的微服务代码模型,在代码模型里面,各代码对象各据其位、各司其职,共同协作完成微服务的业务逻辑。
那关于代码模型我还需要强调两点内容。
第一点:聚合之间的代码边界一定要清晰。聚合之间的服务调用和数据关联应该是尽可能的松耦合和低关联,聚合之间的服务调用应该通过上层的应用层组合实现调用,原则上不允许聚合之间直接调用领域服务。这种松耦合的代码关联,在以后业务发展和需求变更时,可以很方便地实现业务功能和聚合代码的重组,在微服务架构演进中将会起到非常重要的作用。
第二点:你一定要有代码分层的概念。写代码时一定要搞清楚代码的职责,将它放在职责对应的代码目录内。应用层代码主要完成服务组合和编排,以及聚合之间的协作,它是很薄的一层,不应该有核心领域逻辑代码。领域层是业务的核心,领域模型的核心逻辑代码一定要在领域层实现。如果将核心领域逻辑代码放到应用层,你的基于 DDD 分层架构模型的微服务慢慢就会演变成传统的三层架构模型了。

View File

@ -0,0 +1,191 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 代码模型(下):如何保证领域模型与代码模型的一致性?
你好,我是欧创新。
在 [第 12 讲] 中,我们了解了如何用事件风暴来构建领域模型,在构建领域模型的过程中,我们会提取出很多的领域对象,比如聚合、实体、命令和领域事件等。到了 [第 13 讲],我们又根据 DDD 分层架构模型,建立了标准的微服务代码模型,为代码对象定义好了分层和目录结构。
那要想完成微服务的设计和落地,这之后其实还有一步,也就是我们今天的重点——将领域对象映射到微服务代码模型中。那为什么这一步如此重要呢?
DDD 强调先构建领域模型然后设计微服务,以保证领域模型和微服务的一体性,因此我们不能脱离领域模型来谈微服务的设计和落地。但在构建领域模型时,我们往往是站在业务视角的,并且有些领域对象还带着业务语言。我们还需要将领域模型作为微服务设计的输入,对领域对象进行设计和转换,让领域对象与代码对象建立映射关系。
接下来我们围绕今天的重点,详细来讲一讲。
领域对象的整理
完成微服务拆分后,领域模型的边界和领域对象就基本确定了。
我们第一个重要的工作就是,整理事件风暴过程中产生的各个领域对象,比如:聚合、实体、命令和领域事件等内容,将这些领域对象和业务行为记录到下面的表格中。
你可以看到,这张表格里包含了:领域模型、聚合、领域对象和领域类型四个维度。一个领域模型会包含多个聚合,一个聚合包含多个领域对象,每个领域对象都有自己的领域类型。领域类型主要标识领域对象的属性,比如:聚合根、实体、命令和领域事件等类型。
从领域模型到微服务的设计
从领域模型到微服务落地,我们还需要做进一步的设计和分析。事件风暴中提取的领域对象,还需要经过用户故事或领域故事分析,以及微服务设计,才能用于微服务系统开发。
这个过程会比事件风暴来的更深入和细致。主要关注内容如下:
分析微服务内有哪些服务?
服务所在的分层?
应用服务由哪些服务组合和编排完成?
领域服务包括哪些实体的业务逻辑?
采用充血模型的实体有哪些属性和方法?
有哪些值对象?
哪个实体是聚合根等?
最后梳理出所有的领域对象和它们之间的依赖关系,我们会给每个领域对象设计对应的代码对象,定义它们所在的软件包和代码目录。
这个设计过程建议参与的角色有DDD 专家、架构师、设计人员和开发经理。
领域层的领域对象
事件风暴结束时,领域模型聚合内一般会有:聚合、实体、命令和领域事件等领域对象。在完成故事分析和微服务设计后,微服务的聚合内一般会有:聚合、聚合根、实体、值对象、领域事件、领域服务和仓储等领域对象。
下面我们就来看一下这些领域对象是怎么得来的?
1. 设计实体
大多数情况下,领域模型的业务实体与微服务的数据库实体是一一对应的。但某些领域模型的实体在微服务设计时,可能会被设计为多个数据实体,或者实体的某些属性被设计为值对象。
我们分析个人客户时,还需要有地址、电话和银行账号等实体,它们被聚合根引用,不容易在领域建模时发现,我们需要在微服务设计过程中识别和设计出来。
在分层架构里,实体采用充血模型,在实体类内实现实体的全部业务逻辑。这些不同的实体都有自己的方法和业务行为,比如地址实体有新增和修改地址的方法,银行账号实体有新增和修改银行账号的方法。
实体类放在领域层的 Entity 目录结构下。
2. 找出聚合根
聚合根来源于领域模型,在个人客户聚合里,个人客户这个实体是聚合根,它负责管理地址、电话以及银行账号的生命周期。个人客户聚合根通过工厂和仓储模式,实现聚合内地址、银行账号等实体和值对象数据的初始化和持久化。
聚合根是一种特殊的实体,它有自己的属性和方法。聚合根可以实现聚合之间的对象引用,还可以引用聚合内的所有实体。聚合根类放在代码模型的 Entity 目录结构下。聚合根有自己的实现方法,比如生成客户编码,新增和修改客户信息等方法。
3. 设计值对象
根据需要将某些实体的某些属性或属性集设计为值对象。值对象类放在代码模型的 Entity 目录结构下。在个人客户聚合中,客户拥有客户证件类型,它是以枚举值的形式存在,所以将它设计为值对象。
有些领域对象可以设计为值对象,也可以设计为实体,我们需要根据具体情况来分析。如果这个领域对象在其它聚合内维护生命周期,且在它依附的实体对象中只允许整体替换,我们就可以将它设计为值对象。如果这个对象是多条且需要基于它做查询统计,我建议将它设计为实体。
4. 设计领域事件
如果领域模型中领域事件会触发下一步的业务操作,我们就需要设计领域事件。首先确定领域事件发生在微服务内还是微服务之间。然后设计事件实体对象,事件的发布和订阅机制,以及事件的处理机制。判断是否需要引入事件总线或消息中间件。
在个人客户聚合中有客户已创建的领域事件,因此它有客户创建事件这个实体。
领域事件实体和处理类放在领域层的 Event 目录结构下。领域事件的发布和订阅类我建议放在应用层的 Event 目录结构下。
5. 设计领域服务
如果一个业务动作或行为跨多个实体,我们就需要设计领域服务。领域服务通过对多个实体和实体方法进行组合,完成核心业务逻辑。你可以认为领域服务是位于实体方法之上和应用服务之下的一层业务逻辑。
按照严格分层架构层的依赖关系,如果实体的方法需要暴露给应用层,它需要封装成领域服务后才可以被应用服务调用。所以如果有的实体方法需要被前端应用调用,我们会将它封装成领域服务,然后再封装为应用服务。
个人客户聚合根这个实体创建个人客户信息的方法,被封装为创建个人客户信息领域服务。然后再被封装为创建个人客户信息应用服务,向前端应用暴露。
领域服务类放在领域层的 Service 目录结构下。
6. 设计仓储
每一个聚合都有一个仓储,仓储主要用来完成数据查询和持久化操作。仓储包括仓储的接口和仓储实现,通过依赖倒置实现应用业务逻辑与数据库资源逻辑的解耦。
仓储代码放在领域层的 Repository 目录结构下。
应用层的领域对象
应用层的主要领域对象是应用服务和事件的发布以及订阅。
在事件风暴或领域故事分析时,我们往往会根据用户或系统发起的命令,来设计服务或实体方法。为了响应这个命令,我们需要分析和记录:
在应用层和领域层分别会发生哪些业务行为;
各层分别需要设计哪些服务或者方法;
这些方法和服务的分层以及领域类型(比如实体方法、领域服务和应用服务等),它们之间的调用和组合的依赖关系。
在严格分层架构模式下,不允许服务的跨层调用,每个服务只能调用它的下一层服务。服务从下到上依次为:实体方法、领域服务和应用服务。
如果需要实现服务的跨层调用,我们应该怎么办?我建议你采用服务逐层封装的方式。
我们看一下上面这张图,服务的封装和调用主要有以下几种方式。
1. 实体方法的封装
实体方法是最底层的原子业务逻辑。如果单一实体的方法需要被跨层调用,你可以将它封装成领域服务,这样封装的领域服务就可以被应用服务调用和编排了。如果它还需要被用户接口层调用,你还需要将这个领域服务封装成应用服务。经过逐层服务封装,实体方法就可以暴露给上面不同的层,实现跨层调用。
封装时服务前面的名字可以保持一致,你可以用 *DomainService 或 *AppService 后缀来区分领域服务或应用服务。
2. 领域服务的组合和封装
领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。如果它需要暴露给用户接口层,领域服务就需要封装成应用服务。
3. 应用服务的组合和编排
应用服务会对多个领域服务进行组合和编排,暴露给用户接口层,供前端应用调用。
在应用服务组合和编排时,你需要关注一个现象:多个应用服务可能会对多个同样的领域服务重复进行同样业务逻辑的组合和编排。当出现这种情况时,你就需要分析是不是领域服务可以整合了。你可以将这几个不断重复组合的领域服务,合并到一个领域服务中实现。这样既省去了应用服务的反复编排,也实现了服务的演进。这样领域模型将会越来越精炼,更能适应业务的要求。
应用服务类放在应用层 Service 目录结构下。领域事件的发布和订阅类放在应用层 Event 目录结构下。
领域对象与微服务代码对象的映射
在完成上面的分析和设计后,我们就可以建立像下图一样的,领域对象与微服务代码对象的映射关系了。
典型的领域模型
个人客户领域模型中的个人客户聚合,就是典型的领域模型,从聚合内可以提取出多个实体和值对象以及它的聚合根。
我们看一下下面这个图,我们对个人客户聚合做了进一步的分析。提取了个人客户表单这个聚合根,形成了客户类型值对象,以及电话、地址、银行账号等实体,为实体方法和服务做了封装和分层,建立了领域对象的关联和依赖关系,还有仓储等设计。关键是这个过程,我们建立了领域对象与微服务代码对象的映射关系。
下面我对表格的各栏做一个简要的说明。
层:定义领域对象位于分层架构中的哪一层,比如:接口层、应用层、领域层以及基础层等。
领域对象:领域模型中领域对象的具体名称。
领域类型:根据 DDD 知识体系定义的领域对象的类型,包括:限界上下文、聚合、聚合根、实体、值对象、领域事件、应用服务、领域服务和仓储服务等领域类型。
依赖的领域对象:根据业务对象依赖或分层调用的依赖关系,建立的领域对象的依赖关系,比如:服务调用依赖、关联对象聚合等。
包名:代码模型中的包名,对应领域对象所在的软件包。
类名:代码模型中的类名,对应领域对象的类名。
方法名:代码模型中的方法名,对应领域对象实现或操作的方法名。
在建立这种映射关系后,我们就可以得到如下图的微服务代码结构了。
非典型领域模型
有些业务场景可能并不能如你所愿,你可能无法设计出典型的领域模型。这类业务中有多个实体,实体之间相互独立,是松耦合的关系,这些实体主要参与分析或者计算,你找不出聚合根,但就业务本身来说它们是高内聚的。而它们所组合的业务与其它聚合是在一个限界上下文内,你也不大可能将它单独设计为一个微服务。
这种业务场景其实很常见。比如,在个人客户领域模型内有客户归并的聚合,它扫描所有客户,按照身份证号码、电话号码等是否重复的业务规则,判断是否是重复的客户,然后对重复的客户进行归并。这种业务场景你就找不到聚合根。
那对于这类非典型模型,我们怎么办?
我们还是可以借鉴聚合的思想,仍然用聚合来定义这部分功能,并采用与典型领域模型同样的分析方法,建立实体的属性和方法,对方法和服务进行封装和分层设计,设计仓储,建立领域对象之间的依赖关系。唯一可惜的就是我们依然找不到聚合根,不过也没关系,除了聚合根管理功能外,我们还可以用 DDD 的其它设计方法。
总结
今天我们学习了从领域模型到微服务的设计过程,这个过程在微服务设计过程中非常的关键。你需要从微服务系统的角度,对领域模型做深入、细致的分析,为领域对象分层,找出各个领域对象的依赖关系,建立领域对象与微服务代码对象的映射关系,从而保证领域模型与代码模型的一致性,最终完成微服务的设计。
在建立这种业务模型与微服务系统架构的关系后,整个项目团队就可以在统一的通用语言下工作,即使不熟悉业务的开发人员,或者不熟悉代码的业务人员,也可以很快就定位到代码位置。

View File

@ -0,0 +1,97 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 边界:微服务的各种边界在架构演进中的作用?
你好,我是欧创新。
前几讲我们已经介绍过了,在用 DDD 进行微服务设计时,我们可以通过事件风暴来确定领域模型边界,划定微服务边界,定义业务和系统运行边界,从而保证微服务的单一职责和随需而变的架构演进能力。
那重点落到边界的时候,总结一下就是,微服务的设计要涉及到逻辑边界、物理边界和代码边界等等。
那么这些边界在微服务架构演进中到底起到什么样的作用?我们又该如何理解这些边界呢?这就是我们今天重点要解决的问题。
演进式架构
在微服务设计和实施的过程中,很多人认为:“将单体拆分成多少个微服务,是微服务的设计重点。”可事实真的是这样吗?其实并非如此!
Martin Fowler 在提出微服务时,他提到了微服务的一个重要特征——演进式架构。那什么是演进式架构呢?演进式架构就是以支持增量的、非破坏的变更作为第一原则,同时支持在应用程序结构层面的多维度变化。
那如何判断微服务设计是否合理呢?其实很简单,只需要看它是否满足这样的情形就可以了:随着业务的发展或需求的变更,在不断重新拆分或者组合成新的微服务的过程中,不会大幅增加软件开发和维护的成本,并且这个架构演进的过程是非常轻松、简单的。
这也是微服务设计的重点,就是看微服务设计是否能够支持架构长期、轻松的演进。
那用 DDD 方法设计的微服务,不仅可以通过限界上下文和聚合实现微服务内外的解耦,同时也可以很容易地实现业务功能积木式模块的重组和更新,从而实现架构演进。
微服务还是小单体?
有些项目团队在将集中式单体应用拆分为微服务时,首先进行的往往不是建立领域模型,而只是按照业务功能将原来单体应用的一个软件包拆分成多个所谓的“微服务”软件包,而这些“微服务”内的代码仍然是集中式三层架构的模式,“微服务”内的代码高度耦合,逻辑边界不清晰,这里我们暂且称它为“小单体微服务”。
下面这张图也很好地展示了这个过程。
而随着新需求的提出和业务的发展,这些小单体微服务会慢慢膨胀起来。当有一天你发现这些膨胀了的微服务,有一部分业务功能需要拆分出去,或者部分功能需要与其它微服务进行重组时,你会发现原来这些看似清晰的微服务,不知不觉已经摇身一变,变成了臃肿油腻的大单体了,而这个大单体内的代码依然是高度耦合且边界不清的。
“辛辛苦苦好多年,一夜回到解放前啊!”这个时候你就需要一遍又一遍地重复着从大单体向单体微服务重构的过程。想想,这个代价是不是有点高了呢?
其实这个问题已经很明显了,那就是边界。
这种单体式微服务只定义了一个维度的边界,也就是微服务之间的物理边界,本质上还是单体架构模式。微服务设计时要考虑的不仅仅只有这一个边界,别忘了还要定义好微服务内的逻辑边界和代码边界,这样才能得到你想要的结果。
那现在你知道了我们一定要避免将微服务设计为小单体微服务那具体该如何避免呢清晰的边界人人想要可该如何保证呢DDD 已然给出了答案。
微服务边界的作用
你应该还记得 DDD 设计方法里的限界上下文和聚合吧?它们就是用来定义领域模型和微服务边界的。
我们再来回顾一下 DDD 的设计过程。
在事件风暴中,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象。根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合,聚合之间是第一层边界。根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,形成领域模型,限界上下文之间的边界是第二层边界。
为了方便理解,我们将这些边界分为:逻辑边界、物理边界和代码边界。
逻辑边界主要定义同一业务领域或应用内紧密关联的对象所组成的不同聚类的组合之间的边界。事件风暴对不同实体对象进行关联和聚类分析后,会产生多个聚合和限界上下文,它们一起组成这个领域的领域模型。微服务内聚合之间的边界就是逻辑边界。一般来说微服务会有一个以上的聚合,在开发过程中不同聚合的代码隔离在不同的聚合代码目录中。
逻辑边界在微服务设计和架构演进中具有非常重要的意义!
微服务的架构演进并不是随心所欲的,需要遵循一定的规则,这个规则就是逻辑边界。微服务架构演进时,在业务端以聚合为单位进行业务能力的重组,在微服务端以聚合的代码目录为单位进行微服务代码的重组。由于按照 DDD 方法设计的微服务逻辑边界清晰,业务高内聚,聚合之间代码松耦合,因此在领域模型和微服务代码重构时,我们就不需要花费太多的时间和精力了。
现在我们来看一个微服务实例,在下面这张图中,我们可以看到微服务里包含了两个聚合的业务逻辑,两个聚合分别内聚了各自不同的业务能力,聚合内的代码分别归到了不同的聚合目录下。
那随着业务的快速发展,如果某一个微服务遇到了高性能挑战,需要将部分业务能力独立出去,我们就可以以聚合为单位,将聚合代码拆分独立为一个新的微服务,这样就可以很容易地实现微服务的拆分。
另外,我们也可以对多个微服务内有相似功能的聚合进行功能和代码重组,组合为新的聚合和微服务,独立为通用微服务。现在你是不是有点做中台的感觉呢?
物理边界主要从部署和运行的视角来定义微服务之间的边界。不同微服务部署位置和运行环境是相互物理隔离的,分别运行在不同的进程中。这种边界就是微服务之间的物理边界。
代码边界主要用于微服务内的不同职能代码之间的隔离。微服务开发过程中会根据代码模型建立相应的代码目录,实现不同功能代码的隔离。由于领域模型与代码模型的映射关系,代码边界直接体现出业务边界。代码边界可以控制代码重组的影响范围,避免业务和服务之间的相互影响。微服务如果需要进行功能重组,只需要以聚合代码为单位进行重组就可以了。
正确理解微服务的边界
从上述内容中,我们知道了,按照 DDD 设计出来的逻辑边界和代码边界,让微服务架构演进变得不那么费劲了。
微服务的拆分可以参考领域模型,也可以参考聚合,因为聚合是可以拆分为微服务的最小单位的。但实施过程是否一定要做到逻辑边界与物理边界一致性呢?也就是说聚合是否也一定要设计成微服务呢?答案是不一定的,这里就涉及到微服务过度拆分的问题了。
微服务的过度拆分会使软件维护成本上升,比如:集成成本、发布成本、运维成本以及监控和定位问题的成本等。在项目建设初期,如果你不具备较强的微服务管理能力,那就不宜将微服务拆分过细。当我们具备一定的能力以后,且微服务内部的逻辑和代码边界也很清晰,你就可以随时根据需要,拆分出新的微服务,实现微服务的架构演进了。
当然,还要记住一点,微服务内聚合之间的服务调用和数据依赖需要符合高内聚松耦合的设计原则和开发规范,否则你也不能很快完成微服务的架构演进。
总结
今天我们主要讨论了微服务架构设计中的各种边界在架构演进中的作用。
逻辑边界:微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚,可根据需要变成物理边界,也就是说聚合也可以独立为微服务。
物理边界:微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。
代码边界:不同层或者聚合之间代码目录的边界是代码边界。它强调的是代码之间的隔离,方便架构演进时代码的重组。
通过以上边界,我们可以让业务能力高内聚、代码松耦合,且清晰的边界,可以快速实现微服务代码的拆分和组合,轻松实现微服务架构演进。但有一点一定要格外注意,边界清晰的微服务,不是大单体向小单体的演进。

View File

@ -0,0 +1,165 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 视图:如何实现服务和数据在微服务各层的协作?
你好,我是欧创新。
在 DDD 分层架构和微服务代码模型里,我们根据领域对象的属性和依赖关系,将领域对象进行分层,定义了与之对应的代码对象和代码目录结构。分层架构确定了微服务的总体架构,微服务内的主要对象有服务和实体等,它们一起协作完成业务逻辑。
那在运行过程中,这些服务和实体在微服务各层是如何协作的呢?今天我们就来解剖一下基于 DDD 分层架构的微服务,看看它的内部结构到底是什么样的。
服务的协作
1. 服务的类型
我们先来回顾一下分层架构中的服务。按照分层架构设计出来的微服务,其内部有 Facade 服务、应用服务、领域服务和基础服务。各层服务的主要功能和职责如下。
Facade 服务:位于用户接口层,包括接口和实现两部分。用于处理用户发送的 Restful 请求和解析用户输入的配置文件等,并将数据传递给应用层。或者在获取到应用层数据后,将 DO 组装成 DTO将数据传输到前端应用。
应用服务:位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果拼装,对外提供粗粒度的服务。
领域服务:位于领域层。领域服务封装核心的业务逻辑,实现需要多个实体协作的核心领域逻辑。它对多个实体或方法的业务逻辑进行组合或编排,或者在严格分层架构中对实体方法进行封装,以领域服务的方式供应用层调用。
基础服务:位于基础层。提供基础资源服务(比如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务应用逻辑的影响。基础服务主要为仓储服务,通过依赖倒置提供基础资源服务。领域服务和应用服务都可以调用仓储服务接口,通过仓储服务实现数据持久化。
2. 服务的调用
我们看一下下面这张图。微服务的服务调用包括三类主要场景:微服务内跨层服务调用,微服务之间服务调用和领域事件驱动。
微服务内跨层服务调用
微服务架构下往往采用前后端分离的设计模式,前端应用独立部署。前端应用调用发布在 API 网关上的 Facade 服务Facade 定向到应用服务。应用服务作为服务组织和编排者,它的服务调用有这样两种路径:
第一种是应用服务调用并组装领域服务。此时领域服务会组装实体和实体方法,实现核心领域逻辑。领域服务通过仓储服务获取持久化数据对象,完成实体数据初始化。
第二种是应用服务直接调用仓储服务。这种方式主要针对像缓存、文件等类型的基础层数据访问。这类数据主要是查询操作,没有太多的领域逻辑,不经过领域层,不涉及数据库持久化对象。
微服务之间的服务调用
微服务之间的应用服务可以直接访问,也可以通过 API 网关访问。由于跨微服务操作,在进行数据新增和修改操作时,你需关注分布式事务,保证数据的一致性。
领域事件驱动
领域事件驱动包括微服务内和微服务之间的事件(详见 [第 06 讲]。微服务内通过事件总线EventBus完成聚合之间的异步处理。微服务之间通过消息中间件完成。异步化的领域事件驱动机制是一种间接的服务访问方式。
当应用服务业务逻辑处理完成后,如果发生领域事件,可调用事件发布服务,完成事件发布。
当接收到订阅的主题数据时,事件订阅服务会调用事件处理领域服务,完成进一步的业务操作。
3. 服务的封装与组合
我们看一下下面这张图。微服务的服务是从领域层逐级向上封装、组合和暴露的。
基础层
基础层的服务形态主要是仓储服务。仓储服务包括接口和实现两部分。仓储接口服务供应用层或者领域层服务调用,仓储实现服务,完成领域对象的持久化或数据初始化。
领域层
领域层实现核心业务逻辑,负责表达领域模型业务概念、业务状态和业务规则。主要的服务形态有实体方法和领域服务。
实体采用充血模型,在实体类内部实现实体相关的所有业务逻辑,实现的形式是实体类中的方法。实体是微服务的原子业务逻辑单元。在设计时我们主要考虑实体自身的属性和业务行为,实现领域模型的核心基础能力。不必过多考虑外部操作和业务流程,这样才能保证领域模型的稳定性。
DDD 提倡富领域模型,尽量将业务逻辑归属到实体对象上,实在无法归属的部分则设计成领域服务。领域服务会对多个实体或实体方法进行组装和编排,实现跨多个实体的复杂核心业务逻辑。
对于严格分层架构,如果单个实体的方法需要对应用层暴露,则需要通过领域服务封装后才能暴露给应用服务。
应用层
应用层用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,负责不同聚合之间的服务和数据协调,负责微服务之间的事件发布和订阅。
通过应用服务对外暴露微服务的内部功能,这样就可以隐藏领域层核心业务逻辑的复杂性以及内部实现机制。应用层的主要服务形态有:应用服务、事件发布和订阅服务。
应用服务内用于组合和编排的服务,主要来源于领域服务,也可以是外部微服务的应用服务。除了完成服务的组合和编排外,应用服务内还可以完成安全认证、权限校验、初步的数据校验和分布式事务控制等功能。
为了实现微服务内聚合之间的解耦,聚合之间的服务调用和数据交互应通过应用服务来完成。原则上我们应该禁止聚合之间的领域服务直接调用和聚合之间的数据表关联。
用户接口层
用户接口层是前端应用和微服务之间服务访问和数据交换的桥梁。它处理前端发送的 Restful 请求和解析用户输入的配置文件等,将数据传递给应用层。或获取应用服务的数据后,进行数据组装,向前端提供数据服务。主要服务形态是 Facade 服务。
Facade 服务分为接口和实现两个部分。完成服务定向DO 与 DTO 数据的转换和组装,实现前端与应用层数据的转换和交换。
4. 两种分层架构的服务依赖关系
现在我们回顾一下 DDD 分层架构,分层架构有一个重要的原则就是:每层只能与位于其下方的层发生耦合。
那根据耦合的紧密程度,分层架构可以分为两种:严格分层架构和松散分层架构。在严格分层架构中,任何层只能与位于其直接下方的层发生依赖。在松散分层架构中,任何层可以与其任意下方的层发生依赖。
下面我们来详细分析和比较一下这两种分层架构。
松散分层架构的服务依赖
我们看一下下面这张图,在松散分层架构中,领域层的实体方法和领域服务可以直接暴露给应用层和用户接口层。松散分层架构的服务依赖关系,无需逐级封装,可以快速暴露给上层。
但它存在一些问题,第一个是容易暴露领域层核心业务的实现逻辑;第二个是当实体方法或领域服务发生服务变更时,由于服务同时被多层服务调用和组合,不容易找出哪些上层服务调用和组合了它,不方便通知到所有的服务调用方。
我们再来看一张图,在松散分层架构中,实体 A 的方法在应用层组合后,暴露给用户接口层 aFacade。abDomainService 领域服务直接越过应用层,暴露给用户接口层 abFacade 服务。松散分层架构中任意下层服务都可以暴露给上层服务。
严格分层架构的服务依赖
我们看一下下面这张图,在严格分层架构中,每一层服务只能向紧邻的上一层提供服务。虽然实体、实体方法和领域服务都在领域层,但实体和实体方法只能暴露给领域服务,领域服务只能暴露给应用服务。
在严格分层架构中,服务如果需要跨层调用,下层服务需要在上层封装后,才可以提供跨层服务。比如实体方法需要向应用服务提供服务,它需要封装成领域服务。
这是因为通过封装你可以避免将核心业务逻辑的实现暴露给外部,将实体和方法封装成领域服务,也可以避免在应用层沉淀过多的本该属于领域层的核心业务逻辑,避免应用层变得臃肿。还有就是当服务发生变更时,由于服务只被紧邻上层的服务调用和组合,你只需要逐级告知紧邻上层就可以了,服务可管理性比松散分层架构要好是一定的。
我们还是看图A 实体方法需封装成领域服务 aDomainService 才能暴露给应用服务 aAppService。abDomainService 领域服务组合和封装 A 和 B 实体的方法后,暴露给应用服务 abAppService。
数据对象视图
在 DDD 中有很多的数据对象,这些对象分布在不同的层里。它们在不同的阶段有不同的形态。你可以再回顾一下 [第 04 讲],这一讲有详细的讲解。
我们先来看一下微服务内有哪些类型的数据对象?它们是如何协作和转换的?
数据持久化对象 PO(Persistent Object),与数据库结构一一映射,是数据持久化过程中的数据载体。
领域对象 DODomain Object微服务运行时的实体是核心业务的载体。
数据传输对象 DTOData Transfer Object用于前端与应用层或者微服务之间的数据组装和传输是应用之间数据传输的载体。
视图对象 VOView Object用于封装展示层指定页面或组件的数据。
我们结合下面这张图,看看微服务各层数据对象的职责和转换过程。
基础层
基础层的主要对象是 PO 对象。我们需要先建立 DO 和 PO 的映射关系。当 DO 数据需要持久化时,仓储服务会将 DO 转换为 PO 对象,完成数据库持久化操作。当 DO 数据需要初始化时,仓储服务从数据库获取数据形成 PO 对象,并将 PO 转换为 DO完成数据初始化。
大多数情况下 PO 和 DO 是一一对应的。但也有 DO 和 PO 多对多的情况,在 DO 和 PO 数据转换时,需要进行数据重组。
领域层
领域层的主要对象是 DO 对象。DO 是实体和值对象的数据和业务行为载体,承载着基础的核心业务逻辑。通过 DO 和 PO 转换,我们可以完成数据持久化和初始化。
应用层
应用层的主要对象是 DO 对象。如果需要调用其它微服务的应用服务DO 会转换为 DTO完成跨微服务的数据组装和传输。用户接口层先完成 DTO 到 DO 的转换,然后应用服务接收 DO 进行业务处理。如果 DTO 与 DO 是一对多的关系,这时就需要进行 DO 数据重组。
用户接口层
用户接口层会完成 DO 和 DTO 的互转完成微服务与前端应用数据交互及转换。Facade 服务会对多个 DO 对象进行组装,转换为 DTO 对象,向前端应用完成数据转换和传输。
前端应用
前端应用主要是 VO 对象。展现层使用 VO 进行界面展示,通过用户接口层与应用层采用 DTO 对象进行数据交互。

View File

@ -0,0 +1,165 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 从后端到前端:微服务后,前端如何设计?
你好,我是欧创新。
微服务架构通常采用前后端分离的设计方式。作为企业级的中台,在完成单体应用拆分和微服务建设后,前端项目团队会同时面对多个中台微服务项目团队,这时候的前端人员就犹如维修电工一样了。
面对如此多的微服务暴露出来的 API 服务,如何进行正确的连接和拼装,才能保证不出错?这显然不是一件很容易的事情。而当服务出现变更时,又如何通知所有受影响的项目团队,这里面的沟通成本相信也不小。
相应的要从一定程度上解决上述问题我们是不是可以考虑先有效降低前端集成的复杂度呢先做到前端聚合后端解耦——这是一个很有意思的话题。今天我们就一起来聊聊微前端Micro Frontend的设计思想探讨一下中台微服务后前后端的设计和集成方式。
单体前端的困境
传统企业在完成中台转型后,虽然后台的业务完成了微服务架构的升级,但前端仍然是单体模式,由一个团队创建并维护一个前端应用。随着时间推移和业务发展,前端会变得越来越臃肿,越来越难维护。而随着 5G 和移动互联技术的应用,企业业务活动将会进一步移动化和线上化。过去很多企业的做法是为不同的业务开发出独立的 APP。但很显然用户并不想装那么多的 APP
为了提高用户体验,实现统一运营,很多企业开始缩减和整合 APP将企业内所有的业务能力都尽量集中到一个 APP 中。试想如果仍然沿用单体前端的设计模式。前端项目团队将面对多个中台微服务团队,需要集成成千上万的 API 服务,这就需要相当高的沟通成本和技术要求。这绝对会是一场灾难。
相对互联网企业而言,传统企业的渠道应用更加多样化,有面向内部人员的门店类应用、面向外部客户的互联网电商平台或移动 APP还有面向第三方的 API 集成。由于渠道的差异,前端将更加多样化和复杂化。那如何有效降低前端集成的复杂度呢?
从单体前端到微前端
为了解决单体前端的问题,我们可以借鉴微服务的设计思想,引入微前端概念。将微服务理念扩展到前端,解决中台微服务化后,前端由于仍为单体而存在的逻辑复杂和臃肿的问题。
在前端设计时我们需要遵循单一职责和复用原则,按照领域模型和微服务边界,将前端页面进行拆分。同时构建多个可以独立部署、完全自治、松耦合的页面组合,其中每个组合只负责特定业务单元的 UI 元素和功能,这些页面组合就是微前端。
微前端与微服务一样,都是希望将单体应用,按照规则拆分,并重组为多个可以独立开发、独立测试、独立部署和独立运维,松耦合的微前端或者微服务。以适应业务快速变化及分布式多团队并行开发的要求。
微前端页面只包括业务单元前端操作必需的页面要素,它只是企业级完整业务流程中的一个业务拼图块,不包含页面导航等内容。微前端除了可以实现前端页面的解耦外,还可实现页面复用,这也与中台服务共享理念是一脉相承的。
业务单元的组合形态
我们可以参照领域模型和微服务边界,建立与微服务对应的前端操作界面,将它与微服务组成业务单元,以业务组件的方式对外提供服务。业务单元包括微前端和微服务,可以独立开发、测试、部署和运维,可以自包含地完成领域模型中部分或全部的业务功能。
我们看一下下面这个图。一个虚框就是一个业务单元,微前端和微服务独立部署,业务单元内的微前端和微服务已完成前后端集成。你可以将这个业务单元理解为一个特定业务领域的组件。业务单元可以有多种组合方式,以实现不同的业务目标。
1. 单一业务单元
一个微前端和一个微服务组成单一业务单元。微前端和微服务分别实现同一个领域模型从前端到后端的功能。
2. 组合业务单元
一个微前端与多个微服务组成组合业务单元。微前端具有多个微服务的前端功能,完成较复杂的页面和操作。多个微服务实现各自领域模型的功能,向微前端提供可组合的服务。
记住一点:微前端不宜与过多的微服务组合,否则容易变成单体前端。
3. 通用共享业务单元
一个微前端与一个或多个通用中台微服务组合为通用共享业务单元。通用共享微前端以共享页面的方式与其它微前端页面协作,完成业务流程。很多通用中台微服务的微前端是共享的,比如订单和支付等微服务对应的订单和支付微前端界面。
所有业务单元的功能都应该自包含,业务单元之间的边界清晰。业务单元之间要避免功能交叉而出现耦合,一旦出现就会影响项目团队职责边界,进而影响到业务单元独立开发、测试、部署和运维等。
微前端的集成方式
我们看一下下面这个图,微前端位于前端主页面和微服务之间,它需要与两者完成集成。
1. 微前端与前端主页面的集成
前端主页面是企业级的前端页面,微前端是业务单元的前端页面。微前端通过主页面的微前端加载器,利用页面路由和动态加载等技术,将特定业务单元的微前端页面动态加载到前端主页面,实现前端主页面与微前端页面的“拼图式”集成。
微前端完成开发、集成和部署后,在前端主页面完成微前端注册以及页面路由配置,即可实现动态加载微前端页面。
2. 微前端与微服务的集成
微前端与微服务独立开发,独立部署。在微前端注册到前端主页面前,微前端需要与微服务完成集成。它的集成方式与传统前后端分离的集成方式没有差异。微服务将服务发布到 API 网关,微前端调用发布在 API 网关中的服务,即完成业务单元内的前后端集成。
团队职责边界
当你采用业务单元化的开发方式后,前后端项目团队职责和应用边界会更清晰,可以降低前后端集成的复杂度。我们看一下前中台团队的职责分工。
前端项目团队专注于前端集成主页面与微前端的集成,完成前端主页面的企业级主流程的页面和流程编排以及微前端页面的动态加载,确保主流程业务逻辑和流程正确。前端项目除了要负责企业内页面风格的整体风格设计、业务流程的流转和控制外,还需要负责微前端页面动态加载、微前端注册、页面路由和页面数据共享等前端技术的实现。
中台项目团队完成业务单元组件的开发、测试和集成,确保业务单元内的业务逻辑、页面和流程正确,向外提供包含页面逻辑和业务逻辑的业务单元组件。
这样,前端项目团队只需要完成企业级前端主页面与业务单元的融合,前端只关注前端主页面与微前端页面之间的集成。这样就可以降低前端团队的技术敏感度、团队的沟通成本和集成复杂度,提高交付效率和用户体验。
中台项目团队关注业务单元功能的完整性和自包含能力,完成业务单元内微服务和微前端开发、集成和部署,提供业务单元组件。这样,业务单元的微前端与微服务的集成就会由一个中台团队完成,熟悉的人干熟悉的事情,可以降低集成过程中的沟通和技术成本,加快开发效率。
一个有关保险微前端设计的案例
保险公司有很多面向不同场景的保险产品,由于业务场景不同,其核心领域模型就会有差异,在页面要素、业务规则和流程等方面前端界面也会不同。为了避免领域模型差异较大的产品之间的相互影响和干扰,我们可以将相似的领域模型的保险产品聚合在一起,完成核心中台设计。
那有的保险集团为了统一运营,会实现寿险、财险等集团化的全险种销售。这样前端项目团队就需要用一个前端应用,集成非常多的不同产品的核心中台微服务,前端应用与中台微服务之间的集成将会更复杂。
如果仍然采用传统的单体前端模式,将会面临比较大的困难。
第一是前端页面开发和设计的复杂性。以录单前端为例,如果用一个前端页面来适配全险种,由于不同产品的前端页面要素不同,需要妥协并兼容所有产品界面的差异,这会增加前端开发的复杂度,也影响用户体验。而如果为每类产品开发不同的前端,前端项目团队需要在页面开发和设计上,投入巨大的工作量。
第二是前端与微服务集成的复杂性。在前端与微服务集成时,前端项目团队需要了解所有产品的 API 详细信息,完成前端与微服务的集成,还要根据主页面流程,实现不同产品的 API 服务路由。大量的 API 服务集成和服务路由,会增加系统集成的复杂度和出错的概率。
第三是前后端软件版本的协同发布。关联的应用多了以后,一旦某一个中台微服务的 API 服务出现重大调整,就需要协调所有受影响的应用同时完成版本发布,频繁的版本发布会影响不同产品的正常运营。
那如何用一个前端应用实现全险种产品销售呢?怎样设计才能降低集成的复杂度,实现前端界面融合,后端中台解耦呢?
我们看一下下面这个图。我们借鉴了电商的订单模式实现保险产品的全险种订单化销售,在一个前端主页面可以将所有业务流程和业务操作无缝串联起来。虽然后端有很多业务单元(包含微服务和微前端),但用户始终感觉是在一个前端应用中操作。
要在一个前端应用中实现全险种销售,需要完成以下内容的设计。
1. 微服务
微服务分为两类,一类是核心中台微服务,包括:投保微服务,实现核心出单业务逻辑;另一类是通用中台微服务,包括如:商品、订单、购物车和支付等微服务,实现通用共享业务逻辑。
2. 微前端
每个微服务都有自己的微前端页面,实现领域模型的微服务前端页面操作。核心中台投保微服务有出单微前端。订单、商品以及支付微服务都有自己的微前端页面。
3. 业务单元
微服务与微前端组合为一个业务单元。由一个中台团队完成业务单元的开发、集成、测试和部署,确保业务单元内页面操作和业务逻辑正确。比如:投保微服务和出单微前端组合为投保业务单元,独立完成保险产品从前端到后端的投保业务。
4. 前端主页面
前端主页面类似门户,包括页面导航以及部分通用的常驻主页面的共享页面,比如购物车。前端主页面和所有微前端应统一界面风格,符合统一的前端集成规范。按照正确的业务逻辑和规则,动态加载不同业务单元的微前端页面。前端主页面作为一个整体,协调核心和通用业务单元的微前端页面,完成业务操作和业务流程,提供全险种销售接触界面,包括商品目录、录单、购物车、订单、支付等操作。
5. 业务流程说明
我来简要说明一下用户在前端主页面的投保的主要业务流程。
第 1 步:用户在前端主页面,从商品目录微前端页面,选择保险产品。
第 2 步:前端主页面根据选择的产品,从主页面配置数据中,获取产品出单微前端路由地址。加载出单微前端页面,完成录单,投保微服务实现投保业务逻辑,在业务单元内生成投保单。
第 3 步:加载购物车微前端,将投保单加入购物车。
第 4 步:重复 1-3 步,生成多个投保单。
第 5 步:从购物车微前端中选择多个投保单,加载订单微前端,生成订单。
第 6 步:加载支付微前端,完成支付。
第 7 步:在投保微服务中,将订单中的投保单生成保单。
虽然后端有很多业务单元在支持,但用户所有的页面操作和流转是在一个前端主页面完成的。在进行全险种的订单化销售时,用户始终感觉是在操作一个系统。这种设计方式很好地体现了前端的融合和中台的解耦。
总结
今天我们主要探讨了微前端的设计方法。虽然微前端和微服务也采用前后端分离的设计方式,但在业务单元内,它们是在同一个领域模型下,分别实现前端和后端的业务逻辑,对外提供组件化的服务。
微前端和业务单元化的设计模式可以减轻企业级中台,前后端应用开发和集成的复杂度,真正实现前端融合和中台解耦。它的主要价值和意义如下:
1. 前端集成简单:前端项目只需关注前端集成主页面与微前端的集成,实现模块化集成和拼图式的开发,降低前端集成的复杂度和成本。
2. 项目职责专一:中台项目从数据库、中台微服务到微前端界面,端到端地完成领域逻辑功能开发,以业务组件的方式整体提供服务。在业务单元内,由团队自己完成前后端集成,可以降低开发和集成团队的沟通成本和集成复杂度。
3. 隔离和依赖性:业务单元在代码、逻辑和物理边界都是隔离的,可降低应用之间的依赖性。出现问题时可快速定位和修复,问题可以控制在一个业务单元内。业务单元之间相互无影响。
4. 降低沟通和测试成本:中台团队实现从微前端页面到中台微服务的业务单元逻辑,实现业务单元的开发、测试、集成和部署的全流程和全生命周期管理,降低前后端集成的测试和沟通成本。
5. 更敏捷地发布:业务单元之间有很好的隔离性和依赖性低,业务单元的变化都可以被控制在业务单元内。项目团队可以独立按照自己的步调进行迭代开发,实现更快的发布周期。版本发布时不会影响其它业务单元的正常运行。
6. 降低技术敏感性:前端项目关注前端主页面与微前端的集成。降低了前端项目团队对中台微服务技术的敏感性。中台项目团队可以更独立地尝试新技术和架构,实现架构的演进。
7. 高度复用性:微前端和中台微服务都有高度的复用性。微前端可快速加载到多个 APP还可以将一个微前端直接发布为 APP 或微信小程序,实现灵活的前端组合、复用和快速发布。

View File

@ -0,0 +1,241 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 知识点串讲基于DDD的微服务设计实例
你好,我是欧创新。
为了更好地理解 DDD 的设计流程,今天我会用一个项目来带你了解 DDD 的战略设计和战术设计,走一遍从领域建模到微服务设计的全过程,一起掌握 DDD 的主要设计流程和关键点。
项目基本信息
项目的目标是实现在线请假和考勤管理。功能描述如下:
请假人填写请假单提交审批,根据请假人身份、请假类型和请假天数进行校验,根据审批规则逐级递交上级审批,逐级核批通过则完成审批,否则审批不通过退回申请人。
根据考勤规则,核销请假数据后,对考勤数据进行校验,输出考勤统计。
战略设计
战略设计是根据用户旅程分析,找出领域对象和聚合根,对实体和值对象进行聚类组成聚合,划分限界上下文,建立领域模型的过程。
战略设计采用的方法是事件风暴,包括:产品愿景、场景分析、领域建模和微服务拆分等几个主要过程。
战略设计阶段建议参与人员:领域专家、业务需求方、产品经理、架构师、项目经理、开发经理和测试经理。
1. 产品愿景
产品愿景是对产品顶层价值设计,对产品目标用户、核心价值、差异化竞争点等信息达成一致,避免产品偏离方向。
事件风暴时,所有参与者针对每一个要点,在贴纸上写出自己的意见,贴到白板上。事件风暴主持者会对每个贴纸,讨论并对发散的意见进行收敛和统一,形成下面的产品愿景图。
我们把这个产品愿景图整理成一段文字就是:为了满足内外部人员,他们的在线请假、自动考勤统计和外部人员管理的需求,我们建设这个在线请假考勤系统,它是一个在线请假平台,可以自动考勤统计。它可以同时支持内外网请假,同时管理内外部人员请假和定期考勤分析,而不像 HR 系统,只管理内部人员,且只能内网使用。我们的产品内外网皆可使用,可实现内外部人员无差异管理。
通过产品愿景分析项目团队统一了系统名称——在线请假考勤系统明确了项目目标和关键功能与竞品HR的关键差异以及自己的优势和核心竞争力等。
产品愿景分析对于初创系统明确系统建设重点,统一团队建设目标和建立通用语言是很有价值的。但如果你的系统目标和需求非常清晰,这一步可以忽略。
2. 场景分析
场景分析是从用户视角出发,探索业务领域中的典型场景,产出领域中需要支撑的场景分类、用例操作以及不同子域之间的依赖关系,用以支撑领域建模。
项目团队成员一起用事件风暴分析请假和考勤的用户旅程。根据不同角色的旅程和场景分析,尽可能全面地梳理从前端操作到后端业务逻辑发生的所有操作、命令、领域事件以及外部依赖关系等信息。
下面我就以请假和人员两个场景作为示例。
第一个场景:请假
用户:请假人
请假人登录系统:从权限微服务获取请假人信息和权限数据,完成登录认证。
创建请假单:打开请假页面,选择请假类型和起始时间,录入请假信息。保存并创建请假单,提交请假审批。
修改请假单:查询请假单,打开请假页面,修改请假单,提交请假审批。
提交审批:获取审批规则,根据审批规则,从人员组织关系中获取审批人,给请假单分配审批人。
第二个场景:审批
用户:审批人
审批人登录系统:从权限微服务获取审批人信息和权限数据,完成登录认证。
获取请假单:获取审批人名下请假单,选择请假单。
审批:填写审批意见。
逐级审批:如果还需要上级审批,根据审批规则,从人员组织关系中获取审批人,给请假单分配审批人。重复以上 4 步。
最后审批人完成审批。
完成审批后,产生请假审批已通过领域事件。后续有两个进一步的业务操作:发送请假审批已通过的通知,通知邮件系统告知请假人;将请假数据发送到考勤以便核销。
下面这个图是人员组织关系场景分析结果图,详细的分析过程以及考勤的场景分析就不描述了。
3. 领域建模
领域建模是通过对业务和问题域进行分析,建立领域模型。向上通过限界上下文指导微服务边界设计,向下通过聚合指导实体对象设计。
领域建模是一个收敛的过程,分三步:
第一步找出领域实体和值对象等领域对象;
第二步找出聚合根,根据实体、值对象与聚合根的依赖关系,建立聚合;
第三步根据业务及语义边界等因素,定义限界上下文。
下面我们就逐步详细讲解一下。
第一步:找出实体和值对象等领域对象
根据场景分析,分析并找出发起或产生这些命令或领域事件的实体和值对象。将与实体或值对象有关的命令和事件聚集到实体。
下面这个图是分析后的实体与命令的关系。通过分析,我们找到了:请假单、审批意见、审批规则、人员、组织关系、刷卡明细、考勤明细以及考勤统计等实体和值对象。
第二步:定义聚合
定义聚合前,先找出聚合根。从上面的实体中,我们可以找出“请假单”和“人员”两个聚合根。然后找出与聚合根紧密依赖的实体和值对象。我们发现审批意见、审批规则和请假单紧密关联,组织关系和人员紧密关联。
找出这些实体的关系后,我们发现还有刷卡明细、考勤明细和考勤统计,这几个实体没有聚合根。这种情形在领域建模时你会经常遇到,对于这类场景我们需要分情况特殊处理。
刷卡明细、考勤明细和考勤统计这几个实体,它们之间相互独立,找不出聚合根,不是富领域模型,但它们一起完成考勤业务逻辑,具有很高的业务内聚性。我们将这几个业务关联紧密的实体,放在一个考勤聚合内。在微服务设计时,我们依然采用 DDD 的设计和分析方法。由于没有聚合根来管理聚合内的实体,我们可以用传统的方法来管理实体。
经过分析,我们建立了请假、人员组织关系和考勤三个聚合。其中请假聚合有请假单、审批意见实体和审批规则等值对象。人员组织关系聚合有人员和组织关系等实体。考勤聚合有刷卡明细、考勤明细和考勤统计等实体。
第三步:定义限界上下文
由于人员组织关系聚合与请假聚合,共同完成请假的业务功能,两者在请假的限界上下文内。考勤聚合则单独构成考勤统计限界上下文。因此我们为业务划分请假和考勤统计两个限界上下文,建立请假和考勤两个领域模型。
4. 微服务的拆分
理论上一个限界上下文就可以设计为一个微服务,但还需要综合考虑多种外部因素,比如:职责单一性、敏态与稳态业务分离、非功能性需求(如弹性伸缩、版本发布频率和安全等要求)、软件包大小、团队沟通效率和技术异构等非业务要素。
在这个项目,我们划分微服务主要考虑职责单一性原则。因此根据限界上下文就可以拆分为请假和考勤两个微服务。其中请假微服务包含人员组织关系和请假两个聚合,考勤微服务包含考勤聚合。
到这里,战略设计就结束了。通过战略设计,我们建立了领域模型,划分了微服务边界。下一步就是战术设计了,也就是微服务设计。下面我们以请假微服务为例,讲解其设计过程。
战术设计
战术设计是根据领域模型进行微服务设计的过程。这个阶段主要梳理微服务内的领域对象,梳理领域对象之间的关系,确定它们在代码模型和分层架构中的位置,建立领域模型与微服务模型的映射关系,以及服务之间的依赖关系。
战术设计阶段建议参与人员:领域专家、产品经理、架构师、项目经理、开发经理和测试经理等。
战术设计包括以下两个阶段:分析微服务领域对象和设计微服务代码结构。
1. 分析微服务领域对象
领域模型有很多领域对象,但是这些对象带有比较重的业务属性。要完成从领域模型到微服务的落地,还需要进一步的分析和设计。在事件风暴基础上,我们进一步细化领域对象以及它们的关系,补充事件风暴可能遗漏的业务和技术细节。
我们分析微服务内应该有哪些服务?服务的分层?应用服务由哪些服务组合和编排完成?领域服务包括哪些实体和实体方法?哪个实体是聚合根?实体有哪些属性和方法?哪些对象应该设计为值对象等。
服务的识别和设计
事件风暴的命令是外部的一些操作和业务行为,也是微服务对外提供的能力。它往往与微服务的应用服务或者领域服务对应。我们可以将命令作为服务识别和设计的起点。具体步骤如下:
根据命令设计应用服务,确定应用服务的功能,服务集合,组合和编排方式。服务集合中的服务包括领域服务或其它微服务的应用服务。
根据应用服务功能要求设计领域服务,定义领域服务。这里需要注意:应用服务可能是由多个聚合的领域服务组合而成的。
根据领域服务的功能,确定领域服务内的实体以及功能。
设计实体基本属性和方法。
另外,我们还要考虑领域事件的异步化处理。
我以提交审批这个动作为例,来说明服务的识别和设计。提交审批的大体流程是:
根据请假类型和时长,查询请假审批规则,获取下一步审批人的角色。
根据审批角色从人员组织关系中查询下一审批人。
为请假单分配审批人,并将审批规则保存至请假单。
通过分析,我们需要在应用层和领域层设计以下服务和方法。
应用层:提交审批应用服务。
领域层:领域服务有查询审批规则、修改请假流程信息服务以及根据审批规则查询审批人服务,分别位于请假和人员组织关系聚合。请假单实体有修改请假流程信息方法,审批规则值对象有查询审批规则方法。人员实体有根据审批规则查询审批人方法。下图是我们分析出来的服务以及它们之间的依赖关系。
服务的识别和设计过程就是这样了,我们再来设计一下聚合内的对象。
聚合中的对象
在请假单聚合中,聚合根是请假单。
请假单经多级审核后,会产生多条审批意见,为了方便查询,我们可以将审批意见设计为实体。请假审批通过后,会产生请假审批通过的领域事件,因此还会有请假事件实体。请假聚合有以下实体:审批意见(记录审批人、审批状态和审批意见)和请假事件实体。
我们再来分析一下请假单聚合的值对象。请假人和下一审批人数据来源于人员组织关系聚合中的人员实体,可设计为值对象。人员类型、请假类型和审批状态是枚举值类型,可设计为值对象。确定请假审批规则后,审批规则也可作为请假单的值对象。请假单聚合将包含以下值对象:请假人、人员类型、请假类型、下一审批人、审批状态和审批规则。
综上,我们就可以画出请假聚合对象关系图了。
在人员组织关系聚合中,我们可以建立人员之间的组织关系,通过组织关系类型找到上级审批领导。它的聚合根是人员。实体有组织关系(包括组织关系类型和上级审批领导)。其中组织关系类型(如项目经理、处长、总经理等)是值对象。上级审批领导来源于人员聚合根,可设计为值对象。人员组织关系聚合将包含以下值对象:组织关系类型、上级审批领导。
综上,我们又可以画出人员组织关系聚合对象关系图了。
微服务内的对象清单
在确定各领域对象的属性后,我们就可以设计各领域对象在代码模型中的代码对象(包括代码对象的包名、类名和方法名),建立领域对象与代码对象的一一映射关系了。根据这种映射关系,相关人员可快速定位到业务逻辑所在的代码位置。在经过以上分析后,我们在微服务内就可以分析出如下图的对象清单。
2. 设计微服务代码结构
根据 DDD 的代码模型和各领域对象所在的包、类和方法,我们可以定义出请假微服务的代码结构,设计代码对象。
应用层代码结构
应用层包括应用服务、DTO 以及事件发布相关代码。在 LeaveApplicationService 类内实现与聚合相关的应用服务,在 LoginApplicationService 封装外部微服务认证和权限的应用服务。
这里提醒一下:如果应用服务逻辑复杂的话,一个应用服务就可以构建一个类,这样可以避免一个类的代码过于庞大,不利于维护。
领域层代码结构
领域层包括一个或多个聚合的实体类、事件实体类、领域服务以及工厂、仓储相关代码。一个聚合对应一个聚合代码目录,聚合之间在代码上完全隔离,聚合之间通过应用层协调。
请假微服务领域层包含请假和人员两个聚合。人员和请假代码都放在各自的聚合所在目录结构的代码包中。如果随着业务发展,人员相关功能需要从请假微服务中拆分出来,我们只需将人员聚合代码包稍加改造,独立部署,即可快速发布为人员微服务。到这里,微服务内的领域对象,分层以及依赖关系就梳理清晰了。微服务的总体架构和代码模型也基本搭建完成了。
后续的工作
1. 详细设计
在完成领域模型和微服务设计后,我们还需要对微服务进行详细的设计。主要设计以下内容:实体属性、数据库表和字段、实体与数据库表映射、服务参数规约及功能实现等。
2. 代码开发和测试
开发人员只需要按照详细的设计文档和功能要求,找到业务功能对应的代码位置,完成代码开发就可以了。代码开发完成后,开发人员要编写单元测试用例,基于挡板模拟依赖对象完成服务测试。
总结
今天我们通过在线请假考勤项目,把 DDD 设计过程完整地走了一遍。
DDD 战略设计从事件风暴开始,然后我们要找出实体等领域对象,找出聚合根构建聚合,划分限界上下文,建立领域模型。
战术设计从事件风暴的命令开始,识别和设计服务,建立各层服务的依赖关系,设计微服务内的实体和值对象,找出微服务中所有的领域对象,并建立领域对象与代码对象的映射关系。
这样就可以很好地指导项目团队进行微服务开发和测试了。总结完毕,到这你是否已经清楚 DDD 全部的设计过程了呢?有疑问欢迎留言讨论。

View File

@ -0,0 +1,149 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
19 总结(一):微服务设计和拆分要坚持哪些原则?
你好,我是欧创新。
我们前面已经讲了很多 DDD 的设计方法和实践案例。虽然 DDD 的设计思想和方法很好但由于企业发展历程以及企业技术和文化的不同DDD 和微服务的实施策略也会有差异。那么面对这种差异,我们应该如何落地 DDD 和微服务呢?今天我们就来聊聊微服务的设计原则和演进策略。
微服务的演进策略
在从单体向微服务演进时,演进策略大体分为两种:绞杀者策略和修缮者策略。
1. 绞杀者策略
绞杀者策略是一种逐步剥离业务能力,用微服务逐步替代原有单体系统的策略。它对单体系统进行领域建模,根据领域边界,在单体系统之外,将新功能和部分业务能力独立出来,建设独立的微服务。新微服务与单体系统保持松耦合关系。
随着时间的推移,大部分单体系统的功能将被独立为微服务,这样就慢慢绞杀掉了原来的单体系统。绞杀者策略类似建筑拆迁,完成部分新建筑物后,然后拆除部分旧建筑物。
2. 修缮者策略
修缮者策略是一种维持原有系统整体能力不变,逐步优化系统整体能力的策略。它是在现有系统的基础上,剥离影响整体业务的部分功能,独立为微服务,比如高性能要求的功能,代码质量不高或者版本发布频率不一致的功能等。
通过这些功能的剥离,我们就可以兼顾整体和局部,解决系统整体不协调的问题。修缮者策略类似古建筑修复,将存在问题的部分功能重建或者修复后,重新加入到原有的建筑中,保持建筑原貌和功能不变。一般人从外表感觉不到这个变化,但是建筑物质量却得到了很大的提升。
其实还有第三种策略,就是另起炉灶,顾名思义就是将原有的系统推倒重做。建设期间,原有单体系统照常运行,一般会停止开发新需求。而新系统则会组织新的项目团队,按照原有系统的功能域,重新做领域建模,开发新的微服务。在完成数据迁移后,进行新旧系统切换。
对于大型核心系统我一般不建议采用这种策略,这是因为系统重构后的不稳定性、大量未知的潜在技术风险和新的开发模式下项目团队磨合等不确定性因素,会导致项目实施难度大大增加。
不同场景下的领域建模策略
由于企业内情况千差万别,发展历程也不一样,有遗留单体系统的微服务改造,也有全新未知领域的业务建模和系统设计,还有遗留系统局部优化的情况。不同场景下,领域建模的策略也会有差异。下面我们就分几类场景来看看如何进行领域建模。
1. 新建系统
新建系统又分为简单和复杂领域建模两种场景。
简单领域建模
简单的业务领域,一个领域就是一个小的子域。在这个小的问题域内,领域建模过程相对简单,直接采用事件风暴的方法构建领域模型就可以了。
复杂领域建模
对于复杂的业务领域,领域可能需要多级拆分后才能开始领域建模。领域拆分为子域,甚至子域还需要进一步拆分。比如:保险它需要拆分为承保、理赔、收付费和再保等子域,承保子域再拆分为投保、保单管理等子子域。复杂领域如果不做进一步细分,由于问题域太大,领域建模的工程量会非常浩大。你不太容易通过事件风暴,完成一个很大的领域建模,即使勉强完成,效果也不一定好。
对于复杂领域,我们可以分三步来完成领域建模和微服务设计。
第一步,拆分子域建立领域模型
根据业务领域的特点,参考流程节点边界或功能聚合模块等边界因素。结合领域专家和项目团队的讨论,将领域逐级分解为大小合适的子域,针对子域采用事件风暴,划分聚合和限界上下文,初步确定子域内的领域模型。
第二步,领域模型微调
梳理领域内所有子域的领域模型,对各子域领域模型进行微调。微调的过程重点考虑不同领域模型中聚合的重组。同步考虑领域模型和聚合的边界,服务以及事件之间的依赖关系,确定最终的领域模型。
第三步,微服务的设计和拆分
根据领域模型和微服务拆分原则,完成微服务的拆分和设计。
2. 单体遗留系统
如果我们面对的是一个单体遗留系统,只需要将部分功能独立为微服务,而其余仍为单体,整体保持不变,比如将面临性能瓶颈的模块拆分为微服务。我们只需要将这一特定功能,理解为一个简单子领域,参考简单领域建模的方式就可以了。在微服务设计中,我们还要考虑新老系统之间服务和业务的兼容,必要时可引入防腐层。
DDD 使用的误区
很多人在接触微服务后,但凡是系统,一概都想设计成微服务架构。其实有些业务场景,单体架构的开发成本会更低,开发效率更高,采用单体架构也不失为好的选择。同样,虽然 DDD 很好,但有些传统设计方法在微服务设计时依然有它的用武之地。下面我们就来聊聊 DDD 使用的几个误区。
1. 所有的领域都用 DDD
很多人在学会 DDD 后,可能会将其用在所有业务域,即全部使用 DDD 来设计。DDD 从战略设计到战术设计,是一个相对复杂的过程,首先企业内要培养 DDD 的文化,其次对团队成员的设计和技术能力要求相对比较高。在资源有限的情况下,应聚焦核心域,建议你先从富领域模型的核心域开始,而不必一下就在全业务域推开。
2. 全部采用 DDD 战术设计方法
不同的设计方法有它的适用环境我们应选择它最擅长的场景。DDD 有很多的概念和战术设计方法,比如聚合根和值对象等。聚合根利用仓储管理聚合内实体数据之间的一致性,这种方法对于管理新建和修改数据非常有效,比如在修改订单数据时,它可以保证订单总金额与所有商品明细金额的一致,但它并不擅长较大数据量的查询处理,甚至有延迟加载进而影响效率的问题。
而传统的设计方法,可能一条简单的 SQL 语句就可以很快地解决问题。而很多贫领域模型的业务比如数据统计和分析DDD 很多方法可能都用不上,或用得并不顺手,而传统的方法很容易就解决了。
因此,在遵守领域边界和微服务分层等大原则下,在进行战术层面设计时,我们应该选择最适合的方法,不只是 DDD 设计方法,当然还应该包括传统的设计方法。这里要以快速、高效解决实际问题为最佳,不要为做 DDD 而做 DDD。
3. 重战术设计而轻战略设计
很多 DDD 初学者,学习 DDD 的主要目的,可能是为了开发微服务,因此更看重 DDD 的战术设计实现。殊不知 DDD 是一种从领域建模到微服务落地的全方位的解决方案。
战略设计时构建的领域模型,是微服务设计和开发的输入,它确定了微服务的边界、聚合、代码对象以及服务等关键领域对象。领域模型边界划分得清不清晰,领域对象定义得明不明确,会决定微服务的设计和开发质量。没有领域模型的输入,基于 DDD 的微服务的设计和开发将无从谈起。因此我们不仅要重视战术设计,更要重视战略设计。
4. DDD 只适用于微服务
DDD 是在微服务出现后才真正火爆起来的,很多人会认为 DDD 只适用于微服务。在 DDD 沉默的二十多年里,其实它一直也被应用在单体应用的设计中。
具体项目实施时,要吸取 DDD 的核心设计思想和理念,结合具体的业务场景和团队技术特点,多种方法组合,灵活运用,用正确的方式解决实际问题。
微服务设计原则
微服务设计原则中,如高内聚低耦合、复用、单一职责等这些常见的设计原则在此就不赘述了,我主要强调下面这几条:
第一条:要领域驱动设计,而不是数据驱动设计,也不是界面驱动设计。
微服务设计首先应建立领域模型,确定逻辑和物理边界以及领域对象后,然后才开始微服务的拆分和设计。而不是先定义数据模型和库表结构,也不是前端界面需要什么,就去调整核心领域逻辑代码。在设计时应该将外部需求从外到内逐级消化,尽量降低对核心领域层逻辑的影响。
第二条:要边界清晰的微服务,而不是泥球小单体。
微服务上线后其功能和代码也不是一成不变的。随着需求或设计变化,领域模型会迭代,微服务的代码也会分分合合。边界清晰的微服务,可快速实现微服务代码的重组。微服务内聚合之间的领域服务和数据库实体原则上应杜绝相互依赖。你可通过应用服务编排或者事件驱动,实现聚合之间的解耦,以便微服务的架构演进。
第三条:要职能清晰的分层,而不是什么都放的大箩筐。
分层架构中各层职能定位清晰,且都只能与其下方的层发生依赖,也就是说只能从外层调用内层服务,内层通过封装、组合或编排对外逐层暴露,服务粒度也由细到粗。应用层负责服务的组合和编排,不应有太多的核心业务逻辑,领域层负责核心领域业务逻辑的实现。各层应各司其职,职责边界不要混乱。在服务演进时,应尽量将可复用的能力向下层沉淀。
第四条:要做自己能 hold 住的微服务,而不是过度拆分的微服务。
微服务过度拆分必然会带来软件维护成本的上升比如集成成本、运维成本、监控和定位问题的成本。企业在微服务转型过程中还需要有云计算、DevOps、自动化监控等能力而一般企业很难在短时间内提升这些能力如果项目团队没有这些能力将很难 hold 住这些微服务。
如果在微服务设计之初按照 DDD 的战略设计方法,定义好了微服务内的逻辑边界,做好了架构的分层,其实我们不必拆分太多的微服务,即使是单体也未尝不可。随着技术积累和能力提升,当我们有了这些能力后,由于应用内有清晰的逻辑边界,我们可以随时轻松地重组出新的微服务,而这个过程不会花费太多的时间和精力。
微服务拆分需要考虑哪些因素?
理论上一个限界上下文内的领域模型可以被设计为微服务,但是由于领域建模主要从业务视角出发,没有考虑非业务因素,比如需求变更频率、高性能、安全、团队以及技术异构等因素,而这些非业务因素对于领域模型的系统落地也会起到决定性作用,因此在微服务拆分时我们需要重点考虑它们。我列出了以下主要因素供你参考。
1. 基于领域模型
基于领域模型进行拆分,围绕业务领域按职责单一性、功能完整性拆分。
2. 基于业务需求变化频率
识别领域模型中的业务需求变动频繁的功能,考虑业务变更频率与相关度,将业务需求变动较高和功能相对稳定的业务进行分离。这是因为需求的经常性变动必然会导致代码的频繁修改和版本发布,这种分离可以有效降低频繁变动的敏态业务对稳态业务的影响。
3. 基于应用性能
识别领域模型中性能压力较大的功能。因为性能要求高的功能可能会拖累其它功能,在资源要求上也会有区别,为了避免对整体性能和资源的影响,我们可以把在性能方面有较高要求的功能拆分出去。
4. 基于组织架构和团队规模
除非有意识地优化组织架构,否则微服务的拆分应尽量避免带来团队和组织架构的调整,避免由于功能的重新划分,而增加大量且不必要的团队之间的沟通成本。拆分后的微服务项目团队规模保持在 1012 人左右为宜。
5. 基于安全边界
有特殊安全要求的功能,应从领域模型中拆分独立,避免相互影响。
6. 基于技术异构等因素
领域模型中有些功能虽然在同一个业务域内,但在技术实现时可能会存在较大的差异,也就是说领域模型内部不同的功能存在技术异构的问题。由于业务场景或者技术条件的限制,有的可能用.NET有的则是 Java有的甚至大数据架构。对于这些存在技术异构的功能可以考虑按照技术边界进行拆分。
总结
相信你在微服务落地的时候会有很多的收获和感悟。对于 DDD 和微服务,我想总结的就是:深刻理解 DDD 的设计思想和内涵,把握好边界和分层这个大原则,结合企业文化和技术特点,灵活运用战术设计方法,选择最适合的技术和方法解决实际问题,切勿为了 DDD 而做 DDD

View File

@ -0,0 +1,137 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
20 总结分布式架构关键设计10问
你好,我是欧创新。
前面我们重点讲述了领域建模、微服务设计和前端设计方法,它们组合在一起就可以形成中台建设的整体解决方案。而中台大多基于分布式微服务架构,这种企业级的数字化转型有很多地方值得我们关注和思考。
我们不仅要关注企业商业模式、业务边界以及前中台的融合,还要关注数据技术体系、微服务设计、多活等多领域的设计和协同。结合实施经验和思考,今天我们就来聊聊分布式架构下的几个关键问题。
一、选择什么样的分布式数据库?
分布式架构下的数据应用场景远比集中式架构复杂,会产生很多数据相关的问题。谈到数据,首先就是要选择合适的分布式数据库。
分布式数据库大多采用数据多副本的方式,实现数据访问的高性能、多活和容灾。目前主要有三种不同的分布式数据库解决方案。它们的主要差异是数据多副本的处理方式和数据库中间件。
1. 一体化分布式数据库方案
它支持数据多副本、高可用。多采用 Paxos 协议,一次写入多数据副本,多数副本写入成功即算成功。代表产品是 OceanBase 和高斯数据库。
2. 集中式数据库 + 数据库中间件方案
它是集中式数据库与数据库中间件结合的方案,通过数据库中间件实现数据路由和全局数据管理。数据库中间件和数据库独立部署,采用数据库自身的同步机制实现主副本数据的一致性。集中式数据库主要有 MySQL 和 PostgreSQL 数据库,基于这两种数据库衍生出了很多的解决方案,比如开源数据库中间件 MyCat+MySQL 方案TBase基于 PostgreSQL但做了比较大的封装和改动等方案。
3. 集中式数据库 + 分库类库方案
它是一种轻量级的数据库中间件方案,分库类库实际上是一个基础 JAR 包,与应用软件部署在一起,实现数据路由和数据归集。它适合比较简单的读写交易场景,在强一致性和聚合分析查询方面相对较弱。典型分库基础组件有 ShardingSphere。
小结:这三种方案实施成本不一样,业务支持能力差异也比较大。一体化分布式数据库主要由互联网大厂开发,具有超强的数据处理能力,大多需要云计算底座,实施成本和技术能力要求比较高。集中式数据库 + 数据库中间件方案,实施成本和技术能力要求适中,可满足中大型企业业务要求。第三种分库类库的方案可处理简单的业务场景,成本和技能要求相对较低。在选择数据库的时候,我们要考虑自身能力、成本以及业务需要,从而选择合适的方案。
二、如何设计数据库分库主键?
选择了分布式数据库,第二步就要考虑数据分库,这时分库主键的设计就很关键了。
与客户接触的关键业务,我建议你以客户 ID 作为分库主键。这样可以确保同一个客户的数据分布在同一个数据单元内,避免出现跨数据单元的频繁数据访问。跨数据中心的频繁服务调用或跨数据单元的查询,会对系统性能造成致命的影响。
将客户的所有数据放在同一个数据单元,对客户来说也更容易提供客户一致性服务。而对企业来说,“以客户为中心”的业务能力,首先就要做到数据上的“以客户为中心”。
当然,你也可以根据业务需要用其它的业务属性作为分库主键,比如机构、用户等。
三、数据库的数据同步和复制
在微服务架构中,数据被进一步分割。为了实现数据的整合,数据库之间批量数据同步与复制是必不可少的。数据同步与复制主要用于数据库之间的数据同步,实现业务数据迁移、数据备份、不同渠道核心业务数据向数据平台或数据中台的数据复制、以及不同主题数据的整合等。
传统的数据传输方式有 ETL 工具和定时提数程序但数据在时效性方面存在短板。分布式架构一般采用基于数据库逻辑日志增量数据捕获CDC技术它可以实现准实时的数据复制和传输实现数据处理与应用逻辑解耦使用起来更加简单便捷。
现在主流的 PostgreSQL 和 MySQL 数据库外围有很多数据库日志捕获技术组件。CDC 也可以用在领域事件驱动设计中,作为领域事件增量数据的获取技术。
四、跨库关联查询如何处理?
跨库关联查询是分布式数据库的一个短板,会影响查询性能。在领域建模时,很多实体会分散到不同的微服务中,但很多时候会因为业务需求,它们之间需要关联查询。
关联查询的业务场景包括两类:第一类是基于某一维度或某一主题域的数据查询,比如基于客户全业务视图的数据查询,这种查询会跨多个业务线的微服务;第二类是表与表之间的关联查询,比如机构表与业务表的联表查询,但机构表和业务表分散在不同的微服务。
如何解决这两类关联查询呢?
对于第一类场景,由于数据分散在不同微服务里,我们无法跨多个微服务来统计这些数据。你可以建立面向主题的分布式数据库,它的数据来源于不同业务的微服务。采用数据库日志捕获技术,从各业务端微服务将数据准实时汇集到主题数据库。在数据汇集时,提前做好数据关联(如将多表数据合并为一个宽表)或者建立数据模型。面向主题数据库建设查询微服务。这样一次查询你就可以获取客户所有维度的业务数据了。你还可以根据主题或场景设计合适的分库主键,提高查询效率。
对于第二类场景,对于不在同一个数据库的表与表之间的关联查询场景,你可以采用小表广播,在业务库中增加一张冗余的代码副表。当主表数据发生变化时,你可以通过消息发布和订阅的领域事件驱动模式,异步刷新所有副表数据。这样既可以解决表与表的关联查询,还可以提高数据的查询效率。
五、如何处理高频热点数据?
对于高频热点数据,比如商品、机构等代码类数据,它们同时面向多个应用,要有很高的并发响应能力。它们会给数据库带来巨大的访问压力,影响系统的性能。
常见的做法是将这些高频热点数据,从数据库加载到如 Redis 等缓存中,通过缓存提供数据访问服务。这样既可以降低数据库的压力,还可以提高数据的访问性能。
另外,对需要模糊查询的高频数据,你也可以选用 ElasticSearch 等搜索引擎。
缓存就像调味料一样,投入小、见效快,用户体验提升快。
六、前后序业务数据的处理
在微服务设计时你会经常发现,某些数据需要关联前序微服务的数据。比如:在保险业务中,投保微服务生成投保单后,保单会关联前序投保单数据等。在电商业务中,货物运输单会关联前序订单数据。由于关联的数据分散在业务的前序微服务中,你无法通过不同微服务的数据库来给它们建立数据关联。
如何解决这种前后序的实体关联呢?
一般来说,前后序的数据都跟领域事件有关。你可以通过领域事件处理机制,按需将前序数据通过领域事件实体,传输并冗余到当前的微服务数据库中。
你可以将前序数据设计为实体或者值对象,并被当前实体引用。在设计时你需要关注以下内容:如果前序数据在当前微服务只可整体修改,并且不会对它做查询和统计分析,你可以将它设计为值对象;当前序数据是多条,并且需要做查询和统计分析,你可以将它设计为实体。
这样,你可以在货物运输微服务,一次获取前序订单的清单数据和货物运输单数据,将所有数据一次反馈给前端应用,降低跨微服务的调用。如果前序数据被设计为实体,你还可以将前序数据作为查询条件,在本地微服务完成多维度的综合数据查询。只有必要时才从前序微服务,获取前序实体的明细数据。这样,既可以保证数据的完整性,还可以降低微服务的依赖,减少跨微服务调用,提升系统性能。
七、数据中台与企业级数据集成
分布式微服务架构虽然提升了应用弹性和高可用能力,但原来集中的数据会随着微服务拆分而形成很多数据孤岛,增加数据集成和企业级数据使用的难度。你可以通过数据中台来实现数据融合,解决分布式架构下的数据应用和集成问题。
你可以分三步来建设数据中台。
第一,按照统一数据标准,完成不同微服务和渠道业务数据的汇集和存储,解决数据孤岛和初级数据共享的问题。
第二,建立主题数据模型,按照不同主题和场景对数据进行加工处理,建立面向不同主题的数据视图,比如客户统一视图、代理人视图和渠道视图等。
第三,建立业务需求驱动的数据体系,支持业务和商业模式创新。
数据中台不仅限于分析场景,也适用于交易型场景。你可以建立在数据仓库和数据平台上,将数据平台化之后提供给前台业务使用,为交易场景提供支持。
八、BFF 与企业级业务编排和协同
企业级业务流程往往是多个微服务一起协作完成的,每个单一职责的微服务就像积木块,它们只完成自己特定的功能。那如何组织这些微服务,完成企业级业务编排和协同呢?
你可以在微服务和前端应用之间,增加一层 BFF 微服务Backend for Frontends。BFF 主要职责是处理微服务之间的服务组合和编排,微服务内的应用服务也是处理服务的组合和编排,那这二者有什么差异呢?
BFF 位于中台微服务之上,主要职责是微服务之间的服务协调;应用服务主要处理微服务内的服务组合和编排。在设计时我们应尽可能地将可复用的服务能力往下层沉淀,在实现能力复用的同时,还可以避免跨中心的服务调用。
BFF 像齿轮一样,来适配前端应用与微服务之间的步调。它通过 Façade 服务适配不同的前端通过服务组合和编排组织和协调微服务。BFF 微服务可根据需求和流程变化,与前端应用版本协同发布,避免中台微服务为适配前端需求的变化,而频繁地修改和发布版本,从而保证微服务核心领域逻辑的稳定。
如果你的 BFF 做得足够强大,它就是一个集成了不同中台微服务能力、面向多渠道应用的业务能力平台。
九、分布式事务还是事件驱动机制?
分布式架构下,原来单体的内部调用,会变成分布式调用。如果一个操作涉及多个微服务的数据修改,就会产生数据一致性的问题。数据一致性有强一致性和最终一致性两种,它们实现方案不一样,实施代价也不一样。
对于实时性要求高的强一致性业务场景,你可以采用分布式事务,但分布式事务有性能代价,在设计时我们需平衡考虑业务拆分、数据一致性、性能和实现的复杂度,尽量避免分布式事务的产生。
领域事件驱动的异步方式是分布式架构常用的设计方法,它可以解决非实时场景的数据最终一致性问题。基于消息中间件的领域事件发布和订阅,可以很好地解耦微服务。通过削峰填谷,可以减轻数据库实时访问压力,提高业务吞吐量和处理能力。你还可以通过事件驱动实现读写分离,提高数据库访问性能。对最终一致性的场景,我建议你采用领域事件驱动的设计方法。
十、多中心多活的设计
分布式架构的高可用主要通过多活设计来实现,多中心多活是一个非常复杂的工程,下面我主要列出以下几个关键的设计。
\1. 选择合适的分布式数据库。数据库应该支持多数据中心部署,满足数据多副本以及数据底层复制和同步技术要求,以及数据恢复的时效性要求。
\2. 单元化架构设计。将若干个应用组成的业务单元作为部署的基本单位,实现同城和异地多活部署,以及跨中心弹性扩容。各单元业务功能自包含,所有业务流程都可在本单元完成;任意单元的数据在多个数据中心有副本,不会因故障而造成数据丢失;任何单元故障不影响其它同类单元的正常运行。单元化设计时我们要尽量避免跨数据中心和单元的调用。
\3. 访问路由。访问路由包括接入层、应用层和数据层的路由,确保前端访问能够按照路由准确到达数据中心和业务单元,准确写入或获取业务数据所在的数据库。
\4. 全局配置数据管理。实现各数据中心全局配置数据的统一管理,每个数据中心全局配置数据实时同步,保证数据的一致性。
总结
企业级分布式架构的实施是一个非常复杂的系统工程,涉及到非常多的技术体系和方法。今天我罗列了 10 个关键的设计领域,每个领域其实都非常复杂,需要很多的投入和研究。在实施的时候,你和你的公司要结合自身情况来选择合适的技术组件和实施方案。

View File

@ -0,0 +1,61 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
答疑有关3个典型问题的讲解
你好,我是欧创新。
截至今天这一讲,我们的基础篇和进阶篇的内容就结束了。在这个过程中,我一直有关注大家提的问题。那在实战篇正式开始之前啊,我想针对 3 个比较典型的问题,做一个讲解,希望你也能同步思考,调动自己已学过的内容,这对我们后面实战篇的学习也是有一定帮助的。
问题 1有关于领域可以划分为核心域、通用域和支撑域以及子域和限界上下文关系的话题还有是否有边界划分的量化标准
我在 [第 02 讲] 中讲到了,在领域不断划分的过程中,领域会被细分为不同的子域,这个过程实际上是将问题范围不断缩小的过程。
借用读者“密码 123456”的总结他认为“对于领域问题来说可以理解为对一个问题不断地划分直到划分为我们熟悉的、能够快速处理的小问题。然后再对小问题的处理排列一个优先级。”
这个理解是很到位的。在领域细分到一定的范围后,我们就可以对这个子域进行事件风暴,为这个子域划分限界上下文,建立领域模型,然后就可以基于领域模型进行微服务设计了。
虽然 DDD 没有明确说明子域和限界上下文的关系。我个人认为,子域的划分是一种比较粗的领域边界的划分,它不考虑子域内的领域对象、对象之间的关系和结构。子域的划分往往按照业务阶段或者功能模块边界进行粗分,其目的就是为了让你能够在一个相对较小的问题空间内,比较方便地用事件风暴来梳理业务场景。
而限界上下文本质上也是子域,限界上下文是在明确的子域内,用事件风暴划分出来的。它体现的是一种详细的设计过程。这个过程设计出了领域模型,明确了领域对象以及领域对象的依赖等关系,有了领域模型,你就可以直接进行微服务设计了。
关于核心域、通用域和支撑域,划分这三个不同类型子域的主要目的是为了区分业务域的优先级,确定 IT 战略投入。我们会将重要的资源投入在核心域上,确保好钢用在刀刃上。每个企业由于商业模式或者战略方向不一样,核心域会有一些差异,不要用固定的眼光看待不同企业的核心域。
核心域、通用域和支撑域都是业务领域,只不过重要性和功能属性不一样。采用的 DDD 设计方法和过程,是没有差异的。
从目前来看,还没有可以量化的领域以及限界上下文的划分标准。它主要依赖领域专家经验,以及和项目团队在事件风暴过程中不断地权衡和分析。不要奢望一次迭代就能够给复杂的业务,建立一个完美的领域模型。领域模型很多时候也需要多次迭代才能成型,它也需要不断地演进。但如果是用 DDD 设计出来的领域模型的边界和微服务内聚合的边界非常清晰的话,这个演进过程相对来说会简单很多,所需的时间成本也会很低。
问题 2关于聚合设计的问题领域层与基础层为什么要依赖倒置DIP
聚合主要实现核心业务逻辑,里面有很多的领域对象,这些领域对象之间需要通过聚合根进行统一的管理,以确保数据的一致性。
在聚合设计时我们会用到两个重要的设计模式工厂Factory模式和仓储Repository模式。如果你有兴趣详细了解的话推荐你阅读《实现领域驱动设计》一书的第 11 章和第 12 章。
那为什么要引入工厂模式呢?
这是因为有些聚合内可能含有非常多的实体和值对象,我们需要确保聚合根以及所有被依赖的对象实例同时被创建。如果都通过聚合根来构造,将会非常复杂。因此我们可以通过工厂模式来封装复杂对象的创建过程,但并不是所有对象的构造都需要用到工厂,如果构造过程不复杂,只是单一对象的构造,你用简单的构造方法就足够了。
又为什么要引入仓储模式?解答这个问题的同时,我也一起将依赖倒置的问题解答一下。
在传统的 DDD 四层架构中,所有层都是依赖基础层的。这样做有什么不好的地方呢?如果应用逻辑对基础层依赖太大的话,基础层中与资源有关的代码可能会渗透到应用逻辑中。而现在技术组件的更新频率是很快的,一旦出现基础组件的变更,且基础组件的代码被带入到了应用逻辑中,这样会对上层的应用逻辑产生致命的影响。
为了解耦应用逻辑和基础资源,在基础层和上层应用逻辑之间会增加一层,这一层就是仓储层。一个聚合对应一个仓储,仓储实现聚合内数据的持久化。聚合内的应用逻辑通过接口来访问基础资源,仓储实现在基础层实现。这样应用逻辑和基础资源的实现逻辑是分离的。如果变更基础资源组件,只需要替换仓储实现就可以了,不会对应用逻辑产生太大的影响,这样就实现了应用逻辑与基础资源的解耦,也就实现了依赖倒置。
关于聚合设计过程中的一些原则问题。大部分的业务场景我们都可以通过事件风暴,找到聚合根,建立聚合,划分限界上下文,建立领域模型。但也有部分场景,比如数据计算、统计以及批处理业务场景,所有的实体都是独立无关联的,找不到聚合根,也无法建立领域模型。但是它们之间的业务关系是非常紧密的,在业务上是高内聚的。我们也可以将这类场景作为一个聚合处理,除了不考虑聚合根的设计方法外,其它诸如 DDD 分层架构相关的设计方法都是可以采用的。
一些业务场景,如果复杂度并不高,而用 DDD 设计会带来不必要的麻烦的话,比如增加复杂度,有些原则也是可以突破的,不要为做 DDD 而做 DDD。即使采用传统的方式也是没有关系的最终以解决实际问题为最佳。但必须记住一点如果采用传统的设计方式一定要保证领域模型的边界以及微服务内聚合的逻辑边界清晰这样的话以后微服务的演进就不会太复杂。
问题 3领域事件采用消息异步机制发布方和订阅方数据如何保证一致性微服务内聚合之间领域事件是否一定要用事件总线
在领域事件设计中,为了解耦微服务,微服务之间数据采用最终一致性原则。由于发布方是在消息总线发布消息以后,并不关心数据是否送达,或者送达后订阅方是否正常处理,因此有些技术人会担心发布方和订阅方数据一致性的问题。
那在对数据一致性要求比较高的业务场景,我们是有相关的设计考虑的。也就是发送方和订阅方的事件数据都必须落库,发送方除了保存业务数据以外,在往消息中间件发布消息之前,会先将要发布的消息写入本地库。而接收方在处理消息之前,需要先将收到的消息写入本地库。然后可以采用定期对发布方和订阅方的事件数据对账的操作,识别出不一致的数据。如果数据出现异常或不一致的情况,可以启动定时程序再次发送,必要时可以转人工操作处理。
关于事件总线的问题。由于微服务内的逻辑都在一个进程内,后端数据库也是一个,微服务内的事务相比微服务之间会好控制一些。在处理微服务内的领域事件时,如果引入事件总线,会增加开发的复杂度,那是否引入事件总线,就需要你来权衡。
个人感觉如果你的场景中,不会出现导致聚合之间数据不一致的情况,就可以不使用事件总线。另外,通过应用服务也可以实现聚合之间的服务和数据协调。

View File

@ -0,0 +1,55 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 所谓高手,就是跨过坑和大海
你好,我是欧创新。
这是本专栏的最后一讲了,非常感谢你这两个月的陪伴,也非常感谢你的意见和建议。加上前期的专栏筹备,前前后后也有半年了,这半年其实也是自我提升的过程,通过专栏,我将原来不成体系的经验、方法和设计思想,整理成了中台和微服务设计的系统的理论和知识体系。
在撰写专栏时,我站在架构师的角度,尽力将我在实践过程中的经验、思考和体会,以及原创案例等全面详细地呈现给你。希望能够对你的 DDD 实践和架构设计有所帮助,也希望你能快速成长为具有企业级战略视角的架构师和 DDD 设计大师。
那说到成长,相信我们每个人的轨迹都是独特的,但有一点,你一定和我有同样的体会。那就是“所谓高手,就是跨过坑和大海!”每一步都是积累,每一步都是经验,每一步都算数!所以啊,在本专栏的最后,我还是要分享一些干货给你,也是我曾经踩过的一些坑。
很多人接触 DDD可能是从 DDD 战术设计开始的,因此不知道如何开始 DDD 实践。这个专栏开启后,咱们就可以从领域建模开始了。有了领域模型,我们就可以划分出合理的微服务的逻辑和物理边界;也是因为有了它,我们才能识别出微服务内各关键对象,并建立它们之间的依赖关系,然后开始微服务的设计和开发。
而很多 DDD 和微服务设计的书籍,大多侧重于讲述 DDD 战术设计或者一些通用的微服务设计模式。这些书籍大多没有告诉我们:如何从业务领域开始,去构建领域模型?如何用 DDD 的思想,来指导中台和微服务设计?如何将领域模型作为输入,来设计和拆分微服务?如何将 DDD 知识体系组合起来,应用到中台和微服务的设计和开发中…
这也是本专栏与这些书籍的不同点。当然,我并不是说它们不好,只是各有侧重。在真正实践的时候,强大的知识基础自然也是刚需,你可以把专栏和书籍结合起来学习,从而发挥最大效能。
下面是我推荐的几本书,这些内容是可以和本专栏互补的,如果你有意愿进一步学习 DDD它们是非常好的学习资料。
DDD 是一个相对复杂的方法体系,它与传统的软件开发模式或者流程存在一定的差异。在实践 DDD 时,你可能会遇到一些困难。企业需要在研发模式上有一定的调整,同时项目团队也需要提升 DDD 的设计和技术能力,培养适合 DDD 成长的土壤。拔高一点看的话,我觉得你可能会遇到这样三个大坑,下面我来说一说我的看法。
1. 业务专家或领域专家的问题
传统企业中业务人员是需求的主要提出者,但由于部门墙,他们很少会参与到软件设计和开发过程中。如果研发模式不调整,你不要奢望业务人员会主动加入到项目团队中,一起来完成领域建模。没有业务人员的参与,是不是就会觉得没有领域专家,不能领域建模了呢?其实并不是这样的。
对于成熟业务的领域建模,我们可以从团队需求人员或者经验丰富的设计或开发人员中,挑选出能够深刻理解业务内涵和业务管理要求的人员,担任领域专家完成领域建模。对于同时熟悉业务和面向对象设计的项目人员,这种设计经验尤其重要,他们可以利用面向对象的设计经验,更深刻地理解和识别出领域模型的领域对象和业务行为,有助于推进领域模型的设计。
而对于新的创业企业,他们面对的是从来没人做过的全新的业务和领域,没有任何可借鉴的经验,更不要提什么领域专家。对于这种情况,就需要团队一起经过更多次更细致的事件风暴,才能建立领域模型。当然建模过程离不开产品愿景分析,这个过程是确定和统一系统建设目标以及项目的核心竞争力在哪里。这种初创业务的领域模型往往需要经过多次迭代才能成型,不要奢望一次就可以建立一个完美的领域模型。
2. 团队 DDD 的理念和技术能力问题
完成领域建模和微服务设计后,就要投入开发和测试了。这时你可能会发现一些开发人员,并不理解 DDD 设计方法,不知道什么是聚合、分层以及边界?也不知道服务的依赖以及层与层之间的职责边界是什么?
这样容易出现设计很精妙,而开发很糟糕的状况。遇到这种情况,除了要在项目团队普及 DDD 的知识和设计理念外,你还要让所有的项目成员尽早地参与到领域建模中,事件风暴的过程除了统一团队语言外,还可以让团队成员提前了解领域模型、设计要点和注意事项。
3. DDD 设计原则问题
DDD 基于各种考虑,有很多的设计原则,也用到了很多的设计模式。条条框框多了,很多人可能就会被束缚住,总是担心或犹豫这是不是原汁原味的 DDD。其实我们不必追求极致的 DDD这样做反而会导致过度设计增加开发复杂度和项目成本。
DDD 的设计原则或模式,是考虑了很多具体场景或者前提的。有的是为了解耦,如仓储服务、边界以及分层,有的则是为了保证数据一致性,如聚合根管理等。在理解了这些设计原则的根本原因后,有些场景你就可以灵活把握设计方法了,你可以突破一些原则,不必受限于条条框框,大胆选择最合适的方法。
以上就是我对这三个问题的理解了。
用好 DDD 的关键,首先要领悟 DDD 的核心设计思想和理念了解它为什么适合微服务架构然后慢慢体会、消化、吸收和实践。DDD 体系虽然复杂,但也是有矩可循的,照着样例多做几个事件风暴,完成领域建模和微服务设计,体会 DDD 的整个设计过程。相信你很快就能领悟到 DDD 的核心设计理念了,这样就可以做到收放自如,趟出一条适合自己的 DDD 实践之路。
好了,到了该说再见的时候了。再次感谢你的陪伴,期待再相遇!愿我们都能跨过坑和大海,开辟出一片广阔新天地!

View File

@ -0,0 +1,136 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 领域模型是如何指导程序设计的?
基于 DDD 的程序设计,就是将前面设计的领域模型,映射成数据架构中的程序设计,从而通过领域驱动提高软件设计质量。那么,应当怎样进行映射,让领域模型指导程序设计呢?要将领域模型映射到程序设计,最终都会落实到 3 种类型的对象设计:服务、实体和值对象。
服务、实体与值对象
建领域模型的第一步就是需要区分出服务、实体与值对象。
服务Service
服务,标识的是那些在领域对象之外的操作与行为。 在 DDD 中,“服务”通常承担了两种类型的职责:接收用户的请求和执行某些操作。当用户在系统界面中进行一些操作时,就会向系统发送请求。这时,是由“服务”首先去接收用户的这些请求,然后再根据需求去执行相应的方法。在执行这些方法的过程中,“服务”会去操作相应的实体与值对象。最后,当所有操作都完成以后,再将实体或值对象中的数据持久化到数据库中。
譬如当用户需要下单的时候就会从前端发起一个“下单”请求。该请求被“订单”Service 接收到并执行下单的相应操作。在执行过程中“订单”Service 会对“订单”实体中的数据进行校验,完成各种数据操作,最后将其保存到数据库中。
实体Entity
实体,就是那些通过一个唯一标识字段来区分真实世界中的每一个个体的领域对象。例如,在学籍管理系统中的“学员”对象就是一个实体,它通过标识字段“学员编号”将每一个学员进行了区分,通过某个学员编号就能唯一地标识某个学员;并且,这个学员有许多属性,如姓名、性别、年龄等,这些属性也是随着时间不断变化。这样的设计就叫作“实体”。
值对象
值对象,代表的是真实世界中那些一成不变的、本质性的事物,这样的领域对象叫作 “值对象”,如地理位置、行政区划、币种、行业、职位等。
实体和值对象的区分
在 DDD 中,对实体与值对象进行了严格的区分。可变性是实体的特点,而不变性则是值对象的本质。例如,北京是一个城市,架构师是一个职务,人民币是一个币种,这些事物的特性是永远不变的。
在实际项目中,我们可以根据业务需求的不同,灵活选用实体还是值对象。比如,在线订餐系统中,根据业务需求的不同,菜单既可以设计成实体,也可以设计成值对象。例如,“宫保鸡丁”是一个菜品,如果将其按照值对象设计,则整个系统中“宫保鸡丁”只有一条记录,所有饭店的菜单如果有这道菜,都是引用的这条记录;如果按照实体进行设计,则是认为每个饭店的“宫保鸡丁”都是不同的,比如每个饭店的“宫保鸡丁”的价格都是不尽相同的。因此,将其设计成有多条记录、有各自不同的 ID每个饭店都是使用自己的“宫保鸡丁”。
贫血模型 vs 充血模型
服务、实体与值对象是领域驱动设计的基本元素。然而,要将业务领域模型最终转换为程序设计,还要加入相应的设计。通常,将业务领域模型转换为程序设计,有两种设计思路:贫血模型与充血模型。
贫血模型与充血模型
事情是这样的2004 年,软件大师 Eric Evans 发表了他的不朽著作《领域驱动设计》。虽然已经过去十多年了,这本书直到今天依然对我们有相当大的帮助。接着,另一位软件大师 Martin Fowler 在自己的博客中提出了“贫血模型”的概念。这位“马大叔”有一个非常大的特点,那就是软件行业中各种名词都是他发明的,包括如今业界影响巨大的软件重构、微服务,也是他的杰作。然而,马大叔在提出“贫血模型”的时候,却将其作为反模式提出来批评:所谓的“贫血模型”,就是在软件设计中,有很多的 POJOPlain Ordinary Java Object对象它们除了有一堆 get/set 方法,几乎没有任何业务逻辑。这样的设计被称为“贫血模型”。
如上图所示,在领域模型中有 VIP 会员的领域对象,该对象除了有一堆属性以外,还有“会员打折”“会员福利”“会员特权”等方法。如果将该领域模型按照贫血模型进行设计,就会设计一个 VIP 会员的实体对象与 Service实体对象包含该对象的所有属性以及这些属性包含的数据然后将所有的方法都放入 Service 中,在调用它们的时候,必须将领域对象作为参数进行传输。这样的设计,将领域对象中的这些方法,以及这些方法在执行过程中所需的数据,割裂到两个不同的对象中,打破了对象的封装性。它会带来什么问题呢?
如上图所示,在领域模型中的 VIP 会员通过继承分为了“金卡会员”与“银卡会员”。如果将该领域模型按照贫血模型进行设计,则会设计出一个“金卡会员”的实体对象与 Service同时设计出一个“银卡会员”的实体对象与 Service。“金卡会员”的实体对象应当调用“金卡会员”的 Service如果将“金卡会员”的实体对象去调用了“银卡会员”的 Service系统就会出错。所以除了进行以上设计以外还需要有一个客户程序去判断当前的实体对象是“金卡会员”还是“银卡会员”这时系统变更就变得没有那么灵活了。
比如,现在需要在原有基础上,再增加一个“铂金会员”,那么不仅要增加一个“铂金会员”的实体对象与 Service还要修改客户程序的判断系统变更成本就会提高。
针对贫血模型的问题马大叔提出了“充血模型”的概念。所谓“充血模型”就是将领域模型的原貌直接转换为程序中领域对象的设计。这时各种业务操作就不再在“服务”中实现了而是在领域对象中实现。如图所示在程序设计时既有父类的“VIP 会员”,又有子类“金卡会员”与“银卡会员”。
但充血模型与贫血模型不同的是:
那些在领域对象中的方法也同样保留到了程序设计的实体对象中,这样,通过继承,虽然“金卡会员”与“银卡会员”都有“会员打折”,但“金卡会员”的“会员打折”与“银卡会员”的“会员打折”是不一样的;
虽然在充血模型中也有 Service里面也有“会员打折”“会员福利”“会员特权”等方法但是充血模型的 Service 只干一件非常简单的事,那就是接收到用户的请求后,就直接去调用实体对象中的相应方法,其他的什么都不干。
这样“VIP 会员”Service 不需要去关注现在调用的是“金卡会员”还是“银卡会员”,它只需要去调用“会员打折”就行了:
如果当前拿到的是“金卡会员”,就是执行“金卡会员”的“会员打折”;
如果当前拿到的是“银卡会员”,就是执行“银卡会员”的“会员打折”;
如果要再增加一个“铂金会员”就只需要写一个“铂金会员”的子类重写“会员打折”方法而“VIP 会员”Service 不需要做任何修改,变更的维护成本就大大降低了。
两种设计思路的优劣比较
采用充血模型的设计,有诸多的好处:
它保持了领域模型的原貌,领域模型什么样,就直接转换成程序的设计,这样,当领域模型在随着业务变更而频繁甚至大幅度调整时,可以比较直接地映射成程序的变更,代码修改起来比较直接;
如以上案例所述,充血模型保持了对象的封装性,使得领域模型在面临多态、继承等复杂结构时,易于变更。
充血模型在理论上非常优雅,然而在工程实践上却不尽人意。而贫血模型虽然从表面上看简单粗暴,但在工程实践上依然有许多优异的特性,主要体现在以下 3 个方面。
1. 贫血模型比充血模型更加简单易行
充血模型是将领域模型的原貌直接映射成了程序设计,因此在程序设计时需要增加更多的诸如仓库、工厂的组件,对设计能力与架构提出了更高的要求。
譬如,现在要设计一个订单系统,在领域建模时,每个订单需要有多个订单明细,还要对应相关的客户信息、商品信息。因此,在装载一个订单时,需要同时查出它的订单明细,以及对应的客户信息、商品信息,这些需要有强大的订单工厂进行装配;装载订单以后,还需要放到仓库中进行缓存,需要订单仓库具备缓存的能力;此外,在保存订单的时候,还需要同时保存订单和订单明细,并将它们放到一个事务中。所有这些都需要强有力的技术平台的支持。
相反贫血模型就显得更加贫民化。在贫血模型中MVC 层直接调用 ServiceService 通过DAO进行数据访问。在这个过程中每个 DAO 都只查询数据库中的某个表,然后直接交给 Service 去使用,去完成各种处理。
以订单系统为例,订单有订单 DAO负责查询订单订单明细有订单明细 DAO负责查询订单明细。它们查询出来以后不需要装配而是直接交给订单 Service 使用。在保存订单时,订单 DAO 负责保存订单,订单明细 DAO 负责保存订单明细。它们都是通过订单 Service 进行组织,并建立事务。贫血模型不需要仓库,不需要工厂,也不需要缓存,一切都显得那么简单粗暴但一目了然。
2. 充血模型需要具备更强的设计与协作能力
充血模型的设计实现给开发人员提出了更高的能力要求,需要具有更强的 OOA/D面向对象分析/设计) 能力、分析业务、业务建模与设计能力。譬如,在订单系统这个案例中,开发人员要先进行领域建模,分析清楚该场景中的订单、订单明细、用户、商品等领域对象的关联关系;还要分析各个领域对象在真实世界中都有什么行为,对应到软件设计中都有什么方法,在此基础上再进行设计开发。
同时,充血模型需要有较强的团队协作能力。比如,在该场景中,当订单在进行创建时,需要对用户以及用户地址的相关信息进行查询。此时,订单 Service 不能直接去查询用户和用户地址的相关表,而是去调用用户 Service 的相关接口,由用户 Service 去完成对用户相关表的查询。这时候,开发订单模块的团队,需要向开发用户模块的团队提出接口需求。
与充血模型相比,贫血模型就比较简单与直接。所有业务处理过程都交给 Service 去完成。在业务处理过程中,需要哪些表的数据,就去调用相应的 DAO需要订单就找订单 DAO需要用户就找用户 DAO需要商品就找商品 DAO。程序简单就易于理解日后维护起来也比较容易。总之充血模型就有一种贵族气质“讲究人”——昂贵而高雅贫血模型就是“草根”——简单而直接。
3. 贫血模型更容易应对复杂的业务处理场景
充血模型在进行设计时,是将所有的业务处理过程在领域对象的相应方法中实现的。这样的设计,如果业务处理过程比较简单,还可以从容应对;但如果是面对非常复杂的业务处理场景时,就有一些力不从心。在这些复杂的业务处理场景中,如果采用贫血模型,可以将复杂的业务处理场景,划分成多个相对独立的步骤;然后将这些独立的步骤分配给多个 Service 串联起来执行。这样各个步骤就是以一种松耦合的形式串联地组织在一起以领域对象作为参数在各个Service 中进行传递。
在这样的设计中,领域对象既可以作为各个方法调用的输入,又可以作为它们的输出。比如,在上图的案例中,领域对象作为参数首先调用 ServiceA调用完以后将结果数据写入领域对象的前 5 个字段,传递给 ServiceBServiceB 拿到领域对象以后,既可以作为输入去读取前 5 个字段,又可以作为输出将执行结果写入中间 5 个字段;最后,将领域对象传递给 ServiceC执行完操作以后去写后面 5 个字段;当所有字段都写入完成以后,存入数据库,完成所有操作。
在这个过程中,如果日后需要变更,要增加一个处理过程,或者去掉一个处理过程,再或者调整它们的执行顺序,都是比较容易的。这样的设计要求处理过程必须在领域对象之外,在 Service 中实现。然而,如果采用的是充血模型的设计,就必须要将所有的处理过程都写入这个领域对象中去实现,无论这些处理过程有多复杂。这样的设计势必会加大日后变更维护的成本。
所以,不论是贫血模型还是充血模型,它们各有优缺点,到底应当采用贫血模型还是充血模型,争执了这么多年,但我认为它们并不是熊掌和鱼的关系,我们应当把它们结合起来,取长补短,合理利用。关键是要先弄清楚它们的差别,也就是业务逻辑应当在哪里实现:贫血模型的业务逻辑在 Service 中实现,但充血模型是在领域对象中实现。清楚了这一点,在今后的软件设计时,可以将那些需要封装的业务逻辑放到领域对象中,按照充血模型去设计;除此之外的其他业务逻辑放到 Service 中,按照贫血模型去设计。
那么,哪些业务逻辑需要封装起来按照充血模型设计呢?这个仁者见仁智者见智,我总结了以下几个方面的内容。
如前所述,如果在领域模型中出现了类似继承、多态的情况,则应当将继承与多态的部分以充血模型的形式在领域对象中实现。
如果在软件设计的过程中需要将一些类型或者编码进行转换,则将转换的部分封装在领域对象中。例如,一些布尔类型的字段,在数据库中是没有布尔类型的,不同的人习惯不同,有的人习惯采用 0 和 1有的人习惯用 Y 和 N或者 T 和 F这样就会给上层开发人员诸多的困惑到底哪些字段是 Y 和 N哪些是 T 和 F。这时就可以将它们封装在领域对象中然后转换为布尔类型展现给上层开发按充血模型来设计。
希望在软件设计中能更好地表现领域对象之间的关系。比如,在查询订单的时候想要显示每个订单对应的用户,以及每个订单包含的订单明细。这时,除了要将领域模型中的关系体现在领域对象的设计外,还需要有仓库与工厂的支持。如装载订单时需要同时查询订单和订单明细,并通过订单工厂装配;查询订单以后需要通过工厂补填相应的用户与明细。
最后一种情况被称为“聚合”,也就是在真实世界中那些代表整体与部分的事物。比如,在订单中有订单和订单明细,一个订单对应多个订单明细。从业务关系来说,它们是整体与部分的关系,订单明细是订单的一个部分,没有了这张订单,它的订单明细就没有任何意义了。这时,我们在操作订单的时候,就应当将对订单明细的操作封装在订单对象中,按照充血模型的形式进行设计。
总结
本讲讲解了基于 DDD 的程序设计,领域模型分析只是软件需求分析的中间过程,它最终需要落地到程序设计。领域模型的最终落地是三种类型的对象:服务、实体与值对象,而设计思路有两种:贫血模型与充血模型。通过这样的落地,领域模型就能很好地指导程序开发,提高设计质量。
在 DDD 落地的过程中,不必过于纠结到底是实体还是值对象,应当将更多的精力集中于对业务的分析与理解。同时,将贫血模型与充血模型结合起来,取长补短、合理编码。
然而,领域模型的落地还有诸多难题需要解决。因此,下一讲将进一步讲解 DDD 的聚合、仓库与工厂及其设计思路。

View File

@ -0,0 +1,259 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 聚合、仓库与工厂:傻傻分不清楚
上一讲,我们知道了,要将领域模型最终转换为程序设计,可以落实到 3 种类型的对象设计,即服务、实体与值对象,然后进行一些贫血模型与充血模型的设计思路。但这远远不够,还需要有聚合、仓库与工厂的设计。
聚合的设计思路
聚合是领域驱动设计中一项非常重要的设计与概念,它表达的是真实世界中那些整体与部分的关系,比如订单与订单明细、表单与表单明细、发票与发票明细。以订单为例,在真实世界中,订单与订单明细本来是同一个事物,订单明细是订单中的一个属性。但是,由于在关系型数据库中没有办法在一个字段中表达一对多的关系,因此必须将订单明细设计成另外一张表。
尽管如此,在领域模型的设计中,我们又将其还原到真实世界中,以“聚合”的形式进行设计。在领域模型中,即将订单明细设计成订单中的一个属性,具体代码如下:
public class Order {
private Set<Items> items;
public void setItems(Set<Item> items){
this.items = items;
}
public Set<Item> getItems(){
return this.items;
}
……
}
有了这样的设计,在创建订单的时候,将不再单独创建订单明细了,而是将订单明细创建在订单中;在保存订单的时候,应当同时保存订单表与订单明细表,并放在同一事务中;在查询订单时,应当同时查询订单表与订单明细表,并将其装配成一个订单对象。这时候,订单就作为一个整体在进行操作,不需要再单独去操作订单明细。
也就是说,对订单明细的操作是封装在订单对象内部的设计实现。对于客户程序来说,去使用订单对象就好了,这就包括了作为属性去访问订单对象中的订单明细,而不再需要关注它内部是如何操作的。
按照以下思路进行的设计就是聚合:
当创建或更新订单时,在订单对象中填入或更新订单的明细就好了;
当保存订单时,只需要将订单对象作为整体去保存,而不需要关心订单数据是怎么保存的、保存到哪几张表中、是不是有事务,保存数据库的所有细节都封装在了订单对象内部;
当删除订单时,删除订单对象就好了,至于如何删除订单明细,是订单对象内部的实现,外部的程序不需要关注;
当查询或装载订单时,客户程序只需要根据查询语句或 ID 查询订单对象就好了,查询程序会在查询过程中自动地去补填订单对应的订单明细。
聚合体现的是一种整体与部分的关系。正是因为有这样的关系,在操作整体的时候,整体就封装了对部分的操作。但并非所有对象间的关系都有整体与部分的关系,而那些不是整体与部分的关系是不能设计成聚合的。因此,正确地识别聚合关系就变得尤为重要。
所谓的整体与部分的关系,就是当整体不存在时,部分就变得没有了意义。部分是整体的一个部分,与整体有相同的生命周期。比如,只有创建了这张订单,才能创建它的订单明细;如果没有了这张订单,那么它的订单明细就变得没有意义,就需要同时删除掉。这样的关系才具备整体与部分的关系,才是聚合。
譬如:订单与用户之间的关系就不是聚合。因为用户不是创建订单时才存在的,而是在创建订单时早就存在了;当删除订单时,用户不会随着订单的删除而删除,因为删除了订单,用户依然还是那个用户。
那么,饭店和菜单的关系是不是聚合关系呢?关键要看系统如何设计。如果系统设计成每个饭店都有各不相同的菜单,每个菜单都是隶属于某个饭店,则饭店和菜单是聚合关系。这种设计让各个饭店都有“宫保鸡丁”,但每个饭店都是各自不同的“宫保鸡丁”,比如在描述、图片或价格上的不同,甚至在数据库中也是有各不相同的记录。这时,要查询菜单就要先查询饭店,离开了饭店的菜单是没有意义的。
但是,饭店和菜单还可以有另外一种设计思路,那就是所有的菜单都是公用的,去每个饭店只是选择有还是没有这个菜品。这时,系统中有一个菜单对象,“宫保鸡丁”只是这个对象中的一条记录,其他各个饭店,如果他们的菜单上有“宫保鸡丁”,则去引用这个对象,否则不引用。这时,菜单就不再是饭店的一个部分,没有这个饭店,这个菜品依然存在,就不再是聚合关系。
因此,判断聚合关系最有效的方法就是去探讨:如果整体不存在时,部分是否存在。如果不存在,就是聚合;反之,则不是。
聚合根——外部访问的唯一入口
有了聚合关系,部分就会被封装在整体里面,这时就会形成一种约束,即外部程序不能跳过整体去操作部分,对部分的操作都必须要通过整体。这时,整体就成了外部访问的唯一入口,被称为 “聚合根”。
也就是说,一旦将对象间的关系设计成了聚合,那么外部程序只能访问聚合根,而不能访问聚合中的其他对象。这样带来的好处就是,当聚合内部的业务逻辑发生变更时,只与聚合内部有关,只需要对聚合内部进行更新,与外部程序无关,从而有效降低了变更的维护成本,提高了系统的设计质量。
然而,这样的设计有时是有效的,但并非都有效。譬如,在管理订单时,对订单进行增删改,聚合是有效的。但是,如果要统计销量、分析销售趋势、销售占比时,则需要对大量的订单明细进行汇总、进行统计;如果每次对订单明细的汇总与统计都必须经过订单的查询,必然使得查询统计变得效率极低而无法使用。
因此,领域驱动设计通常适用于增删改的业务操作,但不适用于分析统计。在一个系统中,增删改的业务可以采用领域驱动的设计,但在非增删改的分析汇总场景中,则不必采用领域驱动的设计,直接 SQL 查询就好了,也就不必再遵循聚合的约束了。
聚合的设计实现
前面谈到了领域驱动设计中一个非常重要的概念:聚合。通过聚合的设计,可以真实地反映现实世界的状况,提高软件设计的质量,有效降低日后变更的成本。然而,前面只提出了聚合的概念,要想真正将聚合落实到软件设计中,还需要两个非常重要的组件:仓库与工厂。
比如,现在创建了一个订单,订单中包含了多条订单明细,并将它们做成了一个聚合。这时,当订单完成了创建,就需要保存到数据库里,怎么保存呢?需要同时保存订单表与订单明细表,并将其做到一个事务中。这时候谁来负责保存,并对其添加事务呢?
过去我们采用贫血模型,那就是通过订单 DAO 与订单明细 DAO 去完成数据库的保存,然后由订单 Service 去添加事务。这样的设计没有聚合、缺乏封装性,不利于日后的维护。那么,采用聚合的设计应当是什么样呢?
采用了聚合以后订单与订单明细的保存就会封装在订单仓库中去实现。也就是说采用了领域驱动设计以后通常就会实现一个仓库Repository 去完成对数据库的访问。那么仓库与数据访问层DAO有什么区别呢
一般来说,数据访问层就是对数据库中某个表的访问,比如订单有订单 DAO、订单明细有订单明细 DAO、用户有用户 DAO。
当数据要保存到数据库中时,由 DAO 负责保存,但保存的是某个单表,如订单 DAO 保存订单表、订单明细 DAO 保存订单明细表、用户 DAO 保存用户表;
当数据要查询时,还是通过 DAO 去查询,但查询的也是某个单表,如订单 DAO 查订单表、订单明细 DAO 查订单明细表。
那么,如果在查询订单的时候要显示用户名称,怎么办呢?做另一个订单对象,并在该对象里增加“用户名称”。这样,通过订单 DAO 查订单表时,在 SQL 语句中 Join 用户表,就可以完成数据的查询。这时会发现,在系统中非常别扭地设计了两个或多个订单对象,并且新添加的订单对象与领域模型中的订单对象有较大的差别,显得不够直观。系统简单时还好说,但系统的业务逻辑变得越来越复杂时,程序阅读起来越来越困难,变更就变得越来越麻烦。
因此,在应对复杂业务系统时,我们希望程序设计能较好地与领域模型对应上:领域模型是啥样,程序就设计成啥样。我们就将订单对象设计成这样,订单对象的关联设计代码如下:
public class Order {
......
private Long customer_id;
private Customer customer;
private List<OrderItem> orderItems;
/**
* @return the customerId
*/
public Long getCustomerId() {
return customer_id;
}
/**
* @param customerId the customerId to set
*/
public void setCustomerId(Long customerId) {
this.customer_id = customerId;
}
/**
* @return the customer
*/
public Customer getCustomer() {
return customer;
}
/**
* @param customer the customer to set
*/
public void setCustomer(Customer customer) {
this.customer = customer;
}
/**
* @return the orderItems
*/
public List<OrderItem> getOrderItems() {
return orderItems;
}
/**
* @param orderItems the orderItems to set
*/
public void setOrderItems(List<OrderItem> orderItems) {
this.orderItems = orderItems;
}
}
可以看到,在订单对象中加入了对用户对象和订单明细对象的引用:
订单对象与用户对象是多对一关系,做成对象引用;
订单对象与订单明细对象是一对多关系,做成对集合对象的引用。
这样,当订单对象在创建时,在该对象中填入 customerId以及它对应的订单明细集合 orderItems然后交给订单仓库去保存在保存时就进行了一个封装同时保存订单表与订单明细表并在其上添加了一个事务。
这里要特别注意,对象间的关系是否是聚合关系,它们在保存的时候是有差别的。譬如,在本案例中,订单与订单明细是聚合关系,因此在保存订单时还要保存订单明细,并放到同一事务中;然而,订单与用户不是聚合关系,那在保存订单时不会去操作用户表,只有在查询时,比如在查询订单的同时,才要查询与该订单对应的用户。
这是一个比较复杂的保存过程。然而,通过订单仓库的封装,对于客户程序来说不需要关心它是怎么保存的,它只需要在领域对象建模的时候设定对象间的关系,即将其设定为“聚合”就可以了。既保持了与领域模型的一致性、又简化了开发,使得日后的变更与维护变得简单。至于仓库的设计实现,将在后面的课程中讲解。
有了这样的设计,装载与查询又应当怎样去做呢?所谓的 “装载Load就是通过主键 ID 去查询某条记录。比如,要装载一个订单,就是通过订单 ID 去查询该订单,那么订单仓库是如何实现对订单的装载呢?
首先,比较容易想到的是,用 SQL 语句到数据库里去查询这张订单。与 DAO 不同的是:
订单仓库在查询订单时,只是简单地查询订单表,不会去 Join 其他表,比如 Join 用户表,不会做这些事情;
当查询到该订单以后,将其封装在订单对象中,然后再去通过查询补填用户对象、订单明细对象;
通过补填以后,就会得到一个用户对象、多个订单明细对象,需要将它们装配到订单对象中。
这时,那些创建、装配的工作都交给了另外一个组件——工厂来完成。
DDD 的工厂
DDD 中的工厂,与设计模式中的工厂不是同一个概念,它们是有差别的。在设计模式中,为了避免调用方与被调方的依赖,将被调方设计成一个接口下的多个实现,将这些实现放入工厂中。这样,调用方通过一个 key 值就可以从工厂中获得某个实现类。工厂就负责通过 key 值找到对应的实现类,创建出来,返回给调用方,从而降低了调用方与被调方的耦合度。
而 DDD 中的工厂,与设计模式中的工厂唯一的共同点可能就是,它们都要去做创建对象的工作。
DDD 中的工厂,主要的工作是通过装配,创建领域对象,是领域对象生命周期的起点。譬如,系统要通过 ID 装载一个订单:
这时订单仓库会将这个任务交给订单工厂,订单工厂就会分别调用订单 DAO、订单明细 DAO 和用户 DAO 去进行查询;
然后将得到的订单对象、订单明细对象、用户对象进行装配,即将订单明细对象与用户对象,分别 set 到订单对象的“订单明细”与“用户”属性中;
最后,订单工厂将装配好的订单对象返回给订单仓库。
这些就是 DDD 中工厂要做的事情。
DDD 的仓库
然而当订单工厂将订单对象返回给订单仓库以后订单仓库不是简单地将该对象返回给客户程序它还有一个缓存的功能。在DDD 中“仓库”的概念,就是如果服务器是一个非常强大的服务器,那么我们不需要任何数据库。系统创建的所有领域对象都放在仓库中,当需要这些对象时,通过 ID 到仓库中去获取。
但是,在现实中没有那么强大的仓库,因此仓库在内部实现时,会将领域对象持久化到数据库中。数据库是仓库进行数据持久化的一种内部实现,它也可以有另外一种内部实现,就是将最近反复使用的领域对象放入缓存中。这样,当客户程序通过 ID 去获取某个领域对象时,仓库会通过这个 ID 先到缓存中进行查找:
查找到了,则直接返回,不需要查询数据库;
没有找到,则通知工厂,工厂调用 DAO 去数据库中查询,然后装配成领域对象返回给仓库。
仓库在收到这个领域对象以后,在返回给客户程序的同时,将该对象放到缓存中。
以上是通过 ID 装载订单的过程,那么通过某些条件查询订单的过程又是怎么做呢?查询订单的操作同样是交给订单仓库去完成。
订单仓库会先通过订单 DAO 去查询订单表,但这里是只查询订单表,不做 Join 操作;
订单 DAO 查询了订单表以后,会进行一个分页,将某一页的数据返回给订单仓库;
这时,订单仓库就会将查询结果交给订单工厂,让它去补填其对应的用户与订单明细,完成相应的装配,最终将装配好的订单对象集合返回给仓库。
简而言之,采用领域驱动的设计以后,对数据库的访问就不是一个简单的 DAO 了,这不是一种好的设计。通过仓库与工厂,对原有的 DAO 进行了一层封装,在保存、装载、查询等操作中,加入聚合、装配等操作。并将这些操作封装起来,对上层的客户程序屏蔽。这样,客户程序不需要以上这些操作,就能完成领域模型中的各自业务。技术门槛降低了,变更与维护也变得简便了。
总结
本讲讲解了 DDD 中一个非常重要的设计思想:聚合,以及它的设计实现:工厂与仓库,它们是 DDD 中充血模型设计的重要支柱。通过这些设计我们会发现,它们与我们传统的基于 DAO 的贫血模型设计有诸多的不同。
通过聚合实现了整体与部分的关系,客户程序只能操作整体,而将对部分的操作封装在了仓库与工厂中;
客户程序不必关注对数据库的操作,操作仓库就好了。对缓存、对数据库的操作都封装在了仓库与工厂中,从而降低了业务开发的技术门槛与开发工作量;
对数据的查询不再通过 SQL 语句进行 Join而是通过工厂进行补填与装配。这样的设计更有利于微服务的设计与大数据的调优。
它们为软件系统提高设计质量、降低维护成本以及应对高并发,提供了很好的设计。
另外,一个值得思考的问题就是,传统的领域驱动设计,是每个模块自己去实现各自的仓库与工厂,这样会大大增加开发工作量。但这些仓库与工厂的设计大致都是相同的,会催生大量的重复代码。能不能通过抽象,提取出共性,形成通用的仓库与工厂,下沉到底层技术中台中,从而进一步降低领域驱动的开发成本与技术门槛?也就是说,实现领域驱动设计还需要相应的平台架构支持。关于这些方面的思路,我们将在 DDD 的架构设计部分进一步探讨。

View File

@ -0,0 +1,117 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 限界上下文:冲破微服务设计困局的利器
上一讲我们以用户下单这个场景,讲解了领域驱动设计的建模、分析与设计的过程,然而,站在更大的电商网站的角度,用户下单只是其中一个很小的场景。
那么,如果要对整个电商网站进行领域驱动设计,应当怎么做呢?它包含那么多场景,每个场景都要包含那么多的领域对象,进而会形成很多的领域对象,并且每个领域对象之间还有那么多复杂的关联关系。这时候,怎样通过领域驱动来设计这个系统呢?怎么去绘制领域模型呢?是绘制一张密密麻麻的大图,还是绘制成一张一张的小图呢?学完本讲后,将能解决这些问题。
问题域和限界上下文
假如将整个系统中那么多的场景、涉及的那么多领域对象,全部绘制在一张大图上,可以想象这张大图需要绘制出密密麻麻的领域对象,以及它们之间纷繁复杂的对象间关系。绘制这样的图,绘制的人非常费劲,看这张图的人也非常费劲,这样的图也不利于我们理清思路、交流思想及提高设计质量。
正确的做法就是将整个系统划分成许多相对独立的业务场景,在一个一个的业务场景中进行领域分析与建模,这样的业务场景称为 “问题子域”,简称“子域”。
领域驱动核心的设计思想,就是将对软件的分析与设计还原到真实世界中,那么就要先分析和理解真实世界的业务与问题。而真实世界的业务与问题叫作 “问题域”,这里面的业务规则与知识叫 “业务领域知识”,譬如:
电商网站的“问题域”是人们如何进行在线购物,购物的流程是怎样的;
在线订餐系统的“问题域”是人们如何在线订餐,饭店如何在线接单,系统又是如何派送骑士去配送的。
然而,不论是电商网站还是在线购物系统,都有一个非常庞大而复杂的问题域。要一次性分析清楚这个问题域对我们来说是有难度的,因此需要采用 “分而治之”的策略,将这个问题域划分成许多个问题子域。比如:
电商网站包含了用户选购、下单、支付、物流等多个子域;
在线订餐系统包含了用户下单、饭店接单、骑士派送等子域。
如果某个子域比较复杂,在子域的基础上还可以进一步划分子域。
因此一个复杂系统的领域驱动设计就是以子域为中心进行领域建模绘制出一张一张的领域模型设计然后以此作为基础指导程序设计。这一张一张的领域模型设计称为“限界上下文”Context BoundsCB
DDD 中限界上下文的设计,很好地体现了高质量软件设计中 “单一职责原则” 的要求,即每个限界上下文中实现的都是软件变化同一个原因的业务。比如,“用户下单”这个限界上下文都是实现用户下单的相关业务。这样,当“用户下单”的相关业务发生变更的时候,只与“用户下单”这个限界上下文有关,只需要对它进行修改就行了,与其他限界上下文无关。这样,需求变更的代码修改范围缩小了,维护成本也就降低了。
在用户下单的过程中,对用户信息的读取是否也应该在“用户下单”这个限界上下文中实现呢?答案是否定的,因为读取用户信息不是用户下单的职责,当用户下单业务发生变更的时候,用户信息不一定变;用户信息变更的时候,用户下单也不一定变,它们是软件变化的两个原因。
因此应当将读取用户信息的操作交给“用户信息管理”限界上下文“用户下单”限界上下文只是对它的接口进行调用。通过这样的划分实现了限界上下文内的高内聚和限界上下文间的低耦合可以很好地降低日后代码变更的成本、提高软件设计质量。而限界上下文之间的这种相互关系称为“上下文地图”Context Map
限界上下文与微服务
所谓“限界上下文内的高内聚”,也就是每个限界上下文内实现的功能,都是软件变化的同一个原因的代码。因为这个原因的变化才需要修改这个限界上下文,而不是这个原因的变化就不需要修改这个限界上下文,修改与它无关。正是因为限界上下文有如此好的特性,才使得现在很多微服务团队,运用限界上下文作为微服务拆分的原则,即每个限界上下文对应一个微服务。
按照这样的原则拆分出来的微服务系统,在今后变更维护时,可以很好地将每次的需求变更,快速落到某个微服务中变更。这样,变更这个微服务就实现了该需求,升级该服务后就可以交付用户使用了。这样的设计,使得越来越多的规划开发团队,今后可以实现低成本维护与快速交付,进而快速适应市场变化而提升企业竞争力。
譬如,在电商网站的购物过程中,购物、下单、支付、物流,都是软件变化不同的原因,因此,按照不同的业务场景划分限界上下文,然后以此拆分微服务。那么,当购物变更时就修改购物微服务,下单变更就修改下单微服务,但它们在业务处理过程中都需要读取商品信息,因此调用“商品管理”微服务来获取商品信息。这样,一旦商品信息发生变更,只与“商品管理”微服务有关,与其他微服务无关,那么维护成本将得到降低,交付速度得到提升。
所谓“限界上下文间的低耦合”,就是限界上下文通过上下文地图相互调用时,通过接口进行调用。如下图所示,模块 A 需要调用模块 B那么它就与模块 B 形成了一种耦合,这时:
如果需要复用模块 A那么所有有模块 A 的地方都必须有模块 B否则模块 A 就会报错;
如果模块 B 还要依赖模块 C模块 C 还要依赖模块 D那么所有使用模块 A 的地方都必须有模块 B、C、D使用模块 A 的成本就会非常高昂。
然而,如果模块 A 不是依赖模块 B而是依赖接口 B那么所有需要模块 A 的地方就不一定需要模块 B如果模块 F 实现了接口 B那么模块 A 调用模块 F 就可以了。这样,调用方和被调用方的耦合就被解开。
在代码实现时,可以通过微服务来实现“限界上下文间”的“低耦合”。比如,“下单”微服务要去调用“支付”微服务。在设计时:
首先在“下单”微服务中增加一个“支付”接口,这样在“下单”微服务中所有对支付的调用,都是对该接口的调用;
接着,在其他“支付”微服务中实现支付,比如,现在设计了 A、 B 两个“支付”微服务,在系统运行时配置的是 A 服务,那么“下单”微服务调用的就是 A如果配置的是 B 服务,调用的就是 B。
这样,“下单”微服务与“支付”微服务之间的耦合就被解开,使得系统可以通过修改配置,去应对各种不同的用户环境与需求。
有了限界上下文的设计,使得系统在应对复杂应用时,设计质量提高、变更成本降低。
过去,每个模块在读取用户信息时,都是直接读取数据库中的用户信息表,那么一旦用户信息表发生变更,各个模块都要变更,变更成本就会越来越高。
现在,采用领域驱动设计,读取用户信息的职责交给了“用户管理”限界上下文,其他模块都是调用它的接口,这样,当用户信息表发生变更时,只与“用户管理”限界上下文有关,与其他模块无关,变更维护成本就降低了。通过限界上下文将整个系统按照逻辑进行了划分,但从物理上它们都还是一个项目、运行在一个 JVM 中,这种限界上下文只是“逻辑边界”。
今后,将单体应用转型成微服务架构以后,各个限界上下文都是运行在各自不同的微服务中,是不同的项目、不同的 JVM。不仅如此进行微服务拆分的同时数据库也进行了拆分每个微服务都是使用不同的数据库。这样当各个微服务要访问用户信息时它们没有访问用户数据库的权限就只能通过远程接口去调用“用户”微服务开放的相关接口。这时这种限界上下文就真正变成了“物理边界”如下图所示
微服务拆分的困局
现如今,许多软件团队都在加入微服务转型的行列,将原有的越来越复杂的单体应用,拆分为一个一个简单明了的微服务,以降低系统微服务的复杂性,这是没有问题的。然而,现在最大的问题是微服务应当如何拆分。
如上图所示,以往许多的系统是这样设计的。现在,如果还按照这样的设计思路简单粗暴地拆分为多个微服务以后,对系统日后的维护将是灾难性的。
当多个模块都要读取商品信息表时,是直接通过 JDBCJava Database Connectivity去读取这个表。
接着,按照这样的思路拆分微服务,多个微服务都要读取商品信息表。
这样,一旦商品信息表发生变更,多个微服务都需要变更。不仅多个团队都要为了维护这个需求修改代码,而且他们的微服务需要同时修改、同时发布、同时升级。
如果每次的维护都是这样进行,不仅微服务的优势不能发挥出来,还会使得维护的成本更高。如果微服务被设计成这样,还真不如不用微服务。
这里的关键问题在于,当多个微服务都要读取同一个表时,也就意味着同一个软件变化原因(因商品信息而变更)的代码被分散到多个微服务中。这时,当系统因该原因而变化时,代码的修改自然就会分散到多个微服务上。也就是说,以上设计问题的根源违反了“单一职责原则”,使微服务的设计不再高内聚。微服务该怎样设计、怎样拆分?关键就在于“小而专”,这里的“专”就是高内聚。
因此,微服务设计不是简单的拆分,而是对设计提出了更高的要求,即要做到“高内聚”。只有这样,才能让日后的变更能尽量落到某个微服务上维护,从而降低维护成本。唯有这样才能将微服务的优势发挥出来,才是微服务正确的打开方式。
为了让微服务设计做到高内聚,最佳的实践则是 DDD
先从 DDD 开始需求分析、领域建模,逐渐建立起多个问题子域;
再将问题子域落实到限界上下文,它们之间的关联形成上下文地图;
最后,各子域落实到微服务中贫血模型或充血模型的设计,从而在微服务之间依据上下文地图形成接口。
唯有这样的设计,才能很好地做到“微服务之间低耦合,微服务之内高内聚”的设计目标。
总结
总而言之,微服务设计的困局就是拆分,拆分的核心就是“小而专”“高内聚”。因此,破解微服务困局的关键就是 DDD。有了 DDD就使得微服务团队在面对软件越来越复杂的业务时能够分析清楚业务能够想明白设计从而提高微服务的设计质量。

View File

@ -0,0 +1,142 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 在线订餐场景中是如何开事件风暴会议的?
微服务设计最核心的难题是微服务的拆分,不合理的微服务拆分不仅不能提高研发效率,反倒还使得研发效率更低,因此要讲究“小而专”的设计。“小而专”的设计意味着微服务的设计不是简单拆分,而是对设计提出了更高的要求,要“低耦合、高内聚”。那么,如何做到“低耦合、高内聚”,实现微服务的“小而专”呢?那就需要“领域驱动设计”作为方法论,来指导我们的开发。
用“领域驱动设计”是业界普遍认可的解决方案,也就是解决微服务如何拆分,以及实现微服务的高内聚与单一职责的问题。但是,领域驱动设计应当怎样进行呢?怎样从需求分析到软件设计,用正确的方式一步一步设计微服务呢?现在我们用一个在线订餐系统实战演练一下微服务的设计过程。
在线订餐系统项目实战
相信我们都使用过在线订餐系统,比如美团、大众点评、百度外卖等,具体的业务流程如下图所示:
在线订餐系统的业务流程图
当我们进入在线订餐系统时,首先看到的是各个饭店,进入每个饭店都能看到他们的菜单;
下单时,订单中就会包含我们订的是哪家饭店、菜品、数量及我们自己的配送地址;
下单后,相应的饭店就会收到该下单系统;
接着,饭店接单,然后开始准备餐食;
当饭店的餐食就绪以后,通知骑士进行派送;
最后,骑士完成了餐食的派送,订单送达,我们就愉悦地收到了订购的美味佳肴。
现在,我们要以此为背景,按照微服务架构来设计开发一个在线订餐系统。那么,我们应当如何从分析理解需求开始,一步一步通过前面讲解的领域驱动设计,最后落实到拆分微服务,把这个系统拆分出来呢?
统一语言建模
软件开发的最大风险是需求分析,因为在这个过程中谁都说不清楚能让对方了解的需求。
研发不懂客户、客户也不懂研发
在这个过程中,对于客户来说:
客户十分清楚他的业务领域知识,以及他亟待解决的业务痛点;
然而,客户不清楚技术能如何解决他的业务痛点。
因此,用户在提需求时,是在用他有限的认知,想象技术如何解决他的业务痛点。所以这样提出的业务需求往往不太靠谱,要么技术难于实现,要么并非最优的方案。
与此同时,在需求分析过程中,对于研发人员来说:
非常清楚技术以及能解决哪些业务问题,同时也清楚它是如何解决的;
然而,欠缺的是对客户所在的业务领域知识的掌握,使得无法准确理解客户的业务痛点。
这就局限了我们的设计,进而所做的系统不能完美地解决用户痛点。
因此,在需求分析的过程中,不论是客户还是我们,都不能掌握准确理解需求所需的所有知识,这就导致,不论是谁都不能准确地理解与描述软件需求。在需求分析中常常会出现,客户以为他描述清楚需求了,我们也以为我们听清楚了。但当软件开发出来以后,客户才发现这并不是他需要的软件,而我们也发现我们并没有真正理解需求。尽管如此,客户依然没有想清楚他想要什么,而我们还是不知道该怎样做,这就是软件开发之殇。
如何破局需求分析的困境?
如何能够破解这个困局呢?关键的思想就在于“统一语言建模”。也就是说,以上问题的根源在于语言沟通的障碍,使得我不能理解你,而你也不能理解我。因此,解决的思路就是:
我主动学习你的语言,了解你的业务领域知识,并用你的语言与你沟通;
同时,我也主动地让你了解我的语言,了解我的业务领域知识,并用我的语言与你沟通。
回到需求分析领域,我们清楚的是技术,但不了解业务,因此,应当主动地去了解业务。那么,如何了解业务呢?找书慢慢地去学习业务吗?也不是,因为我们不是要努力成为业务领域专家,而仅仅是要掌握与要开发软件相关的业务领域知识。在业务领域漫无目的地学习,学习效率低而收效甚微。
所以,我们应当从客户那里去学习,比如询问客户,仔细聆听客户对业务的描述,在与客户的探讨中快速地学习业务。然而,在这个过程中,一个非常重要的关键就是,注意捕获客户在描述业务过程中的那些专用术语,努力学会用这些专用术语与客户探讨业务。
久而久之,用客户的语言与客户沟通,你们的沟通就会越来越顺畅,客户也会觉得你越来越专业,愿意与你沟通,并可以与你探讨越来越深的业务领域知识。当你对业务的理解越来越深刻,你就能越来越准确地理解客户的业务及痛点,并运用自己的技术专业知识,用更加合理的技术去解决用户的痛点。这样,你们的软件就会越来越专业,让用户能越来越喜欢购买和使用你们的软件,并形成长期合作关系。
我的项目应用举例
以我做过的一个远程智慧诊疗数据模型为例,这是一个面向中医的数据模型。在与客户探讨需求的过程中,我们很快发现,用户在描述中医的诊疗过程中,许多术语与西医有很大的不同。
比如,他们在描述患者症状的时候,通常不用“症状”这个词,而是用“表象”。表象包括症状、体征、检测指标,是医生通过不同方式捕获患者病症的所有外部表现;同时,他们在诊断的时候也不用“疾病”这个词,而是“证候”。中医认为,证候才是患者疾病在身体中的内部根源,抓住证候,将证候的问题解决了,疾病自然就药到病除了。我们把握了这些术语后,用这些术语与业务专家进行沟通,沟通就变得异常顺利。客户会觉得我们非常专业,很懂他们,并且变得异常积极地与我们探讨需求,并很快建立了一种长期合作的关系。
同时,在这个过程中,我们一边在与客户探讨业务领域知识,一边又可以让客户参与到我们分析设计的工作中来,用客户能够理解的语言让客户清楚我们是如何设计软件的。这样,当客户有参与感以后,就会对我们的软件有更强烈的认可度,更有利于软件的推广。此外,客户参与了并理解我们是怎么做软件的,就会逐步形成一种默契。使得客户在日后提需求、探讨需求的时候,提出的需求更靠谱,避免技术无法实现的需求,使得需求质量大幅度得到提高。
事件风暴会议
什么是事件风暴
在领域驱动设计之初的需求分析阶段对需求分析的基本思路就是统一语言建模它是我们的指导思想。但落实到具体操作层面可以采用的实践方法是事件风暴Event Storming。它是一种基于工作坊的 DDD 实践方法,可以帮助我们快速发现业务领域中正在发生的事件,指导领域建模及程序开发。它是由意大利人 Alberto Brandolini 发明的一种领域驱动设计实践方法,被广泛应用于业务流程建模和需求工程。
这个方法的基本思想,就是将软件开发人员和领域专家聚集在一起,一同讨论、相互学习,即统一语言建模。但它的工作方式类似于头脑风暴,让建模过程变得更加有趣,让学习业务变得更加容易。因此,事件风暴中的“风暴”,就是运用头脑风暴会议进行领域分析建模。
那么这里的“事件”是什么意思呢事件即事实Event as Fact即在业务领域中那些已经发生的事件就是事实fact。过去已经发生的事件已经成为了事实就不会再更改因此信息管理系统就可以将这些事实以信息的形式存储到数据库中即信息就是一组事实。
说到底,一个信息管理系统的作用,就是存储这些事实,对这些事实进行管理与跟踪,进而起到提高工作效率的作用。因此,分析一个信息管理系统的业务需求,就是准确地抓住业务进行过程中那些需要存储的关键事实,并围绕着这些事实进行分析设计、领域建模,这就是“事件风暴”的精髓。
召开事件风暴会议
因此,实践“事件风暴”方法,就是让开发人员与领域专家坐在一起,开事件风暴会议。会议的目的就是与领域专家一起进行领域建模,而会议前的准备就是在会场准备一个大大的白板与各色的便笺纸,如下图所示:
事件风暴会议图
当开始事件风暴会议以后,通常分为这样几个步骤。
首先,在产品经理的引导下,与业务专家开始梳理当前的业务中有哪些领域事件,即已经发生并需要保存下来的那些事实。这时,是按照业务流程依次去梳理领域事件的。例如,在本案例中,整个在线订餐过程分为:已下单、已接单、已就绪、已派送和已送达,这几个领域事件。注意,领域事件是已发生的事实,因此,在命名的时候应当采用过去时态。
这里有一个十分有趣的问题值得探讨。在用户下单之前,用户首先是选餐。那么,“用户选餐”是不是领域事件呢?注意,领域事件是那些已经发生并且需要保存的重要事实。这里,“用户选餐”仅仅是一个查询操作,并不需要数据库保存,因此不能算领域事件。那么,难道这些查询功能不在需求分析的过程中吗?
注意DDD 有自己的适用范围,它往往应用于系统增删改的业务场景中,而查询场景的分析往往不用 DDD而是通过其他方式进行分析。分析清楚了领域事件以后就用橘黄色便笺纸将所有的领域事件罗列在白板上确保领域中所有事件都已经被覆盖。
紧接着,针对每一个领域事件,项目组成员开始不断地围绕着它进行业务分析,增加各种命令与事件,进而思考与之相关的资源、外部系统与时间。例如,在本案例中,首先分析“已下单”事件,分析它触发的命令、与之相关的人与事儿,以及发生的时间。命令使用蓝色便笺,人和事儿使用黄色便笺,如下图所示:
“已下单”的领域事件分析图
“已下单”事件触发的命令是“下单”,执行者是“用户”(画一个小人作为标识),执行时间是“下单时间”。与它相关的人和事儿有“饭店”与“订单”。在此基础上进一步分析,用户关联到用户地址,饭店关联到菜单,订单关联到菜品明细。
然后,就是识别模型中可能涉及的聚合及其聚合根。第 05 讲谈到,所谓的“聚合”就是整体与部分的关系,譬如,饭店与菜单是否是聚合关系,关键看它俩的数据是如何组织的。如果菜单在设计时是独立于饭店之外的,如“宫保鸡丁”是独立于饭店的菜单,每个饭店都是在引用这条记录,那么菜单与饭店就不是聚合关系,即使删除了这个饭店,这个菜单依然存在。
但如果菜单在设计时,每个饭店都有自己独立的菜单,譬如同样是“宫保鸡丁”,饭店 A 与饭店 B 使用的都是各自不同的记录。这时,菜单在设计上就是饭店的一个部分,删除饭店就直接删除了它的所有菜单,那么菜单与饭店就是聚合关系。在这里,那个代表“整体”的就是聚合根,所有客户程序都必须要通过聚合根去访问整体中的各个部分。
通过以上分析,我们认为用户与地址、饭店与菜单、订单与菜品明细,都是聚合关系。如果是聚合关系,就在该关系上贴一张紫色便笺。
按照以上步骤,一个一个地去分析每个领域事件:
在线订餐系统的领域事件分析图
当所有的领域事件都分析完成以后,最后再站在全局对整个系统进行模块的划分,划分为多个限界上下文,并在各个限界上下文之间,定义它们的接口,规划上下文地图。
总结
按照 DDD 的思想进行微服务设计,首先是从需求分析开始的。但 DDD 彻底改变了我们需求分析的方式,采用统一语言建模,让我们更加主动地理解业务,用客户的语言与客户探讨需求。统一语言建模是指导思想,事件风暴会议是实践方法。运用事件风暴会议与客户探讨需求、建立模型,我们能更加深入地理解需求,而客户也更有参与感。此外,事件风暴会议可以作为敏捷开发中迭代计划会议前的准备会议的一个部分。
然而,通过事件风暴会议形成的领域模型,又该如何落地到微服务的设计呢?还会遇到哪些设计与技术难题呢?下一讲将进一步讲解领域模型的微服务设计实现。

View File

@ -0,0 +1,100 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 DDD 是如何解决微服务拆分难题的?
微服务的技术架构其实并不难。很多开发团队在微服务转型初期,将关注点主要放到了对微服务技术架构的学习。然而,当他们真正开始将微服务落地到具体的业务中时,才发现,真正的难题是微服务按照什么原则拆分、如何拆分,以及会面对哪些潜在风险。下面我们来一一解决。
微服务拆分的原则
在前面的内容中,我们多次提到过微服务的拆分原则,接下来我将为你详细讲解下。
微服务的拆分原则就是“小而专”,即微服务内高内聚、微服务间低耦合。
“微服务内高内聚”,就是单一职责原则,即每个微服务中的代码都是软件变化的一个原因。因这个原因而需要变更的代码都在这个微服务中,与其他微服务无关,那么就可以将代码修改的范围缩小到这个微服务内。把这个微服务修改好了,独立修改、独立发布,该需求就实现了。这样,微服务的优势就发挥出来了。
“微服务间低耦合”,就是说在微服务实现自身业务的过程中,如果需要执行的某些过程不是自己的职责,就应当将这些过程交给其他微服务去实现,你只需要对它的接口进行调用。譬如,“用户下单”微服务在下单过程中需要查询用户信息,但“查询用户信息”不是它的职责,而是“用户注册”微服务的职责。这样,“用户下单”微服务就不需要再去执行对用户信息的查询,而是直接调用“用户注册”微服务的接口。那么,怎样调用呢?直接调用可能会形成耦合。通过注册中心,“用户下单”微服务调用的只是在注册中心中名称叫“用户注册”的微服务。而在软件设计时,“用户注册”可以有多个实现,哪个注册到注册中心中,就调用哪个。这样,微服务之间的调用就实现了解耦。
通过 DDD 进行业务建模,再基于领域模型进行限界上下文划分,就能保证系统的设计,在限界上下文内高内聚,在限界上下文间低耦合。所以,基于限界上下文进行微服务的拆分,就能保证微服务设计的高质量。同时,通过对上下文地图的分析,就能理清微服务之间的接口调用关系,从而协调多个开发团队协同开发。
子域划分与限界上下文
正如第 06 讲中谈到,领域模型的绘制,不是将整个系统的领域对象都绘制在一张大图上,那样绘制很费劲,阅读也很费劲,不利于相互的交流。因此,领域建模就是将一个系统划分成了多个子域,每个子域都是一个独立的业务场景。围绕着这个业务场景进行分析建模,该业务场景会涉及许多领域对象,而这些领域对象又可能与其他子域的对象进行关联。这样,每个子域的实现就是“限界上下文”,而它们之间的关联关系就是“上下文地图”。
在本案例中,围绕着领域事件“已下单”进行分析。它属于“用户下单”这个限界上下文,但与之相关的“用户”及其“地址”来源于“用户注册”这个限界上下文,与之相关的“饭店”及其“菜单”来源于“饭店管理”这个限界上下文。因此,在这个业务场景中,“用户下单”限界上下文属于“主题域”,而“用户注册”与“饭店管理”限界上下文属于“支撑域”。同理,围绕着本案例的各个领域事件进行了如下一些设计:
“已下单”的限界上下文分析图
通过这样的设计,就能将“用户下单”限界上下文的范围,与之相关的上下文地图以及如何接口,分析清楚了。有了这些设计,就可以按照限界上下文进行微服务拆分。按照这样的设计拆分的微服务,所有与用户下单相关的需求变更都在“用户下单”微服务中实现。但是,订单在读取用户信息的时候,不是直接去 join 用户信息表,而是调用“用户注册”微服务的接口。这样,当用户信息发生变更时,与“用户下单”微服务无关,只需要在“用户注册”微服务中独立开发、独立升级,从而使系统维护的成本得到降低。
“已接单”与“已就绪”的限界上下文分析图
同样,如上图所示,我们围绕着“已接单”与“已就绪”的限界上下文进行了分析,并将它们都划分到“饭店接单”限界上下文中,后面就会设计成“饭店接单”微服务。这些场景的主题域就是“饭店接单”限界上下文,而与之相关的支撑域就是“用户注册”与“用户下单”限界上下文。通过这些设计,不仅合理划分了微服务的范围,也明确了微服务之间的接口,实现了微服务内的高内聚与微服务间的低耦合。
领域事件通知机制
按照 07 讲所讲到的领域模型设计,以及基于该模型的限界上下文划分,将整个系统划分为了“用户下单”“饭店接单”“骑士派送”等微服务。但是,在设计实现的时候,还有一个设计难题,即领域事件该如何通知。譬如,当用户在“用户下单”微服务中下单,那么会在该微服务中形成一个订单;但是,“饭店接单”是另外一个微服务,它必须要及时获得已下单的订单信息,才能执行接单。那么,如何通知“饭店接单”微服务已经有新的订单。诚然,可以让“饭店接单”微服务按照一定的周期不断地去查询“用户下单”微服务中已下单的订单信息。然而,这样的设计,不仅会加大“用户下单”与“饭店接单”微服务的系统负载,形成资源的浪费,还会带来这两个微服务之间的耦合,不利于之后的维护。因此,最有效的方式就是通过消息队列,实现领域事件在微服务间的通知。
在线订餐系统的领域事件通知
如上图所示,具体的设计就是,当“用户下单”微服务在完成下单并保存订单以后,将该订单做成一个消息发送到消息队列中;这时,“饭店接单”微服务就会有一个守护进程不断监听消息队列;一旦有消息就会触发接收消息,并向饭店发送“接收订单”的通知。在这样的设计中:
“用户下单”微服务只负责发送消息,至于谁会接收并处理这些消息,与“用户下单”微服务无关;
“饭店接单”微服务只负责接收消息,至于谁发送的这个消息,与“饭店接单”微服务无关。
这样的设计就实现了微服务之间的解耦,使得日后变更的成本降低。同样,饭店餐食就绪以后,也是通过消息队列通知“骑士接单”。在整个微服务系统中,微服务与微服务之间的领域事件通知会经常存在,所以最好在架构设计中将这个机制下沉到技术中台中。
DDD 的微服务设计
通过第 07 讲所讲到的一系列领域驱动设计:
首先通过事件风暴会议进行领域建模;
接着基于领域建模进行限界上下文的设计。
所有这些设计都是为了指导最终微服务的设计。
在 DDD 指导微服务设计的过程中:
首先按照限界上下文进行微服务的拆分,按照上下文地图定义各微服务之间的接口与调用关系;
在此基础上,通过限界上下文的划分,将领域模型划分到多个问题子域,每个子域都有一个领域模型的设计;
这样,按照各子域的领域模型,基于充血模型与贫血模型设计各个微服务的业务领域层,即各自的 Service、Entity 与 Value Object
同时,按照领域模型设计各个微服务的数据库。
最后,将以上的设计最终落实到微服务之间的调用、领域事件的通知,以及前端微服务的设计。如下图所示:
在线订餐系统的微服务设计
这里可以看到,前端微服务与后端微服务的设计是不一致的。前面讲的都是后端微服务的设计,而前端微服务的设计与用户 UI 是密切关联的,因此通过不同角色的规划,将前端微服务划分为用户 App、饭店 Web 与骑士 App。在用户 App 中,所有面对用户的诸如“用户注册”“用户下单”“用户选购”等功能都设计在用户 App 中。它相当于一个聚合服务,用于接收用户请求:
“用户注册”时,调用“用户注册”微服务;
“用户选购”时,查询“饭店管理”微服务;
“用户下单”时,调用“用户下单”微服务。
总结
采用 DDD 进行需求的分析建模,可以帮助微服务的设计质量提高,实现“低耦合、高内聚”,进而充分发挥微服务的优势。然而,在微服务的设计实现还要解决诸多的难题。本讲一一拆解了微服务设计实现的这些难题,及其解决思路。然而,要更加完美地解决以上问题,不是让每个微服务都去见招拆招,而是应当有一个微服务的技术中台统一去解决。这些方面的设计将在后面微服务技术中台建设的相关章节进行讲解。
下一讲我们将演练在以上领域模型与微服务设计的基础上,如何落实每一个微服务的设计,以及可能面临的设计难题。

View File

@ -0,0 +1,106 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 DDD 是如何落地微服务设计实现的?
自本专栏上线以来,有许多小伙伴跟我交流了很多相关的 DDD 知识。我发现,当大家看到贫血模型、充血模型、策略模式、装饰者模式时,发出这样的感慨:“难道这就是 DDD 吗?和我们平时的开发没有什么不同啊。”殊不知,其实你还没有真正 Get 到 DDD 的真谛。
DDD 的真谛
什么是 DDD 的真谛呢?那就是领域建模,它改变了我们过去对软件开发的认知。如图 1 所示DDD 的精髓是:
首先深刻理解业务;
然后将我们对业务的理解绘制成领域模型;
再通过领域模型指导数据库和程序的设计。
图 1 领域驱动设计的真谛
过去,我们认为软件就是,用户怎么提需求,软件就怎么开发。这种开发模式使得我们对需求的认知浅薄,不得不随着用户的需求变动反复地改来改去,导致我们很累而用户还不满意,软件研发风险巨大。
正是 DDD 改变了这一切,它要求我们更加主动地去理解业务,掌握业务领域知识。这样,我们对业务的理解越深刻,开发出来的产品就越专业,那么客户就越喜欢购买和使用我们的产品。
然而,真实世界是非常复杂的,这就决定了我们不可能一开始就深刻理解业务。起初,我们对业务的理解浅薄,基于它做出来的领域模型也是浅薄的,导致最后开发出来的软件虽然也能用,但用户并不一定满意。然而,如果我们不断地与客户沟通,深入地理解业务,听取他们的意见,我们对业务的理解就会越来越深刻、越来越准确。再结合我们的专业技术知识,就能够理解我们的软件需要解决客户的什么问题,怎样做才是最优,怎样做才让客户感觉好用。
这时就不再是客户提需求了,而是我们主动地提需求、主动地改进功能,去解决客户的痛点,这样做的效果是,客户会感觉“不知道为什么,我就觉得你们的软件好用,用着很顺手”。这时,不但客户不会再改来改去,而且我们的软件做得也越来越专业,越来越有市场竞争力,这才是 DDD 的真谛。
这里有个问题,如果我们对业务理解不深刻就会影响到产品,那么能不能一开始就对业务理解得非常深刻呢?这几乎是不可能的。我们经常说,做事不能仅凭一腔热血,一定要符合自然规律。其实软件的设计开发过程也是这样。
在最开始你对业务理解比较粗略的时候,就从主要流程开始领域建模。
接着,不断往领域模型中加东西。随着功能一个一个地添加,领域模型也变得越来越丰富、越来越完善。每次添加新功能的时候,运用“两顶帽子”的方式先重构再加新功能,不断地完善每个设计。
这样,领域模型就像小树一样一点儿一点儿成长,最后完成所有的功能。
这样的设计过程叫“小步快跑”。采用小步快跑的设计方法,一开始不用思考那么多问题,从简单问题开始逐步深入,设计难度就降低了。同时,系统始终是处于变更中,使设计更加易于变更。
基于限界上下文的领域建模
回到 08 讲微服务设计部分,当在线订餐系统完成了事件风暴的分析以后,接着应当怎样设计呢?通过划分限界上下文,已经将系统划分为了“用户注册”“用户下单”“饭店接单”“骑士派送”与“饭店管理”等几个限界上下文,这样的划分也是后端微服务的划分。紧接着,就开始为每一个限界上下文进行领域建模。
首先,从“用户下单”上下文开始。通过业务领域分析,绘制出了如图 2 所示的领域模型,该模型的核心是“订单”,通过“订单”关联了用户与用户地址。一个订单有多个菜品明细,而每个菜品明细都对应了一个菜单,每个菜单隶属于一个饭店。此外,一个订单还关联了它的支付与发票。起初,它们的属性和方法没有那么全面,随着设计的不断深入,不断地细化与完善模型。
在这样的基础上开始划分限界上下文,用户与用户地址属于“用户注册”上下文,饭店与菜单属于“饭店管理”上下文。它们对于“用户下单”上下文来说都是支撑域,即给“用户下单”上下文提供接口调用的。真正属于“用户下单”上下文的,就只有订单、菜品明细、支付、发票这几个类,它们最终形成了“用户下单”微服务及其数据库设计。由于用户姓名、地址、电话等信息,都在“用户注册”上下文中,每次都需要远程接口调用来获得。这时就需要从系统优化的角度,适当将它们冗余到“订单”领域对象中,以提升查询效率。同样,“菜品名称”也进行了冗余,设计更新如图 3 所示:
完成了“用户下单”上下文以后,开始设计“饭店接单”上下文,设计如图 4 所示。上一讲谈到,“用户下单”微服务通过事件通知机制,将订单以消息的形式发送给“饭店接单”微服务。具体来说,就是将订单与菜品明细发送给“饭店接单”上下文。“饭店接单”上下文会将它们存储在自己的数据库中,并在此基础上增加“饭店接单”类,它与订单是一对一的关系。
同样的思路,通过领域事件通知“骑士派送”上下文,完成“骑士派送”的领域建模。
通过以上设计,就将上一讲的微服务拆分,进一步落实到每一个微服务的设计。紧接着,将每一个微服务的设计,按照第 03 讲的思路落实数据库设计,按照第 04 讲的思路落实贫血模型与充血模型的设计。
特别值得注意的是订单与菜品明细是一对聚合。过去按照贫血模型的设计分别为它们设计订单值对象、Service 与 Dao菜品明细值对象、Service 与 Dao现在按照充血模型的设计只有订单领域对象、Service、仓库、工厂与菜品明细包含在订单对象中而订单 Dao 被包含在订单仓库中。贫血模型与充血模型在设计上有明显的差别。关于聚合的实现,下一讲再详细探讨。
深入理解业务与模型重构
前面讲了,我们不可能一步到位深刻理解业务,它是一个逐步深入的过程。譬如,在设计“用户地址”时,起初没有“联系人”与“手机号”,因为通过关联用户就可以获得。然而,随着业务的不断深入,我们发现,当用户下单的时候,最终派送的不一定是给他本人,可能是另一个人,这是起初没有想到的真实业务场景。为此,在“用户地址”中果断增加了“联系人”与“手机号”,问题得到解决。
此外,如果用户下单以后又需要取消订单,这样的业务场景又该如何设计呢?通过与客户的沟通,确定了该业务的需求:
如果饭店还未接单,可以直接取消;
如果饭店已经接单了,需要经过饭店的确认方可取消;
如果饭店已经就绪了,就不可取消了。
这样,首先需要“饭店接单”上下文提供一个状态查询的接口,以及饭店确认取消的接口。接着,订单取消以后需要记录一个取消时间,并形成一个“订单取消”领域事件,通知“饭店接单”上下文。为此,“用户下单”上下文需要在订单中增加一个“取消时间”。
然而,当“用户下单”上下文对“订单”对象更新以后,“饭店接单”与“骑士派送”上下文是否也要跟着更新呢?前面提到,对微服务的设计,是希望:
每次变更的时候尽可能只更新一个微服务,以降低微服务的维护成本;
即使不能,也应当尽可能缩小更新的范围。
增加“取消时间”这个字段,对“饭店接单”上下文是有意义的,它的相应变更无可厚非。但对于“骑士派送”上下文来说,“取消时间”对它没有一毛钱关系,因此不希望对它进行更新。微服务间的调用是基于 RESTful 的接口调用,参数是通过 Json 对象传递,是一种松耦合调用。因此,在“饭店接单”与“骑士派送”上下文中,即使“订单”对象的数据结构不一致,也不影响它们的调用。因此,在“骑士派送”上下文不需要更新,更新范围就缩小了,维护成本降低了。
在完成了以上设计以后,还有一个难题就是订单状态的跟踪。
订单状态的跟踪
当用户下单后,往往会不断地跟踪订单状态是“已下单”“已接单”“已就绪”还是“已派送”。然而,这些状态信息被分散到了各个微服务中,就不可能在“用户下单”上下文中实现了。如何从这些微服务中采集订单的状态信息,又可以保持微服务间的松耦合呢?解决思路还是领域事件的通知。
通过消息队列,每个微服务在执行完某个领域事件的操作以后,就将领域事件封装成消息发送到消息队列中。比如,“用户下单”微服务在完成用户下单以后,将下单事件放到消息队列中。这样,不仅“饭店接单”微服务可以接收这个消息,完成后续的接单操作;而且“订单查询”微服务也可以接收这个消息,实现订单的跟踪。如图 5 所示。
图 5 订单状态的跟踪图
通过领域事件的通知与消息队列的设计,使微服务间调用的设计松耦合,“订单查询”微服务可以像外挂一样采集各种订单状态,同时不影响原有的微服务设计,使得微服务之间实现解耦,降低系统维护的成本。而“订单查询”微服务通过冗余,将“下单时间”“取消时间”“接单时间”“就绪时间”等订单在不同状态下的时间,以及其他相关信息,都保存到订单表中,甚至增加一个“订单状态”记录当前状态,并增加 Redis 缓存的功能。这样的设计就保障了订单跟踪查询的高效。要知道,面对大数据的高效查询,通常都是通过冗余来实现的。
总结
DDD 的真谛是领域建模,即深入理解业务。只有深入理解业务,将对业务的深入理解设计到领域模型中,设计出来的软件才更加专业,让用户的使用更满意。因此,基于每个限界上下文进行领域建模,不断地将每个功能加入模型中,落地每个微服务的设计。当业务越来越复杂,理解越来越深入的时候,适时地调整原有的模型,就能适应新的功能,使设计始终高质量。

View File

@ -0,0 +1,111 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 微服务落地的技术实践
如今,做一个优秀的程序员越来越难。激烈的市场竞争、互联网快速的迭代、软件系统规模化发展,无疑都大大增加了软件设计的难度。因此,对于架构师的能力要求也越来越高,就像我的一本书里写道的:
作为顶级架构师应当具备这样两个核心能力:
1能够将业务转换为技术
2能合理利用技术支撑业务。
“不想当将军的士兵不是好士兵”,因此作为普通开发人员的你,也应当树立成为顶级架构师的目标,并不断努力。
能够将业务转换为技术,意味着需要将更多的精力放到对业务的理解中。技术本身并不能产生价值,你必须具备超强的业务落地能力,能够将用户的业务需求落地到技术方案,开发出用户乐于使用的产品和功能,用户才能为之买单,企业才能挣钱。具备这样的能力,才能够强力地帮助企业产生效益,才能体现你的价值。学习 DDD 就能让你掌握快速学习业务领域知识的能力。
能合理利用技术支撑业务,意味着你必须具备广博的知识与开阔的视野,能将用户的业务痛点,快速落地形成合理的,甚至是最优的技术方案。做出用户需要的功能,让用户为之买单,从而为企业产生效益。然而,如今是一个技术快速更迭的时代,各种高新技术层出不穷。每次新产品的开发不是将原有的技术拿来炒冷饭,而是运用更多的新技术解决新问题,让产品更有竞争力与生命力。因此,你必须有广博的技术知识与超强的技术落地能力。
上一讲谈到 DDD 落地微服务的分析设计过程,然后将这些设计最终落实到每个微服务的设计开发中。微服务的落地其实并没有那么简单,需要解决诸多设计与实现的技术难题,这一讲我们就来探讨一下吧。
如何发挥微服务的优势
微服务也不是银弹,它有很多的“坑”。开篇词中提到,当我们将一个庞大的业务系统拆分为一个个简单的微服务时,就是希望通过合理的微服务设计,尽量让每次的需求变更都交给某个小团队独立完成,让需求变更落到某个微服务上进行变更。唯有这样,每次变更只需独立地修改这个微服务,独立打包、独立升级,新需求就实现啦,才能发挥微服务的优势。
然而,过去很多系统都是这样设计的(如上图所示),多个模块都需要读取商品信息表,因此都通过 JDBC 直接读取。现在要转型微服务了,起初采用数据共享的微服务设计,就是数据库不变,然后简单粗暴地直接按照功能模块进行微服务拆分。这时,多个微服务都需要读取商品信息表,都通过 SQL 直接访问。这样的设计,一旦商品信息表发生变更,那么多个微服务都需要变更。这样的设计就使得微服务的变更与发布变得复杂,微服务的优势无法发挥。
通过前面 DDD 的指导,是希望做“小而专”的微服务设计。按照这样的思路设计微服务,对商品信息表的读写只有“商品维护”微服务。当其他微服务需要读写商品信息时,就不能直接读取商品信息表,而是通过 API 接口去调用“商品维护”微服务。这样,日后因商品信息变更而修改的代码就只限于“商品维护”微服务。只要“商品维护”微服务对外的 API 接口不变,这个变更则与其他微服务无关。只有这样的设计,才能真正发挥微服务的优势。
为了规范“小而专”的微服务设计,在微服务转型之初,先按照 DDD 对数据库表按照用户权限进行划分。每个微服务只能通过自己的账号访问自己的表。当需要访问其他的表时,只能通过接口访问相应的微服务。这样的划分,就为日后真正的数据库拆分做好了准备,微服务转型将更加平稳。
怎样提供微服务接口
因此,微服务的设计彼此之间不是孤立的,它们需要相互调用接口实现高内聚。然而,当一个微服务团队向另一个微服务团队提出接口调用需求时,另一个微服务团队该如何设计呢?
首先第一个问题,当多个团队都在向你提出 API 接口时,你怎么提供接口。如果每个团队给你提需求,你就必须要做一个新接口,那么你的微服务将变得非常不稳定。因此,当多个团队向你提需求时,必须要对这些接口进行规划,通过复用用尽可能少的接口满足他们的需求;当有新的接口提出时,要尽量通过现有接口解决问题。这样做,你就能用更低的维护成本,更好地维护自己的微服务。
接着,当调用方需要接口变更时怎么办?变更现有接口应当尽可能向前兼容,即接口的名称与参数都不变,只是在内部增加新的功能。这样做是为了不影响其他微服务的调用。如果确实需要更改现有的接口怎么办?宁愿增加一个新的接口也最好不要去变更原有的接口。
此外调用双方传递的值对象需要完全一致吗当然不用。当被调方因为某些变更对值对象增加了字段而这些字段调用方不使用时那么调用方不需要跟着变更值对象。因为微服务间的调用是采用RESTful 接口,以 Json 的形成传递数据,是一种松耦合的调用。因此调用双方的值对象可以不一致,从而降低了需求变更的微服务更新范围。
最后,调用方如何调用接口呢?这里分为同步调用与异步调用。
第 09 讲谈到“用户接单 Service”在完成下单以后用消息队列通知“饭店接单 Service”就是异步调用。
接着“用户接单Service”常常要查找用户表信息但前面说了它没有查询用户表权限因为用户表在“用户注册”微服务中。这时“用户接单 Service”通过同步调用“用户注册 Service”的相关接口。
具体设计实现上,就是在“用户接单”微服务的本地,增加一个“用户注册 Service”的 feign 接口。这样,“用户接单 Service”就像本地调用一样调用“用户注册 Service”再通过这个 feign 接口实现远程调用。这样的设计叫作“防腐层”的设计。如下图所示:
微服务的拆分与防腐层的设计图
譬如,大家想象这样一个场景。过去,“用户注册 Service”是在“用户下单”微服务中的。后来随着微服务设计的不断深入需要将“用户注册 Service”拆分到另外一个微服务中。这时“用户下单Service”与“取消订单 Service”以及其他对“用户注册 Service”的调用都会报错都需要修改维护成本就很高。这时在微服务的本地放一个“用户注册 Service”的 feign 接口,那么其他的 Service 都不需要修改了,维护成本将得以降低。这就是“防腐层”的作用,即接口变更时降低维护成本。
去中心化的数据管理
按照前面 DDD 的设计,已经将数据库按照微服务划分为用户库、下单库、接单库、派送库与饭店库。这时候,如何来落地这些数据库的设计呢?微服务系统最大的设计难题就是要面对互联网的高并发与大数据。因此,可以按照“去中心化数据管理”的思想,根据数据量与用户访问特点,选用不同的数据存储方案存储数据:
微服务“用户注册”与“饭店管理”分别对应的用户库与饭店库,它们的共同特点是数据量小但频繁读取,可以选用小型的 MySQL 数据库并在前面架设 Redis 来提高查询性能;
微服务“用户下单”“饭店接单”“骑士派送”分别对应的下单库、接单库、派送库,其特点是数据量大并且高并发写,选用一个数据库显然扛不住这样的压力,因此可以选用了 TiDB 这样的 NewSQL 数据库进行分布式存储,将数据压力分散到多个数据节点中,从而解决 I/O 瓶颈;
微服务“经营分析”与“订单查询”这样的查询分析业务,则选用 NoSQL 数据库或大数据平台,通过读写分离将生产库上的数据同步过来进行分布式存储,然后经过一系列的预处理,就能应对海量历史数据的决策分析与秒级查询。
基于以上这些设计,就能完美地应对互联网应用的高并发与大数据,有效提高系统性能。设计如下图所示:
在线订餐系统的去中心化数据管理图
数据关联查询的难题
此外,各个微服务在业务进行过程需要进行的各种查询,由于数据库的拆分,就不能像以前那样进行 join 操作了,而是通过接口调用的方式进行数据补填。比如“用户下单”“饭店接单”“骑士派送”等微服务,由于数据库的拆分,它们已经没有访问用户表与饭店表的权限,就不能像以往那样进行 join 操作了。这时,需要重构查询的过程。如下图所示:
查询的过程分为 2 个步骤。
查询订单数据,但不执行 join 操作。这样的查询结果可能有 1 万条,但通过翻页,返回给微服务的只是那一页的 20 条数据。
再通过调用“用户注册”与“饭店管理”微服务的相关接口,实现对用户与饭店数据的补填。
这种方式,既解决了跨库关联查询的问题,又提高了海量数据下的查询效率。注意,传统的数据库设计之所以在数据量越来越大时,查询速度越来越慢,就是因为存在 join 操作。因而,在面对海量数据的查询时,干掉 join 操作,改为分页后的数据补填,就能有效地提高查询性能。
然而在查询订单时如果要通过用户姓名、联系电话进行过滤然后再查询时又该如何设计呢这里千万不能先过滤用户数据再去查询订单这是一个非常糟糕的设计。我们过去的数据库设计采用的都是3NF第 3 范式),它能够帮助我们减少数据冗余,然而却带来了频繁的 join 操作,降低了查询性能。因此,为了提升海量数据的查询性能,适当增加冗余,即在订单表中增加用户姓名、联系电话等字段。这样,在查询时直接过滤订单表就好了,查询性能就得到了提高。
最后,当系统要在某些查询模块进行订单查询时,可能对各个字段都需要进行过滤查询。这时就不再采用数据补填的方式,而是利用 NoSQL 的特性,采用“宽表”的设计。按照这种设计思路,当系统通过读写分离从生产库批量导入查询库时,提前进行 join 操作,然后将 join 以后的数据,直接写入查询库的一个表中。由于这个表比一般的表字段更多,因此被称为“宽表”。
由于 NoSQL 独有的特性,为空的字段是不占用空间的,因此字段再多都不影响查询性能。这样,在日后的查询时,就不再需要 join 操作而是直接在这个单表中进行各种过滤、各种查询从而在海量历史数据中实现秒级查询。因此“订单查询”微服务在数据库设计时就可以通过NoSQL 数据库建立宽表,从而实现高效的数据查询。
总结
基于 DDD 的微服务设计,既强调对业务的分析理解,又强调对业务的技术落地。只有把这两个事情都做好了,产品才能被用户认可,我们才能体现出价值。在这个过程中,微服务间要通过 feign 接口相互调用,数据要通过补填关联查询。此外,还有聚合的实现、仓库和工厂的设计。所有这些内容都需要在 DDD 设计思想的基础上,落地实现。
然而如果每个模块都要反复地写代码去实现这些功能DDD 的设计将显得异常烦琐,因此迫切需要有一个既支持 DDD又支持微服务的技术中台封装这些代码简化微服务的设计。
下一讲我将开始讲解支持 DDD 与微服务的技术中台的设计实践。

View File

@ -0,0 +1,108 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 解决技术改造困局的钥匙:整洁架构
有个“大师与太空”的梗是这样说的:大师通常都站得很高很高、高瞻远瞩,站得有多高呢?都是站在太空里的,所以很多人追随大师容易缺氧。因此,我们学习大师的理论,就要脚踏实地、踏踏实实地将其落地到项目应用中,重新落地到“地面”上,这些理论才是有用的,落地 DDD 同样是这样。
如何落地 DDD 呢?除了在项目中实践 DDD领域建模按照 DDD 的思想设计开发以外,还需要一个支持 DDD 与微服务的技术中台。在 DDD 实现过程中,这个技术中台应当能够封装那些烦琐的聚合操作、仓库与工厂的设计,以及相关的各种技术。有了这个技术中台的支持,开发团队就可以把更多的精力放到对用户业务的理解,对业务痛点的理解,快速开发用户满意的功能并快速交付,而不再受限于那些烦琐的技术细节,从而降本增效。这样,不仅编写代码减少了,技术门槛降低了,还使得日后的变更更加容易,技术更迭也更加方便。
那么,如何设计这样一个技术中台呢?首先应当从现有系统的设计痛点开始分析。
底层技术的更迭
这些年,随着互联网、大数据、人工智能等新技术层出不穷,整个 IT 产业的技术架构也在快速迭代。
过去,我们说“架构是软件系统中最稳定不变的部分”;
而现在,我们说“好的架构源于不停地演变”。
因此,如今的架构设计需要思考如何让底层的架构更易于技术更迭、易于架构调整,以应对不断演进的新技术、新框架,从而获得行业竞争的技术优势。
然而,在实际项目中,特别是很多运行了七八年、十多年的老项目,要做一次技术升级,那叫一个费劲,就像脱一层皮那么痛苦。为什么技术升级那么费劲呢?究其原因,是在系统设计开发时,大量的业务代码依赖于底层的技术框架,形成了耦合。
譬如,过去采用 hibernate 进行数据持久化,每个模块的 DAO 都要继承自 HibernateDaoSupport。这样所有的 DAO 都与 Hibernate 形成了一种依赖。当系统架构由 Hibernate2 升级成 Hibernate3甚至升级成 MyBatis就不是改换一个 jar 包那么简单了。
技术框架一换,底层的类、接口、包名都变了,就意味着上层的所有模块的 DAO 都需要改,改完了还要测试。这样的技术升级成本极高,风险极大,需要我们认真去思考解决方案。
总之,老系统技术架构升级成本极高的根源,在于业务代码与底层技术框架的耦合。因此,解决思路就是对它们进行解耦。如何解耦呢?就是在上层业务代码与底层技术框架之间建立“接口层”。
如何在业务代码与底层框架之间建立“接口层”呢?如上图所示,上层业务代码在进行持久化时,各个模块的 DAO 不再去调用底层框架,而是对接口层的 DaoSupport 进行调用。DaoSupport 接口是我们自己设计的,它应当满足上层的所有业务需求,比如各种类型的 insert、 update、delete、get、load、find并让这个接口保持稳定。上层业务代码的设计实现都依赖于 DaoSupport 接口,只要它稳定了,上层业务代码就稳定了。
接着,在 DaoSupport 接口的基础上编写实现类,由实现类去调用底层技术框架,实现真正的持久化。
起初使用 Hibernate2 作为底层框架,所以为 Hibernate2 编写了一个实现类。
当 Hibernate2 升级成 Hibernate3 时,为 Hibernate3 写一个实现类。
当底层框架要升级成MyBatis 时,再为 MyBatis 写一个实现类。
这样的设计,当系统进行技术架构升级时,其影响就不再扩展到业务层代码,而仅仅局限于调整接口层的实现类,技术升级的成本将得到大幅度的降低。
整洁架构的设计
通过前面对问题的分析与接口层的设计,可以得出一个非常重要的结论:如何既能轻松地实现技术架构演化,又能保证开发团队的快速交付呢,关键的思路是将业务代码与技术框架解耦。如上图所示,在系统分层时,基于领域驱动的设计,将业务代码都整合在业务领域层中去实现。这里的业务领域层包括了 BUS 层中的 Service以及与它们相关的业务实体与值对象。
业务领域层设计的实质,就是将领域模型通过贫血模型与充血模型的设计,最终落实到对代码的设计。在此基础上,通过分层将业务领域层与其他各个层次的技术框架进行解耦,这就是“整洁架构”的核心设计思路。
整洁架构The Clean Architecture是 Robot C. Martin 在《架构整洁之道》中提出来的架构设计思想。如上图所示它以圆环的形式把系统分成了几个不同的层次因此又称为“洋葱头架构The Onion Architecture”。在整洁架构的中心是业务实体黄色部分与业务应用红色部分业务实体就是那些核心业务逻辑而业务应用就是面向用户的那些服务Service。它们合起来组成了业务领域层也就是通过领域模型形成的业务代码的实现。
整洁架构的最外层是各种技术框架,包括:
与用户 UI 的交互;
客户端与服务器的网络交互;
与硬件设备和数据库的交互;
与其他外部系统的交互。
整洁架构的精华在于其中间的适配器层,它通过适配器将核心的业务代码,与外围的技术框架进行解耦。因此,如何设计适配层,让业务代码与技术框架解耦,让业务开发团队与技术架构团队各自独立地工作,成了整洁架构落地的核心。
整洁架构设计的细化图,图片来自《软件架构编年史》
如图,进一步细化整洁架构,将其划分为 2 个部分:主动适配器与被动适配器。
主动适配器,又称为“北向适配器”,就是由前端用户以不同的形式发起业务请求,然后交由应用层去接收请求,交由领域层去处理业务。用户可以用浏览器、客户端、移动 App、微信端、物联网专用设备等各种不同形式发起请求。然而通过北向适配器最后以同样的形式调用应用层。
被动适配器又称为“南向适配器”就是在业务领域层完成各种业务处理以后以某种形式持久化存储最终的结果数据。最终的数据可以存储到关系型数据库、NoSQL 数据库、NewSQL 数据库、Redis 缓存中,或者以消息队列的形式发送给其他应用系统。但不论采用什么形式,业务领域层只有一套,但持久化存储可以有各种不同形式。南向适配器将业务逻辑与存储技术解耦。
整洁架构的落地
按照整洁架构的思想如何落地架构设计呢?如上图所示,在这个架构中,将适配器层通过数据接入层、数据访问层与接口层等几个部分的设计,实现与业务的解耦。
首先,用户可以用浏览器、客户端、移动 App、微信端、物联网专用设备等不同的前端形式多渠道地接入到系统中不同的渠道的接入形式是不同的。通过数据接入层进行解耦然后以同样的方式去调用上层业务代码就能将前端的多渠道接入与后台的业务逻辑实现了解耦。这样前端不管怎么变有多少种渠道形式后台业务只需要编写一套维护成本将大幅度降低。
接着,通过数据访问层将业务逻辑与数据库解耦。前面说了,在未来三五年时间里,我们又将经历一轮大数据转型。转型成大数据以后,数据存储的设计可能不再仅限于关系型数据库与 3NF的思路设计而是通过 JSON、增加冗余、设计宽表等设计思路将其存储到 NoSQL 数据库中,设计思想将发生巨大的转变。但无论怎么转变,都只是存储形式的转变,不变的是业务逻辑层中的业务实体。因此,通过数据访问层的解耦,今后系统向大数据转型的时候,业务逻辑层不需要做任何修改,只需要重新编写数据访问层的实现,就可以转型成大数据技术。转型成本将大大降低,转型将更加容易。
最后,就是底层的技术架构。现在我们谈架构,越来越多地是在谈架构演化,也就是底层技术架构要不断地随着市场和技术的更迭而更迭。但是,话虽如此,很多系统的技术架构更迭,是一个非常痛苦的过程。为什么呢?究其原因,是软件在设计时,将太多业务代码与底层框架耦合,底层框架一旦变更,就会导致大量业务代码的变更,各个业务模块的都要更迭,导致架构调整的成本巨大、风险高昂。
既然这里的问题是耦合,解决的思路就是解耦。在平台建设的过程中,除了通过技术选型将各种技术整合到系统中以外,还应通过封装,在其上建立接口层。通过接口层的封装,封装许多技术的实现,以更加简便的接口开放给上层的业务开发人员。这样,既可以降低业务开发的技术门槛,让他们更加专注于业务,提高开发速度,又让业务代码与技术框架解耦。有了这种解耦,就使得未来可以用更低的成本技术更迭,加速技术架构演进,跟上这个快速变化的时代。
总结
整洁架构的中心是基于 DDD 的业务实现,即那些通过领域模型指导设计与开发的 Service、Entity 与 Value Object。整洁架构的最外层是各种硬件、设备与技术框架。而整洁架构最核心的思想是通过适配器层将业务实现与技术框架解耦这也是 DDD 落地到架构设计的最佳实践。
因此,支持 DDD 与微服务的技术中台,就是基于整洁架构的思想,将 DDD 底层的那些烦琐的聚合操作、仓库与工厂的设计,与微服务的技术框架,以及整洁架构中的适配器,统统封装在技术中台中。有了这个技术中台,就能让上层的业务开发人员,更加轻松地运用 DDD 的思想,更加快捷地更迭与交付用户需求,从而在激烈的市场竞争中获得优势。
下一讲将进一步讲解这样的技术中台是如何设计的。

View File

@ -0,0 +1,121 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12 如何设计支持快速交付的技术中台战略?
我们以往建设的系统都分为前台和后台,前台就是与用户交互的 UI 界面,后台就是服务端完成的业务逻辑操作。然而,在我们以往开发的很多业务系统中,有一些内容是共用的部分,在未来开发的业务系统中也要使用。因此,如果能把这些内容提取出来做成公用组件,那么在未来,开发系统就简单了,不用每次都重头开发,复用这些组件就可以了。
但是,这些公用的组件到底属于前台还是后台呢?都不属于。它既包含前台的界面,也包含后台的逻辑,因此被称为“中台”。所谓的中台,就是将以往业务系统中可以复用的前台与后台代码,剥离个性、提取共性,形成的公用组件。有了这些组件,就可以使日后的系统开发降本增效、提高交付速度。因此,阿里提出了“小前台、大中台”的战略,得到了业界的普遍认可。
从分类上看,中台分为业务中台、技术中台与数据中台。
业务中台,就是将抽象的业务组件,如用户权限、会员管理、仓储管理、物流管理等公用组件,做成微服务,各个业务系统都可以使用。
技术中台,就是封装各个业务系统所要采用的技术框架,设计出统一的 API使上层的业务开发技术门槛降低、开发工作量减少、提升交付速度。
数据中台,则是整理各个业务系统的数据,建立数据存储与运算的平台,为各个系统的数据的分析与利用提供支持。
清楚了这些概念,你就清楚了支持 DDD 与微服务的技术中台的设计思路。它是将 DDD 与微服务的底层技术进行封装,从而支持开发团队在未来实现快速交付,以应对激烈竞争的市场。因此,首先必须要清楚实现快速交付的技术痛点,才能清楚这个技术中台该如何建设。
打造快速交付团队
许多团队都有这样一个经历:项目初期,由于业务简单,参与的人少,往往可以获得一个较快的交付速度;但随着项目的不断推进,业务变得越来越复杂,参与的人越来越多,交付速度就变得越来越慢,使得团队越来越不能适应市场的快速变化,从而处于竞争的劣势。然而,软件规模化发展是所有软件发展的必然趋势。因此,解决规模化团队与软件快速交付的矛盾就成了我们不得不面对的难题。
烟囱式的开发团队
为什么团队越大交付速度越慢呢?如上图是我们从需求到交付的整个过程。在这个过程中,我们要经历多个部门的交互,才能完成最终的交付,大量的时间被耗费在部门间的沟通协调中。这样的团队被称为“烟囱式的开发团队”。
烟囱式的软件开发
烟囱式的开发团队又会导致烟囱式的软件开发。如上图所示在大多数软件项目中每个功能都要设计自己的页面、Controller、Service 以及 DAO需要编写大量的代码并且很多都是重复代码。代码写得越多 Bug 就越多,日后变更也越困难。
最后统一的发布也制约了交付的速度。如上图当业务负责人将需求分配给多个团队开发时A 团队的工作可能只需要 1 周就能完成。但是,当 A 团队完成了他们的工作以后,能立即交付给客户吗?答案是不能,因为 B 团队需要开发 2 周A 团队只能等 B 团队开发完成以后才能统一发布。统一的发布制约了系统的交付速度,即使 A 团队的开发速度再快,不能立即交付用户就不能产生用户价值。
随着系统规模越来越大,功能越来越多、越来越复杂,开发系统的团队规模也越来越大。这样就会导致开发团队的工作效率越来越低,交付周期越来越长,技术转型也越来越困难。
特性团队的组织形式
如何解决这一问题呢?如上图,首先,需要调整团队的组织架构,将筒状的架构竖过来,称为“特性团队”。在特性团队中,每个团队都直接面对终端客户。比如购物团队面对的是购物功能,所有与购物相关的功能都是他们来负责完成,包括从需求到研发,从 UI 到应用再到数据库。最后,经过测试,也是这个团队负责上线部署。这样,整个交付过程都是这个团队负责,没有了那么多团队间的沟通协调,交付速度自然就提升了。
大前端+技术中台
有了特性团队的组织形式,如果还是统一发布,那么交付速度依然提升不了。因此,在特性团队的基础上,软件架构采用了微服务的架构,即每个特性团队各自维护各自的微服务。这样,当该团队完成了一次开发,则自己独立打包、独立发布,不再需要等待其他团队。这样,交付速度就可以得到大幅度提升。如下图所示:
大前端 + 技术中台的组织形式
特性团队 + 微服务架构,可以有效地提高规模化团队的交付速度。然而,仔细思考一下就会惊奇地发现,要这样组建一个特性团队,成本是非常高昂的。团队每个成员都必须既要懂业务,也要懂开发;既要懂 UI、应用还要懂数据库甚至大数据做全栈工程师。如果每个特性团队都是这样组建每个成员都是全栈工程师成本过高是没有办法真正落地的。那么这个问题该怎么解决呢
解决问题的关键在于底层的架构团队。这里的架构团队就不再是架构师一个人,而是一个团队。
架构团队通过技术选型,构建技术中台,将软件开发中诸如 UI、应用、数据库甚至大数据等诸多技术进行了封装
然后以 API 接口的形式开放给上层业务。
这样的组织形式,业务开发的技术门槛将得到降低,开发工作量也会减少。这样,特性团队的主要职责将发生变化,即从软件技术中解脱出来,将更多的精力放到对需求的理解、对业务的实现,从而提高用户的体验,这就是“大前端”。所谓大前端,是一种职能的转变,即业务开发人员不再关注技术,而是更加关注业务,深刻地理解业务,并快速应对市场对业务需求的变化。
采用“大前端+技术中台”的战略,为了团队设计能力以及交付速度的提升,需要架构团队的支撑。架构团队从业务开发的角度进行提炼,提炼共性、保留个性,将这些共性沉淀到技术中台中。这样的技术中台,需要 DDD 与微服务架构的支持。通过将 DDD 与微服务涉及的各个技术组件封装到技术中台中,封装各个技术细节,就能很好地支持各业务团队快速开发业务,快速交付用户,进而让团队获得市场竞争优势。
通过以上的分析,我们理清了技术中台建设的需求。为了提高开发团队的交付速度,提升市场竞争力,需要在系统的底层进行技术中台的建设。打造这样一个支持快速交付的技术中台,应当具备以下特征。
(1) 简单易用、快速便捷的技术中台
它能够明显降低软件开发的工作量,使软件系统易于变更、易于维护、易于技术更迭,进而明显降低业务开发人员的技术门槛。通过前面讲的单 Controller、单 DAO 的架构设计,就能够达到这个目的,关键是这个设计思想如何落地。
(2) 易于技术架构演化
我们打造的技术中台可以帮助开发团队调整技术架构进行技术架构演化并有效地降低技术架构演化的成本。这就要求系统在进行架构设计时能够有效地将技术框架与业务代码解耦。采用整洁架构、六边形架构、CQRS 等架构设计模式,就可以帮助我们完成解耦。
(3) 支持领域驱动与微服务的技术架构
前面讲了领域驱动设计的思想,但要将这样的思想落地到软件项目中,甚至最终落地到微服务架构中,也需要这样一个技术中台,支持领域驱动与微服务技术架构。
简单易用的技术中台建设
首先,我们来看一看,如何打造一个简单易用的技术中台,即如何简化开发。以往的软件项目在研发的过程中需要编写太多的代码,这既加重了软件研发的工作量,延缓了软件交付的速度,又使得日后的维护与变更成分加大。软件研发的一个非常重要的规律就是:
你写的代码越多,可能出现 Bug 的概率就越高,日后的维护与变更就越困难;
你写的代码越少Bug 就越少,日后维护与变更就越容易。
俗话说:小船好掉头,泰坦尼克号看见冰山了为什么要撞上去?因为它实在太大了,根本来不及掉头。写代码也是一样的,一段 10 来行的代码变更会很容易,但一段数百上千行的代码变更就非常复杂。因此,我们设计软件应当秉承这样的态度:宁愿花更多的时间去分析设计,让软件设计精简到极致,从而花更少的时间去编码。俗话说:磨刀不误砍柴工。用这样的态度编写出来的代码,既快又易于维护。
接着,看一看在以往软件研发过程中存在的问题。以往的软件项目在研发的过程中需要编写太多的代码了,每个功能都要编写自己的 UI、Controller、Service 和 DAO。并且在每一个层次中都有不同格式的数据因此我们编写的大量代码都是在进行各个层次之间的数据格式转换。如下图所示
譬如,前端以 Form 的形式传输到后台,这时后台由 MVC 层从 Model 或者 Request 中获得,然后将其转换成值对象,接着去调用 Service。然而从 Model 或者 Request 中获得数据以后,由于我们在 MVC 层的 Controller 中写了太多的判断与操作,再将其塞入值对象中,所以这里耗费了太多的代码。
接着,在 Service 中经过各种业务操作,最后要存盘的时候,又要将 VO 转换为 PO将数据持久化存储到数据库中。这时又要为每一个功能编写一个 DAO。我们写的代码越多日后维护与变更就越困难。那么能不能将这些转换统一成公用代码下沉到技术中台中呢基于这样的思想系统架构调整为这样
在这个架构中,将各个层次的数据都统一成值对象,这是怎样统一的呢?首先,在前端的数据,现在越来越多的前端框架都是以 JSON 的形式传递的。JSON 的数据格式实际上是一种名 - 值对。因此,可以制订一个开发规范,要求前端 JSON 对象的设计,与后台值对象的格式一一对应。这样,当 JSON 对象传递到后台后MVC 层就只需要一个通用的程序,以统一的形式将 JSON 对象转换为值对象。这样,还需要为每个功能编写 Controller 吗?不用了,整个系统只需要一个 Controller并将其下沉到技术中台中。
同样Service 在经过了一系列的业务操作最后要存盘的时候可以这样做制作一个vObj.xml 的配置文件来建立对应关系将每个值对象都对应数据库中的一个表哪个属性就对应哪个字段。这样DAO 拿到哪个值对象,就知道该对象中的数据应当保存到数据库的哪张表中。这时,还需要为每个功能编写一个 DAO 吗?不用了,整个系统只需要一个 DAO。
通过以上的设计思想架构的系统,开发工作量将极大地降低。在业务开发时,每个功能都不用再编写 MVC 层了,就不会将业务代码写到 Controller 中,而是规范地将业务代码编写到 Service或值对象中。接着整个系统只有一个 DAO每个功能的 Service 注入的都是这一个 DAO。这样真正需要业务开发人员编写的仅限于前端 UI、Service 和值对象。而 Service 和值对象都是源于领域模型的映射,因此业务开发人员就会将更多的精力用于功能设计与前端 UI给用户更好的用户体验也提高了交付速度。
总结
我们采用 DDD 是为了更深刻地理解业务,做出用户满意的产品;我们还需要快速交付产品,以应对竞争激烈且瞬息万变的市场。这两方面需要双管齐下,才能获得市场竞争的优势。因此,我们不仅要学习 DDD还要学习如何建立支持 DDD 的技术中台,实现快速交付。
本讲讲解了如何构建一个简单易用的技术中台。下一讲将在此基础上进一步讲解这个中台的设计实现,即如何实现单 Controller如何实现单 DAO如何做通用的仓库与工厂等等。

View File

@ -0,0 +1,372 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13 如何实现支持快速交付的技术中台设计?
前面提到了“大前端”的思想,也就是软件团队组织形式的趋势是“大前端 + 技术中台”,从而通过快速交付提高市场竞争力。所谓的“大前端 + 技术中台”,就是在开发团队中有一个架构支持团队,他们通过架构强大的技术中台,将软件开发中的许多技术架构封装在平台中。有了这样一个技术中台,其他各个开发团队都基于它进行业务开发。
这样,既可以降低业务开发的工作量,提高开发速度,又可以降低技术门槛。业务开发人员也不必过于关注技术,而是将更多的精力集中在对业务的理解,并将对业务深刻的理解融入领域建模的过程中,从而开发出用户更满意的软件,提高用户体验。
因此,如何打造一个强大而实用的技术中台,成了各个软件开发团队迫切的需求。现在我们就从实战的角度看一看,以上这些设计思想该如何落地技术中台建设。
命令与查询职责分离CQRS是软件大师 Martin Fowler 在他的著作《企业应用架构模式》中提出来的一种架构设计模式。该模式将系统按照职责划分为命令(即增删改操作)与查询两个部分。
所有命令部分的增删改操作,应当采用领域驱动设计的思想进行软件设计,从而更好地应对大规模复杂应用;
所有的查询功能则不适用于领域驱动设计而应当采用事务脚本模式Transaction Script即直接通过 SQL 语句进行查询。
遵循该设计模式,是我们在许多软件项目中总结出来的最佳实践。因此,技术中台在建设时,对业务系统的支持也分为增删改与查询两个部分。
增删改的架构设计
增删改部分的技术中台架构设计
在增删改部分中,采用了前面提到的单 Controller、单 Dao 的架构设计。如上图所示,各功能都有各自的前端 UI。但与以往架构不同的是每个功能的前端 UI 对后台请求时,不再调用各自的 Controller而是统一调用一个 Controller。然而每个功能的前端在调用这一个 Controller 时,传递的参数是不一样的。首先从前端传递的是 bean这个 bean 是什么呢?后台各功能都有一个 Service将该 Service 注入 Dao 以后,会在 Spring 框架中配置成一个bean。这时前端只知道调用的是这个 bean但不知道它是哪个 Service。
这样的设计,既保障了安全性(前端不知道具体是哪个类),又有效地实现了前后端分离,将前端代码与后端解耦。
紧接着,前端还要传递一个 method即调用的是哪个方法和哪个 JSON 对象。这样Controller 就可以通过反射进行相应的操作。这里的设计思想是,在软件开发过程中,通过规范与契约的约定,我们认为前端开发人员已经知道了他需要调用后端哪个 bean、哪个method以及什么格式的 JSON就可以大大简化技术中台的设计。
单 Controller 的设计
前端所有功能的增删改操作,以及基于 ID 的 get/load 操作,都是访问的 OrmController。
前端在访问 OrmController 时,输入如下 HTTP 请求:
http://localhost:9003/orm/{bean}/{method}
例如:
GET 请求
http://localhost:9003/orm/product/deleteProduct?id=P00006
POST 请求
http://localhost:9003/orm/product/saveProduct-d”id=P00006&name=ThinkPad+T220&price=4600&unit=%E4%B8%AA&supplierId=20002&classify=%E5%8A%9E%E5%85%AC%E7%94%A8%E5%93%81”
这里的 {bean} 是配置在 Spring 中的 bean.id{method} 是该 bean 中需要调用的方法(注意,此处不支持方法的重写,如果出现重写,它将去调用同名方法中的最后一个)。
如果要调用的方法有值对象,按照规范,必须将值对象放在方法的第一个参数上。
如果要调用的方法既有值对象,又有其他参数,则值对象中的属性与其他参数都放在该 JSON 对象中。如:要调用的方法为 saveProduct(product, saveMode)POST 请求为:
http://localhost:9003/orm/product/saveProduct -d “id=500006&name=ThinkPad+T220&price=4600&unit=%E4%B8%AA&supplierId=20002&classify=%E5%8A%9E%E5%85%AC%E7%94%A8%E5%93%81&saveMode=1”
特别需要注意的是:目前 OrmController 不包含任何权限校验,因此配置在 Spring 中的 bean 的所有方法都可以被前端调用。所以在实际项目中需要在 OrmController 之前进行一个权限校验,来规范前端可以调用的方法。建议使用服务网关或 filter 进行校验。
OrmController 的流程设计如下:
根据前端参数 bean从 Spring 中获得 Service
根据前端参数 method通过反射获得调用方法
通过反射获得调用方法的第一个参数作为值对象;
通过反射创建值对象,根据反射获得值对象的所有属性,从前端 JSON 中获得对应属性的值,写入值对象;
根据前端 JSON 获得其他参数;
将值对象与其他参数,使用反射调用 Service 中的 method 方法。
单 Dao 的设计
当系统在 Service 中完成了一系列的业务操作,最终要存盘时,都统一调用一个单 Dao。但是在调用单 Dao 之前,每个值对象都应当通过 vObj.xml 进行配置。在该配置中,将每个值对象对应的表,以及值对象中每个属性对应的字段,通过 vObj.xml 配置文件进行对应。那么通用的 BasicDao 就可以通过配置文件形成 SQL并最终完成数据库持久化操作。
vObj.xml 配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
<vo class="com.demo2.customer.entity.Customer" tableName="Customer">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="sex" column="sex"></property>
<property name="birthday" column="birthday"></property>
<property name="identification" column="identification"></property>
<property name="phone_number" column="phone_number"></property>
</vo>
</vobjs>
值对象中可以有很多的属性变量,但只有最终作持久化的属性变量才需要配置。这样可以使值对象的设计具有更大的空间,可以去做更多的数据转换与业务操作。前面提到充血模型的设计,就是需要在值对象中加入更多的操作与转换,使值对象可以长得与数据库的表不一样。但只要配置最后要持久化的属性,就会将这些属性写入到数据库相应的表中,或者从数据库中读取数据。
有了以上的设计,每个 Service 在 Spring 中都是统一注入 BasicDao。
如果要使用 DDD 的功能支持,注入通用仓库 Repository
如果要使用 Redis 缓存,注入 RepositoryWithCache。
Spring 配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<description>The application context for orm</description>
<bean id="customer" class="com.demo2...CustomerServiceImpl">
<property name="dao" ref="basicDao"></property>
</bean>
</beans>
特别需要说明的是,虽然当下注解比较流行,并且有诸多优势,但最大的问题是让业务代码对技术框架形成依赖,违背了技术中台设计的初衷。因此,在这里,虽然 Controller、Dao 以及其他功能设计使用了注解,但基于本框架进行的业务开发,包括 Spring 的配置、MyBatis 的配置、vObj 的配置,建议都采用 XML 文件的形式,而不要采用注解。这样,业务开发中设计的 Service 都是纯洁的,没有任何技术依赖,才能在将来移植到各种技术框架中,并长盛不衰。
这样,单 Dao 的流程设计如下。
1.单 Dao 调用 VObjFactory.getVObj(class) 获得配置信息 vObj。
2.根据 vObj.getTable() 获得对应的表名。
3.for(Property prop : vObj.getPreperties() ) {
通过 prop.getColumn() 获得值对象对应的字段;
运用反射从值对象中获得所有属性及其对应的值;
通过以上参数形成 SQL 语句。
4.通过 SQL 语句执行数据库操作。
查询功能的架构设计
接着,是查询功能的技术中台设计,如图所示:
查询功能的技术中台架构设计
与增删改部分一样的是,查询功能中,每个功能的前端 UI 也是统一调用一个 Controller。但与增删改的部分不一样的是查询功能的前端 UI 传递的参数不同,因此是另一个类 QueryController。
在调用时,首先需要传递的还是 bean。但与增删改不同的是查询功能的 Service 只有一个,那就是 QueryService。但是该 Service 在 Spring 中配置的时候,往 Service 中注入的是不同的 Dao就可以装配成各种不同的 bean。这样前端调用的是不同的 bean最后执行的就是不同的查询。
此外,与增删改不同的是,查询部分不需要传递 method 参数因为每次查询调用的方法都是query()。最后,前端还要以 JSON 的形式传递各种查询参数,就能进行后端查询了。
单 Controller 的设计
在进行查询时,前端输入 HTTP 请求:
http://localhost:9003/query/{bean}
例如:
http://localhost:9003/query/customerQry?gender=male&page=1&size=30
该方法既可以接收 GET 请求,也可以接收 POST 请求。{bean} 是配置在 Spring 中的Service。QueryController 通过该请求,在 Spring 中找到 Service并调用Service.query(map) 进行查询,此处的 map 就是该请求传递的所有查询参数。
为此,查询部分的单 Controller 的流程设计如下:
从前端获得 bean、page、size、count以及查询参数
根据 bean 从 Spring 中获得相应的 Serivce
从前端获得查询参数 JSON将其转换为 Map
执行 service.query(map)
执行完查询后,以不同形式返回给前端。
单 Service 的设计
查询的部分采用了单 Service 的设计,即所有的查询都是配置的 QueryService 进行查询,但注入的是不同的 Dao就可以配置成不同的 bean完成各自不同的查询。为了设计更加简化每个 Dao 可以通过 MyBatis 框架,注入同一个 Dao但配置不同的 Mapper就可以完成不同的查询。因此先配置 MyBatis 的 Mapper 文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.demo2.customer.query.dao.CustomerMapper">
<!--筛选条件-->
<sql id="searchParam">
<if test="id != '' and id != null">
and id = #{id}
</if>
</sql>
<!--求count判断-->
<sql id="isCount1">
<if test="count == null and notCount ==1">
select count(*) from (
</if>
</sql>
<sql id="isCount2">
<if test="count == null and notCount ==1">
) count
</if>
</sql>
<!--是否分页判断-->
<sql id="isPage">
<if test="size != null and size !=''">
limit #{size} offset #{firstRow}
</if>
<if test="size ==null or size ==''">
<if test="pageSize != null and pageSize !=''">
limit #{pageSize} offset #{startNum}
</if>
</if>
</sql>
<select id="query" parameterType="java.util.HashMap" resultType="com.demo2.customer.entity.Customer">
<include refid="isCount1"/>
SELECT * FROM Customer WHERE 1 = 1
<include refid="searchParam"/>
<include refid="isPage"/>
<include refid="isCount2"/>
</select>
</mapper>
然后,将其注入 Spring 中,完成相应的配置,就可以进行查询了:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<description>The application context for query</description>
<bean id="customerQry" class="com.demo2.support.service.impl.QueryServiceImpl">
<property name="queryDao">
<bean class="com.demo2.support.dao.impl.QueryDaoMybatisImpl">
<property name="sqlMapper" value="com.demo2.customer.query.dao.CustomerMapper.query"></property>
</bean>
</property>
</bean>
</beans>
每个查询的 bean 都是配置的 QueryServiceImpl但每个 bean 配置的是不同的 sqlMapper就会执行不同的查询。这里的 sqlMapper 应当与前面 MyBatis 配置中的namespace 相对应。
这样,查询部分的单 Service 流程设计如下:
将查询参数 map、 page、size 传递给 Dao执行查询 dao.query(map)
在查询的前后增加空方法 beforeQuery()、afterQuery() 作为 hook当某业务需要在查询前后进行处理时通过重载子类去实现
判断前端是否传递 count如果有则不再求和否则调用 dao.count() 求和计算“第 x 页,共 y 页”;
将数据打包成 ResultSet 对象返回。
通常,在执行查询时,只需要执行 dao.query(map) 就可以了。由于不同的 bean 注入的 Dao不同因此执行 dao.query(map) 就会执行不同的查询。但是,在某些业务中,需要个性地在查询前进行某些处理,如对查询参数进行某些转换,或者在查询后对查询结果进行某些转换与补填。现在的设计中只有一个 Service如何实现查询前后的这些处理呢
首先,在 QueryService 中增加了 beforeQuery() 和 afterQuery() 两个方法但这两个方法在QueryService 中设计成空方法,什么都没写,因此调用它们就跟没有调用一样。这样的设计叫“钩子 hook”如下代码所示
/**
* do something before query.
* It just a hook that override the function in subclass if we need do something before query.
* @param params the parameters the query need
*/
protected void beforeQuery(Map<String, Object> params) {
//just a hood
}
/**
* do something after query.
* It just a hook that override the function in subclass if we need do something after query.
* @param params the parameters the query need
* @param resultSet the result set after query.
* @return
*/
protected ResultSet afterQuery(Map<String, Object> params, ResultSet resultSet) {
//just a hood
return resultSet;
}
这样,如果不需要在查询前后添加处理,直接配置 QueryService 就行了。在执行查询时,就像没有这两个方法一样。然而,如果需要在查询前或查询后添加某些处理时,则通过继承编写一个 QueryService 的子类,并重写 beforeQuery() 或 afterQuery()。在 Spring 配置时配置的是这个子类,就实现了查询前后的处理。
譬如ProductQuery 这个查询需要在查询后,对查询结果集补填 Supplier。这时通过继承编写一个子类 ProductQueryServiceImpl重写 afterQuery()。
public class ProductQueryServiceImpl extends QueryServiceImpl {
@Autowired
private SupplierService supplierService;
@Override
protected ResultSet afterQuery(Map<String, Object> params,
ResultSet resultSet) {
@SuppressWarnings("unchecked")
List<Product> list = (List<Product>)resultSet.getData();
for(Product product : list) {
String supplierId = product.getSupplierId();
Supplier supplier = supplierService.loadSupplier(supplierId);
product.setSupplier(supplier);
}
resultSet.setData(list);
return resultSet;
}
}
最后,将查询结果以 ResultSet 值对象的形式返回给 ControllerController 再返回给前端。在这个 ResultSet 中:
属性 data 是这一页的查询结果集;
page、size 是分页信息;
count 是记录总数。
通过这 3 个值就可以在前端显示“第 x 页,共 y 页z 条记录”。在第一次查询时,除了查询这一页的数据,还要执行 count。将该 count 记录下来后,在进行分页查询时,就不再需要执行 count从而有效提高查询性能。
属性 aggregate 是一个 map如果该查询在前端展现时需要在表格的最下方对某些字段进行汇总并且这个汇总是对整个查询结果的汇总而不是这一页的汇总则将该字段作为 Key 值写入 aggregate 中Value 是汇总的方式,如 count、sum、max 等。通过这样的设置,就可以在查询结果集的最后一行返回一个汇总记录。
通过以上技术中台的设计,各查询功能的编码就会极大地简化。具体来说,设计一个普通的查询,只需要制作一个 MyBatis 的查询语句配置,在 Spring 配置中制作一个 bean。然后就可以通过前端进行查询了甚至都不需要编写任何 class。只有在查询前后添加操作时才需要自己制作一个子类。
此外,对于进行查询结果集的补填,也可以使用通用程序 AutofillQueryServiceImpl在下一讲“如何设计支持领域驱动的技术中台”中会详细讲解。
总结
本讲讲解了一个强大而落地的技术中台设计实践。通过该技术中台的封装:
在增删改操作时只需编写前端界面、Service 与值对象就可以了,更多技术细节被封装起来了,这样,开发人员就可以专心地将领域模型转换成业务代码的设计实现,并随着领域模型的变更而变更代码,不断满足用户需求;
在查询操作时,在大多数情况下只需要编写 MyBatis 与 Spring 的配置就可以完成查询功能的编写,开发工作量大大降低,同时变更也变得轻松快捷。
以上技术中台的设计是普通技术中台的设计,那么支持 DDD 的技术中台又该如何设计呢?是不是可以编写通用的仓库与工厂呢?下一讲将为你讲解。
点击 GitHub 链接,查看源码。

View File

@ -0,0 +1,376 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14 如何设计支持 DDD 的技术中台?
DDD 要落地实践,最大的“坑”就是支持 DDD 的技术架构如何设计。很多团队在工作开展前期,一切都很顺利:通过对业务需求的理解,建立领域模型;将领域模型通过一系列的设计,落实程序设计,准确地说是程序设计中业务领域层的设计。然而就在编码实现的时候,出现了各种问题:
要么是不能准确掌握 DDD 的分层架构;
要么是把程序写得非常乱,频繁地在各种 TDO、DO、PO 之间进行数据转换,耗费大量代码,使得日后变更异常困难。
因此,还需要有一个强有力的技术中台的支持,来简化 DDD 的设计实现解决“最后一公里”的问题。唯有这样DDD 才能在项目中真正落地。
传统 DDD 的架构设计
通常,在支持领域驱动的软件项目中,架构设计如上图所示。
展现层是前端的 UI它通过网络与后台的应用层交互。
应用层类似于 MVC 层,主要用于前后端交互,在接收用户请求后,会去调用领域层的服务,也就是 Service。
在领域层中,用户请求首先由 Service 接收,然后在执行业务操作的过程中,使用领域对象作为参数(贫血模型的实现),或者去调用领域对象中的相应方法(充血模型的实现)。在领域对象的设计上,可以是实体,也可以是值对象,也可以将它们制作成一个聚合(如果多个领域对象间存在整体与部分的关系)。
最后,通过仓库将领域对象中的数据持久化到数据库;使用工厂将数据从数据库中读取、拼装并还原成领域对象。
这些都是将领域驱动落地到软件设计时所采用的方式。从架构分层上说DDD 的仓库和工厂的设计介于业务领域层与基础设施层之间即接口在业务领域层而实现在基础设施层。DDD 的基础设施层相当于支撑 DDD 的基础技术架构,通过各种技术框架支持软件系统完成除了领域驱动以外的各种功能。
然而,传统的软件系统采用 DDD 进行架构设计时,需要在各个层次之间进行各种数据结构的转换:
首先,前端的数据结构是 JSON传递到后台数据接入层时需要将其转换为数据传输对象DTO
然后应用层去调用领域层时,需要将 DTO 转换为领域对象 DO
最后,将数据持久化到数据库时,又要将 DO 转换为持久化对象 PO。
在这个过程中,需要编写大量代码进行数据的转换,无疑将加大软件开发的工作量与日后变更的维护成本。因此,我们可不可以考虑上一讲所提到的设计,将各个层次的数据结构统一起来呢?
另外,传统的软件系统在采用 DDD 进行架构设计时,需要为每一个功能模块编写各自的仓库与工厂,如订单模块有订单仓库与订单工厂、库存模块有库存仓库与库存工厂。各个模块在编写仓库与工厂时,虽然实现了各自不同的业务,却形成了大量重复的代码。这样的问题与前面探讨的 Dao 的问题一样,是否可以通过配置与建模,设计成一个统一的仓库与工厂。如果是这样,那么仓库与工厂又与 Dao 是什么关系呢?基于对以上问题的思考,我提出了统一数据建模、内置聚合的实现、通用仓库和工厂,来简化 DDD 业务开发。因此,进行了如下的架构设计。
通用仓库与通用工厂的设计
该设计与上一讲的架构设计相比,差别仅是将单 Dao 替换为了通用仓库与通用工厂。也就是说,与 Dao 相比DDD 的仓库就是在 Dao 的基础上扩展了一些新的功能。
例如在装载或查询订单时,不仅要查询订单表,还要补填与订单相关的订单明细与客户信息、商品信息,并装配成一个订单对象。在这个过程中,查询订单是 Dao 的功能,但其他类似补填、装配等操作,则是仓库在 Dao 基础上进行的功能扩展。
同样,在保存订单时,不仅要保存订单表,还要保存订单明细表,并将它们放到同一个事务中。保存订单表是 Dao 原有的功能,保存订单明细表并添加事务,则是仓库在 Dao 基础上进行的功能扩展。
这就是 DDD 的仓库与 Dao 的关系。
基于这种扩展关系,该如何设计这个通用仓库呢?如果熟悉设计模式,则会想到“装饰者模式”。“装饰者模式”的目的,就是在原有功能的基础上进行“透明功能扩展”。这种“透明功能扩展”,既可以扩展原有功能,又不影响原有的客户程序,使客户程序不用修改任何代码就能实现新功能,从而降低变更的维护成本。因此,将“通用仓库”设计成了这样。
即在原有的 BasicDao 与 BasicDaoImpl 的基础上,增加了通用仓库 Repository。将 Repository 设计成装饰者,它也是接口 BasicDao 的实现类,是通过一个属性变量引用的 BasicDao。使用时在 BasicDaoImpl 的基础上包一个 Repository就可以扩展出那些 DDD 的功能。因此,所有的 Service 在注入 Dao 的时候:
如果不使用 DDD则像以前一样注入BasicDaoImpl
如果需要使用 DDD则注入 Repository。
配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<description>The application context for orm</description>
<bean id="basicDao" class="com...impl.BasicDaoJdbcImpl"></bean>
<bean id="redisCache" class="com...cache.RedisCache"></bean>
<bean id="repository" class="com...RepositoryWithCache">
<property name="dao" ref="basicDao"></property>
<property name="cache" ref="redisCache"></property>
</bean>
<bean id="product" class="com.demo2...impl.ProductServiceImpl">
<property name="dao" ref="repository"></property>
</bean>
<bean id="supplier" class="com.demo2...impl.SupplierServiceImpl">
<property name="dao" ref="basicDao"></property>
</bean>
<bean id="productQry" class="com.demo2...AutofillQueryServiceImpl">
<property name="queryDao">
<bean class="com.demo2.support.dao.impl.QueryDaoMybatisImpl">
<property name="sqlMapper" value="com.demo2...dao.ProductMapper.query"></property>
</bean>
</property>
<property name="dao" ref="basicDao"></property>
</bean>
</beans>
在这一配置中可以看到Repository 中有一个属性 Dao 配置的是 BasicDao。这样当 Repository 访问数据库时,通过 BasicDao 进行访问。同时这里实现了两个通用仓库Repository 与 RepositoryWithCache。如果配置后者则可以实现缓存的功能。
在以上示例中Product 将 Dao 配置为 Repository。这样Product 在通过 ID 装载时,就会在产品对象中加载与其关联的供应商 Supplier。同时productQry 将 queryDao 配置为 AutofillQueryServiceImpl则在查询产品信息以后会自动补填与其关联的供应商 Supplier。
这里,通用仓库是如何指导 Product 关联 Supplier 的呢?关键就在于文件 vObj.xml 进行了以下配置:
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
<vo class="com.demo2.trade.entity.Product" tableName="Product">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="price" column="price"></property>
<property name="unit" column="unit"></property>
<property name="classify" column="classify"></property>
<property name="supplier_id" column="supplier_id"></property>
<join name="supplier" joinKey="supplier_id" joinType="manyToOne" class="com.demo2.trade.entity.Supplier"></join>
</vo>
<vo class="com.demo2.trade.entity.Supplier" tableName="Supplier">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
</vo>
</vobjs>
在 Product 中增加了join 标签,标注领域对象间的关联关系。其中 joinKey=“supplier_id”代表在 Product 对象中的属性 supplier_id 用于与 Supplier 的 key 值关联。joinType 代表关联类型,支持 oneToOne、manyToOne、oneToMany 三种类型的关联,但基于性能的考虑,不支持 manyToMany。当类型是 oneToMany 时,补填的是一个集合,因此领域对象中也应当是一个集合属性,例如 Customer 中有一个 Address 是 oneToMany因此领域对象设计成这样
/**
* The customer entity
* @author fangang
*/
public class Customer extends Entity<Long> {
......
private List<Address> addresses;
/**
* @return the addresses
*/
public List<Address> getAddresses() {
return addresses;
}
/**
* @param addresses the addresses to set
*/
public void setAddresses(List<Address> addresses) {
this.addresses = addresses;
}
}
因此,在 vObj.xml 中进行如下配置:
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
<vo class="com.demo2.customer.entity.Customer" tableName="Customer">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="sex" column="sex"></property>
<property name="birthday" column="birthday"></property>
<property name="identification" column="identification"></property>
<property name="phone_number" column="phone_number"></property>
<join name="addresses" joinKey="customer_id" joinType="oneToMany" isAggregation="true" class="com.demo2.customer.entity.Address"></join>
</vo>
<vo class="com.demo2.customer.entity.Address" tableName="Address">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="customer_id" column="customer_id"></property>
<property name="country" column="country"></property>
<property name="province" column="province"></property>
<property name="city" column="city"></property>
<property name="zone" column="zone"></property>
<property name="address" column="address"></property>
<property name="phone_number" column="phone_number"></property>
</vo>
</vobjs>
这样,在装载和查询 Customer 时,内置就将它关联的 Address 也加载出来了。在加载中,通过 Dao 去数据库中查询数据,然后将查询到的 Customer 与多个 Address 交给通用工厂去装配。如果配置的是 RepositoryWithCache则加载 Customer 时会先检查缓存中有没有该客户。如果没有则到数据库中查询。
内置聚合功能
聚合是领域驱动设计中一个非常重要的概念它代表在真实世界中的整体与部分的关系。比如Order订单与 OrderItem订单明细就是一个整体与部分的关系。当加载一个订单时应当同时加载其订单明细而保存订单时应当同时保存订单与订单明细并放在同一事务中。在设计支持领域驱动的技术中台时应当简化聚合的设计与实现让业务开发人员不必每次都编写大量代码而是通过一个配置就可以完成聚合的实现。
例如,订单与订单明细存在聚合关系,则在 vObj.xml 中建模时,通过 join 标签关联它们,并置 join 标签的 isAggregation=true。这样在查询或装载订单的同时装载它的所有订单明细而在保存订单时保存订单明细并将它们置于同一事务中。具体配置如下
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
<vo class="com.demo2.order.entity.Customer" tableName="Customer">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="sex" column="sex"></property>
<property name="birthday" column="birthday"></property>
<property name="identification" column="identification"></property>
<property name="phone_number" column="phone_number"></property>
<join name="addresses" joinKey="customer_id" joinType="oneToMany" isAggregation="true" class="com.demo2.order.entity.Address"></join>
</vo>
<vo class="com.demo2.order.entity.Address" tableName="Address">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="customer_id" column="customer_id"></property>
<property name="country" column="country"></property>
<property name="province" column="province"></property>
<property name="city" column="city"></property>
<property name="zone" column="zone"></property>
<property name="address" column="address"></property>
<property name="phone_number" column="phone_number"></property>
</vo>
<vo class="com.demo2.order.entity.Product" tableName="Product">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="price" column="price"></property>
<property name="unit" column="unit"></property>
<property name="classify" column="classify"></property>
<property name="supplier_id" column="supplier_id"></property>
</vo>
<vo class="com.demo2.order.entity.Order" tableName="Order">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="customer_id" column="customer_id"></property>
<property name="address_id" column="address_id"></property>
<property name="amount" column="amount"></property>
<property name="order_time" column="order_time"></property>
<property name="flag" column="flag"></property>
<join name="customer" joinKey="customer_id" joinType="manyToOne" class="com.demo2.order.entity.Customer"></join>
<join name="address" joinKey="address_id" joinType="manyToOne" class="com.demo2.order.entity.Address"></join>
<join name="orderItems" joinKey="order_id" joinType="oneToMany" isAggregation="true" class="com.demo2.order.entity.OrderItem"></join>
</vo>
<vo class="com.demo2.order.entity.OrderItem" tableName="OrderItem">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="order_id" column="order_id"></property>
<property name="product_id" column="product_id"></property>
<property name="quantity" column="quantity"></property>
<property name="price" column="price"></property>
<property name="amount" column="amount"></property>
<join name="product" joinKey="product_id" joinType="manyToOne" class="com.demo2.order.entity.Product"></join>
</vo>
</vobjs>
在该配置中可以看到,订单不仅与订单明细关联,还与客户、客户地址等信息关联。但是,订单与客户、客户地址等信息不存在聚合关系,当保存订单时不需要保存或更改这些信息。只有订单明细与订单具有聚合关系,在订单中配置订单明细的 join 标签时才需要增加isAggregation=true。这样当保存订单时同时也保存订单明细并将它们放到同一事务中。通过这样的设计既简化了聚合的实现又使得聚合实现在底层技术中台中与业务代码无关。因此系统可以通过底层不断优化对聚合的设计实现使变更成本更低。
总结
本讲通过一个支持 DDD 的技术中台,将许多 DDD 繁杂的设计实现,做成通用的仓库与工厂,封装在了底层的技术中台中。这样,业务开发人员就可以更加专注于领域建模,将模型按照一定的规范进行配置,来完成基于 DDD 的设计开发。而底层的技术中台就可以根据这些配置,完成相应的数据持久化与查询装载了。
同时,以上设计简化了系统设计,不再需要将数据在 JSON、TDO、DO、PO 中进行转换,而是通过规范,将 JSON 与 DO 设计一致,将 DO 与数据库进行配置,就可以完成开发。代码减少了,日后的维护与变更也变得容易了。
另外,有同学问了一个有趣的问题:我在查询订单的时候本来不想加载订单明细,而加载了订单明细,是不是会影响性能。答案是肯定的,所以说未来在面对高并发时,应当采用富客户端以减少前后交互次数。因此,在设计上应当尽量多加载一些数据到前端,使更多操作直接在前端进行。这样就有效减少了交互次数,降低了系统压力。
下一讲将进一步探讨支持 DDD 的微服务,技术中台该如何设计。
点击 GitHub 链接,查看源码。

View File

@ -0,0 +1,256 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15 如何设计支持微服务的技术中台?
有了技术中台对领域驱动的支持,那如何应用于微服务架构呢?支持 DDD 与微服务的技术中台应当具备以下几个方面的能力:
解决当前微服务架构的技术不确定性,使得微服务项目可以用更低的成本应对日后技术架构的更迭;
可以更加容易地将领域驱动设计应用到微服务架构中,包括:领域建模、限界上下文的微服务拆分、事件通知机制等;
需要解决领域对象在仓库与工厂进行装配时,如何将本地查询替换为远程接口调用,实现微服务间数据装配的问题。
解决技术不确定性的问题
如今的微服务架构,基本已经形成了 Spring Cloud 一统天下的局势。然而,在 Spring Cloud 框架下的各种技术组件依然存在诸多不确定性,如:注册中心是否采用 Eureka、服务网关是采用 Zuul 还是 Gateway等等。同时服务网格 Service Mesh 方兴未艾,不排除今后所有的微服务都要切换到 Service Mesh 的可能。在这种情况下如何决策微服务的技术架构?代码尽量不要与 Spring Cloud 耦合,才能在将来更容易地切换到 Service Mesh。那么具体又该如何做到呢
单 Controller、单 Dao 的设计在微服务架构的应用
如上图所示,当前端通过服务网关访问微服务时,首先要访问聚合层的微服务。这时,在聚合层的微服务中,采用单 Controller 接收前端请求。这样,只有该 Controller 与 MVC 框架耦合,后面所有的 Service 不会耦合,从而实现了业务代码与技术框架的分离。
同样的,当 Service 执行各种操作调用原子服务层的微服务时,不是通过 Ribbon 进行远程调用,而是将原子服务层的微服务开放的接口,在聚合层微服务的本地编写一个 Feign 接口。那么,聚合层微服务在调用原子微服务时,实际调用的是自己本地的接口,再由这个接口通过加载 Feign 注解,去实现远程调用。
这样,聚合层微服务中的各个 Service 就不会与 Spring Cloud 各个组件发生任何耦合,只有那些 Feign 接口与 Spring Cloud 耦合去实现远程调用,让业务代码与技术框架实现了解耦。
同样的道理,原子服务层的微服务在对外开放接口时,不是由各个 Service 直接开放 API 接口。因为如果让 Service 直接开放 API 接口,就需要编写相关注解,使得 Service 与 Spring Cloud 耦合。因此,由统一的 Controller 开放接口,再由它去调用内部的 Service。这样所有的 Service 就是纯洁的,不与 Spring Cloud 技术框架耦合,只有 Controller 与其耦合。
有了以上这些设计,当未来需要从 Spring Cloud 框架迁移到 Service Mesh 时,只需要将那些纯洁的、不与 Spring Cloud 耦合的 Service与领域对象中的业务代码迁移到新的框架中就可以以非常低的成本在各种技术框架中自由地切换从而快速跟上技术发展的步伐。通过这种方式就能很好地应对未来的技术不确定性问题更好地开展架构演化。
支持微服务远程调用的架构设计
此外,微服务架构设计最大的难题是微服务的合理拆分,拆分要体现单一职责原则,即微服务间低耦合,微服务内高内聚。那么,在软件项目中如何做到这些呢?业界最佳的实践无疑是基于领域的设计,即先按照领域业务建模,然后基于限界上下文进行微服务拆分。这样设计出来的微服务系统,都可以保证每次变更落到某个微服务上。微服务变更完了,自己独立升级,就能达到降低维护成本、快速交付的目的。
基于这样的思路,每个微服务在设计时,都采用支持领域驱动的技术中台。这样,每个微服务都是基于领域驱动建模和设计,然后在该技术中台中编码实现,既提高了开发速度,又降低了维护成本。
然而,转型为微服务后,有一个技术难题亟待解决,那就是跨库的数据操作。当一个单体应用拆分成多个微服务后,不仅应用程序需要拆分,数据库也需要拆分。譬如,经过微服务拆分,订单有订单数据库,用户有用户数据库。这时,当查询订单,需要补填其对应的用户信息时,就不能从自己本地的数据库中查询了,而是调用“用户”微服务的远程接口,在用户数据库中查询,然后返回给“订单”微服务。这时,原有的技术中台就必须做出调整。
如何调整呢?通用 DDD 仓库在执行查询或者装载操作时,查询完订单补填用户信息,不是通过 DAO 去查询本地数据库,而是改成调用远程接口,去调用用户微服务。这时,可以先在订单微服务的本地编写一个用户 Service 的 Feign 接口,订单仓库与工厂调用这个接口就可以了。然后通过 Feign 接口实现对用户微服务的远程调用。
采用 Feign 接口实现远程调用
每个微服务都是一个独立的进程,运行在各自独立的 JVM甚至不同的物理节点上通过网络访问。因此微服务与微服务之间的调用必然是远程调用。以往我们对微服务间的调用采用 Ribbon 的方式,在程序中的任意一个位置,只要注入一个 restTemplate就可以进行远程调用。
这样的代码过于随意,会越来越难于阅读与变更维护。比如,原来某个微服务中有两个模块 A 与 B都需要调用模块 C。随着业务变得越来越复杂需要进行微服务拆分将模块 C 拆分到了另外一个微服务中。这时,原来的模块 A 与 B 就不能像原来一样调用模块 C否则就会报错。
Ribbon 的远程调用方式
如何解决以上问题呢?需要同时改造模块 A 与 B分别加入 restTemplate 实现远程调用,来调用模块 C。也就是说这时所有调用模块 C 的程序都需要改造,改造的成本与风险就会比较高。
因此在实现微服务间调用时我们通常会采用另外一个方案Feign。Feign 不是另起炉灶,而是对 Ribbon 的封装,目的是使代码更加规范、变更更加易于维护。采用的方案是,不修改模块 A 与 B 的任何代码,而是在该微服务的本地再制作一个模块 C 的接口 C。该接口与模块 C 一模一样,拥有模块 C 的所有方法,因此模块 A 与 B 还可以像以前一样在本地调用接口 C。但接口 C 只是一个接口,什么都做不了,因此需要通过添加 Feign 的注解,实现远程调用,去调用模块 C。这个方案既没有修改模块 A 与 B又没有修改模块 C而仅仅添加了一个接口 C使维护成本降到了最低。
Feign 的远程调用方式
如何通过 Feign 实现微服务的远程调用呢?
首先,创建项目时,在 POM.xml 文件中添加 Eureka Client、Hystrix 与 Actuator 等组件以外,将 ribbon 改为 feign
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 断路器 hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!-- 断路器监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
接着,在启动类 FeignApplication 中,不仅添加 Discovery Client还要添加 Feign Client
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author fangang
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableHystrix
public class FeignApplication {
/**
* @param args
*/
public static void main(String[] args) {
SpringApplication.run(FeignApplication.class, args);
}
}
用 Feign 实现调用时,首先在消费者这端编写一个与生产者开放的 API 一模一样的接口,然后添加 Feign 的注解:
/**
* The service of suppliers.
* @author fangang
*/
@FeignClient(value="service-supplier", fallback=SupplierHystrixImpl.class)
public interface SupplierService {
/**
* @param id
* @return the supplier
*/
@RequestMapping(value = "orm/supplier/loadSupplier", method = RequestMethod.GET)
public Supplier loadSupplier(@RequestParam("id")Long id);
/**
* @param ids
* @return the list of suppliers
*/
@PostMapping("orm/supplier/loadSuppliers")
public List<Supplier> loadSuppliers(@RequestParam("ids")List<Long> ids);
/**
* @return the list of suppliers
*/
@GetMapping("orm/supplier/listOfSuppliers")
public List<Supplier> listOfSuppliers();
}
在这一代码例子中,具体的流程是这样的:
在生产者那一端有个 SupplierService 的类因此首先在消费者这端编写一个一模一样的SupplierService 的接口;
接着,在 interface 前面添加注解 @FeignClient
这里的 value 为生产者在 Eureka 注册中心中注册的名称;
在每个方法前添加注解GET 请求就写 @GetMappingPOST 请求就写 @PostMapping,名称就是生产者那端开放的接口名称;
然后,如果需要将参数加入 url 中,就在参数前添加注解 @RequestParam
有了以上注解Feign 就可以从接口中取出相应的数据,拼装成 url最后去执行 ribbon 实现微服务远程调用。
采用 Ref 标签调用 Feign 接口
采用 Feign 实现微服务间的远程调用以后,在 vObj.xml 中进行建模时也需要做出改变将join 标签改为 ref 标签。其配置如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
<vo class="com.demo2.product.entity.Product" tableName="Product">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="price" column="price"></property>
<property name="unit" column="unit"></property>
<property name="classify" column="classify"></property>
<property name="supplier_id" column="supplier_id"></property>
<ref name="supplier" refKey="supplier_id" refType="manyToOne" bean="com.demo2.product.service.SupplierService" method="loadSupplier" listMethod="loadSuppliers"></ref>
</vo>
<vo class="com.demo2.product.entity.Supplier" tableName="Supplier">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
</vo>
</vobjs>
在这一配置中,将 supplier 由 join 标签改为 ref 标签,其中:
bean代表在“产品”微服务本地调用“供应商”微服务的 Feign 接口;
method是指定要调用这个 Feign 接口的方法;
而listMethod是在批量查询“产品”数据集时进行批量补填的优化措施。
通过这样的配置,在查询产品过程中,通用仓库补填供应商信息,就不会去调用本地的 DAO而是调用 SupplierService 的 Feign 接口,由它实现对“供应商”微服务的远程调用,从而实现跨微服务的数据装配。
总结
本讲提出了支持 DDD + 微服务架构的技术中台设计思想。通过以上的设计,既实现了业务代码与技术框架解耦的整洁架构思想,使得系统在日后更易于架构演化,又实现了领域模型在微服务间的数据装配,解决了 DDD 转型微服务架构的关键技术难题。开发团队有了这样的技术中台,就能将 DDD 运用起来,在项目中真正的落地实践。
下一讲将在 GitHub 上分享代码,进一步讲解这个技术中台的代码设计与项目实践。
点击 GitHub 链接,查看源码。

View File

@ -0,0 +1,378 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16 基于 DDD 的代码设计演示(含 DDD 的技术中台设计)
我这些年的从业经历,起初是作为项目经理带团队做软件研发,后来转型成为架构师,站在更高的层面去思考软件研发的那些事儿。我认为,一个成熟的软件研发团队:
不仅在于团队成员研发水平的提高;
更在于将不断积累的通用的设计方法与技术框架,沉淀到底层的技术中台中。
只要有了这样的技术中台作为支撑,才能让研发团队具备更强的能力,用更快的速度,研发出更多的产业,以快速适应激烈竞争而快速变化的市场。
譬如,团队某次接到了一个数据推送的需求,在完成了该需求并交付用户以后,就在这个功能设计的基础上,抽取共性、保留个性,将其下沉到技术中台形成“数据共享平台”的设计。有了这个功能,团队日后在接到类似需求时,只需要进行一些配置或者简单开发,就能交付用户啦。
这样,团队的研发能力就大大提升了。团队研发的功能越多,沉淀到技术中台的功能就越多,团队研发能力的提升就越大。只有这样的技术中台才能支撑研发团队的快速交付,关键是要有人、有意识地去做这些工作的整理,而我们团队是在“使能故事”中完成这些工作的。
现如今,越来越多的团队采用敏捷开发,在 2~3 周的迭代周期中规划并完成“用户故事”。“用户故事”是需要紧急应对的用户需求,但如果不能提升团队的能力,那么团队就会像救火队员一样永远是在应对用户需求的“火”而疲于奔命。
相反“使能故事Enabler Story”就是为了提升我们的能力从而更快速地应对用户需求。俗话说“磨刀不误砍柴工”“使能故事”就是“磨刀”它虽然要耗费一些时间但可以让日后的“砍柴”更快更好是很值得的。
因此,一个成熟的团队在每次的迭代中不能只是完成“用户故事”,而应该拿出一定比例的时间完成“使能故事”,使团队日后的“用户故事”做得更快,实现快速交付。
我的支持 DDD + 微服务的技术中台就是在这种指导下逐渐形成的。之前在我的团队实践 DDD + 微服务的过程中,遇到了很多的阻力。这种阻力要求团队成员花更多的时间学习 DDD 相关知识,用正确的方法与步骤去设计开发,并做到位。然而,当他们真正做到位以后,却发现 DDD 的设计开发非常烦琐,要频繁地实现各种工厂、仓库、数据补填等开发工作,使开发人员对 DDD 的开发心生厌恶。以往项目经理在面对这些问题时,只能从管理上制定开发规范,但这样的措施于事无补。
而我站在架构师的角度,去设计技术框架,在原有代码的基础上,抽取共性、保留个性,将烦琐的 DDD 开发封装在了技术中台中。这样做,不仅简化了设计开发,使得 DDD 更容易在项目中落地,还规范了代码,使得业务开发人员没有机会去编写 Controller 与 Dao 代码,自然而然地将业务代码基于领域模型设计在了 Service 与领域对象中了。接着,来看看这个框架的设计。
整个演示代码的架构
我把整个演示代码分享在了 GitHub 中,它分为这样几个项目。
demo-ddd-trade一个基于 DDD 设计的单体应用。
demo-parent本示例所有微服务项目的父项目。
demo-service-eureka微服务注册中心 eureka。
demo-service-config微服务配置中心 config。
demo-service-turbine各微服务断路器监控 turbine。
demo-service-zuul服务网关 zuul。
demo-service-parent各业务微服务无数据库访问的父项目。
demo-service-support各业务微服务无数据库访问底层技术框架。
demo-service-customer用户管理微服务无数据库访问
demo-service-product产品管理微服务无数据库访问
demo-service-supplier供应商管理微服务无数据库访问
demo-service2-parent各业务微服务有数据库访问的父项目。
demo-service2-support各业务微服务有数据库访问底层技术框架。
demo-service2-customer用户管理微服务有数据库访问
demo-service2-product产品管理微服务有数据库访问
demo-service2-supplier供应商管理微服务有数据库访问
demo-service2-order订单管理微服务有数据库访问
总之,这里有一个基于 DDD 的单体应用与一个完整的微服务应用。在微服务应用中:
demo-service-xxx 是我基于一个早期的框架设计的,你可以看到我们以往设计开发的原始状态;
而 demo-service2-xxx 是我需要重点讲解的基于 DDD 的微服务设计。
其中demo-service2-support 是这个框架的核心,即底层技术中台,而其他都是演示对它的具体应用。
单 Controller 的设计实现
与以往不同,在整个系统中只有几个 Controller并下沉到了底层技术中台 demo-service2-support 中,它们包括以下几部分。
OrmController用于增删改操作以及基于 key 值的 load、get 操作它们通常基于DDD 进行设计。
QueryController用于基于 SQL 语句形成的查询分析报表,它们通常不基于 DDD 进行设计,但查询结果会形成领域对象,并基于 DDD 进行数据补填。
其他 Controller用于如 ExcelController 等特殊的操作,是继承以上两个类的功能扩展。
OrmController 接收诸如 orm/{bean}/{method} 的请求bean 是配置在 Spring 中的 beanmethod 是 bean 中要调用的方法。由于这是一个基础框架,没有限定前端可以调用哪些方法,因此实际项目需要在此之上增加权限校验。该方法既可以接收 GET 方法,也可以接收 POST 方法,因此其他的参数可以根据 GET/POST 各自的方式进行传递。
这里的 bean 对应的是后台的 Service。Service 的编写要求所有的方法,如果需要使用领域对象必须放在第一个参数上。如果第一个参数是简单的数字、字符串、日期等类型,就不是领域对象,否则就作为领域对象,依次从前端上传的 JSON 中获取相应的数据予以填充。这里暂时不支持集合,也不支持具有继承关系的领域对象,待我日后完善。判定代码如下:
/**
* check a parameter whether is a value object.
* @param clazz
* @return yes or no
* @throws IllegalAccessException
* @throws InstantiationException
*/
private boolean isValueObject(Class<?> clazz) {
if(clazz==null) return false;
if(clazz.equals(long.class)||clazz.equals(int.class)||
clazz.equals(double.class)||clazz.equals(float.class)||
clazz.equals(short.class)) return false;
if(clazz.isInterface()) return false;
if(Number.class.isAssignableFrom(clazz)) return false;
if(String.class.isAssignableFrom(clazz)) return false;
if(Date.class.isAssignableFrom(clazz)) return false;
if(Collection.class.isAssignableFrom(clazz)) return false;
return true;
}
这里的开发规范除了要求 Service 的所有方法中的领域对象放第一个参数,还要求前端的 JSON 与领域对象中的属性一致,这样才能完成自动转换,而不需要为每个模块编写 Controller。
QueryController 接收诸如 query/{bean} 的请求,这里的 bean 依然是 Spring 中配置的bean。同样该方法也是既可以接收 GET 方法,也可以接收 POST 方法,并用各自的方式传递查询所需的参数。
如果该查询需要分页,那么在传递查询参数以外,还要传递 page第几页与 size每页多少条记录。第一次查询时除了分页还会计算 count 并返回前端。这样,在下次分页查询时,将 count 也作为参数传递,将不再计算 count从而提升查询效率。此外这里还将提供求和功能敬请期待。
单 Dao 的设计实现
以往系统设计的硬伤在于一头一尾Controller 与 Dao。它既要为每个模块编写大量代码也使得系统设计非常不 DDD令日后的变更维护成本巨大。因此我在大量系统设计问题分析的基础上提出了单 Controller 与单 Dao 的设计思路。前面讲解了单 Controller 的设计,现在来看一看单 Dao 的设计。
诚然,当今的主流是使用注解。然而,注解的使用存在诸多的问题。
首先,它会带来业务代码与技术框架的依赖,因此当在 Service 中加入注解时,就不得不与 Spring、Springcloud 耦合,使得日后转型其他技术框架困难重重。
此外,注解往往适用于一对一、多对一的场景,而一对多、多对多的场景往往非常麻烦。而本框架存在大量一对多、多对多的场景,因此我建议你还是回归到 XML 的配置方式。
在项目中的所有 Service 都要有一个 BasicDao 的属性变量,例如:
public class CustomerServiceImpl implements CustomerService {
private BasicDao dao;
/**
* @return the dao
*/
public BasicDao getDao() {
return dao;
}
/**
* @param dao the dao to set
*/
public void setDao(BasicDao dao) {
this.dao = dao;
}
...
}
接着,在 applicationContext-orm.xml 中,配置业务操作的 Service
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<description>The application context for orm</description>
<bean id="customer" class="com.demo2.trade.service.impl.CustomerServiceImpl">
<property name="dao" ref="repositoryWithCache"></property>
</bean>
<bean id="product" class="com.demo2.trade.service.impl.ProductServiceImpl">
<property name="dao" ref="repositoryWithCache"></property>
</bean>
<bean id="supplier" class="com.demo2.trade.service.impl.SupplierServiceImpl">
<property name="dao" ref="basicDao"></property>
</bean>
<bean id="order" class="com.demo2.trade.service.impl.OrderServiceImpl">
<property name="dao" ref="repository"></property>
</bean>
</beans>
这里可以看到,每个 Service 都要注入 Dao但可以根据需求注入不同的 Dao。
如果该 Service 是纯贫血模型,那么注入 BasicDao 就可以了。
如果采用了充血模型,包含了一些聚合的操作,那么注入 repository 从而实现仓库与工厂的功能。
但如果还希望该仓库与工厂能提供缓存的功能,那么就注入 repositoryWithCache。
例如,在以上案例中:
SupplierService 实现的是非常简单的功能,注入 BasicDao 就可以了;
OrderService 实现了订单与明细的聚合,但数据量大不适合使用缓存,所以注入 repository
CustomerService 实现了用户与地址的聚合,并且需要缓存,所以注入 repositoryWithCache
ProductService 虽然没有聚合但在查询产品时需要补填供应商因此也注入repositoryWithCache。
这里需要注意,是否使用缓存,也可以在日后的运维过程中,让运维人员通过修改配置去决定,从而提高系统的可维护性。
完成配置以后,核心是将领域建模映射成程序设计的模型。开发人员首先编写各个领域对象。譬如,产品要关联供应商,那么在增加 supplier_id 的同时,还要增加一个 Supplier 的属性:
public class Product extends Entity<Long> {
private static final long serialVersionUID = 7149822235159719740L;
private Long id;
private String name;
private Double price;
private String unit;
private Long supplier_id;
private String classify;
private Supplier supplier;
...
}
注意,在本框架中的每个领域对象都必须要实现 Entity 这个接口,系统才知道你的主键是哪个。
接着,配置 vObj.xml将领域对象与数据库对应起来
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
<vo class="com.demo2.trade.entity.Customer" tableName="Customer">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="sex" column="sex"></property>
<property name="birthday" column="birthday"></property>
<property name="identification" column="identification"></property>
<property name="phone_number" column="phone_number"></property>
<join name="addresses" joinKey="customer_id" joinType="oneToMany" isAggregation="true" class="com.demo2.trade.entity.Address"></join>
</vo>
<vo class="com.demo2.trade.entity.Address" tableName="Address">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="customer_id" column="customer_id"></property>
<property name="country" column="country"></property>
<property name="province" column="province"></property>
<property name="city" column="city"></property>
<property name="zone" column="zone"></property>
<property name="address" column="address"></property>
<property name="phone_number" column="phone_number"></property>
</vo>
<vo class="com.demo2.trade.entity.Product" tableName="Product">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="price" column="price"></property>
<property name="unit" column="unit"></property>
<property name="classify" column="classify"></property>
<property name="supplier_id" column="supplier_id"></property>
<join name="supplier" joinKey="supplier_id" joinType="manyToOne" class="com.demo2.trade.entity.Supplier"></join>
</vo>
<vo class="com.demo2.trade.entity.Supplier" tableName="Supplier">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
</vo>
<vo class="com.demo2.trade.entity.Order" tableName="Order">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="customer_id" column="customer_id"></property>
<property name="address_id" column="address_id"></property>
<property name="amount" column="amount"></property>
<property name="order_time" column="order_time"></property>
<property name="flag" column="flag"></property>
<join name="customer" joinKey="customer_id" joinType="manyToOne" class="com.demo2.trade.entity.Customer"></join>
<join name="address" joinKey="address_id" joinType="manyToOne" class="com.demo2.trade.entity.Address"></join>
<join name="orderItems" joinKey="order_id" joinType="oneToMany" isAggregation="true" class="com.demo2.trade.entity.OrderItem"></join>
</vo>
<vo class="com.demo2.trade.entity.OrderItem" tableName="OrderItem">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="order_id" column="order_id"></property>
<property name="product_id" column="product_id"></property>
<property name="quantity" column="quantity"></property>
<property name="price" column="price"></property>
<property name="amount" column="amount"></property>
<join name="product" joinKey="product_id" joinType="manyToOne" class="com.demo2.trade.entity.Product"></join>
</vo>
</vobjs>
注意,在这里,所有用到 join 或 ref 标签的领域对象,其 Service 都必须使用 repository 或repositoryWithCache以实现数据的自动补填或者有聚合的地方实现聚合的操作而注入 BasicDao 是无法实现这些操作的。
此外,各属性中的 name 配置的是该领域对象私有属性变量的名字,而不是 GET 方法的名字。例如OrderItem 中配置的是 product_id而不是 productId并且该名字必须与数据库字段一致这是 MyBatis 的要求,我也很无奈)。
有了以上的配置,就可以轻松实现 Service 对数据库的操作,以及 DDD 中那些烦琐的缓存、仓库、工厂、聚合、补填等操作。通过底层技术中台的封装,上层业务开发人员就可以专注于业务理解、领域建模,以及基于领域模型的业务开发,让 DDD 能更好、更快、风险更低地落地到实际项目中。
总结
本讲为你讲解了我设计的支持 DDD 的技术中台的设计开发思路,包括如何设计单 Controller、如何设计单 Dao以及它们在项目中的应用。
下一讲我将更进一步讲解该框架如何设计单 Service 进行查询、通用仓库与通用工厂的设计,以及它们对微服务架构的支持。
点击 GitHub 链接,查看源码。

View File

@ -0,0 +1,273 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17 基于 DDD 的微服务设计演示(含支持微服务的 DDD 技术中台设计)
上一讲,我们讲解了基于 DDD 的代码设计思路,这一讲,接着来讲解我设计的、支持微服务的 DDD 技术中台的设计开发思路。
单 Service 实现数据查询
前面讲过通过单 Dao 实现对数据库的增删改,然而在查询的时候却是反过来,用单 Service 注入不同的 Dao实现各种不同的查询。这样的设计也是在我以往的项目中被“逼”出来的。
几年前,我组织了一个大数据团队,开始大数据相关产品的设计开发。众所周知,大数据相关产品,就是运用大数据技术对海量的数据进行分析处理,并且最终的结果是通过各种报表来查询并且展现。因此,这样的项目,除了后台的各种分析处理以外,还要在前端展现各种报表,而且这些报表非常多而繁杂,动辄就是数百张之多。同时,使用这个系统的都是决策层领导,他们一会儿这样分析,一会儿那样分析,每个需求还非常急,恨不得马上就能用上。因此,我们必须具备快速开发报表的能力,而传统的从头一个一个制作报表根本来不及。
通过对现有报表进行反复分析,提取共性、保留个性,我发现每个报表都有许多相同或者相似的地方。每个报表在 Service 中的代码基本相同,无非就是从前端获取查询参数,然后调用 Dao 执行查询,最多再做一些翻页的操作。既然如此,那么何必要为每个功能设计 Service 呢?把它们合并到一个 Service然后注入不同的 Dao不就可以进行不同的查询了吗
那么,这些 Dao 怎么设计呢?以往采用 MyBatis 的方式,每个 Dao 都要写一个自己的接口,然后配置一个 Mapper。然而这些 Dao 接口都长得一模一样,只是接口名与 Mapper 不 同。此外,过去的设计,每个 Service 都对应一个 Dao现在一个 Service 要对应很多个Dao那用注解的方式就搞不定了。针对以上的设计难题经过反复的调试将架构设计成这样。
首先,整个系统的查询只有一个 QueryService它有一个 QueryDao可以注入不同的 Dao。然而也不需要为每个 Dao 写接口,这样设计过于麻烦。用一个 QueryDaoImpl 注入不同的 Mapper就可以做成不同的 Dao再装配 Service就能在 Spring 中装配成不同的 bean做不同的查询
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<description>The application context for query</description>
<bean id="customerQry" class="com.demo2.support.service.impl.QueryServiceImpl">
<property name="queryDao">
<bean class="com.demo2.support.dao.impl.QueryDaoMybatisImpl">
<property name="sqlMapper" value="com.demo2.trade.query.dao.CustomerMapper"></property>
</bean>
</property>
</bean>
</beans>
在代码中,我们回归 xml 的形式,编写了一个 applicationContext-qry.xml。在名为customerQry 的 bean 中class 都是 QueryServiceImpl注入的都是QueryDaoMybatisImpl但 sqlMapper 配置不同的 mapper就可以进行不同的查询。
这个 mapper 就是 MyBatis 的 mapper它们被放在 classpath 的 mapper 目录下详见MyBatis 的设计开发),然后将内容按照以下的格式进行编写:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.demo2.trade.query.dao.CustomerMapper">
<sql id="select">
SELECT * FROM Customer WHERE 1 = 1
</sql>
<!-- 查询条件部分 -->
<sql id="conditions">
<if test="id != '' and id != null">
and id = #{id}
</if>
</sql>
<!-- 翻页查询部分 -->
<sql id="isPage">
<if test="size != null and size !=''">
limit #{size} offset #{firstRow}
</if>
</sql>
<select id="query" parameterType="java.util.HashMap" resultType="com.demo2.trade.entity.Customer">
<include refid="select"/>
<include refid="conditions"/>
<include refid="isPage"/>
</select>
<!-- 计算count部分 -->
<select id="count" parameterType="java.util.HashMap" resultType="java.lang.Long">
select count(*) from (
<include refid="select"/>
<include refid="conditions"/>
) count
</select>
<!-- 求和计算部分 -->
<select id="aggregate" parameterType="java.util.HashMap" resultType="java.util.HashMap">
select ${aggregation} from (
<include refid="select"/>
<include refid="conditions"/>
) aggregation
</select>
</mapper>
在这段配置中,我们通常只需要修改 Select 与 Condition 部分就可以了。
Select 部分是查询语句,但这部分通常是单表查询,而不采用 join 操作去 join 其他表,这样在数据量大时性能会比较差。同时,最后的 WHERE 1 = 1 是必写的,为了避免在没有查询条件时出错。
Condition 部分是查询条件,参数中有这个条件就加入,没有则去掉。
接着,系统在后台查询时可能会执行多次:分页查询时执行一次,计算 count 时执行一次求和计算时执行一次。但无论执行多少次Select 与 Condition 部分只需要编写一次,从而减少了开发工作量,也避免了编写错误。
该 mapper 在最前面编写的 namespace就是在 queryDao 中配置 mapper 的内容,它们必须完全一致。此外,在 query 部分的 resultType 可以写为某个领域对象,这样在查询结果集中就是这个对象的集合。
以上查询最好都是单表查询,那么需要 join 怎么办呢?最好采用数据补填,即在单表查询并分页的基础上,对那一页的数据执行补填。如果需要补填,那么 QueryService 就这样配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<description>The application context for query</description>
<bean id="productQry" class="com.demo2.support.repository.AutofillQueryServiceImpl">
<property name="queryDao">
<bean class="com.demo2.support.dao.impl.QueryDaoMybatisImpl">
<property name="sqlMapper" value="com.demo2.trade.query.dao.ProductMapper"></property>
</bean>
</property>
<property name="dao" ref="basicDao"></property>
</bean>
</beans>
在该案例中productQry 本来应该配置 QueryServiceImpl却替换为AutofillQueryServiceImpl。同时在 vObj.xml 中进行了如下配置:
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
<vo class="com.demo2.trade.entity.Product" tableName="Product">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="price" column="price"></property>
<property name="unit" column="unit"></property>
<property name="classify" column="classify"></property>
<property name="supplier_id" column="supplier_id"></property>
<join name="supplier" joinKey="supplier_id" joinType="manyToOne" class="com.demo2.trade.entity.Supplier"></join>
</vo>
<vo class="com.demo2.trade.entity.Supplier" tableName="Supplier">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
</vo>
</vobjs>
在配置中可以看到Product 的配置中增加了一个 join 标签,配置的是 Supplier同时又配置了 Supplier 的查询。这样,当完成对 Product 的查询以后,发现有 join 标签就会根据Supplier 的配置批量查询供应商,并自动补填到 Product 中。所以,只要有了这个 join就必须要配置后面 Supplier才能通过它查询数据库中的供应商数据完成数据补填。
有了这样的设计,业务开发人员不必实现数据补填的烦琐代码,只需要在建模的时候配置好就可以了。要补填就配置 AutofillQueryServiceImpl不补填就配置 QueryServiceImpl整个系统就可以变得灵活而易于维护。特别需要注意的是如果配置的是 AutofillQueryServiceImpl那么除了配置 queryDao还要配置 basicDao。因为在数据补填时是通过 basicDao 采用 load() 或 loadForList() 进行补填的。
数据补填对微服务的支持
从以上案例可以看到,对 Product 补填 Supplier这两个表的数据必须要在同一个数据库里这在单体应用是 OK 的,但到微服务就不 OK 了。微服务不仅拆分了应用,还拆分了数据库。当 Product 微服务要补填 Supplier 时,是没有权限读取 Supplier 所在的数据库,只能远程调用 Supplier 微服务的相应接口。这样,通过以上配置完成数据补填就不行了,必须要技术中台提供对微服务的相应支持。
在微服务系统中,通过远程接口进行数据补填的需求,在基于 DDD 的设计开发中非常常见,因此技术中台必须针对这样的情况提供支持。为此,我在 join 标签的基础上又提供了 ref 标签。假设系统通过微服务的拆分,将 Product 与 Supplier 拆分到两个微服务中。这时,要在 Product 微服务中的 vObj.xml 文件中进行如下配置:
<?xml version="1.0" encoding="UTF-8"?>
<vobjs>
<vo class="com.demo2.product.entity.Product" tableName="Product">
<property name="id" column="id" isPrimaryKey="true"></property>
<property name="name" column="name"></property>
<property name="price" column="price"></property>
<property name="unit" column="unit"></property>
<property name="classify" column="classify"></property>
<property name="supplier_id" column="supplier_id"></property>
<ref name="supplier" refKey="supplier_id" refType="manyToOne"
bean="com.demo2.product.service.SupplierService" method="loadSupplier" listMethod="loadSuppliers">
</ref>
</vo>
</vobjs>
以上配置将 join 标签改为了 ref 标签。在 ref 标签中bean 就是在 Product 微服务中对Supplier 微服务进行远程调用的 Feign 接口(详见第 15 讲)。这时,需要 Supplier 微服务提供 2 个查询接口:
Supplier loadSupplier(id),即通过某个 ID 进行查找;
List loadSupplier(Listids),通过多个 ID 进行批量查找。
在这里method 配置的是对单个 ID 进行查找的方法listMethod 配置的是对多个 ID 批量查找的方法。通过这 2 个配置,就可以用 Feign 接口实现微服务的远程调用,完成跨微服务的数据补填。通过这样的设计,在 Product 微服务的 vObj.xml 中就不用配置 Supplier 了。
通用仓库与工厂的设计
没有采用 DDD 之前,在系统的设计中,每个 Service 都是直接注入 Dao通过 Dao 来完成业务对数据库的操作。然而DDD 的架构设计增加了仓库与工厂除了读写数据库以外还要实现对领域对象的映射与装配。那么DDD 的仓库与工厂,和以往的 Dao 是什么关系呢?又应当如何设计呢?
传统的 DDD 设计,每个模块都有自己的仓库与工厂,工厂是领域对象创建与装配的地方,是生命周期的开始。创建出来后放到仓库的缓存中,供上层应用访问。当领域对象在经过一系列操作以后,最后通过仓库完成数据的持久化。这个领域对象数据持久化的过程,对于普通领域对象来说就是存入某个单表,然而对于有聚合关系的领域对象来说,需要存入多个表中,并将其放到同一事务中。
在这个过程中,聚合关系会出现跨库的事务操作吗?即具有聚合关系的多个领域对象会被拆分为多个微服务吗?我认为是不可能的,因为聚合就是一种强相关的封装,是不可能因微服务而拆分的。如果出现了,要么不是聚合关系,要么就是微服务设计出现了问题。因此,仓库是不可能完成跨库的事务处理的。
弄清楚了传统的 DDD 设计,与以往 Dao 的设计进行比较,就会发现仓库和工厂就是对 Dao 的替换。然而,这种替换不是简单的替换,它们对 Dao 替换的同时,还扩展了许多的功能,如数据的补填、领域对象的映射与装配、聚合的处理,等等。当我们把这些关系思考清楚了,通用仓库与工厂的设计就出来了。
如上图所示,仓库就是一个 Dao它实现了 BasicDao 的接口。然而,仓库在读写数据库时,是把 BasicDao 实现类的代码重新 copy 一遍吗?不!那样只会形成大量重复代码,不利于日后的变更与维护。因此,仓库通过一个属性变量将 BasicDao 包在里面。这样,当仓库要读写数据库时,实际上调用的是 BasicDao 实现类,仓库仅仅实现在 BasicDao 实现类基础上扩展的那些功能。这样,仓库与 BasicDao 实现类彼此之间的职责与边界就划分清楚了。
有了这样的设计,原有的遗留系统要通过改造转型为 DDD除了通过领域建模增加 vObj.xml以外将原来注入 Dao 改为注入仓库就可以快速完成领域驱动的转型。同样的道理要在仓库中增加缓存的功能不是直接去修改仓库而是在仓库的基础上包一个RepositoryWithCache专心实现缓存的功能。这样设计既使各个类的职责划分非常清楚日后因哪种缘由变更就改哪个类又使得系统设计松耦合可以通过组件装配满足各种需求。
总结
通过本讲的讲述,我为你提供了一个可以落地的技术中台。这个中台与传统的 DDD 架构有所不同,它摒弃了一些非常烦琐的 TDO、PO、仓库与工厂的设计而是将其封装在了底层技术框架中。这样业务开发人员可以将更多的精力放到业务建模以及基于业务建模的设计开发过程中。开发工作量减少了一方面可以实现快速交付另一方面也让日后的变更与维护变得轻松可以随着领域模型的变更而变更更好更深刻地设计我们的产品给用户更好的体验。
下一讲我们将围绕事件驱动,来谈一谈其在微服务中的设计实现。
点击 GitHub 链接,查看源码。

View File

@ -0,0 +1,214 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18 基于事件溯源的设计开发
上一讲通过代码演示,全面展示了基于 DDD 的设计开发思路,包括:如何实现聚合、如何设计仓库、如何将领域对象与数据库映射,以及我设计的基于 DDD 与微服务的技术架构。通过这些讲解为你展开了一幅如何实现领域驱动的系统开发的全景图。然而,这些设计还缺少一个重要的内容,即基于领域事件的设计与开发。
基于事件溯源的设计思路
[第 07 讲“在线订餐场景中是如何开事件风暴会议的?”]谈到了 DDD 中“事件风暴Event Storming”的实践方法。该方法认为事件即事实Event as Fact即在业务领域中已经发生的事件就是事实Fact。过去发生的事件已经成为了事实就不能再更改因此信息管理系统就可以将这些事实以信息的形式存储到数据库中即信息就是一组事实。
所以,一个信息化管理系统的作用,就是存储这些事实,对这些事实进行管理与跟踪,进而起到提高工作效率的作用。基于这样的思路,分析一个信息管理系统的业务需求,就是准确地抓住业务进行过程中需要存储的关键事实,并围绕着这些事实进行分析设计、领域建模,这就是“事件风暴”的精髓。
然而,按照“事件风暴”法完成对系统的分析与设计,最终落实到系统建设又应当怎样做呢?前面[第 08 讲“DDD 是如何解决微服务拆分难题的?”]通过讲解在线订餐系统,最终落实到领域事件的发布与通知机制:
“用户下单”微服务在完成下单以后,通过事件通知机制通知“饭店接单”微服务;
“饭店接单”微服务在准备就绪以后,通过事件通知机制通知“骑士派送”微服务。
这种领域事件的消息通知机制就是“事件溯源”的设计思路。
“事件溯源”是一种全新的设计思路,它将过去耦合在一起的业务流程有效地解耦,让越来越复杂的业务系统能够松耦合地拆分为一个个独立组件,实现组件式的设计开发与插拔式的业务变更。现在通过案例来看一看“事件溯源”的设计与传统的设计有哪些方面的不同。
拿“用户下单”这个业务场景来说。从业务需求的角度看,当用户下单以后,需要完成哪些操作,在需求上有非常大的不确定性。
譬如,在用户下单后最初的需求就是库存扣减,这时传统的做法就是在保存订单以后,直接调用库存扣减的方法,完成相应的操作;接着,又提出了启动物流的需求,需要调用一个启动物流配送的方法。然而,事情还没完,过了一段时间,产品经理又提出了会员管理的需求,用于计算会员积分,或提供会员福利。
每提出一个新的需求,都需要修改“用户下单”的代码,在用户下单以后增加某些操作。这样的设计就使得“用户下单”的功能变得非常不稳定,总是要不停地修改。
与传统的设计思路不同,“事件溯源”的设计思路是,当完成用户下单以后,只需要实现一个“用户下单”的领域事件,至于用户下单以后需要做什么事情,与“用户下单”无关。因此,通过“事件溯源”的设计,就使得业务流程中的上下游相互解耦。上游只需要发布领域事件,而由下游自己去定义后续要做什么事情,从而实现了复杂系统的松耦合与可维护。
领域事件的设计实现
清楚了“事件溯源”的设计思路,那么应当如何实现呢?我们的思路就是根据“事件风暴”中分析识别的领域事件,在每次完成相应工作以后增加一个对领域事件的发布,其发布的内容包括:事件名称、发布者、发布时间与相关的数据。譬如,当用户下单以后,发布这样一个领域事件:
{ event_id: “createOrder”, publisher: “service_order”, publish_time: “2021-01-07 18:38:00.000”, data: { id: “300001”, customer_id: “200005”, … } }
在这里不同的领域事件后面的参数是不一样的有的可能是一个领域对象有的可能是一个数组参数抑或是一个Map甚至没有参数。譬如一些领域事件就是一个状态的改变所以不包含参数。什么领域事件跟着什么参数是事件的发布者设计的然后将协议告知所有订阅者。这样所有的订阅者就根据这个协议自己去定义后续的操作。
依据这样的思路落地到项目中,事件发布者要在方法的最后完成一个事件的发布。至于到底要做什么事件,交由底层技术中台去定义,比如发送消息队列,或者写入领域事件表中。例如,在“用户接单”中完成事件发布:
@Override
public void createOrder(Order order) {
...
createOrderEvent.publish(serviceName, order);
}
@Override
public void modifyOrder(Order order) {
...
modifyOrderEvent.publish(serviceName, order);
}
接着,事件订阅者需要为每一个事件编写相应的领域事件类,在 apply() 方法中定义该事件需要做什么操作,例如,在“饭店接单”中定义“用户下单”事件的操作:
public class CreateOrderEvent implements DomainEvent<Order> {
@Override
public void apply(Order order) {
...
}
}
事件溯源就是将事件的发布与操作分离,业务的上游负责发布,下游负责订阅并完成某些操作,从而实现上下游的解耦。上游只有一个发布者,但下游可以有很多发布者,各自执行不同的操作。
此外,一个值得讨论的问题是,事件风暴中定义的每个事件,是不是都需要发布领域事件呢?譬如在线订餐系统中,“用户下单”需要发布领域事件,然后“饭店接单”需要接收这个事件,但“饭店接单”这个领域事件需要发布吗?它的下游似乎没有人接收。但是,未来需求怎么变更,谁又会知道呢?当系统增加“订单跟踪”时,就需要跟踪每一个领域事件。所以我们说,因为无法预知未来的变化,最好的做法就是老老实实地将每一个领域事件都予以发布。
基于消息的领域事件发布
前面讲解了领域溯源的设计思路,最后要落地到项目实践中,依然需要技术中台的相应支持。譬如,业务系统的发布者只负责事件的发布,订阅者只负责事件的后续操作。但这个过程该如何发布事件呢?发布事件到底要做什么呢?又如何实现事件的订阅呢?这就需要下沉到技术中台去设计。
首先,事件的发布方在发布事件的同时,需要在数据库中予以记录。数据库可以进行如下设计:
接着,领域事件还需要通过消息队列进行发布,这里可以采用 Spring Cloud Stream 的设计方案。Spring Cloud Stream 是 Spring Cloud 技术框架中一个实现消息驱动的技术框架。它的底层可以支持 RabbitMQ、Kafka 等主流消息队列,通过它的封装实现统一的设计编码。
譬如,以 RabbitMQ 为例,首先需要在项目的 POM.xml 中加入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
</dependencies>
接着,在 bootstrap.yml 文件中,将领域事件与消息队列绑定。例如,在“用户下单”微服务中定义领域事件的发布,如下代码所示:
spring:
rabbitmq:
host: xxx.xxx.xxx.xxx
port: 5672
username: guest
password: guest
cloud:
stream:
bindings:
createOrder:
destination: createOrder
modifyOrder:
destination: modifyOrder
然后,定义领域事件及其客户端,如下代码所示:
public interface CreateOrderEventClient {
String OUTPUT = "createOrder";
@Output(CreateOrderEventClient.OUTPUT)
MessageChannel output();
}
@EnableBinding(value=CreateOrderEventClient.class)
@Component
public class CreateOrderEvent {
@Autowired
private CreateOrderEventClient client;
/**
* @param publisher
* @param data
*/
public void publish(String publisher, Object data) {
String eventId = "createOrder";
Date publishTime = DateUtils.getNow();
DomainEventObject event = new DomainEventObject(eventId,
publisher, publishTime, data);
event.save();
client.output().send(MessageBuilder.withPayload(event).build());
}
}
在“用户下单”微服务中,如上所述依次定义每个领域事件,如用户下单、修改订单、取消订单,等等。这样,在“用户下单”微服务完成相应操作时,领域事件就会发布到消息队列中。
最后,再由订阅者去完成对消息队列的订阅,并完成相应操作。这时,还是先在 bootstrap.yml文件中绑定领域事件如下代码所示
spring:
profiles: dev
rabbitmq:
host: 118.190.201.78
port: 31672
username: guest
password: guest
cloud:
stream:
bindings:
createOrder:
destination: createOrder
group: ${spring.application.name}
modifyOrder:
destination: modifyOrder
group: ${spring.application.name}
这里增加了一个 group当该服务进行多节点部署时每个事件只会有一个微服务接收并予以处理。接着定义领域事件类一方面监听消息队列一方面定义后续需要完成什么操作
public interface CreateOrderEventClient {
String INPUT = "createOrder";
@Input(CreateOrderEventClient.INPUT)
SubscribableChannel input();
}
@Component
@EnableBinding(value= {CreateOrderEventClient.class})
public class CreateOrderEvent {
@StreamListener(CreateOrderEventClient.INPUT)
public void apply(DomainEventObject obj) {
...
}
}
这时,在“饭店接单”与“订单跟踪”微服务都有 CreateOrderEvent 这个领域事件,然而它们各自的 apply() 方法要完成的事情是不一样的,就可以彼此独立地完成各自的工作。比如:“饭店接单”是发送消息给前端,通知饭店完成接单操作,而“订单跟踪”则是接收到信息以后,更新订单的相应状态。但不论是谁,都会在各自的数据库中记录下接收的领域事件。
总结
事件溯源是 DDD 设计实践中另一个重量级的工具包。它解耦了领域事件的上下游,将事件的发布与做什么操作解耦,即事件的上游负责执行 publish() 方法发布事件,而事件的下游负责各自去定义各自的 apply() 方法,完成后续的操作。这样的设计使得复杂的业务流程,可以松耦合地分解到多个组件中独立完成,也会更加广泛地应用到微服务的设计中。
通过 Spring Cloud Stream 的消息驱动,将领域事件发布到消息队列中,就可以更好地在软件项目中实践“事件溯源”的设计方法。但这样的设计更需要 DDD 技术中台的底层支持。
下一讲,我们将从实战的角度,去看一看一个更大规模的人工智能系统是如何实现领域驱动设计的。

View File

@ -0,0 +1,78 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
00 开篇词 从默默无闻到风靡全球DevOps究竟有什么魔力
你好我是石雪峰目前在京东商城负责工程效率体系建设和平台研发。从业十多年我一直在软件行业深耕尤其是从2015年接触DevOps至今我一直在企业内部从事DevOps的落地实践工作也曾帮助多家大型企业进行DevOps的相关能力评估积累了很多实战经验。
在写开篇词的时候我才意识到DevOps从诞生至今已经整整十个年头了。十年之间DevOps从默默无闻到风靡全球很多人都在反思和总结DevOps究竟有什么魔力。
十年前的2009年我在一家日本软件公司工作长期被外派到日本尼康公司做项目。虽然当时敏捷已经兴起但在日本软件开发还是瀑布模式的天下。每当一个新项目来临时我们经常不分白天黑夜地埋头苦干几个月完全不敢想象如果不能顺利交付会怎么样。
可是,怕什么就来什么。有一次,我负责开发一款客户端软件,给客户交付的方式是事先刻录一张光盘,把光盘带去现场,一边部署,一边演示。刚开始还挺顺利的,可是到了生产数据拉取的环节,系统竟然异常退出了。我至今都还记得那位项目负责人不满的表情。
调试后我发现客户的生产环境使用的是Oracle数据库而我们使用的是微软的Access数据库数据访问协议不一致数据自然会同步失败。
之后的三个月,我总共休息了两天,每天的节奏就是吃饭睡觉写程序,干到搭乘最后一班电车回家,唯一的娱乐活动就是在吃加班餐的时候吐槽老板。
所以当时我就在想一定会有一种更好的软件开发方式在这种方式下团队间沟通和协作的重要性一点也不亚于写代码、写文档、做测试之类的常规工作。但我不知道的是远在大洋彼岸DevOps的旅程才刚刚开始。
十年后也就是2019年以移动互联网、云计算、微服务、大数据、人工智能等为代表的技术日新月异技术的迭代和演进都在以十倍速的方式向前发展数字化转型浪潮正在席卷各行各业。“软件正在吞噬世界”“每一家企业终将成为软件企业”……行业领袖口中的这些预言都在慢慢地变成现实。
如今,软件正在深刻地改变着我们的生活方式。前段时间,我去新疆旅行。在旅行途中,我发现即便是在沙漠边缘的小镇,微信支付也是畅通无阻。另外,用户喜新厌旧的成本已经低到可以忽略不计,企业之间的竞争已经升级为软件即服务的竞争。
所以,如何快速地持续交付高质量的软件,满足用户的多样化需求,并借此提升企业的利润和市场占有率,已经成为企业必须要面对的现实问题。
可问题是,现在很多企业采用的软件开发方式,同十年前我所在的公司其实并没有什么区别,甚至由于组织分工的细化,内部沟通的消耗成本更加高昂。
你应该也遇到过这样的场景吧两个部门为了数据打通来回拉锯各种方案和排期一天一个样还美其名曰“PK”。原本特别简单的一件事情非要扯上几天甚至几周才能有点眉目。每当这个时候我都忍不住想说“嘿兄弟我不是来抢你饭碗的我只是想通过系统间的打通来简化一些工作而已何必搞得这么复杂呢
所以你看软件开发过程的改进除了依赖于技术进步还依赖于流程、理念、文化等全方位的改进而这正是DevOps带给软件开发方式的一场革命。
从2017年DevOpsDays大会北京站举办以来DevOps在国内的发展正式驶向了快车道。作为从业者之一我深刻地感受到DevOps的影响力与日剧增不仅仅是互联网行业就连传统的电信、金融甚至是政府机构也都把DevOps作为核心能力在快速建设。
现在已经很少有人会问DevOps有什么用、DevOps是否适合我之类的问题了更多人开始关注要如何落地实践DevOps并且让DevOps充分发挥它的价值真正改善软件交付方式提高IT工程师的幸福指数。
除此之外越来越多的企业开始招聘DevOps方面的人才对DevOps的技能和经验背景的要求越来越高DevOps专家的岗位薪资甚至仅次于高级管理层一跃成为IT行业的金字塔顶端。
我个人认为DevOps已经成为了所有IT从业人员应知应会的必备技能。在这些技能中技术和实践当然非常重要但文化和理念更是尤为珍贵。如果每个从业者都认同DevOps的文化和理念认同快速交付价值远胜于部门间的零和博弈认同我们应该共享一个目标并从自身做起持续改善上下游的关系那么怎么可能还会出现刚刚我提到的PK的例子呢
也许你从各种渠道了解过DevOps的相关信息但是因为市场上资料庞杂、个人精力有限等原因还存在着以下几个困惑
如何梳理出一套清晰的DevOps理念和完整的知识体系
如何获得一线大厂的实践经验让DevOps真正落地
如何获得一条渐进式的DevOps学习曲线让自己在正确的方向上不断增值
这些问题,正是多年来我一直在思考的,也希望在这个专栏中传递给你的核心内容。
学习DevOps的过程对你来说将会是一场探索之旅。DevOps涉及软件开发的方方面面因此你将漫步于需求、开发、测试、运维的完整开发流程途经管理实践和工程实践的领域探寻方法论、最佳实践和工具平台的有机结合方式让自己在全栈工程师和斜杠青年的道路上更进一步。
DevOps涉及的领域如此之广想在一个专栏中学遍所有内容几乎是不可能的事情所以我从实战的角度出发臻选出最重要的内容帮你梳理出一条DevOps的最佳学习路径。
本专栏主要由4个部分组成。
第1部分是“基础知识篇”。我将详细介绍DevOps的定义、价值、实施与衡量在最开始帮你建立起正确的DevOps体系认知。
第2部分是“落地实践篇”。这一部分占据整个专栏一半的篇幅是最核心的部分。我将带你通盘梳理DevOps的转型路径你一定不要错过。
第3部分是“平台工具篇”。它涵盖平台建设的3个阶段、产品研发和设计、不可忽视的开源工具等帮你找到快速搭建平台的钥匙。
第4部分是“转型案例篇”。我将分享12个实际案例将前面提到的理论、落地实践和工具融入其中让你能够融会贯通。
另外我还设置了特别放送环节。在这个环节我会跟你分享一些经典的学习资料、DevOps工程师的必备技能等内容让你全方位、多层次地掌握DevOps。
其实整个专栏的整理和写作对我来说也是一场修行。毕竟作为DevOps多年的实践者我在用它解决问题的同时也发现了更多的问题好奇心和对效率建设的执着追求让我乐此不疲。现在能够静下心来把我多年的经验与反思整理出来跟你分享也是一件非常有意义的事情。
在这个过程中我也越发地感受到DevOps的思想和文化的落地依然任重道远。每个时代都会有一群先锋走在时代的前沿中流击水鹰击长空希望通过本专栏的学习你也可以成为DevOps的思想者和实践者实现个人价值和企业价值的双赢。
最后我想请你聊一聊关于DevOps你都有哪些困惑对于专栏你又有哪些期待欢迎你写在留言区我们一起交流期待你的反馈。
好了从现在开始就让我们一起踏上这场DevOps的奇妙旅程一路同行不断进步。

View File

@ -0,0 +1,105 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 DevOps的“定义”DevOps究竟要解决什么问题
你好我是石雪峰。今天我们来聊一聊DevOps的“定义”。
近些年来DevOps在我们身边出现的频率越来越高了。各种大会上经常出现DevOps专场行业内的公司纷纷在都招聘DevOps工程师企业的DevOps转型看起来迫在眉睫公司内部也要设计和开发DevOps平台……这么看来DevOps似乎无处不在。
可回过头来想想关于DevOps很多问题我们真的想清楚了吗所谓的DevOps平台是否等同于自动化运维平台或持续交付平台呢DevOps工程师的岗位描述中又需要写哪些技能要求呢另外该如何证明企业已经实现了DevOps转型呢这些问题真是难倒了一众英雄好汉。说到底听了这么久的DevOps它的“定义”到底是什么好像从来没有人能说清楚。
现在我们先来看看维基百科对DevOps的定义。不过估计也没谁能看懂这到底是在说什么。
DevOps开发Development与运维Operations的组合词是一种文化、一场运动或实践强调在自动化软件交付流程及基础设施变更过程中软件开发人员与其他信息技术IT专业人员彼此之间的协作与沟通。它旨在建立一种文化与环境使构建、测试、软件发布得以快速、频繁以及更加稳定地进行。
于是乎每当提及DevOps是什么的时候最常出现的比喻就是“盲人摸象”。有意思的是DevOps之父Patrick第一次参加DevOpsDays中国站活动的时候也使用了这个比喻看来在这一点上中西方文化是共通的。毕竟每个人的视角都不相同看到的DevOps自然也是千差万别。
DevOps大潮汹涌而来很多人都被裹挟着去探索和实践DevOps甚至有一种极端的看法认为一切好的实践都属于DevOps而一切不好的实践都是DevOps的反模式。
当年敏捷开始流行的时候似乎也是相同的论调但这种笼统的定义并不能帮助我们理清思路甚至会带来很多负面的声音比如DevOps就是开发干掉运维又或者DevOps就是要让运维抛弃老本行开始全面转型做开发。这让很多IT从业人员一度很焦虑。
客观来说从DevOps运动诞生开始那些先行者们就从来没有试图给DevOps下一个官方的定义。当然这样做的好处很明显由于不限定人群和范围每个人都能从自己的立场来为DevOps做贡献从而使DevOps所涵盖的范围越发宽广。
但是坏处也是显而易见的。随着DevOps的不断发展刚开始接触DevOps的人往往不得要领只见树木不见森林认知的偏差使得DevOps越发地神秘起来。
与其纠结于DevOps的定义不如让我们一起回归原始来看看DevOps究竟要解决的是什么问题。
其实DevOps的秘密就来源于它的名字所代表的两种角色——开发和运维。那么这两种角色之间究竟有什么问题呢我们从软件工程诞生以来所历经的三个重要发展阶段说起。
瀑布式开发模式
瀑布式开发模式将软件交付过程划分成几个阶段,从需求到开发、测试和运维,它的理念是软件开发的规模越来越大,必须以一种工程管理的方式来定义每个阶段,以及相应的交付产物和交付标准,以期通过一种重流程,重管控,按照计划一步步推进整个项目的交付过程。
可是,随着市场环境和用户需求变化的不断加速,这种按部就班的方式有一个严重的潜在问题。
软件开发活动需要在项目一开始就确定项目目标、范围以及实现方式,而这个时间点往往是我们对用户和市场环境信息了解最少的时候,这样做出来的决策往往带有很大的不确定性,很容易导致项目范围不断变更,计划不断延期,交付上线时间不断推后,最后的结果是,即便我们投入了大量资源,却难以达到预期的效果。
从业界巨头IBM的统计数字来看有34%的新IT项目延期交付将近一半的应用系统因为缺陷导致线上回滚这是一件多么令人沮丧的事情。
敏捷式开发模式
基于这种问题,敏捷的思潮开始盛行。它的核心理念是,既然我们无法充分了解用户的真实需求是怎样的,那么不如将一个大的目标不断拆解,把它变成一个个可交付的小目标,然后通过不断迭代,以小步快跑的方式持续开发。
与此同时,将测试工作从研发末端的一个独立环节注入整个开发活动中,对开发交付的内容进行持续验证,保证每次可交付的都是一个可用的功能集合,并且由于质量内建在研发环节中,交付功能的质量也是有保障的。
很显然敏捷是一种更加灵活的研发模式。经常有人会问敏捷会直接提升团队的开发速度吗答案是否定的。试想一下难道说采用了敏捷方法研发编码的速度就会提高两倍甚至三倍吗回想一下很多年前在IT行业广为流传的“人月神话”我们就能发现正确的认知有多么重要。
敏捷之所以更快,根本原因在于持续迭代和验证节省了大量不必要的浪费和返工。关于这一点,我会在敏捷和精益的相关内容中做更加详细的介绍。
说到底,敏捷源于开发实践,敏捷的应用使得开发和测试团队抱团取暖。可是问题又来了,开发和测试团队发现,不管研发的速度变得多快,在软件交付的另一端,始终有一群人在冷冰冰地看着他们,一句“现在没到发布窗口”让多少新开发的功能倒在了上线的门槛上。
毕竟,无论开发了多少“天才”的功能,如果没有经过运维环节的部署上线,并最终发布给真实用户,那么这些功能其实并没有什么用。
DevOps模式
于是活在墙的另一端的运维团队成了被拉拢的对象。这些在软件交付最末端的团队始终处于一种“背锅”的状态他们也有改变的意愿所以DevOps应运而生也就是说DevOps最开始想要打破的就是开发和运维之间的对立和隔阂。
在传统模式下,度量开发团队效率的途径就是看开发完成了多少需求。于是,开发为了达成绩效目标,当然也是为了满足业务需求,不断地堆砌新功能,却很少有时间认真思考这些功能的可运维性和可测试性,只要需求状态流转到开发完成就万事大吉了。
而对于运维团队而言他们的考核指标却是系统的稳定性、可用性和安全性。但现代IT系统是如此复杂以至于每一次的上线发布都是一场战役整个团队如临大敌上线失败的焦虑始终如影随形。
很多时候,我们并不知道上线之后会发生什么,只能按照部署手册一步步操作,完成之后就听天由命。所以,每逢大促活动,就会有各种“拜服务器教”的照片广为流传。
另一方面,在无数次被开发不靠谱的功能缺陷蹂躏得体无完肤之后,运维团队意识到,变更才是影响他们绩效目标的最大敌人。于是,预先设立的上线窗口就成了运维团队的自留地,不断抬高的上线门槛也使得开发团队的交付变成了不可能完成的任务,最后,“互相伤害”就成了这个故事注定的结局。
即便到了今天,部署上线在大多数公司依然是一件很神圣的事。我给你讲一件有趣的事情。
去年我在欧洲拜访DevOps之父Patrick的时候曾经去过他的公司。那天风雪交加比利时根特显得非常冷清。我们停好车后刚要推门进入他们公司恰好碰到Patrick和他的一个同事下楼抽烟。
简单寒暄之后我们才知道原来Patrick公司负责的一个系统要在15分钟后上线他们趁这个间歇出来换换脑子然后再回去大干一场。所以你看连DevOps之父在面临上线的时候都如此正式可见DevOps的发展之路依然任重而道远啊。
从一开始想要促进开发和运维的协作,团队慢慢发现,其实在整个软件交付过程中,不仅只有开发和运维,业务也是重要的一环。
比方说如果业务制定了一个不靠谱的需求那么无论开发和运维怎样协作得到的终究是一个不靠谱的结果以及对人力的浪费。可是业务并不清楚用户的真实情况于是运维团队慢慢转向运营团队他们需要持续不断地把线上的真实数据和用户行为及时地反馈给需求团队来帮助需求团队客观评估需求的价值并及时作出有利于产品发展的调整这样一来业务也被引入到了DevOps之中甚至诞生了BizDevOps这样一个专门的词汇。
那么既然沟通协作放之四海皆准安全也开始积极地参与进来。安全不再是系统上线发布之后的“定时炸弹”而是介入到整个软件开发过程中在每个过程中注入安全反馈机制来帮助团队在第一时间应对安全风险那么对于安全团队来说DevSecOps就成了他们眼中的DevOps。
这样的例子比比皆是包括职能部门、战略部门等都纷纷加入其中使得DevOps由最开始的点扩展为线再到面不断发展壮大。每个人都参与其中这使得DevOps成了每一个IT从业人员都需要学习和了解的知识和技能体系。
说到最后我还是希望基于我对DevOps的理解给出一个我自己的“定义”
DevOps是通过平台Platform、流程Process和人People的有机整合以C协作A自动化L精益M度量S共享文化为指引旨在建立一种可以快速交付价值并且具有持续改进能力的现代化IT组织。
总结
今天我带你一起梳理了DevOps的发展历程以及软件开发模式的变迁。有人说DevOps是软件工程发展至今的第三次革命可见它带给整个行业的影响是很深远的。人云亦云并不能帮助我们更好地理解DevOps建立正确的认知才是体系化学习的第一步希望你能通过今天的课程建立起你自己对于DevOps的独特认知。
思考题
最后给你提一个问题我给出的定义符合你心目中对DevOps的预期吗DevOps具有与生俱来的开放性你能谈一谈你对DevOps的理解和定义吗
欢迎在留言区写下你的思考和答案,我们一起讨论,共同学习进步。如果你觉得这篇文章对你有所帮助,欢迎你把文章分享给你的朋友。

View File

@ -0,0 +1,102 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 DevOps的价值数字化转型时代DevOps是必选项
你好我是石雪峰。今天我们来聊聊DevOps的价值。
前段时间,因为工作的缘故,我参访了一家在国内数一数二的金融企业。在跟他们科技处的同事交流的过程中,有一件事情让我非常吃惊,想跟大家分享一下。
虽然在一般人眼中这家企业是典型的传统企业但他们的绩效目标采用的却是OKR模式。
我简单介绍一下OKR。OKR也就是目标与关键成果法是在硅谷互联网公司很流行的绩效制定方法。简单来说O代表目标也就是我们要做什么KR代表关键结果用于验证我们是否已经达到了目标。
这家金融企业的大老板也就是科技处的老大给全体员工制定的众多OKR中有且只有一条属于愿景指标。说出来你可能不相信这个愿景指标就是到今年年底让DevOps在全行的三个试点项目中成功落地。
而且这并不是简单的说说而已如果最终达成了这个愿景指标所有员工的年终奖将在原有的基础上上浮10%20%。由此可见关于实施DevOps他们是在玩真的了。
全行的核心系统改造都没能成为愿景指标那为啥DevOps会有如此大的魔力让大老板都为之着迷并且成为愿望清单列表中的第一名呢这就是我今天要跟大家讨论的话题DevOps的价值以及它对现代企业的意义。
如果要选一个近年来在各大企业战略中曝光率最高的关键词,数字化转型绝对是排名最高的,没有之一。
比如传统汽车巨头大众公司今年宣布启动全面数字化转型计划到2023年年底投资约40亿美元实现管理和生产的数字化。而且预计到2025年大众集团的软件研发的比例将从目前的不到10%增长到60%。
为什么软件如此重要?
对于软件从业人员来说,这绝对是令人欢欣鼓舞的事情。同时,这也再一次印证了那句流传已久的名言,每一家公司都将成为软件公司。那么问题来了,在数字化转型时代,为什么软件会如此重要呢?
互联网的普及和移动通讯技术的发展所带来的移动互动网的兴起,深刻地影响了我们每个人的生活方式。
举个最简单的例子,几年前如果我们要办理银行业务,我们首先要找到附近的营业厅,抽空跑过去排号,经过很长时间的等待,才能坐到柜台前,同银行的柜员面对面地完成业务办理。当然这还是在顺利的情况下,如果忘记带证件或者排队人太多,可能还要再跑一次,办事成本相当高。
现在呢大多数情况下我们只要掏出手机打开银行的APP点击几下屏幕业务就办好了完全不用受时间和空间的限制。用户对银行服务的体验直接来源于手机应用本身。如果哪家银行的应用界面很丑操作还总是出现各种问题就会直接影响用户对这家银行的印象甚至会在潜意识里觉得这家银行不靠谱。显然没有任何一家银行愿意给人留下这样的印象。
所以,软件慢慢从企业内部的支撑系统和成本中心,变成了企业服务的直接载体和利润中心。企业通过软件降低运营成本,提升服务水平,而用户在获得便利的同时,也加强了同企业之间的联系。
这本是一件双赢的事情可问题是我们所身处的是一个VUCA的时代VUCA是指易变性Volatility、不确定性Uncertainty、复杂性Complexity和模糊性Ambiguity它代表了这个时代的典型特征。比如共享单车这个行业从冉冉兴起炙手可热到逐渐归于平静前后不过短短几年的时间。
企业能快速满足用户的需求在行业大势之下灵活转身在跨界打击越发普遍的情况下脱颖而出已经不仅仅是good to have的能力而是must have的能力。
可以说软件交付的效率和质量成了当今企业的核心价值和核心竞争力所以任何一家企业无论是行业巨头还是初创公司无论是互联网行业还是传统行业无论是领先者还是颠覆者都有强烈的意愿去改善自身的软件交付能力而这恰恰和DevOps的理念和诞生背景不谋而合。这么看来DevOps能够成为企业愿望清单中的第一名也就不足为奇了吧。
可是,即便软件如此重要,却依然有很多公司在用一种手工作坊的方式开发软件,引用国家智库的某位领导的话来说,“工业革命消灭了绝大多数的手工业群体,却催生了程序员这个现存最大的手工业群体”。这句话看似危言耸听,但这种开发软件的方式的确存在,其中伴随着大量的效率浪费。企业内部的软件开发交付效率已经成了一座值得探索挖掘的金矿,效率经济可能成为新的业绩增长点。
DevOps的价值
那么实施DevOps带给企业的价值究竟是什么呢要回答这个问题我们就不得不提到DevOps业内非常著名的现状调查报告了。
高效的软件交付方式
从2014年至今这个报告每年都会发布一份由业内大咖和行业领袖基于科学的分析方法通过大量的数据分析得出可以说是业内最具权威性的报告其中的很多数据和理念都被广为传播。我发现在这洋洋洒洒大几十页的报告中被引用频率或者说出镜率最高的就是DevOps的4个结果指标。
部署频率:指应用和服务向生产环境部署代码的频率。
变更前置时间:指代码从提交到成功运行在生产环境的时长。
服务恢复时间:指线上应用和服务出现故障到恢复运行的时长。
变更失败率:指应用和服务在生产环境部署失败或者部署后导致服务降级的比例。
每年这个报告都会基于这4个核心指标统计行业内高效能团队和低效能团队之间的差距。从去年的数据来看与低效能团队相比高效能团队的部署频率高了46倍变更前置时间快了2500多倍服务恢复时间也快了2600多倍失败率低了7倍。
我们先不管这份数据是怎么计算出来的,当你第一次看到这个数据的时候,它带给你的冲击是不是很强大呢?用具体的数字形式来呈现企业之间效率的差距,是很有震撼力的。
而世界上最令人“绝望”的事情就是那些比你优秀的人实际上比你还要更加努力。当你仔细查看这份报告的时候你会发现那些常年被人提及的明星公司很多都在践行DevOps甚至很多来源于这些公司的实践案例都成为了DevOps行业的经典案例。
另一方面DevOps状态报告中提到的四项结果指标分别代表了软件交付的两个最重要的方面也就是交付效率和交付质量。而且从数据结果中我们还能得到一个惊人的发现那就是高效能的组织不仅做到了高效率还实现了高质量由此可见鱼与熊掌可以兼得。
可是这就颠覆了很多人心目中的“慢工出细活”的传统软件开发理念。因为按照传统软件开发的V模式来说软件开发完成后需要经过单元测试、集成测试、系统测试和验收测试等层层关卡以此来保证软件的质量符合预期。但是对于现代软件开发而言如此重的流程和管控显然有点跟不上时代的节奏。
我们在不断提高软件交付效率时,往往是以牺牲质量为代价的,结果做得越多,错得越多,从而陷入进退两难的境地。
DevOps却反其道而行之它试图通过体系化的研发实践导入、软件架构的整体革新、组织管理理念的不断升级和企业文化的影响塑造来帮助企业改善整个软件交付过程在实现高吞吐量的同时保证服务的总体稳定性从而真正实现又快又好的软件交付目标。
激发团队的创造力
我们刚刚谈到的这些内容当然是DevOps带给企业的重要价值但并非全部。在专栏中我不仅希望能跟你分享知识还希望能跟你分享一些不同的观点我们一起思考和讨论获取灵感和新知。
之前我在跟Jenkins创始人KK聊天的时候他提出过这样一个问题熟悉云计算的同学可能或多或少地了解过容器编排领域的事实标准Kubernetes以及它背后的CNCF基金会那么企业为什么热衷于加入这样的基金会呢即使要付出一笔不菲的费用也在所不惜企业这么做的收益究竟是什么
不可否认CNCF是一个非常成功的运营案例成为会员还能享受白纸黑字上的福利但是对于很多中小企业而言他们的诉求可能不止如此。
很多时候,企业加入这样的组织,也是为了向内部员工表态,我们正和世界上最著名的公司站在同一条起跑线上,关注着同样的问题。这对他们的员工来说,既能起到激励作用,也能增强对企业自身的信心。
对于DevOps而言道理也是同样的因为说到底企业的问题都是人的问题最核心的价值最终都会归结到人身上所以单纯关注软件交付的能力而忽视人的感受结果往往都是片面的。
在企业内部建设DevOps工具平台的时候我也经常在思考这个问题我们费尽心思通过平台能力建设提升了5%的交付效率即便节省下来的时间只是让员工多休息了一会儿也是非常有意义的事情。因为DevOps本身也包含了改善软件从业人员的生存状态提升他们的幸福水平的理念。
这么看来实施DevOps一方面可以通过种种流程优化和自动化能力改善软件开发团队的工作节奏另一方面也可以让大家关注同一个目标彼此信任高效协作调动员工的积极性和创新能力从而让整个团队进入一种积极创造价值的状态而这所带来的影响远非建设一两个工具平台可比拟的。
总结
DevOps作为软件工程的第三次革命在数字化转型的大潮之下几乎成了所有通过交付软件来提供服务的企业的必选项。因为DevOps不仅可以改善企业的软件交付过程实现高质量和高效率兼得同时也可以持续改善企业内部的工程师文化提升员工信心激发员工的活力和价值创造从而帮助企业在VUCA时代占得先机获得更大的成功。如果一家企业真的可以通过DevOps落地达到以上目标而只需要多付出10%20%的年终奖,岂不是大大赚到了吗?
思考题
最后给你留一个思考题如果你觉得DevOps可以解决公司现有的问题想要跟领导申请立项的话你会如何说明DevOps的价值呢
欢迎在留言区写下你的思考和答案,我们一起讨论,共同进步。如果你觉得这篇文章对你有所帮助,欢迎你把文章分享给你的朋友。