架构六大思维养成记

允许我老生常谈,先从什么是架构说起。

当谈到软件架构的时候你不能只想到spirng、springmvc、mysql,你也真不应该想到它们,虽然它们是你落地的载体。 至少你不能先想到它们 ,软件架构不依赖这些框架或者具体的数据库,这些东西统统需要延后,延后。

正像《架构整洁之道》序言中余晟老师讲到的,架构设计是一门复杂的学问,要综合考虑编码、质量、部署、运维、排障、升级等各种因素,并作出权衡。所以,架构设计就不是指首先想到的那些框架,那么的简单了。

软件架构应该要 独立于框架、独立于UI、独立于数据库、独立于任何外部库。

如果是想要描述架构的话,可以用4+1视图,这里的4是指逻辑视图、实现视图、进程视图、部署视图,这里的1是指场景。

图来自网络

刚才我们也提到了软件架构设计是一件比较复杂的事情,包含了很多种因素,同时呢,它不仅要满足用户的功能需求,某个场景的用户需求流程是怎样的,对应到设计中应该是什么样的模式和设计流程。

还要满足系统的可靠性、可用性、安全性、性能、容量等架构的质量属性,这往往会涉及到基础平台、框架应用、甚至还有硬件。这就需要多方合作才能最终落地实现架构的设计目标,这中间会有软件研发人员、测试人员、运维人员、产品人员、业务运营人员的参与,也就是说我们设计的软件架构包含了他们所有人员的参与。

有这么多角色参与的工程,如果有一个框架或模式能够把它们串起来,是再好不过的了,而4+1视图就可以起到这样的作用。4+1视图可以让架构师自顶向下的去分解架构设计,还可以形成合理的抽象,从而将我们刚才说的这么多角色”拉在一起“。

因为要使得架构落地必须有他们的参与和协同工作。另外,在做架构设计的时候,我们也不能仅仅高谈阔论,总聊”高大上”的理论,虽然那些理论可能是有用的。一个被架构设计指导的软件项目总归是要通过一行行的代码实现的,”脱离了一行行的代码,脱离了具体的细节设计,架构设计就无从谈起”。

以至于在《架构整洁之道》一书中,提到,软件质量,不但依赖于架构及项目管理,而且与代码质量紧密相关,甚至还有说到,“对于代码,应无情地做重构”。所以你在架构设计的时候还要引入一些模式或者原则性的编码指导,比如SOLID原则。

除此之外,我们还要考虑架构的可扩展性,毕竟一个落地的架构,不仅要满足用户、系统的一时的需求,还要满足后面他们持续的需求。如果你要设计一个弹性、”皮实”的架构,至少你要考虑这三方面的事情,避免过度设计,向内依赖设计、面向失败设计。说了以上这么多,到底什么是软件架构设计呢,正像《架构整洁之道》这本书描述的。

所谓软件架构,就是你希望项目在一开始就能做对,但是却不一定能够做的对的决策的集合。

接下来我们讲要具备的六大架构思维。

1、演进式思维

为什么这么说呢,架构设计,架构设计,虽然有【设计】俩字,但架构真不是”设计”出来的,而是演进出来的。优秀的软件架构也不是一成不变的,需要经过不断的打磨,迭代改进。

在《演进式架构》这本书里面,在如何构建可演进式的架构方面给了我们指导性建议,从三方面考虑:适应度函数、增量变化、适当耦合。

我们首先来看什么是适应度函数。

按照这本书中介绍的,适应度函数是进化计算中的一个概念,进化计算有多种机制,通过对每一代软件进行小的变更使方案逐渐成形。另外书中也给了一个明确的定义:架构的适应度函数为某些【架构特征】提供了客观的完整性评估。那什么是架构特征呢。

系统被提的要求可能不太相同,比如有的系统要求高吞吐量和低延迟,有的系统要求安全性要高,有的系统要求自我故障恢复的能力要强,这些呢,都是架构的特征。举一个比较容易理解的适应度函数的例子,就是功能测试,一个测试用例可以看成是某一个单一功能的适应度函数,增量开发应该在测试成功的约束下进行变更。

而适应度函数这个概念,则就很好的体现了这些架构特征的保护,有关适应度函数更多的理解大家可以去详细阅读这本书。

再来看增量变化。

