码农戏码

新生代农民工的自我修养

0%

中台,在过去两年是个流量顶级词汇,谈什么都得带上中台,干什么都得扯点中台,不说中台,那绝对不是个合格的技术人

但经过了谈中台、建中台、拆中台,潮起潮落,不管是看好,还是贬低;可以看出在技术大词的浪潮里,不管是应激者怕掉队,还是投机者想上位,真正懂它的人不多,或者大多都还停留在以以往经验来判定的新事物

在历史遗忘之际,我来重温一下它

起源

在中台的发展进程中,首先得回到它的起源,至少有两个版本:

一是正史,马云一行,参观了芬兰一家游戏公司supercell,大受震撼,回来就提出了“中台战略”

一是野史,张勇的挟中台以令诸侯

从这两史中至少可以看出一些东西:

  1. 中台是由企业掌舵者提出以及使用的,它不是一个技术人员提出的,甚至说是CTO级别提出的,提出的起源与技术占不上边
  2. 中台的战略地位,不管是愿景实现还是战略落地,都发挥着巨大作用

所以很多技术人不解的地方,为什么谈到中台,需要谈企业战略,需要企业级组织变更,而不仅仅在于研发内部,不是使用的技术多牛,而是因为中台本身就有战略属性

因此在讨论中台时,需要从业务环境,组织结构,人力构成和技术架构各方面统筹考虑,抛开这些单从技术角度,是没有全局视角衡量中台的优劣,也是没有意义的,所以中台得结合企业本身综合情况,而不是技术迁移就能完成的

定义

现在提到中台,一连串的词汇会涌现出现,比如共享、复用、积木化

那么什么才叫中台呢?至少指出者没有给出定义,但它有特性,就是在高速发展环境之下,企业需要具备相应的响应速度去支撑企业运营的需求,而依靠小而灵活的前台团队,频繁而低成本的试错是一种应对此商业环境具有竞争力的模式

“小而灵活”是关键:小意味着人员少,成本低;灵活意味着对外快速响应市场,对内流程敏捷,快速失败

而能支撑这种小而灵活前台团队的系统就称为中台,这是从中台的作用来描述它,经过这几年的发展,有各式各样的定义

我比较认同王健老师的定义:企业级能力复用平台

企业级:

定义了中台的范围。它不是单业务级,是从企业全局出发,考虑多条业务线;一个企业也不是只有一个中台,可以有多个中台。也就是企业与中台的关系是多对多的

企业级这表明了中台不单是技术问题,而是上升到企业架构的问题

能力:

定义了中台主要承载的对象。能力的抽象解释了为什么有那么多种类的中台,也解释了为什么每家中台都是不一样的,因为每家企业的核心能力是不一样的

复用:

定义了中台的核心价值,建设中台的过程就是推倒烟囱系统的过程,也是去重复用的过程;“去重”讲的更多是向后看,是技术驱动;“复用”讲的更多是向前看,是业务驱动和用户驱动的

中台需要从“去重”到“复用”的视角转变

“复用”是中台更加关注的目标

“可复用性”和“易复用性”是衡量中台建设好坏的重要指标

“业务响应力”和“业务满意度”是考核中台建设进度的重要标准

平台:

定义了中台的主要形式。区别于传统的应用系统拼凑的方式,通过对于更细粒度能力的识别与平台化沉淀,实现企业能力的柔性复用,更好地支撑前台业务

种类

自从中台概念流行,各个词都与中台组词了,研发中台、技术中台、组织中台、业务中台…只要把以前谈的词语带上中台,就是高大上的

经过过去几年的喧嚣,沉寂。人们对中台品种达成了一定的共识:业务数据双中台

网易副总裁汪源曾在网易云创峰会上提到:“所有中台都是业务中台”。从中台起源出发,的确,中台就是为业务,为企业更好地以更低成本、更高质量、更快响应速度售出产品、换取利润服务的

而数据中台,更多的是大数据时代到来,大势所趋,业务中台是产生数据,数据中台是做数据二次加工,并将结果再服务于业务,为业务进行数据和智能的赋能

创新

这两年,拆中台的声音呼啸而起。尤其以当年带起中台的阿里等一系列巨头,都在拆。人们又开始跟风中台不行了

戏称,我们作业才抄到一半,你说写错了

为什么要拆呢?想那盒马不就是中台成功的典范,但在犀牛制造却提出不拿中台一针一线了,自己从零开始

这其实就是任何软件平台的特性

平台的能力越丰富,上层应用可以利用的越多,去完成某类功能的成本就越低,因而平台能力通常被看作效能下限

应用利用平台能力获得效能,是通过放弃一部分自主性获取,而低自主性就影响创新的可能,所以应用自主度被看作创新上限

“效能下限”与“创新上限”就像翘翘板,产生了哑铃效应,而中台则是追求效能的极致,同时却也降低了创新上限

对于像巨头在中台已经沉淀多年,有了相当应对当前市场的能力,但想要争取更多的市场份额,创新需求日益剧增,尤其需要颠覆式创新

因此,别人拆的时候,你能拆吗?建中台需要综合考虑,拆中台同样需要考虑

总结

中台曾经的顶级流量热词,不管当初的是应激怕掉队,还是投机想上位,浪潮退去之时,我们才能静下心来思考它是什么,它能干什么

虽然现在已经冷却,但威力不减,提升企业竞争力一把好手,它的出发点不是技术基建,而是寻找更好的组织结构和技术架构,以支持业务的快速增长和发展

最近学到一个词“耦合创伤应激障碍”,讲的是程序员对耦合条件反射式恐惧,对于这个新词,我再重新理解一篇

对于一名程序员,从入行开始,就听到前辈们对“高内聚低耦合”的谆谆教诲,所以对于低耦合的意识深入骨髓。知行合一,看看是怎么践行的,打开任何一个项目工程,可以看到,每一个service都有一个interface和impl,代码看起来整整齐齐,所有变化点都考虑到了,但其实没有降低问题复杂度,只是自己看着舒服

《SOLID总结》中提到过面向接口编程中接口到底是什么含义,并不是所有实现类都得需要一个接口,才是面向接口编程

而现在实践中对实现依赖心理恐惧,成了一种行业通病,见不得对实现的依赖,这是典型的耦合创伤应激障碍,像“猴子实验”

五只猴子被关进笼子里,笼子一角挂着一串香蕉,如果有猴子试图摘取香蕉,就会被开水泼到。猴子们吃了几次苦头之后,就再也不想摘香蕉了。
此时用一只新猴子替换老猴子,新猴子看到有香蕉刚想去摘,就被老猴子们拉住一顿暴打。新猴子挨了几次打之后,也不再去摘香蕉了。
此时再换进一只新猴子,它也看到香蕉想去摘,也被老猴子们一顿暴打,下手最狠的恰恰是那一只没被开水烫到过的。
最后老猴子们都被换干净了,仍然没有猴子去碰那串香蕉,因为它们知道——碰香蕉意味着被打,而为什么会被打,没有猴子知道。

当参与一个新项目,不再创建interface时,肯定会变成那只被打的“猴子”

然而现实并不是这样的,真的加个interface就减少耦合了吗?耦合少得了吗?比如,需要使用支付宝或微信支付,那么这就是业务需求,与支付宝和微信就必然会耦合,才能达到业务要求。不管怎么组织代码结构,是明显直接调用,还是隐晦地抛出支付事件,终将需要调用支付宝微信的支付接口;再比如现在很多应用需要推送消息,短信、邮件亦或微信,那么与支付类似,不管如何,必将调用第三方接口才能实现这些功能,这也就是耦合的必然性

代码大致是这样的:

先来一个接口:

1
2
3
4
5
6
7
package com.zhuxingsheng.infrastructure.port

public interface AlipayService {

public PayResult pay(AliInfo aliInfo,decimal payAmount);
}

再来对接口的具体实现,调用支付宝SDK

1
2
3
4
5
6
7
8
package com.zhuxingsheng.infrastructure.apapter
public class AlipayServiceImpl implements AlipayService {

public PayResult pay(AliInfo aliInfo,decimal payAmount) {
AlipaySdk.pay();
return result;
}
}

