first commit
This commit is contained in:
101
专栏/DDD微服务落地实战/00开篇词让我们把DDD的思想真正落地.md
Normal file
101
专栏/DDD微服务落地实战/00开篇词让我们把DDD的思想真正落地.md
Normal file
@ -0,0 +1,101 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词 让我们把 DDD 的思想真正落地
|
||||
你好,我是范钢,曾任航天信息首席架构师,《大话重构》一书的作者。
|
||||
|
||||
作为互联网及大数据转型的实践者和倡导者,我先后参与过数十个国内大型软件项目,涉及国家财政、军工、税务、医疗等领域,多次参与大型遗留系统改造、系统重构等重大项目,对系统优化与改造方面有丰富的实践经验。同时,在提供架构设计、软件重构、微服务、大数据方面的培训与企业咨询的这十余年,我服务过的企业包括中国银行、中国人民银行、华为、中兴、贝尔/诺基亚、西门子、富士康等众多知名企业。
|
||||
|
||||
不管是做研发,还是做培训与咨询的过程中,我发现大部分公司都面临着一系列软件研发、架构转型的难题,而很大一部分难题,我认为是可能通过 DDD 来解决的。但是在实际工作中,我听到关于 DDD 的说法往往是:DDD 是很抽象,难以学习,无从下手;DDD 听着很厉害,但不能落地;DDD 真的适用微服务吗? 不妨现在就请暂时放下成见和疑问,先听听我这些年关于 DDD 的故事。
|
||||
|
||||
我和我的 DDD
|
||||
|
||||
第 1 幕:我与 DDD 的美好邂逅
|
||||
|
||||
记得 2006 年,我怀着无比激动的心情开始研读 Eric Evans 写的《领域驱动设计》一书。这的确是本重量级的巨著,我从中学到了太多软件开发的真谛,随后也开始积极地运用在实践中。
|
||||
|
||||
但是,多年以后,当经历了无数软件项目的磨炼以后,我扪心自问 DDD 真正用起来了吗?没有,只学到了它的思想,但却没有按照它的方法去实践,这是为什么呢?
|
||||
|
||||
DDD 是软件核心复杂性的应对之道,但当时都在忙着开发新项目,如何快速编码开发系统、快速上线才是王道,领域驱动对于客户来说太慢了。并且那个时代,业务也并没有那么复杂,DDD 远远发挥不出应有的优势。但是,最近几年,事情却慢慢发生了变化。
|
||||
|
||||
第 2 幕:令人心塞的遗留系统
|
||||
|
||||
2012 年,我接手了一个遗留系统改造的任务,该系统是 2002 年开发的,其间经历了大大小小数十次变更,程序已经凌乱不堪了,维护的成本也越来越高。此时,需要通过重构好好优化改造一下,但我发现有许多动辄数千行的大函数与大对象,是软件退化的重灾区,为什么会这样?
|
||||
|
||||
深刻思考后,很快意识到问题的根源:这是软件的业务由简单向复杂转变的必然结果。软件会随着变更而越来越复杂、代码也越来越多,这样就不能在原有的简单程序结构里塞代码了,而是要调整程序结构,该解耦的解耦,该拆分的拆分,再实现新的功能,才能保持设计质量。
|
||||
|
||||
但是,怎样调整呢?也许第 1 次、第 2 次、第 3 次变更,我们能想得清楚,但第 10 次、第 20 次、第 30 次变更时,我们就想不清楚了,设计开始迷失方向。怎么办? 我再次陷入了沉思……
|
||||
|
||||
经过几番苦苦的思索与探寻,我终于找到了阻止软件退化的钥匙,那就是 DDD。当系统经过数十次变更,设计迷失方向的根源还是复杂性,即业务逻辑与代码变得越来越复杂而难于理解,这不是个例,而是当今所有软件都必须得面对的难题。
|
||||
|
||||
运用 DDD,当系统业务变得越来越复杂时,将我们对业务的理解绘制成领域模型,就可以正确地指导软件开发。当系统变更时,将变更业务透过领域模型,还原到真实世界,再根据真实世界去变更领域模型,根据领域模型的变更指导程序变更,就能做出正确的设计,从而低成本地持续维护一个系统。这对于如今生命周期越来越长的软件系统来说,显得尤为重要。
|
||||
|
||||
第 3 幕:谁来拯救微服务
|
||||
|
||||
2015 年,互联网技术的飞速发展带给了我们无限发展的空间。越来越多的行业在思考:如何转型互联网?如何开展互联网业务?这时,一个互联网转型的利器——微服务,它恰恰能够帮助很多行业很好地应对互联网业务。于是乎,我们加入了微服务转型的滚滚洪流之中。
|
||||
|
||||
但是,微服务也不是银弹,它也有很多的“坑”。
|
||||
|
||||
|
||||
|
||||
当按照模块拆分微服务以后才发现,每次变更都需要修改多个微服务,不但多个团队都要变更,还要同时打包、同时升级,不仅没有降低维护成本,还使得系统的发布比过去更麻烦,真不如不用微服务。是微服务不好吗?我又陷入了沉思。
|
||||
|
||||
这时我才注意到 Martin Flower 在定义微服务时提到的“小而专”,很多人理解了“小”却忽略了“专”,就会带来微服务系统难于维护的糟糕境地。这里的“专”,就是要“小团队独立维护”,也就是尽量让每次的需求变更交给某个小团队独立完成,让需求变更落到某个微服务上进行变更,唯有这样才能发挥微服务的优势。
|
||||
|
||||
|
||||
|
||||
通过这样的一番解析,才发现微服务的设计真的不仅仅是一个技术架构更迭的事情,而是对原有的设计提出了更高的要求,即“微服务内高内聚,微服务间低耦合”。如何才能更好地做到这一点呢?答案还是 DDD。
|
||||
|
||||
我们转型微服务的重要根源之一就是系统的复杂性,即系统规模越来越大,维护越来越困难,才需要拆分微服务。然而,拆分成微服务以后,并不意味着每个微服务都是各自独立地运行,而是彼此协作地组织在一起。这就好像一个团队,规模越大越需要一些方法来组织,而 DDD 恰恰就是那个帮助我们组织微服务的实践方法。
|
||||
|
||||
第 4 幕:DDD,想说爱你不容易
|
||||
|
||||
2018 年,经过一番挣扎,我终于说服了开发团队开始使用 DDD,在这个过程中发现,要让 DDD 在团队中用得好,还需要一个支持 DDD 与微服务的技术中台。
|
||||
|
||||
有了这个技术中台的支持,开发团队就可以把更多的精力放到对用户业务的理解,对业务痛点的理解,快速开发用户满意的功能并快速交付上。这样,不仅编写代码减少了,技术门槛降低了,还使得日后的变更更加容易,技术更迭也更加方便。因此,我又开始苦苦求索。
|
||||
|
||||
很快,Bob 大叔的整洁架构(Clean Architecture)给了我全新的思路。整洁架构最核心的是业务(图中的黄色与红色部分),即我们通过领域模型分析,最后形成的那些 Service、Entity 与 Value Object。
|
||||
|
||||
然而,整洁架构最关键的设计思想是通过一系列的适配器(图中的绿色部分),将业务代码与技术框架解耦。通过这样的解耦,上层业务开发人员更专注地去开发他们的业务代码,技术门槛得到降低;底层平台架构师则更低成本地进行架构演化,不断地跟上市场与技术的更迭。唯有这样,才能跟上日益激烈的市场竞争。
|
||||
|
||||
|
||||
|
||||
图片来自 Robert C. Martin 的《架构整洁之道》
|
||||
|
||||
不仅如此,我在实践摸索过程中,还创新性地提出了单 Controller、通用仓库、通用工厂,以及完美支持 DDD + 微服务的技术中台架构设计。通过这些设计,开发团队能够更好地将 DDD 落地到项目开发中,真正地打造出一支支理解业务、高质量开发与快速交付的团队。
|
||||
|
||||
这门课能让你学到什么?
|
||||
|
||||
在我的故事里,你有没有看到你自己的影子呢?或者是否会想起在开发中的种种困境呢?
|
||||
|
||||
比如:
|
||||
|
||||
|
||||
作为微服务开发人员的你,是不是不知道如何拆分和设计?从而导致微服务的拆分不合理,使得软件维护与发布很是困难。
|
||||
作为业务开发人员的你,当业务越来越复杂,特别是需要应对不断出现的新需求时,是不是会发现自己改不动了,从而导致开发的质量越来越差,交付速度也越来越慢?
|
||||
作为系统架构师的你,不知道该如何通过领域建模确认和规划系统边界,抑或不知道如何搭建技术中台,支持业务的快速更迭与架构的快速演进?
|
||||
|
||||
|
||||
所以为了带你走出目前的“窘境”,我和我的团队在使用 DDD 的过程中的实战经验,以及那些踩过的“坑”,共同构成了这个专栏的主体内容。
|
||||
|
||||
本专栏综合了重构、高质量软件设计与微服务,将 DDD 的实践最终落实到如何指导开发团队从现有状态逐步转型为领域驱动设计与微服务架构,并通过重构打造支持微服务、支持领域驱动的技术中台,进而实现软件开发交付速度的提升。
|
||||
|
||||
|
||||
模块一,软件复杂性的应对之道(01 ~ 06):通过一系列真实的案例,讲解了如何通过 DDD 去应对越来越复杂的业务系统,并始终保持低成本的维护与高质量的设计。学完这一模块,你将会了解 DDD 的基本概念,以及 DDD 在实际工作中该如何应用。
|
||||
模块二,领域建模过程演练(07 ~ 09):通过一个在线订餐系统,演练了 DDD 从业务建模、事件风暴,到微服务拆分、技术实现的完整开发过程,深入 DDD 的底层原理与具体实现。学完这一模块,你将可以掌握 DDD 的建模过程,以及在微服务系统中的实战运用。
|
||||
模块三,支持 DDD 的微服务技术中台(10 ~ 13):主要讲解了如何运用整洁架构,构建一个既支持 DDD,又支持微服务的技术中台,以及如何通过该中台实现业务的快速交付与技术的架构演化。学完这一模块,你将可以以架构师的角度去架构技术中台,并支持开发团队的快速交付与架构演化。
|
||||
模块四,项目实战演练(14 ~ 15):用两个实战案例去演练,在微服务+人工智能、嵌入式+物联网的项目中,如何运用 DDD 进行业务建模、系统规划与设计实践的过程。这部分可以更好地指导你如何将 DDD 应用到未来的实际项目中。
|
||||
模块五,代码落地演示(16 ~ 17):为你带来 2 个落地实战的代码演示,帮助你将知识与理论落地到开发实战中。这 2 个演示代码简洁流畅,并且复用性也比较高,你可以直接拿你需要的部分来用。
|
||||
|
||||
|
||||
讲师寄语
|
||||
|
||||
最后我想说的是,虽然 DDD 的学习和应用都没有那么容易,但是在这门课里,我将像你身边的一位导师一样,用我这些年的经历、经验、实践与思考,帮你尽可能在轻松高效的状态下学好这门课,并且能够更快、更有效地将 DDD 应用到你的工作中。
|
||||
|
||||
相信我,无论你是高级架构师,还是初级开发人员,DDD 整体的架构方法以及其中的设计思路,都会给你的工作带来能量。
|
||||
|
||||
|
||||
|
||||
|
141
专栏/DDD微服务落地实战/01DDD:杜绝软件退化的利器.md
Normal file
141
专栏/DDD微服务落地实战/01DDD:杜绝软件退化的利器.md
Normal file
@ -0,0 +1,141 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01 DDD :杜绝软件退化的利器
|
||||
2004 年,软件大师 Eric Evans 的不朽著作《领域驱动设计:软件核心复杂性应对之道》面世,从书名可以看出,这是一本应对软件系统越来越复杂的方法论的图书。然而,在当时,中国的软件业才刚刚起步,软件系统还没有那么复杂,即使维护了几年,软件退化了,不好维护了,推倒重新开发就好了。因此,在过去的那么多年里,真正运用领域驱动设计开发(DDD)的团队并不多。一套优秀的方法论,因为现实阶段原因而一直不温不火。
|
||||
|
||||
不过,这些年随着中国软件业的快速发展,软件规模越来越大,生命周期也越来越长,推倒重新开发的风险越来越大。这时,软件团队急切需要在较低成本的状态下持续维护一个系统很多年。然而,事与愿违。随着时间的推移,程序越来越乱,维护成本越来越高,软件退化成了无数软件团队的噩梦。
|
||||
|
||||
这时,微服务架构成了规模化软件的解决之道。不过,微服务对设计提出了很高的要求,强调“小而专、高内聚”,否则就不能发挥出微服务的优势,甚至可能令问题更糟糕。
|
||||
|
||||
因此,微服务的设计,微服务的拆分都需要领域驱动设计的指导。那么,领域驱动为什么能解决软件规模化的问题呢? 我们先从问题的根源谈起,即软件退化。
|
||||
|
||||
软件退化的根源
|
||||
|
||||
最近 10 年的互联网发展,从电子商务到移动互联,再到“互联网+”与传统行业的互联网转型,是一个非常痛苦的转型过程。而近几年的人工智能与 5G 技术的发展,又会带动整个产业向着大数据与物联网发展,另一轮的技术转型已经拉开帷幕。
|
||||
|
||||
那么,在这个过程中,一方面会给我们带来诸多的挑战,另一方面又会给我们带来无尽的机会,它会带来更多的新兴市场、新兴产业与全新业务,给我们带来全新的发展机遇。
|
||||
|
||||
然而,在面对全新业务、全新增长点的时候,我们能不能把握住这样的机遇呢?我们期望能把握住,但每次回到现实,回到正在维护的系统时,却令人沮丧。我们的软件总是经历着这样的轮回,软件设计质量最高的时候是第一次设计的那个版本,当第一个版本设计上线以后就开始各种需求变更,这常常又会打乱原有的设计。
|
||||
|
||||
因此,需求变更一次,软件就修改一次,软件修改一次,质量就下降一次。不论第一次的设计质量有多高,软件经历不了几次变更,就进入一种低质量、难以维护的状态。进而,团队就不得不在这样的状态下,以高成本的方式不断地维护下去,维护很多年。
|
||||
|
||||
这时候,维护好原有的业务都非常不易,又如何再去期望未来更多的全新业务呢?比如,这是一段电商网站支付功能的设计,最初的版本设计质量还是不错的:
|
||||
|
||||
|
||||
|
||||
当第一个版本上线以后,很快就迎来了第一次变更,变更的需求是增加商品折扣功能,并且这个折扣功能还要分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣。当我们拿到这个需求时怎么做呢?很简单,增加一个 if 语句,if 限时折扣就怎么怎么样,if 限量折扣就怎么怎么样……代码开始膨胀了。
|
||||
|
||||
接着,第二次变更需要增加 VIP 会员,除了增加各种金卡、银卡的折扣,还要为会员发放各种福利,让会员享受各种特权。为了实现这些需求,我们又要在 payoff() 方法中加入更多的代码。
|
||||
|
||||
第三次变更增加的是支付方式,除了支付宝支付,还要增加微信支付、各种银行卡支付、各种支付平台支付,此时又要塞入一大堆代码。经过这三次变更,你可以想象现在的 payoff() 方法是什么样子了吧,变更是不是就可以结束了呢?其实不能,接着还要增加更多的秒杀、预订、闪购、众筹,以及各种返券。程序变得越来越乱而难以阅读,每次变更也变得越来越困难。
|
||||
|
||||
|
||||
|
||||
问题来了:为什么软件会退化,会随着变更而设计质量下降呢?在这个问题上,我们必须寻找到问题的根源,才能对症下药、解决问题。
|
||||
|
||||
要探寻软件退化的根源,先要从探寻软件的本质及其规律开始,软件的本质就是对真实世界的模拟,每个软件都能在真实世界中找到它的影子。因此,软件中业务逻辑正确与否的唯一标准就是是否与真实世界一致。如果一致,则软件是 OK 的;不一致,则用户会提 Bug、提新需求。
|
||||
|
||||
在这里发现了一个非常重要的线索,那就是,软件要做成什么样,既不由我们来决定,也不由用户来决定,而是由客观世界决定。用户为什么总在改需求,是因为他们也不确定客观世界的规则,只有遇到问题了他们才能想得起来。因此,对于我们来说,与其唯唯诺诺地按照用户的要求去做软件,不如主动地理解业务的基础上去分析软件,而后者会更有利于我们减少变更的成本。
|
||||
|
||||
那么,真实世界是怎样,我们就怎样开发软件,不就简单了吗?其实并非如此,因为真实世界是非常复杂的,要深刻理解真实世界中的这些业务逻辑是需要一个过程的。因此,我们最初只能认识真实世界中那些简单、清晰、易于理解的业务逻辑,把它们做到我们的软件里,即每个软件的第一个版本的需求总是那么清晰明了、易于设计。
|
||||
|
||||
然而,当我们把第一个版本的软件交付用户使用的时候,用户却会发现,还有很多不简单、不明了、不易于理解的业务逻辑没做到软件里。这在使用软件的过程中很不方便,和真实业务不一致,因此用户就会提 Bug、提新需求。
|
||||
|
||||
在我们不断地修复 Bug,实现新需求的过程中,软件的业务逻辑也会越来越接近真实世界,使得我们的软件越来越专业,让用户感觉越来越好用。但是,在软件越来越接近真实世界的过程中,业务逻辑就会变得越来越复杂,软件规模也越来越庞大。
|
||||
|
||||
你一定有这样一个认识:简单软件有简单软件的设计,复杂软件有复杂软件的设计。
|
||||
|
||||
比如,现在的需求就是将用户订单按照“单价 × 数量”公式来计算应付金额,那么在一个 PaymentBus 类中增加一个 payoff() 方法即可,这样的设计没有问题。不过,如果现在的需求需要在付款的过程中计算各种折扣、各种优惠、各种返券,那么我们必然会做成一个复杂的程序结构。
|
||||
|
||||
|
||||
|
||||
但是,真实情况却不是这样的。真实情况是,起初我们拿到的需求是那个简单需求,然后在简单需求的基础上进行了设计开发。但随着软件的不断变更,软件业务逻辑变得越来越复杂,软件规模不断扩大,逐渐由一个简单软件转变成一个复杂软件。
|
||||
|
||||
这时,如果要保持软件设计质量不退化,就应当逐步调整软件的程序结构,逐渐由简单的程序结构转变为复杂的程序结构。如果我们总是这样做,就能始终保持软件的设计质量,不过非常遗憾的是,我们以往在维护软件的过程中却不是这样做的,而是不断地在原有简单软件的程序结构下,往 payoff() 方法中塞代码,这样做必然会造成软件的退化。
|
||||
|
||||
也就是说,软件退化的根源不是软件变更,软件变更只是一个诱因。如果每次软件变更时,适时地进行解耦,进行功能扩展,再实现新的功能,就能保持高质量的软件设计。但如果在每次软件变更时没有调整程序结构,而是在原有的程序结构上不断地塞代码,软件就会退化。这就是软件发展的规律,软件退化的根源。
|
||||
|
||||
杜绝软件退化:两顶帽子
|
||||
|
||||
前面谈到,要保持软件设计质量不退化,必须在每次需求变更的时候,对原有的程序结构适当地进行调整。那么应当怎样进行调整呢?还是回到前面电商网站付款功能的那个案例,看看每次需求变更应当怎样设计。
|
||||
|
||||
在交付第一个版本的基础上,很快第一次需求变更就到来了。第一次需求变更的内容如下。
|
||||
|
||||
增加商品折扣功能,该功能分为以下几种类型:
|
||||
|
||||
|
||||
限时折扣
|
||||
限量折扣
|
||||
对某类商品进行折扣
|
||||
对某个商品进行折扣
|
||||
不折扣
|
||||
|
||||
|
||||
以往我们拿到这个需求,就很不冷静地开始改代码,修改成了如下一段代码:
|
||||
|
||||
|
||||
|
||||
这里增加了一段 if 语句,并不是一种好的变更方式。如果每次都这样变更,那么软件必然就会退化,进入难以维护的状态。这种变更为什么就不好呢?因为它违反了“开放-封闭原则”。
|
||||
|
||||
开放-封闭原则(OCP) 分为开放原则与封闭原则两部分。
|
||||
|
||||
|
||||
开放原则:我们开发的软件系统,对于功能扩展是开放的(Open for Extension),即当系统需求发生变更时,可以对软件功能进行扩展,使其满足用户新的需求。
|
||||
封闭原则:对软件代码的修改应当是封闭的(Close for Modification),即在修改软件的同时,不要影响到系统原有的功能,所以应当在不修改原有代码的基础上实现新的功能。也就是说,在增加新功能的时候,新代码与老代码应当隔离,不能在同一个类、同一个方法中。
|
||||
|
||||
|
||||
前面的设计,在实现新功能的同时,新代码与老代码在同一个类、同一个方法中了,违反了“开放-封闭原则”。怎样才能既满足“开放-封闭原则”,又能够实现新功能呢?在原有的代码上你发现什么都做不了!难道“开放-封闭原则”错了吗?
|
||||
|
||||
问题的关键就在于,当我们在实现新需求时,应当采用“两顶帽子”的方式进行设计,这种方式就要求在每次变更时,将变更分为两个步骤。
|
||||
|
||||
两顶帽子:
|
||||
|
||||
|
||||
在不添加新功能的前提下,重构代码,调整原有程序结构,以适应新功能;
|
||||
实现新的功能。
|
||||
|
||||
|
||||
按以上案例为例,为了实现新的功能,我们在原有代码的基础上,在不添加新功能的前提下调整原有程序结构,我们抽取出了 Strategy 这样一个接口和“不折扣”这个实现类。这时,原有程序变了吗?没有。但是程序结构却变了,增加了这样一个接口,称为“可扩展点”。在这个可扩展点的基础上再实现各种折扣,既能满足“开放-封闭原则”来保证程序质量,又能够满足新的需求。当日后发生新的变更时,什么类型的折扣就修改哪个实现类,添加新的折扣类型就增加新的实现类,维护成本得到降低。
|
||||
|
||||
|
||||
|
||||
“两顶帽子”的设计方式意义重大。过去,我们每次在设计软件时总是担心日后的变更,就很不冷静地设计了很多所谓的“灵活设计”。然而,每一种“灵活设计”只能应对一种需求变更,而我们又不是先知,不知道日后会发生什么样的变更。最后的结果就是,我们期望的变更并没有发生,所做的设计都变成了摆设,它既不起什么作用,还增加了程序复杂度;我们没有期望的变更发生了,原有的程序依然不能解决新的需求,程序又被打回了原形。因此,这样的设计不能真正解决未来变更的问题,被称为“过度设计”。
|
||||
|
||||
有了“两顶帽子”,我们不再需要焦虑,不再需要过度设计,正确的思路应当是“活在今天的格子里做今天的事儿”,也就是为当前的需求进行设计,使其刚刚满足当前的需求。所谓的“高质量的软件设计”就是要掌握一个平衡,一方面要满足当前的需求,另一方面要让设计刚刚满足需求,从而使设计最简化、代码最少。这样做,不仅软件设计质量提高了,设计难点也得到了大幅度降低。
|
||||
|
||||
简而言之,保持软件设计不退化的关键在于每次需求变更的设计,只有保证每次需求变更时做出正确的设计,才能保证软件以一种良性循环的方式不断维护下去。这种正确的设计方式就是“两顶帽子”。
|
||||
|
||||
但是,在实践“两顶帽子”的过程中,比较困难的是第一步。在不添加新功能的前提下,如何重构代码,如何调整原有程序结构,以适应新功能,这是有难度的。很多时候,第一次变更、第二次变更、第三次变更,这些事情还能想清楚;但经历了第十次变更、第二十次变更、第三十次变更,这些事情就想不清楚了,设计开始迷失方向。
|
||||
|
||||
那么,有没有一种方法,让我们在第十次变更、第二十次变更、第三十次变更时,依然能够找到正确的设计呢?有,那就是“领域驱动设计”。
|
||||
|
||||
保持软件质量:领域驱动
|
||||
|
||||
前面谈到,软件的本质就是对真实世界的模拟。因此,我们会有一种想法,能不能将软件设计与真实世界对应起来,真实世界是什么样子,那么软件世界就怎么设计。如果是这样的话,那么在每次需求变更时,将变更还原到真实世界中,看看真实世界是什么样子的,根据真实世界进行变更。这样,日后不论怎么变更,经过多少轮变更,都按照这样的方法进行设计,就不会迷失方向,设计质量就可以得到保证,这就是“领域驱动设计”的思想。
|
||||
|
||||
那么,如何将真实世界与软件世界对应起来呢?这样的对应就包括以下三个方面的内容:
|
||||
|
||||
|
||||
真实世界有什么事物,软件世界就有什么对象;
|
||||
真实世界中这些事物都有哪些行为,软件世界中这些对象就有哪些方法;
|
||||
真实世界中这些事物间都有哪些关系,软件世界中这些对象间就有什么关联。
|
||||
|
||||
|
||||
|
||||
|
||||
真实世界与软件世界的对应图
|
||||
|
||||
在领域驱动设计中,就将以上三个对应,先做成一个领域模型,然后通过这个领域模型指导程序设计;在每次需求变更时,先将需求还原到领域模型中分析,根据领域模型背后的真实世界进行变更,然后根据领域模型的变更指导软件的变更,设计质量就可以得到提高。
|
||||
|
||||
总结
|
||||
|
||||
总之,软件发展的规律就是逐步由简单软件向复杂软件转变。简单软件有简单软件的设计,复杂软件有复杂软件的设计。因此,当软件由简单软件向复杂软件转变时,就需要通过两顶帽子适时地对程序结构进行调整,再实现新需求,只有这样才能保证软件不退化。然而,在变更的时候,如何调整代码以适应新的需求呢?
|
||||
|
||||
DDD 给了我们思路:在每次变更的时候,先回到领域模型,基于业务进行领域模型的变更。然后,再基于领域模型的变更,指导程序的变更。这样,不论经历多少次需求变更,始终能够保持设计质量不退化。这样的设计,才能保障系统始终在低成本的状态下,可持续地不断维护下去。
|
||||
|
||||
|
||||
|
||||
|
153
专栏/DDD微服务落地实战/02以电商支付功能为例演练DDD.md
Normal file
153
专栏/DDD微服务落地实战/02以电商支付功能为例演练DDD.md
Normal file
@ -0,0 +1,153 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
02 以电商支付功能为例演练 DDD
|
||||
上一讲我们花了不少篇幅讲解了软件退化的根源,以及 DDD 如何解决软件退化的问题。现在,我们以电商网站的支付功能为例,来重新演练一下基于 DDD 的软件设计及其变更的过程。
|
||||
|
||||
运用 DDD 进行软件设计
|
||||
|
||||
开发人员在最开始收到的关于用户付款功能的需求描述是这样的:
|
||||
|
||||
|
||||
在用户下单以后,经过下单流程进入付款功能;
|
||||
通过用户档案获得用户名称、地址等信息;
|
||||
记录商品及其数量,并汇总付款金额;
|
||||
保存订单;
|
||||
通过远程调用支付接口进行支付。
|
||||
|
||||
|
||||
以往当拿到这个需求时,开发人员往往草草设计以后就开始编码,设计质量也就不高。
|
||||
|
||||
而采用领域驱动的方式,在拿到新需求以后,应当先进行需求分析,设计领域模型。 按照以上业务场景,可以分析出:
|
||||
|
||||
|
||||
该场景中有“订单”,每个订单都对应一个用户;
|
||||
一个用户可以有多个用户地址,但每个订单只能有一个用户地址;
|
||||
此外,一个订单对应多个订单明细,每个订单明细对应一个商品,每个商品对应一个供应商。
|
||||
|
||||
|
||||
|
||||
|
||||
最后,我们对订单可以进行“下单”“付款”“查看订单状态”等操作。因此形成了以下领域模型图:
|
||||
|
||||
|
||||
|
||||
有了这样的领域模型,就可以通过该模型进行以下程序设计:
|
||||
|
||||
|
||||
|
||||
通过领域模型的指导,将“订单”分为订单 Service 与值对象,将“用户”分为用户 Service 与值对象,将“商品”分为商品 Service 与值对象……然后,在此基础上实现各自的方法。
|
||||
|
||||
商品折扣的需求变更
|
||||
|
||||
当电商网站的付款功能按照领域模型完成了第一个版本的设计后,很快就迎来了第一次需求变更,即增加折扣功能,并且该折扣功能分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣与不折扣。当我们拿到这个需求时应当怎样设计呢?很显然,在 payoff() 方法中去插入 if 语句是不 OK 的。这时,按照领域驱动设计的思想,应当将需求变更还原到领域模型中进行分析,进而根据领域模型背后的真实世界进行变更。
|
||||
|
||||
|
||||
|
||||
这是上一个版本的领域模型,现在我们要在这个模型的基础上增加折扣功能,并且还要分为限时折扣、限量折扣、某类商品的折扣等不同类型。这时,我们应当怎么分析设计呢?
|
||||
|
||||
首先要分析付款与折扣的关系。
|
||||
|
||||
付款与折扣是什么关系呢?你可能会认为折扣是在付款的过程中进行的折扣,因此就应当将折扣写到付款中。这样思考对吗?我们应当基于什么样的思想与原则来设计呢?这时,另外一个重量级的设计原则应该出场了,那就是“单一职责原则”。
|
||||
|
||||
单一职责原则:软件系统中的每个元素只完成自己职责范围内的事,而将其他的事交给别人去做,我只是去调用。
|
||||
|
||||
单一职责原则是软件设计中一个非常重要的原则,但如何正确地理解它成为一个非常关键的问题。在这句话中,准确理解的关键就在于“职责”二字,即自己职责的范围到底在哪里。以往,我们错误地理解这个“职责”就是做某一个事,与这个事情相关的所有事情都是它的职责,正因为这个错误的理解,带来了许多错误的设计,而将折扣写到付款功能中。那么,怎样才是对“职责”正确的理解呢?
|
||||
|
||||
“一个职责就是软件变化的一个原因”是著名的软件大师 Bob 大叔在他的《敏捷软件开发:原则、模式与实践》中的表述。但这个表述过于精简,很难深刻地理解其中的内涵,从而不能有效地提高我们的设计质量。这里我好好解读一下这句话。
|
||||
|
||||
先思考一下什么是高质量的代码。你可能立即会想到“低耦合、高内聚”,以及各种设计原则,但这些评价标准都太“虚”。最直接、最落地的评价标准就是,当用户提出一个需求变更时,为了实现这个变更而修改软件的成本越低,那么软件的设计质量就越高。 当来了一个需求变更时,怎样才能让修改软件的成本降低呢?如果为了实现这个需求,需要修改 3 个模块的代码,完后这 3 个模块都需要测试,其维护成本必然是“高”。那么怎样才能降到最低呢?维护 0 个模块的代码?那显然是不可能的,因此最现实的方案就是只修改 1 个模块,维护成本最低。
|
||||
|
||||
那么,怎样才能在每次变更的时候都只修改一个模块就能实现新需求呢?那就需要我们在平时就不断地整理代码,将那些因同一个原因而变更的代码都放在一起,而将因不同原因而变更的代码分开放,放在不同的模块、不同的类中。这样,当因为这个原因而需要修改代码时,需要修改的代码都在这个模块、这个类中,修改范围就缩小了,维护成本降低了,自然设计质量就提高了。
|
||||
|
||||
总之,单一职责原则要求我们在维护软件的过程中需要不断地进行整理,将软件变化同一个原因的代码放在一起,将软件变化不同原因的代码分开放。 按照这样的设计原则,回到前面那个案例中,那么应当怎样去分析“付款”与“折扣”之间的关系呢?只需要回答两个问题:
|
||||
|
||||
|
||||
当“付款”发生变更时,“折扣”是不是一定要变?
|
||||
当“折扣”发生变更时,“付款”是不是一定要变?
|
||||
|
||||
|
||||
当这两个问题的答案是否定时,就说明“付款”与“折扣”是软件变化的两个不同的原因,那么把它们放在一起,放在同一个类、同一个方法中,合适吗?不合适,就应当将“折扣”从“付款”中提取出来,单独放在一个类中。
|
||||
|
||||
同样的道理:
|
||||
|
||||
|
||||
当“限时折扣”发生变更的时候,“限量折扣”是不是一定要变?
|
||||
当“限量折扣”发生变更的时候,“某类商品的折扣”是不是一定要变?
|
||||
……
|
||||
|
||||
|
||||
最后发现,不同类型的折扣也是软件变化不同的原因。将它们放在同一个类、同一个方法中,合适吗?通过以上分析,我们做出了如下设计:
|
||||
|
||||
|
||||
|
||||
在该设计中,将折扣功能从付款功能中独立出去,做出了一个接口,然后以此为基础设计了各种类型的折扣实现类。这样的设计,当付款功能发生变更时不会影响折扣,而折扣发生变更的时候不会影响付款。同样,当“限时折扣”发生变更时只与“限时折扣”有关,“限量折扣”发生变更时也只与“限量折扣”有关,与其他折扣类型无关。变更的范围缩小了,维护成本就降低了,设计质量提高了。这样的设计就是“单一职责原则”的真谛。
|
||||
|
||||
接着,在这个版本的领域模型的基础上进行程序设计,在设计时还可以加入一些设计模式的内容,因此我们进行了如下的设计:
|
||||
|
||||
|
||||
|
||||
显然,在该设计中加入了“策略模式”的内容,将折扣功能做成了一个折扣策略接口与各种折扣策略的实现类。当哪个折扣类型发生变更时就修改哪个折扣策略实现类;当要增加新的类型的折扣时就再写一个折扣策略实现类,设计质量得到了提高。
|
||||
|
||||
VIP 会员的需求变更
|
||||
|
||||
在第一次变更的基础上,很快迎来了第二次变更,这次是要增加 VIP 会员,业务需求如下。
|
||||
|
||||
增加 VIP 会员功能:
|
||||
|
||||
|
||||
对不同类型的 VIP 会员(金卡会员、银卡会员)进行不同的折扣;
|
||||
在支付时,为 VIP 会员发放福利(积分、返券等);
|
||||
VIP 会员可以享受某些特权。
|
||||
|
||||
|
||||
我们拿到这样的需求又应当怎样设计呢?同样,先回到领域模型,分析“用户”与“VIP 会员”的关系,“付款”与“VIP 会员”的关系。在分析的时候,还是回答那两个问题。
|
||||
|
||||
|
||||
“用户”发生变更时,“VIP 会员”是否要变?
|
||||
“VIP 会员”发生变更时,“用户”是否要变?
|
||||
|
||||
|
||||
通过分析发现,“用户”与“VIP 会员”是两个完全不同的事物。
|
||||
|
||||
|
||||
“用户”要做的是用户的注册、变更、注销等操作;
|
||||
“VIP 会员”要做的是会员折扣、会员福利与会员特权;
|
||||
而“付款”与“VIP 会员”的关系是在付款的过程中去调用会员折扣、会员福利与会员特权。
|
||||
|
||||
|
||||
通过以上的分析,我们做出了以下版本的领域模型:
|
||||
|
||||
|
||||
|
||||
有了这些领域模型的变更,然后就可以以此作为基础,指导后面程序代码的变更了。
|
||||
|
||||
支付方式的需求变更
|
||||
|
||||
同样,第三次变更是增加更多的支付方式,我们在领域模型中分析“付款”与“支付方式”之间的关系,发现它们也是软件变化不同的原因。因此,我们果断做出了这样的设计:
|
||||
|
||||
|
||||
|
||||
而在设计实现时,因为要与各个第三方的支付系统对接,也就是要与外部系统对接。为了使第三方的外部系统的变更对我们的影响最小化,在它们中间果断加入了“适配器模式”,设计如下:
|
||||
|
||||
|
||||
|
||||
通过加入适配器模式,订单 Service 在进行支付时调用的不再是外部的支付接口,而是“支付方式”接口,与外部系统解耦。只要保证“支付方式”接口是稳定的,那么订单 Service 就是稳定的。比如:
|
||||
|
||||
|
||||
当支付宝支付接口发生变更时,影响的只限于支付宝 Adapter;
|
||||
当微信支付接口发生变更时,影响的只限于微信支付 Adapter;
|
||||
当要增加一个新的支付方式时,只需要再写一个新的 Adapter。
|
||||
|
||||
|
||||
日后不论哪种变更,要修改的代码范围缩小了,维护成本自然降低了,代码质量就提高了。
|
||||
|
||||
总结
|
||||
|
||||
这一讲通过以上的过程,我们演练了如何运用 DDD 进行软件的设计与变更,以及在设计与变更的过程中如何分析思考、如何评估代码、如何实现高质量。后面,我们将演练如何将领域模型的设计进一步落实到软件系统的微服务设计与数据库设计。
|
||||
|
||||
|
||||
|
||||
|
246
专栏/DDD微服务落地实战/03DDD是如何落地到数据库设计的?.md
Normal file
246
专栏/DDD微服务落地实战/03DDD是如何落地到数据库设计的?.md
Normal file
@ -0,0 +1,246 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
03 DDD 是如何落地到数据库设计的?
|
||||
过去,系统的软件设计是以数据库设计为核心,当需求确定下来以后,团队首先开始进行数据库设计。因为数据库是各个模块唯一的接口,当整个团队将数据库设计确定下来以后,就可以按照模块各自独立地进行开发了,如下图所示。
|
||||
|
||||
|
||||
|
||||
在上面的过程中,为了提高团队开发速度,尽量让各个模块不要交互,从而达到各自独立开发的效果。但是,随着系统规模越来越大,业务逻辑越来越复杂,我们越来越难于保证各个模块独立不交互了。
|
||||
|
||||
随着软件业的不断发展,软件系统变得越来越复杂,各个模块间的交互也越来越频繁,这时,原有的设计过程已经不能满足我们的需要了。 因为如果要先进行数据库设计,但数据库设计只能描述数据结构,而不能描述系统对这些数据结构的处理。因此,在第一次对整个系统的梳理过程中,只能梳理系统的所有数据结构,形成数据库设计;接着,又要再次梳理整个系统,分析系统对这些数据结构的处理过程,形成程序设计。为什么不能一次性地把整个系统的设计梳理到位呢?
|
||||
|
||||
|
||||
|
||||
现如今,我们已经按照面向对象的软件设计过程来分析设计系统了。当开始需求分析时,首先进行用例模型的设计,分析整个系统要实现哪些功能;接着进行领域模型的设计,分析系统的业务实体。在领域模型分析中,采用类图的形式,每个类可以通过它的属性来表述数据结构,又可以通过添加方法来描述对这个数据结构的处理。因此,在领域模型的设计过程中,既完成了对数据结构的梳理,又确定了系统对这些数据结构的处理,这样就把两项工作一次性地完成了。
|
||||
|
||||
在这个设计过程中,其核心是领域模型的设计。以领域模型作为核心,可以指导系统的数据库设计与程序设计,此时,数据库设计就弱化为了领域对象持久化设计的一种实现方式。
|
||||
|
||||
领域对象持久化的思想
|
||||
|
||||
什么叫领域对象的持久化呢?在当今软件架构设计的主流思想中,面向对象设计成了主流思想,在整个系统运行的过程中,所有的数据都是以领域对象的形式存在的。譬如:
|
||||
|
||||
|
||||
要插入一条记录就是创建一个领域对象;
|
||||
要更新一条记录就是根据 key 值去修改相应的领域对象;
|
||||
删除数据则是摧毁这个领域对象。
|
||||
|
||||
|
||||
假如我们的服务器是一台超级强大的服务器,那实际上不需要任何数据库,直接操作这些领域对象就可以了,但在现实世界中没有那么强大的服务器。因此,必须将暂时不用的领域对象持久化存储到磁盘中,而数据库只是这种持久化存储的一种实现方式。
|
||||
|
||||
按照这种设计思想,我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。当日后需要再次使用这个领域对象时,根据 key 值到数据库查找到这条记录,然后将其恢复成领域对象,应用程序就可以继续使用它了,这就是领域对象持久化存储的设计思想。
|
||||
|
||||
所以,今天的数据库设计,实际上就是将领域对象的设计按照某种对应关系,转换成数据库的设计。同时,随着整个产业的大数据转型,今后的数据库设计思想也将发生巨大的转变,有可能数据库就不一定是关系型数据库了,也许是 NoSQL 数据库或者大数据平台。数据库的设计也不一定遵循 3NF(第三范式)了,可能会增加更多的冗余,甚至是宽表。
|
||||
|
||||
数据库设计在发生剧烈的变化,但唯一不变的是领域对象。这样,当系统在大数据转型时,可以保证业务代码不变,变化的是数据访问层(DAO)。这将使得日后大数据转型的成本更低,让我们更快地跟上技术快速发展的脚步。
|
||||
|
||||
领域模型的设计
|
||||
|
||||
此外,这里有个有趣的问题值得探讨:领域模型的设计到底是谁的职责,是需求分析人员还是设计开发人员?我认为,它是两个角色相互协作的产物。而未来敏捷开发的组织形成,团队将更加扁平化。过去是需求分析人员做需求分析,然后交给设计人员设计开发,这种方式就使得软件设计质量低下而结构臃肿。未来“大前端”的思想将支持更多设计开发人员直接参与需求分析,实现从需求分析到设计开发的一体化组织形式。这样,领域模型就成为了设计开发人员快速理解需求的利器。
|
||||
|
||||
|
||||
|
||||
总之,DDD 的数据库设计实际上已经变成了:以领域模型为核心,如何将领域模型转换成数据库设计的过程。那么怎样进行转换呢?在领域模型中是一个一个的类,而在数据库设计中是一个一个的表,因此就是将类转换成表的过程。
|
||||
|
||||
|
||||
|
||||
上图是一个绩效考核系统的领域模型图,该绩效考核系统首先进行自动考核,发现一批过错,然后再给一个机会,让过错责任人对自己的过错进行申辩。这时,过错责任人可以填写一张申辩申请单,在申辩申请单中有多个明细,每个明细对应一个过错行为,每个过错行为都对应了一个过错类型,这样就形成了一个领域模型。
|
||||
|
||||
接着,要将这个领域模型转换成数据库设计,怎么做呢?很显然,领域模型中的一个类可以转换成数据库中的一个表,类中的属性可以转换成表中的字段。但这里的关键是如何处理类与类之间的关系,如何转换成表与表之间的关系。这时候,就有 5 种类型的关系需要转换,即传统的 4 种关系 + 继承关系。
|
||||
|
||||
传统的 4 种关系
|
||||
|
||||
传统的关系包含一对一、多对一、一对多、多对多这 4 种,它们既存在于类与类之间,又存在于表与表之间,所以可以直接进行转换。
|
||||
|
||||
1. 一对一关系
|
||||
|
||||
在以上案例中,“申辩申请单明细”与“过错行为”就是一对“一对一”关系。在该关系中,一个“申辩申请单明细”必须要对应一个“过错行为”,没有一个“过错行为”的对应就不能成为一个“申辩申请单明细”。这种约束在数据库设计时,可以通过外键来实现。但是,一对一关系还有另外一个约束,那就是一个“过错行为”最多只能有一个“申辩申请单明细”与之对应。
|
||||
|
||||
也就是说,一个“过错行为”可以没有“申辩申请单明细”与之对应,但如果有,最多只能有一个“申辩申请单明细”与之对应,这个约束暗含的是一种唯一性的约束。因此,将过错行为表中的主键,作为申辩申请单明细表的外键,并将该字段升级为申辩申请单明细表的主键。
|
||||
|
||||
|
||||
|
||||
2. 多对一关系
|
||||
|
||||
是日常的分析设计中最常见的一种关系。在以上案例中,一个过错行为对应一个税务人员、一个纳税人与一个过错类型;同时,一个税务人员,或纳税人,或过错类型,都可以对应多个过错行为。它们就形成了“多对一”关系。在数据库设计时,通过外键就可以建立这种“多对一”关系。因此,我们进行了如下数据库的设计:
|
||||
|
||||
|
||||
|
||||
多对一关系在数据库设计上比较简单,然而落实到程序设计时,需要好好探讨一下。比如,以上案例,在按照这样的方式设计以后,在查询时往往需要在查询过错行为的同时,显示它们对应的税务人员、纳税人与过错类型。这时,以往的设计是增加一个 join 语句。然而,这样的设计在随着数据量不断增大时,查询性能将受到极大的影响。
|
||||
|
||||
也就是说,join 操作往往是关系型数据库在面对大数据时最大的瓶颈之一。因此,一个更好的方案就是先查询过错行为表,分页,然后再补填当前页的其他关联信息。这时,就需要在“过错行为”这个值对象中通过属性变量,增加对税务人员、纳税人与过错类型等信息的引用。
|
||||
|
||||
3. 一对多关系
|
||||
|
||||
该关系往往表达的是一种主-子表的关系。譬如,以上案例中的“申辩申请单”与“申辩申请单明细”就是一对“一对多”关系。除此之外,订单与订单明细、表单与表单明细,都是一对多关系。一对多关系在数据库设计上比较简单,就是在子表中增加一个外键去引用主表中的主键。比如本案例中,申辩申请单明细表通过一个外键去引用申辩申请单表中的主键,如下图所示。
|
||||
|
||||
|
||||
|
||||
除此之外,在程序的值对象设计时,主对象中也应当有一个集合的属性变量去引用子对象。如本例中,在“申辩申请单”值对象中有一个集合属性去引用“申辩申请单明细”。这样,当通过申辩申请单号查找到某个申辩申请单时,同时就可以获得它的所有申辩申请单明细,如下代码所示:
|
||||
|
||||
public class Sbsqd {
|
||||
|
||||
private Set<SbsqdMx> sbsqdMxes;
|
||||
|
||||
public void setSbsqdMxes(Set<SbsqdMx> sbsqdMxes){
|
||||
|
||||
this.sbsqdMxes = sbsqdMxes;
|
||||
|
||||
}
|
||||
|
||||
public Set<SbsqdMx> getSbsqdMxes(){
|
||||
|
||||
return this.sbsqdMxes;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
4. 多对多关系
|
||||
|
||||
比较典型的例子就是“用户角色”与“功能权限”。一个“用户角色”可以申请多个“功能权限”;而一个“功能权限”又可以分配给多个“用户角色”使用,这样就形成了一个“多对多”关系。这种多对多关系在对象设计时,可以通过一个“功能-角色关联类”来详细描述。因此,在数据库设计时就可以添加一个“角色功能关联表”,而该表的主键就是关系双方的主键进行的组合,形成的联合主键,如下图所示:
|
||||
|
||||
|
||||
|
||||
以上是领域模型和数据库都有的 4 种关系。因此,在数据库设计时,直接将相应的关系转换成数据库设计就可以了。同时,在数据库设计时还要将它们进一步细化。如在领域模型中,不论对象还是属性,在命名时都采用中文,这样有利于沟通与理解。但到了数据库设计时,就要将它们细化为英文命名,或者汉语拼音首字母,同时还要确定它们的字段类型与是否为空等其他属性。
|
||||
|
||||
继承关系的 3 种设计
|
||||
|
||||
第 5 种关系就不太一样了:继承关系是在领域模型设计中有,但在数据库设计中却没有。 如何将领域模型中的继承关系转换成数据库设计呢?有 3 种方案可以选择。
|
||||
|
||||
1. 继承关系的第一种方案
|
||||
|
||||
首先,看看以上案例。“执法行为”通过继承分为“正确行为”和“过错行为”。如果这种继承关系的子类不多(一般就 2 ~ 3 个),并且每个子类的个性化字段也不多(3 个以内)的话,则可以使用一个表来记录整个继承关系。在这个表的中间有一个标识字段,标识表中的每条记录到底是哪个子类,这个字段的前面部分罗列的是父类的字段,后面依次罗列各个子类的个性化字段。
|
||||
|
||||
|
||||
|
||||
采用这个方案的优点是简单,整个继承关系的数据全部都保存在这个表里。但是,它会造成“表稀疏”。在该案例中,如果是一条“正确行为”的记录,则字段“过错类型”与“扣分”永远为空;如果是一条“过错行为”的记录,则字段“加分”永远为空。假如这个继承关系中各子类的个性化字段很多,就会造成该表中出现大量字段为空,称为“表稀疏”。在关系型数据库中,为空的字段是要占用空间的。因此,这种“表稀疏”既会浪费大量存储空间,又会影响查询速度,是需要极力避免的。所以,当子类比较多,或者子类个性化字段多的情况是不适合该方案(第一种方案)的。
|
||||
|
||||
2. 继承关系的第二种方案
|
||||
|
||||
如果执法行为按照考核指标的类型进行继承,分为“考核指标1”“考核指标2”“考核指标3”……如下图所示:
|
||||
|
||||
|
||||
|
||||
并且每个子类都有很多的个性化字段,则采用前面那个方案就不合适了。这时,用另外两个方案进行数据库设计。其中一个方案是将每个子类都对应到一个表,有几个子类就有几个表,这些表共用一个主键,即这几个表的主键生成器是一个,某个主键值只能存在于某一个表中,不能存在于多个表中。每个表的前面是父类的字段,后面罗列各个子类的字段,如下图所示:
|
||||
|
||||
|
||||
|
||||
如果业务需求是在前端查询时,每次只能查询某一个指标,那么采用这种方案就能将每次查询落到某一个表中,方案就最合适。但如果业务需求是要查询某个过错责任人涉及的所有指标,则采用这种方案就必须要在所有的表中进行扫描,那么查询效率就比较低,并不适用。
|
||||
|
||||
3. 继承关系的第三种方案
|
||||
|
||||
如果业务需求是要查询某个过错责任人涉及的所有指标,则更适合采用以下方案,将父类做成一个表,各个子类分别对应各自的表(如图所示)。这样,当需要查询某个过错责任人涉及的所有指标时,只需要查询父类的表就可以了。如果要查看某条记录的详细信息,再根据主键与类型字段,查询相应子类的个性化字段。这样,这种方案就可以完美实现该业务需求。
|
||||
|
||||
|
||||
|
||||
综上所述,将领域模型中的继承关系转换成数据库设计有 3 种方案,并且每个方案都有各自的优缺点。因此,需要根据业务场景的特点与需求去评估,选择哪个方案更适用。
|
||||
|
||||
NoSQL 数据库的设计
|
||||
|
||||
前面我们讲的数据库设计,还是基于传统的关系型数据库、基于第三范式的数据库设计。但是,随着互联网高并发与分布式技术的发展,另一种全新的数据库类型孕育而生,那就是NoSQL 数据库。正是由于互联网应用带来的高并发压力,采用关系型数据库进行集中式部署不能满足这种高并发的压力,才使得分布式 NoSQL 数据库得到快速发展。
|
||||
|
||||
也正因为如此,NoSQL 数据库与关系型数据库的设计套路是完全不同的。关系型数据库的设计是遵循第三范式进行的,它使得数据库能够大幅度降低冗余,但又从另一个角度使得数据库查询需要频繁使用 join 操作,在高并发场景下性能低下。
|
||||
|
||||
所以,NoSQL 数据库的设计思想就是尽量干掉 join 操作,即将需要 join 的查询在写入数据库表前先进行 join 操作,然后直接写到一张单表中进行分布式存储,这张表称为“宽表”。这样,在面对海量数据进行查询时,就不需要再进行 join 操作,直接在这个单表中查询。同时,因为 NoSQL 数据库自身的特点,使得它在存储为空的字段时不占用空间,不担心“表稀疏”,不影响查询性能。
|
||||
|
||||
因此,NoSQL 数据库在设计时的套路就是,尽量在单表中存储更多的字段,只要避免数据查询中的 join 操作,即使出现大量为空的字段也无所谓了。
|
||||
|
||||
|
||||
|
||||
增值税发票票样图
|
||||
|
||||
正因为 NoSQL 数据库在设计上有以上特点,因此将领域模型转换成 NoSQL 数据库时,设计就完全不一样了。比如,这样一张增值税发票,如上图所示,在数据库设计时就需要分为发票信息表、发票明细表与纳税人表,而在查询时需要进行 4 次 join 才能完成查询。但在 NoSQL 数据库设计时,将其设计成这样一张表:
|
||||
|
||||
{ _id: ObjectId(7df78ad8902c)
|
||||
|
||||
fpdm: '3700134140', fphm: '02309723‘,
|
||||
|
||||
kprq: '2016-1-25 9:22:45',
|
||||
|
||||
je: 70451.28, se: 11976.72,
|
||||
|
||||
gfnsr: {
|
||||
|
||||
nsrsbh: '370112582247803',
|
||||
|
||||
nsrmc:'联通华盛通信有限公司济南分公司',…
|
||||
|
||||
},
|
||||
|
||||
xfnsr: {
|
||||
|
||||
nsrsbh: '370112575587500',
|
||||
|
||||
nsrmc:'联通华盛通信有限公司济南分公司',…
|
||||
|
||||
},
|
||||
|
||||
spmx: [
|
||||
|
||||
{ qdbz:'00', wp_mc:'蓝牙耳机 车语者S1 蓝牙耳机', sl:2, dj:68.37,… },
|
||||
|
||||
{ qdbz:'00', wp_mc:'车载充电器 新在线', sl:1, dj:11.11,… },
|
||||
|
||||
{ qdbz:'00', wp_mc:'保护壳 非尼膜属 iPhone6 电镀壳', sl:1, dj:24,… }
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
|
||||
在该案例中,对于“一对一”和“多对一”关系,在发票信息表中通过一个类型为“对象”的字段来存储,比如“购方纳税人(gfnsr)”与“销方纳税人(xfnsr)”字段。对于“一对多”和“多对多”关系,通过一个类型为“对象数组”的字段来存储,如“商品明细(spmx)”字段。在这样一个发票信息表中就可以完成对所有发票的查询,无须再进行任何 join 操作。
|
||||
|
||||
同样,采用 NoSQL 数据库怎样实现继承关系的设计呢?由于 NoSQL 数据库自身的特点决定了不用担心“表稀疏”,同时要避免 join 操作,所以比较适合采用第一个方案,即将整个继承关系放到同一张表中进行设计。这时,NoSQL 数据库的每一条记录可以有不一定完全相同的字段,可以设计成这样:
|
||||
|
||||
{ _id: ObjectId(79878ad8902c),
|
||||
|
||||
name: ‘Jack’,
|
||||
|
||||
type: ‘parent’,
|
||||
|
||||
partner: ‘Elizabeth’,
|
||||
|
||||
children: [
|
||||
|
||||
{ name: ‘Tom’, gender: ‘male’ },
|
||||
|
||||
{ name: ‘Mary’, gender: ‘female’}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
{ _id: ObjectId(79878ad8903d),
|
||||
|
||||
name: ‘Bob’,
|
||||
|
||||
type: ‘kid’,
|
||||
|
||||
mother: ‘Anna’,
|
||||
|
||||
father: ‘David’
|
||||
|
||||
}
|
||||
|
||||
|
||||
以上案例是一个用户档案表,有两条记录:Jack 与 Bob。但是,Jack 的类型是“家长”,因此其个性化字段是“伴侣”与“孩子”;而 Bob 的类型是“孩子”,因此他的个性化字段是“父亲”与“母亲”。显然,在 NoSQL 数据库设计时就会变得更加灵活。
|
||||
|
||||
总结
|
||||
|
||||
将领域模型落地到系统设计包含 2 部分内容,本讲演练了第一部分内容——从 DDD 落实到数据库设计的整个过程:
|
||||
|
||||
|
||||
传统的 4 种关系可以直接转换;
|
||||
继承关系有 3 种设计方案;
|
||||
转换成 NoSQL 数据库则是完全不同的思路。
|
||||
|
||||
|
||||
有了 DDD 的指导,可以帮助我们理清数据间的关系,以及对数据的操作。 不仅如此,在未来面对大数据转型时更加从容。
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user