架构应该支持增量开发和增量部署。增量开发也就是,我编写了一个功能的代码,可以立马提交,并可以被进行功能测试,性能测试。增量部署的意思可以理解为AB发布,比如书中举了github的例子,每次功能重构上线,github都先随机挑选1%的用户进行旧功能与新功能同步执行,持续4天输出结果相同时,将用新代码替换掉旧代码。

这是一个非常值得借鉴的思路。

最后,来看适当耦合。

我们反对强耦合,但世界上却没有不耦合的系统,你试想,如果你的系统之间没有一点耦合,企业系统还怎么对外统一提供服务呢。因此,我们才一直强调的是弱耦合,过渡耦合使得架构难以演进,但我们要保持适当耦合。关于适应度函数、增量变化、适当耦合的内容,同时也参考了 知乎上面的这篇文章https://zhuanlan.zhihu.com/p/133517172

软件设计如果又从宏观视角来看的话,包括需求、研发、运维,那么其中维护的成本又是最高的。另外呢,还有一条真理性的警句: 世上没有不出问题的系统。

那么致力于高可用上的成本也是不小的隐性成本,甚至有的公司专门对此有对称职位,比如google就有SRE工程师,全称是 Site Reliability Engineer 网站可靠性工程师。

2、分合思维

讲到分的时候,就不能不提到读写分离,也就是我们说的写主读从,一般这样的场景都是读多写少,那么读多,就需要更多的从,来分摊读的压力。可是,从的数据是从主上面复制过来的,如果从的数量增多,难免给主带来压力,毕竟主还承担了写的请求,这个时候就需要一个额外的从来负责把数据复制到其它的从上面。

也就变成了下面这样的主-从-从结构了。

在回到我们说的主从上面来,主从结构的数据,主要是通过复制,那么,有的同学就会问到了如果复制存在延迟怎么办。的确,下面这位同学在极客时间上面的课程中就问到了这样的问题。

其实,我理解这里问问题的同学是想问,读从的时候,数据延迟了,业务上怎么解决。当然作者的答复也没有错,复制延迟是“天生”的本就没有解。一般的主从架构,是一台主,多台从,适合读多写少的业务场景,大量的读操作请求都落在了从服务器上面。那么如果是写写多的业务场景呢,一般意义上的主从架构就不适合了,这个时候需要引入分散式集群的概念,就是多台主同时负责读写。

这种情况下就需要将请求合理的落到不同的服务器上面,也就是需要结合分配算法的使用,比如区分不同用户的请求落到不同的服务器上面。在分散式集群的指导下,实际上是一种“分”的概念,将数据分散到不同的存储服务器上面,这样集群的方式提高了系统整体写和读的性能。

但同时我们也会面临“合”的问题,比如上面如果按照用户区分来写入各个服务器之后,当我们需要按照订单维度来查询的时候,就需要重新将它们整合在一起,当然合的方式,我们可以使用数据异构的方式,来重新存储一份数据供这种场景使用。

说到“分”,还有一层含义的解读,跟我们说的“拆”异曲同工,也就是拆服务,最后是我们说的微服务,从一个大的单体,被拆成好几个独立的系统服务,这也是一个分的过程。

同样这些服务有各自独立的数据库,在业务上也会面临“合”的问题。这里的“合“又分为两种。一种”合“是上面跟我们上面讲的查询是一致的,也就是需要把散落在各个系统服务上的数据做一个整合查询,比如订单服务对应的订单数据,商品服务对应的商品数据,需要做一个关联查询。

这种情况下的解决方式跟分散式集群环境下的处理方式基本一致,常用的做法就是数据异构一份新的存储数据。另外一种”合“是当我们面临前台服务的时候,需要整体输出一份数据给到用户,这个时候各个微服务系统的上面往往会有一个API GATEWAY层。

此时,就需要一个API组合的功能要开发,那么这个API组合功能理论上来讲有三个地方可以做,一个是前台自己,一个是API GATEWAY层,一个是某个主微服务系统来做。一般是不能让前台服务做的,因为人家是用户端,如果它要来做,就会进行多次REQUEST调用,最好的方式是通过一次REQUEST请求返回。

对于简单的API组合可以放到API GATEWAY来去做,这里一定要注意,这里的简单就是不涉及API返回字段的二次业务计算的。多简单的呢,就是类似下面这张图所展现的,这三个API是并行调用,由网关来统一组合返回给前端。