微信支付代码结构类似,对于这些代码味道是不是相当熟悉,有service必有interface和impl,但看看接口的意义在哪儿?

机智的你,肯定发现这样不对,我们需要的应该是在线支付能力,而支付宝支付或微信支付只是具体的实现而已,也就是与支付宝和微信耦合其实不是必然的,必然的是在线支付能力

这儿其实有两种演变过程:

第一种:先实现了支付宝支付功能,当再实现微信支付时,此时发现要抽象出在线支付接口,策略模式

第二种:从业务需求用户故事:作为用户需要完成订单线上支付,完成订单的全流程

第一种从技术入手,而第二种从最原始的业务入手,这两种演变虽然第二种方是大道,可技术人却喜欢第一种,这也就回到篇首所说,代码看起来整整齐齐,却没有简化问题,甚至只因为技术风格的强统一,带来了业务语义的更加隐晦

技术其实是解决业务的工具,需要从业务本源着手,挖掘业务背后隐藏的业务能力,构建出对应的技术模型,达到模型即代码的统一,也就知道了必然性耦合,怎么降低耦合

所以再看上面的接口,package com.zhuxingsheng.infrastructure.port,是在基础设施层,只是技术上单纯的一个接口,在《DDD》专栏中,提到过的接口在领域层,实现在基础设施层,因为maven模块循环依赖或者DIP的需要,所以必需要把接口放在领域层,从业务角度分析,线上支付能力是构建模型的必要能力,是领域模型必不可少的部分,需要在线支付能力,也是业务必然性耦合的体现,应该在领域层

可从代码形式上看,你是不是觉得不管是对业务的深刻理解,还是单纯技术抽象,不都是一个接口吗?无非是叫接口还是叫业务能力而已吗?

再看一个接口

1
2
3
4
5
6
package com.zhuxingsheng.infrastructure.port

public interface UserService {

public User getUser(long userId);
}

从命名就知道,获取用户信息,不管是业务系统自身处理用户信息,还是从用户中心式外部服务获取用户信息,这是整个系统的基本能力,而不会再需要去抽象一个深奥的业务能力,当业务需求故事阐述用户下订单业务行为时,业务方已然抽象了整个用户体系,只有研究用户上下文子域问题时,才去深入用户领域模型,但从当前业务上下文看,业务系统与这些基础服务是深度绑定,并在系统架构初始已经确定了整体架构

因此在业务系统中去定义一个userservice接口,是没啥意义的,除非系统大动,否则是不会变动的

延伸一下,此时领域层如何依赖基础设施层呢?怎么DIP呢?有没有丝毫感受到分层有问题呢。对此下会分解

总结一下,我们需要正确看待耦合必然性,不是从技术实现的角度去硬生抽象,而需要从业务角度,挖掘出业务真正耦合的能力,坦然接受这样的耦合,清晰化表达业务语义

资本家主要目标是赚钱、赚很多很多的钱;他们给提出的要求是降本增效

那么作为架构师,目标是什么呢?

在《整洁架构》书中作者写到架构的主要目的是支持系统的生命周期。良好的架构使系统易于理解,易于开发,易于维护和易于部署。
最终目标是最小化系统的寿命成本并最大化程序员的生产力

大多数程序员心里觉得应该是展示最牛B的技术才对,可现实却只是资本家的工具而已,是不是有些惊讶

软件的核心是它为用户解决领域相关问题的能力,保持业务价值的持续交付

可在软件行业,交付能力的持续性是相当有挑战性的,也许前期交付很快,但慢慢交付就很慢,质量也会下降,甚至哪怕一次小小的改动都要经历很久,更可怕的是无法交付,为什么呢?

在之前的相关文章中也提过,有两张图:

《架构师》中提到软件需求并不只是功能需求:

软件复杂度并不仅仅是业务复杂度:

在一起起看似快速交付背后,不合理的设计或者实现积累了过多的技术债,造成无法交付

所以架构师最重要的事就是解决软件中的复杂性

在软件项目中,任何方法论如果最终不能落在“减少代码复杂度”,都是有待商榷的

软件架构设计的实质,是让系统能够更快地响应外界业务变化,并且使得系统能够持续演进

架构设计的主要目的是为了解决软件复杂度带来的问题

《DDD应对复杂》中也提到复杂的来源,对于软件复杂性以及应对方案,特定总结画了一幅图

在之前《应对变化》中提到模块之间合的策略:缩小依赖范围,API是两个模块间唯一的联结点

怎么才是一个好的API设计呢?最近项目中正好碰到一件关于一个API引起的相爱相恨的事件

数据来源于外部系统,外部系统通过回调把数据传输过来,内部系统通过系统A进行接受,接受完之后,转发给系统B

接受回调api大概是:

1
systemA.callback(long userId,Object data);

整体两个参数,一个userId表示这个数据是谁的、一个data表示数据本身

对于系统B来讲,他的业务要求,这数据必须要知道用户名字,必须在数据上打标签,所以跟系统A商量,要不你把username也随便转给我吧

系统A一想,那也是两秒的事,因为本身在接受数据时也得对userId校验,取个username也不麻烦,不废话了,你要就给你

因此系统B接受数据api设计成:

1
systemB.receive(long userId,String username,Object data);

一切都是行云流水,大家都很happy,如期发布上线

爱情总是在转角处遇到,上线完,QA同学一顿操作,却发现系统B没有如期显示出数据,系统B觉得是系统A没传来数据;咋办呢?心虚的系统A只能查查日志

1
log:systemB return error,username is empty 

原来原来是因为这个用户的username是空,系统B拒绝接受了,怎么username会为空呢?username怎么能为空呢?

找到用户系统,用户系统解释了,一个用户在注册时并不一定有username,有username,email,usercode三个值中的任何一个值就可以了

这时该怎么办呢?相爱相杀时刻到了

系统B:要不你把这三个值都传给我?

系统A:我还得再改下代码,测试后发版本,要不你自己从用户系统取吧

系统B:传一个可以,怎么三个就不可以了,不都一样吗?

系统A:太麻烦了,你自己取了,想怎么控制就怎么控制

系统B:你是不爱我了

系统A:你怎么就不理解我呢


温习一下一个好的API设计要求:

缩小依赖范围,就是要精简API;API要稳定得站在需求角度,而不是how角度

  1. API包含尽可能小的知识。因为任何一项知识的变化都会导致双方变化
  2. API也要高内聚,不应强迫API的客户依赖不需要的东西
  3. 站在what角度;而不是how,怎么技术实现的角度

上面示例的问题就在系统B接受数据api:

1
systemB.receive(long userId,String username,Object data);

关照上面的要求:

问题一:API中包含的知识有重复:userid,username

问题二:客户端也就是systemA并不需要username,但被强迫要知晓并要按规则赋值

问题三:站在设术实现角度,api中增加参数username,而不是需求角度

总结

示例虽小,日常工作中常常碰到这类问题,如果这个例子上线成功,每个人都觉得这是一次成功的交付,但回头复盘,发现了很多理论缺乏,惯性思维使然造成的不合理,难维护,难扩展的设计

由此看出,日常的CRUD并不是没有技术含量,而是我们有没有深刻认知

之前对SOLID做了一个总结 《SOLID》总结

这些原则是前辈们经过无数实践提炼出来的,百炼成刚,那是不是成了放之四海皆准的道理呢?某种程度上讲,还真就是准的,常被人耳提面命写的代码要遵守这些原则,想想code review时,是不是代码常常对比这些原则,被人指出没有遵循哪个原则

总结篇中画了这幅图,SOLID也的确是我们达到高内聚低耦合很重要的手段

1
2
3
4
5
6
7
8
9
10
//读取配置文件和计算
class Computer{
public int add() throws NumberFormatException, IOException {
File file = new File("D:/data.txt");
BufferedReader br = new BufferedReader(new FileReader(file));
int a = Integer.valueOf(br.readLine());
int b = Integer.valueOf(br.readLine());
return a+b;
}
}

这是一段被用来演示SRP的反例,从示例代码中看出,这个方法职责的确不单一,引起它变化的至少有两个地方:一是数据不一定从配置文件读取、二是计算方式可能会变

