共计 8847 个字符,预计需要花费 23 分钟才能阅读完成。
2004 年,软件巨匠 Eric Evans 的不朽著述《畛域驱动设计:软件外围复杂性应答之道》面世,从书名能够看出,这是一本应答软件系统越来越简单的方法论的图书。然而,在过后,中国的软件业才刚刚起步,软件系统还没有那么简单,即便保护了几年,软件进化了,不好保护了,推倒从新开发就好了。因而,在过来的那么多年里,真正使用畛域驱动设计开发(DDD)的团队并不多。一套优良的方法论,因为事实阶段的起因而始终不温不火。
不过,这些年随着中国软件业的疾速倒退,软件规模越来越大,生命周期也越来越长,推倒从新开发的老本和危险越来越大。这时,软件团队急切需要在较低成本的状态下继续保护一个零碎很多年。然而,大失所望。随着工夫的推移,程序越来越乱,保护老本越来越高,软件进化成了有数软件团队的噩梦。
这时,微服务架形成了规模化软件的解决之道。不过,微服务对设计提出了很高的要求,强调“小而专、高内聚”,否则就不能施展出微服务的劣势,甚至可能令问题更蹩脚。
因而,微服务的设计,微服务的拆分都须要畛域驱动设计的领导。那么,畛域驱动为什么能解决软件规模化的问题呢?咱们先从问题的本源谈起,即软件进化。
软件进化的本源
最近 10 年的互联网倒退,从电子商务到挪动互联,再到“互联网 +”与传统行业的互联网转型,是一个十分苦楚的转型过程。而近几年的人工智能与 5G 技术的倒退,又会带动整个产业向着大数据与物联网倒退,另一轮的技术转型曾经拉开帷幕。
那么,在这个过程中,一方面会给咱们带来诸多的挑战,另一方面又会给咱们带来无尽的机会,它会带来更多的新兴市场、新兴产业与全新业务,给咱们带来全新的倒退时机。
然而,在面对全新业务、全新增长点的时候,咱们能不能把握住这样的时机呢?咱们冀望能把握住,但每次回到事实,回到正在保护的零碎时,却令人丧气。咱们的软件总是经验着这样的轮回,软件设计品质最高的时候是第一次设计的那个版本,当第一个版本设计上线当前就开始各种需要变更,这经常又会打乱原有的设计。
因而,需要变更一次,版本迭代一次,软件就批改一次,软件批改一次,品质就降落一次。不管第一次的设计品质有多高,软件经验不了几次变更,就进入一种低质量、难以保护的状态。进而,团队就不得不在这样的状态下,以高老本的形式一直地保护上来,保护很多年。
这时候,保护好原有的业务都十分不易,又如何再去冀望将来更多的全新业务呢?比方,这是一段电商网站领取性能的设计,最后的版本设计品质还是不错的:
当第一个版本上线当前,很快就迎来了第一次变更,变更的需要是减少商品折扣性能,并且这个折扣性能还要分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣。当咱们拿到这个需要时怎么做呢?很简略,减少一个 if 语句,if 限时折扣就怎么怎么样,if 限量折扣就怎么怎么样……代码开始收缩了。
接着,第二次变更须要减少 VIP 会员,除了减少各种金卡、银卡的折扣,还要为会员发放各种福利,让会员享受各种特权。为了实现这些需要,咱们又要在 payoff() 办法中退出更多的代码。
第三次变更减少的是领取形式,除了支付宝领取,还要减少微信领取、各种银行卡领取、各种领取平台领取,此时又要塞入一大堆代码。通过这三次变更,你能够设想当初的 payoff() 办法是什么样子了吧,变更是不是就能够完结了呢?其实不能,接着还要减少更多的秒杀、预订、闪购、众筹,以及各种返券。程序变得越来越乱而难以浏览和保护,每次变更也变得越来越艰难。
问题来了:为什么软件会进化,会随着变更而设计品质降落呢?在这个问题上,咱们必须寻找到问题的本源,能力隔靴搔痒、解决问题。
要探寻软件进化的本源,先要从探寻软件的实质及其法则开始,软件的实质就是对真实世界的模仿,每个软件都能在真实世界中找到它的影子。因而,软件中业务逻辑正确与否的唯一标准就是是否与真实世界统一。如果统一,则软件是 OK 的;不统一,则用户会提 Bug、提新需要。
在这里发现了一个十分重要的线索,那就是,软件要做成什么样,既不禁咱们来决定,也不禁用户来决定,而是由主观世界决定。用户为什么总在改需要,是因为他们也不确定主观世界的规定,只有遇到问题了他们能力想得起来。因而,对于咱们来说,与其气宇轩昂地依照用户的要求去做软件,不如在充沛了解业务的根底下来剖析软件,这样会更有利于咱们缩小软件维护的老本。
那么,真实世界是怎么的,咱们就怎么开发软件,不就简略了吗?其实并非如此,因为真实世界是非常复杂的,要深刻理解真实世界中的这些业务逻辑是须要一个过程的。因而,咱们最后只能意识真实世界中那些简略、清晰、易于了解的业务逻辑,把它们做到咱们的软件里,即每个软件的第一个版本的需要总是那么清晰明了、易于设计。
然而,当咱们把第一个版本的软件交付用户应用的时候,用户却会发现,还有很多不简略、不明了、不易于了解的业务逻辑没做到软件里。这在应用软件的过程中很不不便,和实在业务不统一,因而用户就会提 Bug、提新需要。
在咱们一直地修复 Bug,实现新需要的过程中,软件的业务逻辑也会越来越靠近真实世界,使得咱们的软件越来越业余,让用户感觉越来越好用。然而,在软件越来越靠近真实世界的过程中,业务逻辑就会变得越来越简单,软件规模也越来越宏大。
你肯定有这样一个意识:简略软件有简略软件的设计,简单软件有简单软件的设计。
比方,当初的需要就是将用户订单依照“单价 × 数量”公式来计算应酬金额,那么在一个 PaymentBus 类中减少一个 payoff() 办法即可,这样的设计没有问题。不过,如果当初的须要在付款的过程中计算各种折扣、各种优惠、各种返券,那么咱们必然会做成一个简单的程序结构。
然而,真实情况却不是这样的。真实情况是,起初咱们拿到的需要是那个简略需要,而后在简略需要的根底上进行了设计开发。但随着软件的一直变更,软件业务逻辑变得越来越简单,软件规模不断扩大,逐步由一个简略软件转变成一个简单软件。
这时,如果要放弃软件设计品质不进化,就该当逐渐调整软件的程序结构,逐步由简略的程序结构转变为简单的程序结构。如果咱们总是这样做,就能始终保持软件的设计品质,不过十分遗憾的是,咱们以往在保护软件的过程中却不是这样做的,而是一直地在原有简略软件的程序结构下,往 payoff() 办法中塞代码,这样做必然会造成软件的进化。
也就是说,软件进化的本源不是版本迭代和需要变更,版本迭代和需要变更只是一个诱因。如果每次软件变更时,适时地进行解耦,进行性能扩大,再实现新的性能,就能放弃高质量的软件设计。但如果在每次软件变更时没有调整程序结构,而是在原有的程序结构上一直地塞代码,软件就会进化。这就是软件倒退的法则,软件进化的本源。
杜绝软件进化:两顶帽子
后面谈到,要放弃软件设计品质不进化,必须在每次需要变更的时候,对原有的程序结构适当地进行调整。那么该当怎么进行调整呢?还是回到后面电商网站付款性能的那个案例,看看每次需要变更该当怎么设计。
在交付第一个版本的根底上,很快第一次需要变更就到来了。第一次需要变更的内容如下。
减少商品折扣性能,该性能分为以下几种类型:
- 限时折扣
- 限量折扣
- 对某类商品进行折扣
- 对某个商品进行折扣
- 不折扣
以往咱们拿到这个需要,就很不沉着地开始改代码,批改成了如下一段代码:
这里减少了的 if else 语句,并不是一种好的变更形式。如果每次都这样变更,那么软件必然就会进化,进入难以保护的状态。这种变更为什么不好呢?因为它违反了“凋谢 - 关闭准则”。
开闭准则(OCP)分为凋谢准则与关闭准则两局部。
- 凋谢准则:咱们开发的软件系统,对于性能扩大是凋谢的(Open for Extension),即当零碎需要产生变更时,能够对软件性能进行扩大,使其满足用户新的需要。
- 关闭准则:对软件代码的批改该当是关闭的(Close for Modification),即在批改软件的同时,不要影响到零碎原有的性能,所以该当在不批改原有代码的根底上实现新的性能。也就是说,在减少新性能的时候,新代码与老代码该当隔离,不能在同一个类、同一个办法中。
后面的设计,在实现新性能的同时,新代码与老代码在同一个类、同一个办法中了,违反了“开闭准则”。怎样才能既满足“开闭准则”,又可能实现新性能呢?在原有的代码上你发现什么都做不了!难道“开闭准则”错了吗?
问题的要害就在于,当咱们在实现新需要时,该当采纳“两顶帽子”的形式进行设计,这种形式就要求在每次变更时,将变更分为两个步骤。
两顶帽子:
- 在不增加新性能的前提下,重构代码,调整原有程序结构,以适应新性能;
- 实现新的性能。
按以上案例为例,为了实现新的性能,咱们在原有代码的根底上,在不增加新性能的前提下调整原有程序结构,咱们抽取出了 Strategy 这样一个接口和“不折扣”这个实现类。这时,原有程序变了吗?没有。然而程序结构却变了,减少了这样一个接口,称之为“可扩大点”。在这个可扩大点的根底上再实现各种折扣,既能满足“凋谢 - 关闭准则”来保障程序品质,又可能满足新的需要。当日后产生新的变更时,什么类型的折扣有变动就批改哪个实现类,增加新的折扣类型就减少新的实现类,保护老本失去升高。
“两顶帽子”的设计形式意义重大。过来,咱们每次在设计软件时总是放心日后的变更,就很不沉着地设计了很多所谓的“灵便设计”。然而,每一种“灵便设计”只能应答一种需要变更,而咱们又不是先知,不晓得日后会产生什么样的变更。最初的后果就是,咱们冀望的变更并没有产生,所做的设计都变成了陈设,它既不起什么作用,还减少了程序复杂度;咱们没有冀望的变更产生了,原有的程序仍然不能解决新的需要,程序又被打回了原形。因而,这样的设计不能真正解决将来变更的问题,被称为“适度设计”。
有了“两顶帽子”,咱们不再须要焦虑,不再须要适度设计,正确的思路该当是“活在明天的格子里做明天的事儿”,也就是为以后的需要进行设计,使其刚刚满足以后的需要。所谓的“高质量的软件设计”就是要把握一个均衡,一方面要满足以后的需要,另一方面要让设计刚刚满足需要,从而使设计最简化、代码起码。这样做,不仅软件设计品质进步了,设计难点也失去了大幅度降低。
简而言之,放弃软件设计不进化的关键在于每次需要变更的设计,只有保障每次需要变更时做出正确的设计,能力保障软件以一种良性循环的形式一直保护上来。这种正确的设计形式就是“两顶帽子”。
然而,在实际“两顶帽子”的过程中,比拟艰难的是第一步。在不增加新性能的前提下,如何重构代码,如何调整原有程序结构,以适应新性能,这是有难度的。很多时候,第一次变更、第二次变更、第三次变更,这些事件还能想分明;但经验了第十次变更、第二十次变更、第三十次变更,这些事件就想不分明了,设计开始迷失方向。
那么,有没有一种办法,让咱们在第十次变更、第二十次变更、第三十次变更时,仍然可能找到正确的设计呢?有,那就是“畛域驱动设计”。
放弃软件品质:畛域驱动
后面谈到,软件的实质就是对真实世界的模仿。因而,咱们会有一种想法,能不能将软件设计与真实世界对应起来,真实世界是什么样子,那么软件世界就怎么设计。如果是这样的话,那么在每次需要变更时,将变更还原到真实世界中,看看真实世界是什么样子的,依据真实世界进行变更。这样,日后不论怎么变更,通过多少轮变更,都依照这样的办法进行设计,就不会迷失方向,设计品质就能够失去保障,这就是“畛域驱动设计”的思维。
那么,如何将真实世界与软件世界对应起来呢?这样的对应就包含以下三个方面的内容:
- 真实世界有什么事物,软件世界就有什么对象;
- 真实世界中这些事物都有哪些行为,软件世界中这些对象就有哪些办法;
- 真实世界中这些事物间都有哪些关系,软件世界中这些对象间就有什么关联。
(真实世界与软件世界的对应图)
在畛域驱动设计中,就将以上三个对应,先做成一个畛域模型,而后通过这个畛域模型领导程序设计;在每次需要变更时,先将需要还原到畛域模型中剖析,依据畛域模型背地的真实世界进行变更,而后依据畛域模型的变更领导软件的变更,设计品质就能够失去进步。
联合电商领取理论演练 DDD
当初,咱们以电商网站的领取性能为例,来演练一下基于 DDD 的软件设计及其变更的过程。
使用 DDD 进行软件设计
开发人员在最开始收到的对于用户付款性能的需要形容是这样的:
- 在用户下单当前,通过下单流程进入付款性能;
- 通过用户档案取得用户名称、地址等信息;
- 记录商品及其数量,并汇总付款金额;
- 保留订单;
- 通过近程调用领取接口进行领取。
以往当拿到这个需要时,开发人员往往草草设计当前就开始编码,设计品质也就不高。
而采纳畛域驱动的形式,在拿到新需要当前,该当先进行需要剖析,设计畛域模型。依照以上业务场景,能够剖析出:
- 该场景中有“订单”,每个订单都对应一个用户;
- 一个用户能够有多个用户地址,但每个订单只能有一个用户地址;
- 此外,一个订单对应多个订单明细,每个订单明细对应一个商品,每个商品对应一个供应商。
最初,咱们对订单能够进行“下单”“付款”“查看订单状态”等操作。因而造成了以下畛域模型图:
有了这样的畛域模型,就能够通过该模型进行以下程序设计:
通过畛域模型的领导,将“订单”分为订单 Service 与值对象,将“用户”分为用户 Service 与值对象,将“商品”分为商品 Service 与值对象……而后,在此基础上实现各自的办法。
商品折扣的需要变更
当电商网站的付款性能依照畛域模型实现了第一个版本的设计后,很快就迎来了第一次需要变更,即减少折扣性能,并且该折扣性能分为限时折扣、限量折扣、某类商品的折扣、某个商品的折扣与不折扣。当咱们拿到这个需要时该当怎么设计呢?很显然,在 payoff() 办法中去插入 if else 语句是不 OK 的。这时,依照畛域驱动设计的思维,该当将需要变更还原到畛域模型中进行剖析,进而依据畛域模型背地的真实世界进行变更。
这是上一个版本的畛域模型,当初咱们要在这个模型的根底上减少折扣性能,并且还要分为限时折扣、限量折扣、某类商品的折扣等不同类型。这时,咱们该当怎么剖析设计呢?
首先要剖析付款与折扣的关系。
付款与折扣是什么关系呢?你可能会认为折扣是在付款的过程中进行的折扣,因而就该当将折扣写到付款中。这样思考对吗?咱们该当基于什么样的思维与准则来设计呢?这时,另外一个重量级的设计准则应该出场了,那就是“繁多职责准则”。
繁多职责准则:软件系统中的每个元素只实现本人职责范畴内的事,而将其余的事交给他人去做,我只是去调用。
繁多职责准则是软件设计中一个十分重要的准则,但如何正确地了解它成为一个十分要害的问题。在这句话中,精确了解的要害就在于“职责”二字,即本人职责的范畴到底在哪里。以往,咱们谬误地了解这个“职责”就是做某一个事,与这个事件相干的所有事件都是它的职责,正因为这个谬误的了解,带来了许多谬误的设计,而将折扣写到付款性能中。那么,怎么才是对“职责”正确的了解呢?
“一个职责就是软件变动的一个起因”是驰名的软件巨匠 Bob 大叔在他的《麻利软件开发:准则、模式与实际》中的表述。但这个表述过于精简,很难粗浅地了解其中的外延。这里我好好解读一下这句话。
先思考一下什么是高质量的代码?你可能立刻会想到“低耦合、高内聚”,以及各种设计准则,但这些评估规范都太“虚”。最间接、最落地的评估规范就是,当用户提出一个需要变更时,为了实现这个变更而批改软件的老本越低,那么软件的设计品质就越高。当来了一个需要变更时,怎样才能让批改软件的老本升高呢?如果为了实现这个需要,须要批改 3 个模块的代码,完后这 3 个模块都须要测试,其保护老本必然是“高”。那么怎样才能降到最低呢?如果只须要批改 1 个模块就能够实现这个需要,保护老本就要低很多了。
那么,怎样才能在每次变更的时候都只批改一个模块就能实现新需要呢?那就须要咱们在平时就一直地整顿代码,将那些因同一个起因而变更的代码都放在一起,而将因不同起因而变更的代码分凋谢,放在不同的模块、不同的类中。这样,当因为这个起因而须要批改代码时,须要批改的代码都在这个模块、这个类中,批改范畴就放大了,保护老本升高了,批改代码带来的危险天然也升高了,设计品质也就进步了。
总之,繁多职责准则要求咱们在保护软件的过程中须要一直地进行整顿,将软件变动同一个起因的代码放在一起,将软件变动不同起因的代码分凋谢。依照这样的设计准则,回到后面那个案例中,那么该当怎么去剖析“付款”与“折扣”之间的关系呢?只须要答复两个问题:
- 当“付款”产生变更时,“折扣”是不是肯定要变?
- 当“折扣”产生变更时,“付款”是不是肯定要变?
当这两个问题的答案是否定时,就阐明“付款”与“折扣”是软件变动的两个不同的起因,那么把它们放在一起,放在同一个类、同一个办法中,适合吗?不适合,就该当将“折扣”从“付款”中提取进去,独自放在一个类中。
同样的情理:
当“限时折扣”产生变更的时候,“限量折扣”是不是肯定要变?
当“限量折扣”产生变更的时候,“某类商品的折扣”是不是肯定要变?
……
最初发现,不同类型的折扣也是软件变动不同的起因。将它们放在同一个类、同一个办法中,适合吗?通过以上剖析,咱们做出了如下设计:
在该设计中,将折扣性能从付款性能中独立进来,做出了一个接口,而后以此为根底设计了各种类型的折扣实现类。这样的设计,当付款性能产生变更时不会影响折扣,而折扣产生变更的时候不会影响付款。同样,当“限时折扣”产生变更时只与“限时折扣”无关,“限量折扣”产生变更时也只与“限量折扣”无关,与其余折扣类型无关。变更的范畴放大了,保护老本就升高了,设计品质进步了。这样的设计就是“繁多职责准则”的真谛。
接着,在这个版本的畛域模型的根底上进行程序设计,在设计时还能够退出一些设计模式的内容,因而咱们进行了如下的设计:
显然,在该设计中退出了“策略模式”的内容,将折扣性能做成了一个折扣策略接口与各种折扣策略的实现类。当哪个折扣类型产生变更时就批改哪个折扣策略实现类;当要减少新的类型的折扣时就再写一个折扣策略实现类,设计品质失去了进步。
VIP 会员的需要变更
在第一次变更的根底上,很快迎来了第二次变更,这次是要减少 VIP 会员,业务需要如下。
减少 VIP 会员性能:
- 对不同类型的 VIP 会员(金卡会员、银卡会员)进行不同的折扣;
- 在领取时,为 VIP 会员发放福利(积分、返券等);
- VIP 会员能够享受某些特权。
咱们拿到这样的需要又该当怎么设计呢?同样,先回到畛域模型,剖析“用户”与“VIP 会员”的关系,“付款”与“VIP 会员”的关系。在剖析的时候,还是答复那两个问题:
- “用户”产生变更时,“VIP 会员”是否要变;
- “VIP 会员”产生变更时,“用户”是否要变。
通过剖析发现,“用户”与“VIP 会员”是两个齐全不同的事物。
- “用户”要做的是用户的注册、变更、登记等操作;
- “VIP 会员”要做的是会员折扣、会员福利与会员特权;
而“付款”与“VIP 会员”的关系是在付款的过程中去调用会员折扣、会员福利与会员特权。
通过以上的剖析,咱们做出了以下版本的畛域模型:
有了这些畛域模型的变更,而后就能够以此作为根底,领导前面程序代码的变更了。
领取形式的需要变更
同样,第三次变更是减少更多的领取形式,咱们在畛域模型中剖析“付款”与“领取形式”之间的关系,发现它们也是软件变动不同的起因。因而,咱们果决做出了这样的设计:
而在设计实现时,因为要与各个第三方的领取零碎对接,也就是要与内部零碎对接。为了使第三方的内部零碎的变更对咱们的影响最小化,在它们两头果决退出了“适配器模式”,设计如下:
通过退出适配器模式,订单 Service 在进行领取时调用的不再是内部的领取接口,而是“领取形式”接口,与内部零碎解耦。只有保障“领取形式”接口是稳固的,那么订单 Service 就是稳固的。比方:
- 当支付宝领取接口产生变更时,影响的只限于支付宝 Adapter;
- 当微信领取接口产生变更时,影响的只限于微信领取 Adapter;
- 当要减少一个新的领取形式时,只须要再写一个新的 Adapter。
日后不管哪种变更,要批改的代码范畴放大了,保护老本天然升高了,代码品质就进步了。
写在最初
软件倒退的法则就是逐渐由简略软件向简单软件转变。简略软件有简略软件的设计,简单软件有简单软件的设计。因而,当软件由简略软件向简单软件转变时,就须要通过两顶帽子适时地对程序结构进行调整,再实现新需要,只有这样能力保障软件不进化。然而,在变更的时候,如何调整代码以适应新的需要呢?
DDD 给了咱们思路:在每次变更的时候,先回到畛域模型,基于业务进行畛域模型的变更。而后,再基于畛域模型的变更,领导程序的变更。这样,不管经验多少次需要变更,始终可能放弃设计品质不进化。这样的设计,能力保障系统始终在低成本的状态下,可继续地一直保护上来。
本文咱们演练了如何使用 DDD 进行软件的设计与变更,以及在设计与变更的过程中如何剖析思考、如何评估代码、如何实现高质量。后续文章,咱们将联合具体案例剖析如何将畛域模型的设计进一步落实到软件系统的微服务设计与数据库设计。
起源:二马读书
作者:范钢 曾任航天信息首席架构师,《大话重构》一书的作者。
申明:文章取得作者受权在 IDCF 社区公众号(devopshub)转发。优质内容共享给思否平台的技术伙伴,如原作者有其余思考请分割小编删除,致谢。
7 月每周四晚 8 点,【冬哥有话说】研发效力工具专场,公众号留言“效力”可获取地址
- 7 月 8 日,LEANSOFT- 周文洋《微软 DevOps 工具链的 “ 爱恨情仇 ”(Azure DevOps)》
- 7 月 15 日,阿里云智能高级产品专家 - 陈逊《复杂型研发合作模式下的效力晋升实际》
- 7 月 22 日,极狐 (GitLab) 解决⽅案架构师 - 张扬分享《基础设施即代码的⾃动化测试摸索》
- 7 月 29 日,字节跳动产品经理 - 胡贤彬分享《自动化测试,如何做到「攻防兼备」?》
- 8 月 5 日,声网 AgoraCICD System 负责人 - 王志分享《从 0 到 1 打造软件交付质量保证的闭环》