那涉及到二次业务计算怎么个理解呢,比如一个订单API返回的字段中有订单金额,那么还需要对这个金额进行再计算,这种肯定不能放进API GATEWAY。凡是涉及到字段属性需要再进行业务计算的统统都要放到某个微服务上面去做,或者是有专门的BFF层来去做。

那么,到这里,你可能都能想到了,分是解决问题的一个好方法,没错,当我们试着去解决一个问题的时候都是先对任务来分解。你会发现就连我们的TCP/IP都是分层的。其实有时候在业务上面我们做分的动作,就是来分离变化。

变化是伴随着程序员长久的话题,按照粗粒度来分,一个业务功能对应的代码段,有更新速度的变,有更新原因的变。分层是匹配了更新速度的变,比如网关层和业务领域层,其中权限、流控这样的变更速度跟业务增长的变更速度是不同的。

而我们常说的微服务是匹配了更新原因的变,比如订单服务对应业务和商品服务对应的业务的变更原因也肯定是不同的。

3、高可用思维

系统的高可用定义是什么。主要特点就是【可用】前面有个【高】字,加上了高,就是代表系统在发生故障的情况下仍然是可用的,甚至是在极端故障下依然坚挺。比如,有人挖断了通往你一个机房的光缆,但是,你是双机房,所以用户不受影响,这就是【高】可用的体现,【高】的地方在于你有两个机房。

高可用可以量化么,可以。

计算公式如下,

可用性 = (1 – 年度不可用时间 :heavy_division_sign: 年度总时间)* 100%

比如你常听到的某个服务的可用性为99.99%,也就是我们常说的4个9,就是这么计算出来的,也就是相当于对系统的可用性做了一个量化显示。

那么,反过来讲,确保系统的高可用,也就是通过架构设计减少系统不可用的时间。为了达到这一的高可用我们都经常要做哪些工作呢。让我们首先来看一个正常的互联网分层架构,如下图所示。

在这幅图里面,从客户端请求反向代理,请求web应用服务,请求业务服务,请求数据库,那么大家注意在图中用浅黄色标出来的节点, 只要有多份,再加上故障转移,就能够做到高可用

大家想想看,是不是这样。另外呢,我们还可以从地理位置上设计异地多活、还可以从系统内让系统具备容错能力,比如包括降级限流、熔断、快速失败、线程池隔离等措施。那我们要是都做了这些工作,系统还出现不可用,是怎么回事呢。

我们可以将这个问题看成是一个攻守失衡的问题,守的是上面那些措施,那么攻的就是故障, 当引起故障的变量增速能力大于守的措施能力的时候,便发生了系统的不可用 。比如我们说的快速失败,也就是给我们凡是调用RPC的地方都加上超时时间,当前请求的量足够大,以致于请求量的增速时间大于失败的窗口时间,快速失败措施便也无能为力了。

在同步调用模式下,请求的线程数据变化急剧增加,最终导致整个系统不可用。那么,我们一般都有哪些故障呢,我们可以把上面那张图继续丰富,来一起看下各技术架构层所处的环境,如下图所示。

也就是我们将其分成了IAAS层、PAAS层、SAAS层,因为一个企业的IT系统组成就是由它们来组成的,从这个角度来看,就便于我们认清每层会遇到什么故障了。

(1)、IAAS层的故障

服务器宕机、断电、磁盘满、丢包等等都属于IASS层的故障。

(2)、PAAS层的故障

数据库连接池满、数据库主从延迟,另外我们所使用的一切技术中间件也属于这一次,所以还有比如缓存击穿、缓存热点、RPC配置不可读等等,都属于PAAS层的故障。

(3)、SAAS层的故障

依赖超时、OOM、幂等失效等等我们所常见的这些,都属于SAAS层的故障。

这三层对于我们业务研发人员较为熟悉的是SAAS层和PAAS层的故障,尤其是SAAS层故障距离业务研发人员会更近。

这三层任何一层发生故障都有机会让我们的系统不可用。

我们再来分享一下高可用这三个字,高我们上面介绍了,可用这俩字,我认为就是一种具有包含的味道在里面了。性能不好,对于要求高性能的系统,还能叫做可用吗,安全出了问题,对于要求安全性高的系统,还能叫做可用吗。

所以呢,我个人认为高可用是一个范畴更大的词汇,那么引起不可用的范畴也随之变大了,因为这个时候性能不达标,安全不达标都是不可用了。