在code review时,不管是自己还是别人,的确让人觉得不够完美

因此,我们会花一番功夫,来让方法达到SOLID的要求,可如果此方法从系统上线运行几个月,甚至几年都无需变动,那我们花费的这些时间也只是自我感动,毕竟我们最终目标是给客户交付带来价值的系统,以最快的速度给公司带来效益

这其实是成本的问题,没错,程序员要有技术追求,但也得考虑成本

可总不能为了成本,忽略一切吧,那怎么处理呢,我们要达到“高内聚、低耦合”,SOLID是重要路径,但又不能不计成本地进行SOLID,更不能为了SOLID而SOLID

所以不能走两个极端,既不能坐视不管,也不能一味求全,在这两层之间应该还有一片灰色地带


这灰色地带是什么样的?怎么做才能不去穷举变化疲惫应对,而当真正变化来临时又能轻松应对?大佬提供了思路,不能以这些原则去应对软件变化,应该以无法为有法,以无限为有限。以实际需求变化来帮助我们识别变化,管理变化。这思路就是袁英杰提出的正交设计,有四大策略

四大策略

策略一:消除重复

重复代码,不管接手老项目还是住持新项目,都特别重视重复代码的比率,为什么呢?

  1. 重复代码增加维护成本,变动同一个逻辑会遗漏修改
  2. 重复代码说明团队沟通不畅,团员间没有交流或者没有必要的code review

这只是实践带来的观察,那么从理论角度说说消除重复的重要性

“重复”极度违背高内聚、低耦合原则,从而会大幅提升软件的长期维护成本;而我们所求的高内聚是指关联紧密的事物放在一起,两段完全相同的代码关联最为紧密,重复就意味着低内聚

更糟糕的是,本质重复的代码,都在表达同一项知识。如果他们表达(即依赖)的知识发生了变化,这些重复代码都需要修改,因而重复代码也意味着高耦合

重复意味着耦合

当完全重复的代码进行消除,会让系统更加高内聚、低耦合

小到代码块,大到模块也一样,如果两个模块之间部分重复,那么这两个模块都至少存在两个变化原因或两重职责;站在系统的角度看,它们之间有重复部分,也有差异部分,那这两个模块都存在两个变化原因

对于这一类型重复,比较典型的情况有两种:实现重复和客户重复

实现型重复

客户型重复

这个策略非常明确,极具操作性,消除重复后,明显提高系统内聚性,降低耦合性,在消除重复过程中,也提高了系统的可重用性,而且对于客户重复,还提高了扩展性

策略二:分离不同的变化方向

对于策略一使用时机,可以随时进行,重复也容易判定。除重复代码外,另一个驱动系统朝向高内聚方向演进的信号是:我们经常需要因为同一类原因,修改某个模块。而这个模块的其它部分却保持不变

分离不同变化方向,目标在于提高内聚度。因为多个变化方向,意味着一个模块存在多重职责。将不同的变化方向进行分离,也意味着各个变化职责的单一化

分离变化方向

对于变化方向分离,也得到了另外一个我们追求的目标:可扩展性

策略二相对策略一,最重要的就是时机,不然就会回到我们文章开头时的窘境:早了,过度设计;晚了,则被再次愚弄

当你发现需求导致一个变化方向出现时,将其从原有的设计中分离出去。此时时机刚刚好,不早不晚;Uncle Bob也曾给出答案:被第一颗子弹击中时,也就是当第一个变化方向出现时

这个世界里,本质上只存在三个数字:0,1,和N。

0意味着当一个需求还没有出现时,我们就不应该在系统中编写一行针对它的代码。

1意味着某种需求已经出现,我们只需要使用最简单的手段来实现它,无需考虑任何变化。

N则意味着,需求开始在某个方向开始变化,其次数可能是2,3,…N。但不管最终的次数是多少,你总应该在由1变为2时就需要解决此方向的变化。随后,无论最终N为何值,你都可以稳坐钓鱼台,通过一个个扩展来满足需求

如果我们足够细心,会发现策略消除重复和分离不同变化方向是两个高度相似和关联的策略:

它们都是关注于如何对原有模块进行拆分,以提高系统的内聚性。(虽然同时也往往伴随着耦合度的降低,但这些耦合度的降低都发生在别处,并未触及该如何定义API以降低客户与API之间耦合度)。

另外,如果两个模块有部分代码是重复的,往往意味着不同变化方向。

尽管如此,我们依然需要两个不同的策略。这是因为:变化方向,并不总是以重复代码的形式出现的(其典型症状是散弹式修改,或者if-else、switch-case、模式匹配);尽管其背后往往存在一个以重复代码形式表现的等价形式(这也是为何copy-paste-modify如此流行的原因)。

策略三:缩小依赖范围

前面两个策略解决了软件单元如何划分问题,现在需要关注合的问题:模块之间的粘合点API的定义

  • 首先,客户和实现模块的数量,会对耦合度产生重大的影响。它们数量越多,意味着 API 变更的成本越高,越需要花更大的精力来仔细斟酌。
  • 其次,对于影响面大的API(也意味着耦合度高),需要使用更加弹性的API定义框架,以有利于向前兼容性。

因此缩小依赖范围,就是要精简API

  1. API包含尽可能小的知识。因为任何一项知识的变化都会导致双方变化
  2. API也要高内聚,不应强迫API的客户依赖不需要的东西

策略四:向稳定的方向依赖

虽然缩小依赖范围,但终究还是要有依赖范围,还是必然存在耦合点。降低耦合度已到尽头。

耦合最大的问题在于:耦合点的变化,会导致依赖方跟着变化。这儿意味着如果耦合点不变,那依赖方也不会变化。换句话说,耦合点越稳定,依赖方受耦合变化影响的概率就越低

因此得出第四个策略:向稳定的方向依赖

耦合点也就是API,什么样的API更侧向于稳定?站在What,而不是 How 的角度;即站在需求的角度,而不是实现方式的角度定义API;也就是站在客户的角度,思考用户的本质需要,由此来定义API,而不是站在技术实现的方便程度角度来思考API定义

SOLID

一个好的面向对象设计,自然是符合高内聚,低耦合原则的对象划分和协作方式。

单一职责和开放封闭,更多的在强调类划分时的高内聚;而里氏替换,依赖倒置,接口隔离则更多的强调类与类之间协作接口(即API)定义的低耦合

单一职责,通过对变化原因的识别,将一个承担多重职责的类,不断分割为更小的,只具备单一变化原因的类。而单一变化原因指的是:一个变化,会引起整个类都发生变化。只有关联极其紧密的情况,才会导致这样的局面。因而,单一职责和高内聚某种程度是同义词。

但单一职责原则本身,并没有明确指示我们该如何判定一个类属于单一职责的,以及如何达到单一职责的状态。而策略消除重复,分离不同变化方向,正是让类达到单一职责的策略与途径

开放封闭原则,正是通过将不同变化方向进行分离,从而达到对于已经出现的变化方向,对于修改是封闭的,对于扩展是开放的

里氏替换原则强调的是,一个子类不应该破坏其父类与客户之间的契约。唯有如此,才能保证:客户与其父类所暴露的接口(即API)所产生的依赖关系是稳定的。子类只应该成为隐藏在API背后的某种具体实现方式。

依赖倒置原则则强调:为了让依赖关系是稳定的,不应该由实现侧根据自己的技术实现方式定义接口,然后强迫上层(即客户)依赖这种不稳定的API定义,而是应该站在上层(即客户)的角度去定义API(正所谓依赖倒置)

但是,虽然接口由上层定义,但最终接口的实现却依然由下层完成,因此依赖倒置描述为:上层不依赖下层,下层也不依赖上层,双方共同依赖于抽象。

最后,接口隔离原则强调的是:不应该强迫客户依赖它不需要的东西。显然,这是缩小依赖范围策略在面向对象范式下的产物

总结

尽管理论上讲,任意复杂的系统都可以被放入同一个函数里。但随着软件越来复杂,即便是智商最为发达的程序员也发现,单一过程的复杂度已经超出他的掌控极限。这逼迫人们必须对大问题进行分解,分而治之,这也是必须模块化的原因