举一个秒杀系统的例子,你预估流量不合理,资源服务器准备不足,秒杀降临,系统不可用。又来一次秒杀活动,流量预估重复合理,资源服务器准备充足,也使用上了队列技术,增加了基于redis排队系统的支撑,但是redis遇到了缓存热点问题。

那天只有一个SKU参与秒杀,你设计的一个SKU一个队列,正常情况下一个4c8g的分片可以8-10w用户,但上午10点瞬间来了20w用户,达到了redis单片的瓶颈上限,系统不可用。更可怕的是,被提示不能下单的用户,正在反复的刷你的服务器,因为他们为了等待秒杀,早早的定好了闹钟,就等这一刻,不愿轻易放弃。

再举一个redis缓存失效的例子,你可能会问了,为什么又拿缓存说事,因为redis缓存常常是我们抗量的“必杀器”。那么,如果这样的“必杀器”也出了问题,高可用是不是就“崩坍”了呢。使用缓存的时候,最常用的方法就是在set的时候设置过期时间了,这种使用方式简单、方便,但同时也带来了”纰漏“。

那就是可能会造成“缓存风暴”,怎么理解这个风暴呢,如果短时间内有大量的数据时间过期,这些数据请求就回源到了数据库。本身我们使用redis缓存就是为了增加系统的QPS,redis本身的IOPS也要比数据库高很多,那么在这样的情况下,原本“打”在redis身上的请求,都“回击”到了DB上。

数据库怎么能抗住,本来由redis抗住的量呢,结果可想而知了。

以上说的都是技术层面导致原本高可用的系统变成了不可用,当然类似那样的故障种类还有很多。不过,最后我想跟你从故障处理流程上再来说说。故障和问题是一回事吗,咋一听是,其实严格来讲并不是,比如ITIL对这它俩的定义,我帮你总结了下,并画在了一张PPT里面,截图如下。

现在,让我们谈一谈,出现了线上故障,你第一件事最应该做什么?找出引起故障的根源问题吗?

记住,一定是先止损。

止损的方式有很多,如果你事先有了降级策略,可以立马执行这个策略,如果可以回顾版本,你要立即回顾,如果可以增加机器你要迅速扩容。总之,你要先止损,而不是去苦苦的钻研问题,而不解决故障,找问题可以同步安排其他人员来进行,比如隔离某台服务器,去在上面打出OOM日志。

4、复用思维

在谈复用思维之前,我们先回到架构上。我们有说到当今软件设计的世界流行着四大架构DCI架构、DDD架构、六边形架构和整洁架构,那什么是整洁架构。不好描述对吧,那就直接上图,这张图是Robert C. Martin博客上面的一张整洁架构图。

按照《架构整洁之道》这本书中的介绍,这张图描述的核心思想就是: 向内依赖 。怎么讲,就是外层圆依赖内层圆。

整洁架构也是分层的架构,只不过这里的层,不再是我们之前认为的上中下,这样的层,而是变成了内外层。这里要注意的是,以前的上中下的层次结构中,我们说最上面的就是高层,最下面的是底层,都是由高层依赖底层,比如Controller依赖Service,Service依赖Dao。

到了整洁架构这里面,越靠近中心的层,越高,也就是内层圆是【高层】,外层圆变成了底层,而且依赖关系只能是外层圆依赖内层圆,也就是变成了低层依赖高层, 就是上面我们说的向内依赖,记住这是整洁架构的核心思想。

在只能外层依赖内层的机制下,或者说是规则,那么既然是规则,就是我们要遵守,但是,如果业务上就有,内层依赖外层的情况怎么办呢。其实,如果你仔细看,并认真思考的话,你会发现,这张整洁架构图的右下角就描述了这样的场景:从控制器开始,穿过用例,最后执行展示层的代码。

用例需要”依赖“展示层的代码,用例需求把自己的表达通过展示层展示出来。可是,我们不能破坏整洁架构的依赖原则,这种情况下就需要我们采用依赖倒置的方式了。

依赖倒置是一个很巧妙的方式,直接点说就是依赖接口,依赖抽象,或者面向接口编程,或者面向抽象编程,这些都是依赖倒置的表现形式。这样用例没有直接依赖展示层的代码,而是依赖了一个“用例输出端”这样的接口。

我们这里在来聊聊这个依赖倒置,刚刚我们只是说了依赖接口,依赖抽象等等,只是说了依赖,但是【倒置】呢,哪里有倒置的味道呢。搞点小插曲,我还真上网搜了下,给大家截张图。

看到没有,把这位同学捉急成啥样了,把《设计模式之禅》搬出来了,也没有找到倒置在哪里,更有同学去查了【倒】的汉语词典。依赖倒置,不仅仅就是我们说完面向接口编程或者面向抽象编程完事,更深刻的是 一种对事物思考方式的转换。

按照正常的思考方式,我写一个A类需要依赖B类,那么首先我要先定义出这个B类,比如定义出B类的方法等,然后我再写A类,这时的顺序是B->A。

此时,如果这个时候我写一个接口G,在G中定义好了方法,让A类依赖接口G,最后我再让B类去实现接口G,这时的顺序就是A->B, 与原来的思考顺序方式倒置了 。所以,依赖倒置设计原则中的倒置,本质是思考顺序的倒置。

好了,再回到我们的主题上面来,我们现在瞄准上面那个图的靶心,也就是最里面的那个圆圈,业务实体。上面我们说了越往里,越是高层,越是核心,也就是整洁架构所述的“外层圆代表的是机制,内层圆代表的是策略。”那么,当然地,处在靶心位置的业务实体,就是我们整个系统的关键业务逻辑,我们可以看一下《架构整洁之道》中对业务实体的描述。

业务实体这一层中封装的是整个系统的关键业务逻辑,一个实体能是一个带有方法的对象,或者是一系列数据结构和函数,只要这个实体能够被不同的应用程序使用即可。

如果你没有编写企业软件,只是编写简单的应用程序,这些实体就是应用的业务对象,它们封装着最普通的高级别业务规则,你不能希望这些实体对象被一个页面的分页导航功能改变,也不能被安全机制改变,操作实现层面的任何改变不能影响实体层,只有业务需求改变了才可以改变实体。

足见,业务实体是多么重要,关键还有一点,业务实体不会依赖外层圆的任何内容, 这就给了我们做业务复用最强有力的支撑。

终于要到了我们说到复用了。

复用可以分为技术复用和业务复用。技术复用比较好理解,比如,小到你写了一段计算日期的代码,封装到了一个jar,供别人使用,大到比如公司的所有业务线都在使用中间件研发团队的MQ,这些都是指的技术复用。

那么什么是业务复用呢。这个还可以继续细分,有业务实体的复用,业务线的复用,业务产品的复用,它们的层级也是逐渐升高的,业务产品复用的等级最高。

这其中业务实体的复用是其它两种复用的基础,这也就是我们在整洁架构中重点阐述的业务实体层,那个核心圆,所以做好了业务实体的复用是业务复用的关键。

5、领域思维

所有的软件最终是要解决用户问题的,而软件的落地最终是要靠一行行的代码垒起来的,那么这个时候从识别出用户问题到代码实现之间就需要一种过度,而架构设计就是这种过度的【桥梁】。

DDD是架构设计的一种方法,在DDD中的模型驱动设计里面有两种设计方法,一种是战略设计,用来识别用户问题,一种是战术设计,用来指导落地问题的解决方法。因此DDD是可以担当起建设那座桥梁的重任。

如果要对DDD所要干的事情分类的话,可以分为两类,一是建立通用语言,二是进行模型驱动设计,其中在模型驱动设计中又包含战略设计和战术设计。通用语言的介绍,我在上一篇文章里面有介绍过,就是要有一个可以描述业务事物,也可以描述技术人员理解的事物,拉齐业务人员和技术人员的语言,减少他们之间的沟通成本。

要做这件拉齐的事,一般情况下,我们可能会这么干,业务人员和研发人员坐在一起聊,其实,无论怎么个形式,都得聊。可是呢,聊也最好有个聊的形式或者组织方法,这就有了DDD中的事件风暴。给这种组织方法做个类比的话,可以参考敏捷团队的实践。

大家如果参与过敏捷团队组织的话,比较好理解这点,进行敏捷实践的时候一般有好几个会,比如需求梳理会,迭代计划会,以及还有敏捷物理看板,种种这些都是建立在一种组织过程方法上,在这个方法中呢,我们就把需求梳理出来,计划会上把任务确定下来,物理看板把任务流动起来。