模块化主要是两方面:

  1. 软件模块该如何划分?(怎么分)
  2. 模块间API该如何定义?(怎么合)

本文四个策略,前两个指导怎么高内聚,也就是怎么分;后两个指导耦合方式,怎么合

重要的是使用各个策略的使用时机,变化驱动识别变化、重构变化

变化导致的修改有两类:

  • 一个变化导致多处修改(重复);
  • 多个变化导致一处修改(多个变化方向);

由此得到前两个策略:消除重复;分离不同变化方向。

除此之外,我们要努力消除变化发生时不必要的修改,也有两种方式:

  • 不依赖不必要的依赖;
  • 不依赖不稳定的依赖;

这就是后面两个策略:缩小依赖范围,向着稳定的方向依赖。

Reference

变化驱动:正交设计

之前已经把SOLID的每人原则都阐述过一遍,此篇主要是从全局角度复述一下SOLID,对于细节概念再做少许补充

SOLID原则的历史已经很悠久,早在20世纪80年代末期,都已经开始逐渐成型了

通常来讲,想构建一个好的软件系统,应该从写整洁的代码开始做起。毕竟如果建筑的砖头质量不佳,那么架构所能起到的作用也会很有限。反之亦然,如果建筑的架构设计不佳,那么其所用砖头质量再好也没用

SOLID原则的主要作用就是告诉我们如何将数据和函数组织成为类,以及如何将这些类链接起来成为程序,类似于指导我们如何将砖块彻成墙与房间

对照几张前辈们画的图,看图说话

这张图把SOLID的整体关系描述清楚了,不再是把各个原则单独看待

单一职责是所有设计原则的基础,开闭原则是设计的终极目标。

里氏替换原则强调的是子类替换父类后程序运行时的正确性,它用来帮助实现开闭原则。

而接口隔离原则用来帮助实现里氏替换原则,同时它也体现了单一职责。

依赖倒置原则是过程式编程与OO编程的分水岭,同时它也被用来指导接口隔离原则


这些原则每个单独看都是简单的,但他们却是优秀代码的指导思想,不得不常读,常思;犹如设计模式,很多时候你感觉懂了,不过只是懂了介绍模式的示例,并没有真正理解模式

反观这些原则,道理类似

如SRP:是公认最容易理解的原则,却是被违反得最多的设计原则之一;再比如ISP,看着简单,更小和更具体的瘦接口比庞大臃肿的胖接口好,很多时候都没有明白接口的定义

在实现编写代码时,只要是service都会加上一个 service interface,但想想,从项目开启到后期维护,几乎没有一个 service interface 有一个以上的实现,那为什么要加个接口呢?美其名曰面向接口编程,其实是人云亦云,让自己也让别人看着是那么一回事而已

面向接口编程所指的“接口”并非Java语言中的interface类型,而是指面向调用者对外暴露的接口,代表一种交互与协作,是对信息的隐藏和封装,而不是具体的interface类型。即使是普通的java方法仍然满足隐藏细节的原则,如果是public的,就可以认为该方法是“面向接口设计”中的接口,也就是说:不要针对实现细节编程,而是针对接口编程

接口之所以存在,是为了解耦。开发者常常有一个错误的认知,以为是实现类需要接口。其实是消费者需要接口,实现类只是提供服务,因此应该由消费者(客户端)来定义接口。理解了这一点,才能正确地站在消费者的角度定义Role interface,而不是从实现类中提取Header Interface。

对于Role interface 与 header interface , Martin Fowler给出了定义:

A role interface is defined by looking at a specific interaction between suppliers and consumers. A supplier component will usually implement several role interfaces, one for each of these patterns of interaction. This contrasts to a HeaderInterface, where the supplier will only have a single interface

如果你先定义了一个类,然后因为你需要定义接口对其抽象,然后就简单地将这个类的所有公有方法都提取到抽象的接口中,这样设计的接口,被Martin Fowler称为Header Interface,这种接口也正是胖接口的来源,而 Role interface 才是能达到瘦接口目标

想起一位投资前辈说的话,成功就是对简单道理的深刻理解和灵活运用;我们很多时候有种无力感,为什么这么简单的道理都做不好,落地不了呢?其实是没有深刻理解而自以为懂了


Kent Beck对软件设计的定义:软件设计是为了在让软件在长期范围内容易应对变化

为了软件更容易应对变化,就需要符合软件的道:高内聚低耦合

单一职责和开放封闭,更多的在强调类划分时的高内聚;而里氏替换,依赖倒置,接口隔离则更多的强调类与类之间协作接口(即API)定义的低耦合,单独应用SOLID的某一个原则并不能让收益最大化。应该把它作为一个整体来理解和应用,从而更好地指导软件设计。

这个同心圆的原图本来是:

要实现道就得遵循正交设计四原则:

  1. 消除重复
  2. 分离关注点
  3. 缩小依赖范围
  4. 向稳定的方向依赖

「正交设计」的理论、原则、及其方法论出自前ThoughtWorks软件大师「袁英杰」先生。这一块对我来讲很新颖,消化之后再总结


这幅图揭示了模块化设计的全部:首先将一个低内聚的模块首先拆分为多个高内聚的模块;然后再考虑这多个模块之间的API设计,以降低这些高内聚的软件单元之间的耦合度。

除了内聚与耦合之外,上面这幅图还揭示了另外一种关系:正交。具备正交关系的两个模块,可以做到一方的变化不会影响另外一方的变化。换句话说,双方各自独自变化,互不影响。

而这幅图的右侧,正是我们模块化的目标。它描述了永恒的三方关系:客户,API,实现,以及它们之间的关系。这个三方关系图清晰的指出了我们应该关注的内聚性,耦合性,以及正交性都发生在何处

总结:

软件的复杂性已经是世界性难题,但最原始的道是相当简单的,就是要高内聚低耦合,在追求道的过程中,前人总结出了很多原则,这些原则相互协作、相互碰撞,我们需要平衡,取舍,这考验架构师的功力,也要求架构师对这些基本概念有深刻理解

参考:

《正交设计,OO 与 SOLID》

你真的了解SOLID吗?

RoleInterface

这两天一直在看被誉为“现代管理学之父”与“管理大师中的大师”的彼得·德鲁克(Peter F.Drucker)写的《自我管理》这篇文章

我的长处是什么?这是自我管理8个问题的其中一个,也是第一个

只有当所有工作都从自己的长处着眼,你才能真正做到卓尔不群

以前的人没有什么必要去了解自己的长处,因为一个人的出身就决定了他一生的地位和职业:农民的儿子也会当农民,工匠的女儿会嫁给另一个工匠等。但是,现在人们有了选择。我们需要知己所长,才能知己所属

要发现自己的长处,唯一途径就是回馈分析法(feedback analysis)。每当做出重要决定或采取重要行动时,你都可以事先记录下自己对结果的预期。9到12个月后,再将实际结果与自己的预期比较

比如开始写作,希望一年达到什么效果。跳槽了,一年内达到什么改变?

在九到十二个月之后,拿出之前的预期对比实际效果,看看预期是达成了,超标了,还是偏离了

运用这个简单的方法,就能知道自己的长处,也知道自己正在做的哪些事情不能发挥自己的长处,看到哪些方面能力不是特别强,哪些方面完全不擅长,做不出成绩来

根据回馈分析法的结果,需要采取如下行动:

“施展长处”:

首先专注于你的长处,把自己放到能发挥长处的地方,不要试图去完成自己不在行的领域,要从无能到平庸比从一流到卓越需要付出多得多的努力

“改善长处”:

其次加强你的长处。回馈分析会迅速显示在哪些方面需要改善自己的技能或学习新技能;同时纠正影响工作成效和工作表现的不良习惯

例如,一位企划人员可能发现自己美妙的计划最终落空,原因是他没有把计划贯彻到底。同那些才华横溢的人一样,他也相信好的创意能够移动大山。但是,真正移山的是推土机,创意只不过是为推土机指引方向,让它知道该到何处掘土。这位企划人员必须意识到不是计划做好就大功告成,接下来还得找人执行计划,并向他们解释计划,在付诸行动前须做出及时的调整和修改,最后要决定何时终止计划