如果在具象一点的话,事件风暴是一个参与式工作坊。如果理解了参与式工作坊是干啥的,其实我们就能够理解事件风暴了。按照《工作坊》这本书的介绍

“参与式工作坊”是一个多人数共同参与的场域与过程,且让参与者在参与的过程中能够相互对话沟通、共同思考、进行调查与分析、提出方案或规划,并一起讨论让这个方案如何推动,甚至可以实际行动,这样“聚会”与“一连串的过程”,就叫做参与式工作坊。换句话说,工作坊就是利用一个比较轻松、有趣的互动方式,将上述这些事情串联起来,成为一个有系统的过程。

所以,事件风暴是一种工作坊的形式,在这个过程就是要建立业务与研发人员,在描述需求事物上的通用语言。下面这张图是我从网络上找的,它所展现的这种形式就是我们进行事件风暴工作坊期间的产出,通过这种工作坊把业务和研发人员拉在了一起,冲破了业务和研发人员之间的”那堵墙“。

图自网络

这种工作坊,除了建立通用语言以外呢,还有一件事,那就是要识别出领域事件,因为毕竟人家叫做事件风暴嘛。那么什么是是领域事件呢,举一个京东商城的例子,作为一个商家,他的业务场景之一就是发布商品,那么这里的领域事件就是商品已上架。

作为一个C端消费者,他的业务场景之一就是购买的商品是否发货成功,那么这里的领域事件就是已发货。所以估计大家看出来了,领域事件所描述的就是动作发生之后的结果,反映到代码描述上就是一种动作的过去式。

图自网络

我们再来看模型驱动设计里面的战略设计和战术设计。

战略设计,属于高层设计,这里的高层是相对于抽象和具体来说的,到了代码层面那就是具体,在抽象层面那就是高层,我们抽象的是什么,比如订单业务模型、商品业务模型等等,这些模型就是我们抽象出来的。

领域驱动设计里面的【领域】两个字,透露出了一种【范围】意识,这也就是我们经常在DDD中强调边界的原因,因为范围本身就有边界的概念。再联想到微服务设计要按照功能来进行拆分,这也是为什么DDD能够很好的指导微服务建设的原因之一,它们都强调了【边界】。

这种范围或者边界,对应到DDD中就是域,比如我们已经建立的订单业务模型和商品业务模型,也变成了订单域和商品域。那么战略设计最重要的是要干什么呢,我认为就是为了“归堆”,象上面我们说的边界也好,范围也好,乃至【域】也好,都是为了进行分组,当然,DDD中还有个更专业的词,限界上线文。

图自网络百度搜索

如果说战略设计是站在高层来识别问题,识别范围或者边界,那么战术设计就是来进行解决问题的了,毕竟,软件设计也好,架构设计也好都是要解决问题的,正如我们文章刚开始的时候说的,架构设计就是业务问题和解决方案之间的桥梁。

DDD有了战略设计来识别问题,然后通过战术设计来落地解决问题的方法,自然DDD便担负起了桥梁的角色。在战略设计中,我们已经识别出来了业务域,比如订单域,那么该怎样映射到我们的设计上呢,这就是战术设计干的事情。

战术设计中有很多个DDD概念,实体、值对象、聚合等等,关于这部分知识大家可以参看《实现领域驱动设计》这本书,里面都有更详细的介绍。我们这里简单描述一个例子,比如京东商城上面的订单信息,它有一个订单号,来表示唯一,另外它还有其它的属性,订单金额、下单时间、出库时间、购买人等等,订单号永远不会变,但是其它的属性有可能会变化,比如出库时间可能会因为库存问题而发生变化。

这里的订单就是一个有订单号来表示的实体。

一个订单里面,可能会包含很多个订单项,比如一次购买了3件不同的商品,那么手机是一项,耳机是一项,笔记本是一项,这些项目分别都会一个商品ID。这里的订单项就是一个值对象。在战术设计中就是通过类似上面的方法,把战略设计中识别出来的订单域进行了一次【过度】,进而更贴近了代码的语言,并最终实现当初的订单域中所需要的功能。

最后。我们也常说没有银弹,或者没有双利解,DDD当然也不是双利解,更不是银弹,但领域驱动设计目前在大多数业务场景中都能很好的被使用,这足够了。关于学习DDD的方法,亘古不变的真理,理论结合实践,理论的知识我们可以通过阅读书籍,比如《实现领域驱动设计》,实践呢,就是要我们在工作过程中去不断的尝试了。

都说师傅领进门,修行在个人,如果你是DDD的初学者,那么首先有个知识的框架就再好不过了,这方面推荐郑晔老师在极客专栏《软件设计之美》中的关于设计一个软件的软件设计方法中所讲到的三篇文章,大家可以去看下。

6、面向对象思维

我把这个思维放到最后一个,恰是因为它是当今软件设计之根本,它的本质是软件如何才能做到是软的。那要先定义什么才算是软的,你说,能屈能伸,就是软,对,弹性。啥叫弹性,“打不死的必使你强大”,就是弹性的一种体现,软件系统出了局部问题,还仍然能够提供服务,此时就是有弹性。

一个软件系统,是由什么来组成的,最大的组成部分就是代码了,代码谁写的,程序员,代码在哪里执行,机器。 程序员负责管代码,代码负责管机器,程序员不需要管机器。 如果有一天机器“不听话了”,为啥不听话了呢,有一段代码突然被hang住,随着请求的访问逐渐增大,线程数持续飚高。

又转回来,代码谁写的呢,程序员,当时程序员为什么没有考虑到做一次线程隔离呢。这个原因,我说本质上的,可能是,如下,我是说可能。

大多数人的目标不是设计出一个优质的软件或架构,而是快速地解决一个具体的问题,完成自己的工作。

在经过产品PRD评审之后,工作中发现太多的程序员都只关注产品里面的功能如何实现,却未曾考虑如进行代码设计。当然,这里面,很是能够理解,处于垂直业务线研发人员的辛苦,但是呢, 若只是为了仅仅完成业务功能的实现,和一个优秀程序员的距离始终还是那么远。

有时候,经常被聊的是架构设计,那其实代码也是需要设计的,而不仅仅是为了完成某一个业务功能了事。比如如何将业务逻辑实现和流程控制分离,如何对代码进行分层,如何将意图与实现分离,以便将来可以动态替换实现。

好了,上面提到逻辑实现和流程控制。那么,什么是逻辑实现。这个很好理解,对吧,我们为了解决用户实际的问题,在代码里面写的实实在在的逻辑,比如我们的购物车逻辑,下单流程逻辑。

什么是流程控制呢。既然我们上面说了要将逻辑实现和流程控制相分离,那么也就提示了我们,流程控制就是跟业务逻辑不相关的代码或系统控制,比如我们微服务中的服务发现,代码中的多线程、异步等等。

如何对代码进行分层呢。一个由代码构建出来的系统,内部结构,从概念上大都是由模型、方法、实现组成的。比如,我们写的一个个的Java类之间的调用关系,属于模型,调用的什么呢,是方法,方法内部是干啥的呢,是实现。

但是呢,如果我们的模型所在的模块没有清晰且一致的层次,各个子系统之间交互的时候呢,又都是通过散落的接口来交互,而且接口之间还那么任性的任意调用,乱作一团。

可想而知,不但软不起来,而且很难进行后续的日常维护。关于分层,现在DDD架构中有较好的DDD的分层建议,从上到下依次为用户接口层、应用层、领域层、基础层,而且呢,层与层之间还遵守着一个约束:

每层只能与位于其下方的层发生耦合的约束。

当你谈论一个类的时候,你主要是在谈什么。就是在谈如何把数据和行为封装一起,这里的“一起”显然是需要经过精心组织的。封装是面向对象的本质。好,既然提到了本质,就从本质上来说:

一个类应该隐藏其它类无需了解的任何事情。

那继承又是什么,继承是面向对象的三个概念之一,封装、继承、多态。但是呢, 继承会减弱封装的“气质”。 如果你写了类A,又写了一个类B来继承类A,从而继承了类A的一个实现,然后父类A修改了这个实现,那么这次修改就会波及潜在影响所有的子类。

这也是为什么有很多面向对象大师都提倡使用组合优于使用继承的原因之一。但是呢,这也不是说继承就是那么的”邪恶“,一定不是这样的,我们要在正确的上下文中来判断使用继承或者组合。

关于组合我想我还没有说完,先暂停下。

再说说多态。如果你学习过,看过关于它的相关书籍内容,或者文章资料,你一定对一个例子不陌生。