“增强长处”:

发现自己的不知道,以免由于恃才傲物而造成的偏见和无知,不知道自己不知道过渡到知道自己不知道,放空自己,虚怀若谷,其实意思是虽然一技之长很重要,但现如今各领域相互融合,边界模糊,更需要综合能力,让自己的一技之长更加有的放矢

回馈分析法不是总结,也不是复盘,是当生活中的重大改变和做出的重大决定,长期坚持使用可以让你找到自己擅长做的事情,并在职场中找到适合自己的定位


古人云要成大事,必得天时、地利、人和

天时是相当重要的,常读历史就知道,这世间太多的事犹如有无形之手,一切都是冥冥中有注定,都是在那些关键节点某人某事发生了

前几天看到周鸿祎说的一段话:再给他1000万美金也无法复制360,再给李彦宏一亿美金也无法成功从零再做一个百度。很多时候所谓的成功都是马后炮,只不过在正确的时间做了正确的事

正确的时间就是天时,但什么时候是正确时间,很难讲,可能真是天定。主持人康辉就讲过:其实所有后来被认为,给你带来这种高光时刻,带来肯定的机遇,都是回头看你才能说它是机遇,在此之前你永远无法预知你所谓的这个机遇到底在哪里。当这个所谓的机遇来到的时候,其实你只有去冲上去,把它做到它才会是你的机遇,否则它就是你的失败

大到历史时刻,中到个人成功,就是小到个人做事,我都觉得时也很重要,时也是一种能量,势气

去年公司帮TL报名参与了华为老师的一线经理人培训课,一直想整理总结一下,可到最近才腾出时间,不仅想单单总结那次课程,也想梳理一下自己走向管理的历程,想回顾一下几年前自己写的文章,有没有过总结,发现自己还写过一个系列文章《游戏小传》总共十篇

看时间,尽然是在两个月内写完的,而且每一篇都不短,现在回看都感觉不太可能,文章里面的文字,有点不太相信是自己输出的

我想这也许那时是一种时,造就了能写出这些文字的势

所以有时,心里想干一件事,哪怕知道自己只有三分钟热度,也要凭着一时冲劲干起来,至少开始了,践行了,不再只是理论的层面


回顾一下自己写博客的过程,也是一时兴起,磨磨蹭蹭坚持到现在

为什么要写呢?在之前的文章《功夫在诗外》中也提到过

“形成有效的输入输出系统,以输出倒逼输入”,“写作也记录下学习过程,防止狗咬尾巴”

基于这么简单的初心,开始写作,把写作变成了一种习惯

很多事情是想得清楚,但说不出来,说明还是没搞明白;有时说得清楚,但写不出来,说明没有懂透

对于任何问题的思考,想清楚、讲清楚、写清楚是三个完全不同的维度。

写作可以把一件事搞透,更重要的是写作也是一种反省,对知、或者自身的一种复盘

后来不仅维护了自己的个人网站,还搞了公众号

很多公号大V,在两三个月,甚至一个月就有了几万粉丝,他们也表述了写作的好处:结识了很多志同道合的朋友,增加了影响力

相比他们,我实在微小得多,粉丝不到他们零头,也没有因此结识各种大佬,平平庸庸

但也不是一丝丝回报都没有:自我感觉更充实、知识有意识的体系化、更善于总结问题、复盘反省

自我充实

写作起初,制定计划,一周写一篇,后来发现只能一月写一篇,再后来得逼自己,一月写两篇,不然年末总结,发现一年就写了12篇,太少了,如果一月两篇,再拼一拼,可以有30篇的输出,也差不多了,相对大V,数量少了很多,但这也是当前能力的体现,输入速度低于输出速度

那么这30篇写什么呢?需要思考,需要回顾,需要总结,刚开始写时,会觉得有很多内容可以写,但写着写着就发现没内容可写了,怎么办呢?

就需要平时多积累,多思考一些问题,不管是突发灵感,还是阶段性目标,都记到to do list中,这样to do list会越来越多,当没有内容时,就说明自己懈怠了,大脑没有思考,倒逼自己成长

这样就有了很多可选写作主题,也有了很多学习目标

不单要去温习旧知识,也得学习新知识,还得阶段性整理总结

先不管这些是不是只是战术勤奋,至少也算是日拱一卒

体系化

这其实是上一个回报的延伸,当温习旧知识时,需要点、线、面、体四维;学习新知识时,得由浅入深,从广度推向深度

在这两个维度学习的过程中,知识必须体系化,一是为了有东西有写,二是的确需要梳理知识树

比如近期的《DDD》,对于这方面知识,以前也知道,但是东一块西一块,虽然学习中也发现了问题,但没有体系化解决,通过写作,算是把一些问题思考清楚了

《程序员成长体系》中,也提到得复用碎片化时间,体系化学习

总结复盘

对于这一点,还是差了很多,比如写作这件事,并没有常总结复盘,没有去刻意练习,现在写了不少,虽然不追求什么名气,成为大V,但写作能力还是需要成长的

也写了几年了,回头翻看自己文章的次数很少,更别提去润色文章了,更加没有去刻意练习怎么写好一篇文章,只追求的在自我的世界,成长速度有限

这不只是文章好不好,还要涉及运营,如果文章是产品,一个好产品不只是本身的质量,营销也是有必要的,推销自我也是一门学问

就写作本身,什么是好文章?

优秀的内容 + 清晰的结构 + 标题 = 好文章

什么是优秀内容:内容足够丰富,包含真正有价值的内容,不能含太多水分

如果你写了一篇文章但是觉得内容很单薄,可以先当成一篇笔记存起来,等有了更丰富的积累之后再整理成文章。扩展文章内容的方法,并不是添加无意义的空话套话,而是根据文章探讨的问题延展开来。

比如说介绍自己解决的一个老大难 Bug,可能真正修改的代码并没有几行,把过程讲出来也不过寥寥几段。这时候你就可以再分析一下 Bug 存在的原因,为什么一直拖到现在,再思考一下如何避免这类问题,遇到同类 Bug 怎样快速排查。这样自己想问题的角度更全面了,文章内容也更丰富了。

比如你想介绍一项自己在学的新技术,发现自己写的东西其实就是官方文档的简化版,去重之后几乎什么都不剩了。这时候不要再继续抄文档了,把自己的思考总结先记下来,继续学习技术,持续记录新的内容,有更全面的了解之后,再写文章。

清晰的结构:

先想好标题,再划分好目录结构,再一段一段的填充内容,最后再润色一下连接部分。文章可以不按顺序看,也可以不按顺序写

在平时工作的时候,可以建个文档库,把日常的一些琐碎的想法记录下来,随时写随时存。我是用手机的便签 App 随手记东西,比较喜欢它的语音转汉字功能,工作相关、生活相关,随时随地想起任何话题都可以记录下来。

在有了明确话题,准备写文章之前,先把各种碎片化的记录收集起来,形成一份“素材”文档,然后梳理文章脉络,把素材应用进去。操作起来很简单,刚开始的时候会遇到前后不通畅的问题,那就不要直接复制素材的内容,重新换个表达方式写出来。多练习练习就好了

好标题:

现在太多的标题党,但标题党吸引眼球,这背后也有很多大脑科学

第一个是数字法则,人的大脑和视觉系统在处理数字的时候,要比处理复杂的文字优先得多,如果你的文章里那些比较有亮点的数字、金额、排名、要点什么的,记得一定要把它们往标题上放,会让人感觉信息含量比较高。

第二个是尽量通俗易懂地直接给出结论/价值感,避免出现生僻、专业的词汇。能让普通人和专业人士都理解的标题,才叫好标题。

第三个是建立好奇,要想办法让读者有兴趣、建立内容与受众之间的相关性


再回到主题,时也势也,把握天时,在三分钟热度内,充分挖掘,形成一种势能,推动自己养成一种良好习惯