有一个Shape类,它有个抽象方法getArea(),有另外两个类Rectangle和Circle实现了Shape,当计算矩形和圆形面积的时候,它们各自显然可以拥有不同的计算面积公式。再来一个菱形、扇形、XX形,都不怕,让客户端直接去对接Shape类就好了,多态可以动态改变类的运行时行为,客户端需要哪个形状,都可以更换。

另外呢, 多态也是对继承的最优雅的使用方式之一

没有依赖就没有伤害 。宏观和微观来看吧,大到一个方法去通过RPC调用别的进程的方法,小到一个进程内的一个方法调用另外一个类的方法。这些都是依赖。

那些依赖,一旦有个“三好两歹”,你就会受到牵连。“你的命啊有时候掌握在别人手里”。我们做大促备战的时候,要梳理黄金功能和黄金流程,就会捋出来,强依赖和弱依赖,但不管哪个依赖,始终一个观点:所有的依赖都不可信。

你要学会自保。上面,我也提到了组合,大家也都已经知道,继承是is-a,组合是has-a,继承代表是:是一个,组合代表的是:有一个。 继承和组合都是对象之间交互的方式,只不过呀,继承更强调了一种新的已知对象,而组合呢,表达对象之间交互的“味道更浓”。

另外,当我们使用组合的时候,我们会用一个类来构建其它的类。通过组合的方式,可以让依赖具备可替换性,如果依赖方将要受到“伤害”,就替换掉被依赖方。

(言外1:有时候,在组合的使用过程中,被依赖的对象,我们也管它叫做:组件)

(言外2:在软件开发领域,如果一个组件能够被很多方依赖,也就意味着复用)

我拥有一辆车,有个轮胎坏了,我就更换一个。

为了构建高质量的软件,必须遵循一种规则来取得成功,这个规则就是尽可能保持简单。

分阶段构建。Herbert simon 是20世纪一位奇特的通才,在很多领域都有建树,而且都已经深刻的影响了我们已知的世界,关于他的传奇,你可以搜索一下,一定会让你叹为观止。

大多数人终其一生也仅仅可能在一个领域有所建树,但是Herbert simon却在众多领域都做出了惊人的成就,其中在计算机领域他曾经发表过“架构的复杂性”的文章。

那这和分阶段构建有什么关系呢,别急,先让我们先看他发表的这篇文章的主要观点。

“稳定的复杂系统通常具有一定的层次结构,每个系统由简单的子系统构建而成,这些子系统又由更简单的子系统构建而成。”

这不正是我们已经熟悉的软件开发中基本的解耦方式么,在我们面向对象编程的过程中,组合也是这条规则的”使用者“,通过简单的对象来构建复杂的对象。

“稳定的复杂系统是可分解的。”

这个意思是说,我们可以识别一个系统是由哪些部分组成的,以及这些组成部分之间的交互关系,同时呢我们要记住一个大小关系:

稳定的系统中组成部分之间的交互要少于组成部分内部的交互。

“稳定的复杂系统往往由不同类型的子系统以不同的方式组合而成。”

同时呢,这些子系统还包含更小的组成部分。

“可工作的复杂系统往往是从可工作的简单系统演化而来。”

有时候呢,我们是不会从零构建一个新系统的,往往会基于已经被生产环境验证过的相仿的系统来构建出一个新的系统。

从以上的描述中,你发现了分阶段构建了吗。

以上便是对这个主题做了大致描述,不见得都对,但也是阐述了我个人对这方面的学习思考,希望有点帮助。另外呢,现在我们使用的都是高级语言,高级语言高级的地方就在于距离要解决的用户的问题越来越近,再也不象“古老”的年代,要考虑“0和1在机器上是怎么执行的”。

在当今,作为一名程序员,这个角度讲,是幸福的。

参考文献:

https://www.infoq.cn/article/KCX40i6JVMQSKrv9V63p 架构整洁之道的实用指南

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html  The Clean Code Blog by Robert C. Martin (Uncle Bob)

https://www.cnblogs.com/cbf4life/archive/2009/12/15/1624435.html 依赖倒置原则

https://q.cnblogs.com/q/72496/

书籍《架构整洁之道》、《演进式架构》、《面向对象思考过程》,极客专栏《架构实战案例解析》、《Redis核心技术与实战》、《软件设计之美》

封面图选择网络