最近在温习之前管理课程时,在“自我发展”这一章节看到一篇文章彼得·德鲁克的《自我管理》,作者:彼得·德鲁克(Peter F.Drucker,1909年-2005年)被誉为“现代管理学之父”与“管理大师中的大师”。德鲁克以他建立于广泛实验基础之上的30余部著作,奠定了其现代管理学开创者的地位。他在《哈佛商业评论》发表了近30篇文章,本文《自我管理》是《哈佛商业评论》创刊以来重印次数最多的文章之一。

特别不错,专门做了个思维导图,应该可以指引每一位知识工作者

  • 自我管理
    • 为什么需要自我管理
      • 公司并不怎么管员工的职业发展
      • 知识工作者必须成为自己的首席执行官
      • 我们必须学会自我发展,必须知道把自己放在什么样的位置上,才能做出最大的贡献,而且还必须在长达50年的职业生涯中保持着高度的警觉和投入
      • 从一切听从别人吩咐的体力劳动者到不得不自我管理的知识工作者
    • 我的长处是什么
      • 回馈分析法
        • 每当做出重要决定或采取重要行动时,你都可以事先记录下自己对结果的预期。9到12个月后,再将采取实际结果与自己预期比较
      • 根据回馈分析启示,采取行动
        • 专注于自己的长处,把自己放到那些能发挥长处的地方
        • 加强你的长处
        • 发现任何由于恃才傲物而造成的偏见和无知,并且加以克服
          • 要让自己的长处得到充分发挥,就应该努力学习新技能、汲取新知识
        • 纠正不良习惯
          • 不良习惯–那些会影响工作成效和工作表现的事情
        • 礼貌是一个组织的润滑剂
    • 我的工作方式是怎样的
      • 很少有人知道自己平时是怎么把事情做成的
      • 对于知识工作者来说,这个问题比“我的长处是什么”更加重要
      • 读者型,听者型?
    • 我如何学习
      • 学习方式
        • 邱吉尔靠写学习
      • 我能与别人合作?还是单干
      • 在怎么关系下与他人共事
        • 当部属
        • 当教练
        • 当导师
      • 如何才能取得成果
        • 决策者
        • 顾问
      • 在压力下的表现,还是适应按部就班、可预测工作环境
      • 大公司,还是小公司
      • 不要试图改变自我,因为这样你不大可能成功,应该努力改进工作方式,另外不要从事干不了或干不好的工作
    • 我的价值观是什么
      • 镜子测试
        • 每天早晨在镜子里想看到一个什么样的人?
      • 一个组织的价值体系不为自己所接受或自己价值观不相容,人们备感沮丧,工作效力低下
      • 价值观是并且应该是最终的试金石
    • 我属于何处
      • 知道了“我的长处是什么”、“我的工作方式是怎样的”、“我的价值观是什么”就应该决定自己该向何处投入精力
      • 成功的事业不是预先规划的,而是在人们知道自己的长处、工作方式和价值观后,准备把握机遇时水到渠成的
      • 知道自己属于何处,可使一个勤奋、有能力但原本表现平平的普通人,变成出类拔萃的工作者
    • 我该做出什么贡献
      • 考虑三因素
        • 当前形势要求是佬
        • 鉴于我的长处、工作方式及价值观,怎样才能做出最大贡献
        • 必须取得什么结果才产生重要影响
      • 我在哪些方面能取得将在今后一年半内见效的结果?如何取得这样的结果?
        • 结果应该比较难实现,有“张力”,但也就应该是能力所及
        • 这些结果富有意义
        • 结果应该明显可见,能够衡量
    • 对人际关负责
      • 接受别人是和你一样的个体这个事实
        • “管理”上司秘诀
          • 老板不是组织结构图上的一个头衔,也不是一个“职能”
          • 他们是有个性的人,有权以自己最得心应手的方式来工作
          • 有责任观察他们,了解他们的工作方式,并做出自我调整,适应老板最有效的工作方式
        • 工作方式,人各有别
          • 提高效力第一秘诀是了解跟你合作和你要依赖的人,以利用他们的长处、工作方式和价值观
          • 工作关系应当既以工作为基础,也以人为基础
      • 沟通责任
        • 与个性冲突有关:不知道别人在做什么,在采取什么工作方式
        • 不去问明情况,是历史使然
        • 怕别人把自己看成是一个冒昧、愚蠢、爱打听的人
        • 谢谢你来问我,但是,你为什么不早点来问我?
        • 组织不再建立在强权基础上,而是建立在信任基础上
        • 人与人相互信任,不一定意味他们彼此喜欢对方,而是意味彼此了解
    • 管理后半生
      • 发展第二职业原因
        • 厌倦
        • 遭遇严重挫折
      • 发展第二职业方式
        • 完全投身新工作
        • 发展一个平行职业
        • 社会创业
      • 先决条件:进入后半生之前就开始行动
      • 在一个崇尚成功的社会里,拥有各种选择变得越来越重要
总结

知识工作者必须在思想和行为上成为自己的首席执行官,在《领导梯队》中也看到同样的话,每个人都是自己的领导者,在《软技能-代码之外的生存技能》中作者也指出要把软件开发事业当成一门生意,要像企业一样思考,每个人都是CEO,领导自己,成就自己,那就得早日管理自己

DDD这个主题已经写了好多篇文章了,结合最近的思考实践是时候总结一下,对于战略部分有点宏大,现在都是在微服务划分中起着重要作用,暂且总结战术部分

DDD意义

每种理论的诞生都是站在前人的基础之上,总得要解决一些痛点;DDD自己标榜的是解决复杂软件系统,对于复杂怎么理解,至少在DDD本身理论中并没有给出定义,所以是否需要使用DDD并没有规定,事务脚本式编程也有用武之地,DDD也不是放之四海皆准,也就是常说的没有银弹

但重点是每种方法论都得落地,必须要以降低代码复杂度为目标,因此对于“统一语言”、“界限上下文”对于一线码农有点远,那战术绝对是一把利剑

回顾一下,在没有深入DDD之前,基本上就是事务脚本式编程,当然还会重构,怎么重构呢?基本也是大方法变小方法+公共方法

随着业务需求越来越多,代码自然伴随增长,就算重构常相伴,后期再去维护时也是力不从心,要么小方法太多,要么方法太大,老人也只能匍匐前行,新人是看得懂语法却不知道语义,这也是程序员常面对的挑战,不是在编写代码,而是在摸索业务领域知识

那怎么办呢?有没有其它模式,把代码写漂亮,降低代码复杂度,真正的可扩展、可维护、可测试呢?

很多人会说面向对象啊,可谁没在使用面向对象语言呢?可又怎样。事实是不能简单的使用面向对象语言,得要有面向对象思维,还得再加上一些原则,如SOLID

但虽然有了OOP,SOLID,设计模式,还是逃不脱事务脚本编程,这里面有客观原因,业务系统太简单了,OO化不值得,不能有了锤子哪里都是钉子;主观原因,长时间的事务脚本思维实践,留在了舒适区,缺乏跳出的勇气

DDD战术部分给了基于面向对象更向前一步的范式,这就是它的意义


在实践DDD过程中,我也一直在寻找基于完美理论的落地方案,追求心中的那个DDD,常常在理论与实践的落差间挣扎,在此过程中掌握了一些套路,心中也释然了对理论的追求,最近关注到业务架构,看到一张PPT,更是减少了心中的偏执,这份偏执也是一种对银弹的追求,虽然嘴大多数时候说没有,但身体很诚信

在这张方法融合论里面,DDD只是一小块,为什么要心中充满DDD呢,不都是进阶路上的垫脚石。想起牛人的话,站到更高的维度让问题不再是问题才是最牛的解决问题之道

事务脚本式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

@RestController
@RequestMapping("/")
public class CheckoutController {

@Resource
private ItemService itemService;

@Resource
private InventoryService inventoryService;

@Resource
private OrderRepository orderRepository;

@PostMapping("checkout")
public Result<OrderDO> checkout(Long itemId, Integer quantity) {
// 1) Session管理
Long userId = SessionUtils.getLoggedInUserId();
if (userId <= 0) {
return Result.fail("Not Logged In");
}

// 2)参数校验
if (itemId <= 0 || quantity <= 0 || quantity >= 1000) {
return Result.fail("Invalid Args");
}

// 3)外部数据补全
ItemDO item = itemService.getItem(itemId);
if (item == null) {
return Result.fail("Item Not Found");
}

// 4)调用外部服务
boolean withholdSuccess = inventoryService.withhold(itemId, quantity);
if (!withholdSuccess) {
return Result.fail("Inventory not enough");
}

// 5)领域计算
Long cost = item.getPriceInCents() * quantity;

// 6)领域对象操作
OrderDO order = new OrderDO();
order.setItemId(itemId);
order.setBuyerId(userId);
order.setSellerId(item.getSellerId());
order.setCount(quantity);
order.setTotalCost(cost);

// 7)数据持久化
orderRepository.createOrder(order);

// 8)返回
return Result.success(order);
}
}

这是经典式编程,入参校验、获取数据、逻辑计算、数据存储、返回结果,每一个use case基本都是这样处理的,套路就是取数据、计算数据、存数据;当然,有时我们常把中间的一块放到service中。随着use case越来越多,会把一些重复代码提取出来,比如util,或者公共的service method,但这些仍然是一堆代码,可读性、可理解性还是很差,这两个很差,那可维护性就没法保证,更不用提可扩展性,为什么?因为这些代码缺少了灵魂。何为灵魂,业务模型。

对于事务脚本式也有模型,单只有数据模型,而没有对象模型。模型是对业务的表达,没有了业务表达能力的代码,人怎么能读懂

而DDD在领域模型方式就有很强的表达能力,当然在编码时也不会以数据流向为指导。先写Domain层的业务逻辑,然后再写Application层的组件编排,最后才写每个外部依赖的具体实现,这就是Domain-Driven Design,其实这类似于TDD,谁驱动谁就得先行

反DDD

任何事物都是过犹不及,如文章开头所述,没有银弹,千万别因为DDD的火热而一股脑全身心投入DDD,不管场景是否适合,都要DDD;犹如设计模式,后面出现了大量的反模式。

错误的抽象比没有抽象伤害力更大

DDD分层

Interface层

对于这一层的作用就是接受外部请求,主要是HTTP和RPC,那也就依赖于具体的使用技术,是spring mvc、还是dubble

在DDD正统分层里面是有这一层的,但实践时,像我们的controller却有好几种归类

一、User Interface归属于大前端,不在后端服务,后端服务从application层开始

二、正统理论,就是放在interface层

三、controller毕竟是基于具体框架实现,在六边形架构中就是是个 adapter,归于 Infrastructure 层

对于以上三种归类,都有实践,都可以,但不管怎么归属,他的属性依然是 Interface

对于Interface落地时指导方针:

  1. 统一返回值,interface是对外,这样可以统一风格,降低外部认知成本
  2. 全局异常拦截,通过aop拦截,对外形成良好提示,也防止内部异常外溢,减少异常栈序列化开销
  3. 日志,打印调用日志,用于统计或问题定位
  4. 遵循ISP,SRP原则,独立业务独立接口,职责清晰,轻便应对需求变更,也方便服务治理,不用担心接口的逻辑重复,知识沉淀放在application层,interface只是协议,要薄,厚度体现在application层
1
2
3
4
5
6
7
8
9
10
11
12
@Data
public class Result<T> {

/** 错误码 */
private Integer code;

/** 提示信息 */
private String msg;

/** 具体的内容 */
private T data;
}

Application层

应用层主要作用就是编排业务,只负责业务流程串联,不负责业务逻辑

application层其实是有固定套路的,在之前的文章有过阐述,大致流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
application service method(Command command) {
//参数检验
check(command);

Aggregate aggregate = repository.findAggregate(command);

//复杂的需要domain service
aggregate.operate(command);

repository.saveOrUpdate(aggregate);

publish(event);

return DTOAssembler.to(aggregate);

}

业务流程 VS 业务规则

对于这两者怎么区分,也就是application service 与 domain service 的区分,最简单的方式:业务规则是有if/else的,业务流程没有

现在都是防御性编程,在check(command)部分,会做很多的precondition

比如转帐业务中,对于余额的前提判断:

1
2
3
4
5
6
public void preDebit(Account account, double amount) {
double newBalance = account.balance() - amount;
if (newBalance < 0) {
throw new DebitException("Insufficient funds");
}
}

这算是业务规则还是业务流程呢?这一段代码可以算是precondition,但也是业务规则的一部分,颇有争议,但没有正确答案,只是看你代码是否有复用性,目前我个人倾向于放在业务规则中,也就是domain层

厚与薄

常人讲,application service是很薄的一层,要把domain做厚,但从最开始的示例,发现其实application service特别多,而domain只有一行代码,这不是application厚了,domain薄了

对于薄与厚不再于代码的多与少,application层不是厚,而是编排多而已,逻辑很简单,一般厚的domain大多都是有比较复杂的业务逻辑,比如大量的分支条件。一个例子就是游戏里的伤害计算逻辑。另一种厚一点的就是Entity有比较复杂的状态机,比如订单

出入参数

先讲一个代码示例:

从controller接受到请求,传入application service中,需要做一层转换,controller层

示例一段创建目录功能的对象转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class DirectoryDto extends BaseRequest {

private long id;
@NotBlank
@ApiModelProperty("目录编号")
private String directoryNo;
@NotBlank
@ApiModelProperty("目录名称")
private String directoryName;

private String directoryOrder;
private String use;
private Long parentId;

}

com.jjk.application.dto.directory.DirectoryDto to(com.jjk.controller.dto.DirectoryDto directoryDto);

创建目录,入参只需要directoryNo,directoryName,为了少写代码,把编辑目录(directoryDto中带了id属性),response(directoryDto包含了目录所有信息)都揉合在一个dto中了

这样就会有几个问题:

  1. 违背SRP,创建与编辑两个业务功能却混杂在了一个dto中
  2. 相对SRP,更大的问题是业务语义不明确,DDD中一个优势就是要业务语义显示化

怎么解决呢?

引入CQRS元素:

  • Command指令:指调用方明确想让系统操作的指令,其预期是对一个系统有影响,也就是写操作。通常来讲指令需要有一个明确的返回值(如同步的操作结果,或异步的指令已经被接受)
  • Query查询:指调用方明确想查询的东西,包括查询参数、过滤、分页等条件,其预期是对一个系统的数据完全不影响的,也就是只读操作

这样把创建与编辑拆分,CreateDirectoryCommand、EditDirectoryCommand,这样有了明确的”意图“,业务语义也相当明显;其次就是这些入参的正确性,之前事务脚本代码中大量的非业务代码混杂在业务代码中,违背SRP;可以利用java标准JSR303或JSR380的Bean Validation来前置这个校验逻辑,或者使用Domain Primitive,既能保证意图的正确性,又能让application service代码清爽

而出参,则使用DTO,如果有异常情况则直接抛出异常,如果不需要特殊处理,由interface层兜底处理

对于异常设计,可根据具体情况处理,整体由业务异常BusinessException派生,想细化可以派生出DirectoryNameExistException,让interface来定制exception message,若无需定制使用默认message

Domain层

domain层是业务规则的集合,application service编排业务,domain service编排领域;

domain体现在业务语义显现化,不仅仅是一堆代码,代码即文档、代码即业务;要达到高内聚就得充分发挥domain层的优势,domain层不单单是domain service,还有entity、vo、aggregate

domain层是最最需要拥抱变化的一层,为什么?domain代表了业务规则,业务规则来自于需求,日常开发中,需求是经常变化的

我们需要逆向思维,以往我们去封装第三方服务,解耦外部依赖,大多数时候是考虑外部的变化不要影响自身,而现实中,更多的变化来自内部:需求变了,所以我们应该更多关注一个业务架构的目标:独立性,不因外部变化而变化,更要不因自身变化影响外部服务的适应性

在《DDD之Repository》中指出Domain Service是业务规则的集合,不是业务流程,所以Domain Service不应该有需要调用到Repo的地方。如果需要从另一个地方拿数据,最好作为入参,而不是在内部调用。DomainService需要是无状态的,加了Repo就有状态了。domainService是规则引擎,appService才是流程引擎。Repo跟规则无关

也就是domain层应该是一个纯内存操作,不依赖外部任何服务,这样提高了domain层的可测试性,拥抱变化的底气也来自于完整的UT,而application层UT全部得mock

Infrastructure层

Infrastructure层是基础实施层,为其他层提供通用的技术能力:业务平台,编程框架,持久化机制,消息机制,第三方库的封装,通用算法,等等

Martin Fowler将“封装访问外部系统或资源行为的对象”定义为网关(Gateway),在限界上下文的内部架构中,它代表了领域层与外部环境之间交互的出入口,即:

gateway = port + adapter

这一点契合了六边形架构

在实际落地时,碰到的问题就是DIP问题,Repository在DDD中是在Domain层,但具体实现,如DB具体实现是在Infrastructure层,这也是符合整洁架构,但DDD限界上下文可能不仅限于访问数据库,还可能访问同样属于外部设备的文件、网络与消息队列。为了隔离领域模型与外部设备,同样需要为它们定义抽象的出口端口,这些出口端口该放在哪里呢?如果依然放在领域层,就很难自圆其说。例如,出口端口EventPublisher支持将事件消息发布到消息队列,要将这样的接口放在领域层,就显得不伦不类了。倘若不放在位于内部核心的领域层,就只能放在领域层外部,这又违背了整洁架构思想

这个问题张逸老师提出了菱形架构,后面的章节中再论述

再次比较interface与infrastructure,在前面讲述到controller的归属,其实就隐含了interface与infra的关联,这两者都与具体框架或外部实现相关,在六边形架构中,都归属为port与adapter

我一般的理解:从外部收到的,属于interface层,比如RPC接口、HTTP接口、消息里面的消费者、定时任务等,这些需要转化为Command、Query,然后给到App层。

App主动能去调用到的,比如DB、Message的Publisher、缓存、文件、搜索这些,属于infra层

所以消息相关代码可能会同时存在2层里。这个主要还是看信息的流转方式,都是从interface -> Application -> infra

整洁架构

一个好的架构应该需要实现以下几个目标:

  1. 独立于框架:架构不应该依赖某个外部的库或框架,不应该被框架的结构所束缚
  2. 独立于UI:前台展示的样式可能会随时发生变化
  3. 独立于底层数据源:无论使用什么数据库,软件架构不应该因不同的底层数据储存方式而产生巨大改变
  4. 独立于外部依赖:无论外部依赖如何变更、升级,业务的核心逻辑不应该随之而大幅变化
  5. 可测试:无论外部依赖什么样的数据库、硬件、UI或服务,业务的逻辑应该都能够快速被验证正确性

这几项目标,也对应我们对domain的要求:独立性和可测试;我们的依赖方向必须是由外向内

DIP与Maven

要想实现整洁架构目标,那必须遵循面向接口编程,达到DIP

1
2
3
4
5
6
7
8
<modules>
<module>assist-controller</module>
<module>assist-application</module>
<module>assist-domain</module>
<module>assist-infrastructure</module>
<module>assist-common</module>
<module>starter</module>
</modules>

在使用maven构建项目时,整个依赖关系是:starter -> assist-controller -> assist-application -> assist-domain -> assit-infrastructure

domain层并不是中心层,为什么呢?为什么domain不在最中心?

主要是存在一个循环依赖问题:repository接口在domain层,但现实在infra层,可从maven module依赖讲,domain又是依赖infra模块,domain依赖infra的原由是因为前文所述

DDD限界上下文可能不仅限于访问数据库,还可能访问同样属于外部设备的文件、网络与消息队列。为了隔离领域模型与外部设备,同样需要为它们定义抽象的出口端口,这些出口端口该放在哪里呢

按此划分module,这些出口端口都放在了infra层,当domain需要外部服务时,不得不依赖infra module

对此问题的困惑持续很久,一直认为菱形架构是个好的解决方案,但今年跟阿里大佬的交流中,又得到些新的启发

EventPublisher接口就是放在Domain层,只不过namespace不是xxx.domain,而是xxx.messaging之类的

像repsoitory是在Domain层,但是从理论上是infra层,混淆了两个概念一个是maven module怎么搞,一个是什么是Domain层

以namespace区分后,得到的依赖关系就是DIP后的DDD

图片来自阿里P9大佬

菱形架构

上文中多次提到菱形架构,这是张逸老师发明的,去年项目中,我一直使用此架构

一是解决了上文中的DIP问题,二是整个架构结构清晰职责明确

简单概述一下:

把六边形架构与分层架构整合时,发现六边形架构与领域驱动设计的分层架构存在设计概念上的冲突

出口端口用于抽象领域模型对外部环境的访问,位于领域六边形的边线之上。根据分层架构的定义,领域六边形的内部属于领域层,介于领域六边形与应用六边形的中间区域属于基础设施层,那么,位于六边形边线之上的出口端口就应该既不属于领域层,又不属于基础设施层。它的职责与属于应用层的入口端口也不同,因为应用层的应用服务是对外部请求的封装,相当于是一个业务用例的外观。

根据六边形架构的协作原则,领域模型若要访问外部设备,需要调用出口端口。依据整洁架构遵循的“稳定依赖原则”,领域层不能依赖于外层。因此,出口端口只能放在领域层。事实上,领域驱动设计也是如此要求的,它在领域模型中定义了资源库(Repository),用于管理聚合的生命周期,同时,它也将作为抽象的访问外部数据库的出口端口。

将资源库放在领域层确有论据佐证,毕竟,在抹掉数据库技术的实现细节后,资源库的接口方法就是对聚合领域模型对象的管理,包括查询、修改、增加与删除行为,这些行为也可视为领域逻辑的一部分。

然而,限界上下文可能不仅限于访问数据库,还可能访问同样属于外部设备的文件、网络与消息队列。为了隔离领域模型与外部设备,同样需要为它们定义抽象的出口端口,这些出口端口该放在哪里呢?如果依然放在领域层,就很难自圆其说。例如,出口端口EventPublisher支持将事件消息发布到消息队列,要将这样的接口放在领域层,就显得不伦不类了。倘若不放在位于内部核心的领域层,就只能放在领域层外部,这又违背了整洁架构思想。

如果我们将六边形架构看作是一个对称的架构,以领域为轴心,入口适配器和入口端口就应该与出口适配器和出口端口是对称的;同时,适配器又需和端口相对应,如此方可保证架构的松耦合。

1
2
3
4
5
6
<modules>
<module>assist-ohs</module>
<module>assist-service</module>
<module>assist-acl</module>
<module>starter</module>
</modules>

这有点类似《DDD之形》中提到的端口模式,把资源库Repository从domain层转移到端口层和其它端口元素统一管理,原来的四层架构变成了三层架构,对repository的位置从物理与逻辑上一致,相当于扩大了ACL范围

这个架构结构清晰,算是六边形架构与分层架构的融合体,至于怎么选择看个人喜爱

Event

相对Event Source,这儿更关注一下event的发起,是不是需要区分应用事件和领域事件

根据application的套路,会publish event,那在domain service中要不要publish event呢?

Domain Event更多是领域内的事件,所以应该域内处理,甚至不需要是异步的。Application层去调用消息中间件发消息,或调用三方服务,这个是跨域的。

从目前的实践来看,直接抛Domain Event做跨域处理这件事,不是很成熟,特别是容易把Domain层的边界捅破,带来完全不可控的副作用

所以结合application,除了Command、Query入参,还需要Event入参,处理事件

总结

本文主要是按DDD分层,介绍各层落地时的具体措施,以及各层相应的规范,引入CQRS使代码语义显现化,通过DIP达到整洁架构的目标

对于domain层,有个重要的aggregate,涉及模型的构建,千人千模,但domain层的落地是一样的

在业务代码中有几个比较核心的东西:抽象领域对象合并简单单实体逻辑,将多实体复杂业务规则放到DomainService里、封装CRUD为Repository,通过App串联业务流程,通过interface提供对外接口,或者接收外部消息

其实不论使用DDD,还是事务脚本,合适的才是最好的,任何方法论都得以降低代码复杂度为目的