码农戏码

新生代农民工的自我修养

0%

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

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

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

大多数程序员心里觉得应该是展示最牛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,还是事务脚本,合适的才是最好的,任何方法论都得以降低代码复杂度为目的

老施现在是一家高速发展的互联网公司CTO,去年约他喝茶,聊到这几年在公司的三个方向稳定性、透明性、独立性

这三性对很多公司也很有代表性,最近公司也在提要把项目销售情况与研发部门同步,想起来老施讲的这三性,写出来回顾一下,这三点相辅相成,每个阶段重点不同而已

稳定性

提到稳定性,第一个想到的就是系统稳定,这是必然。怎么才算稳定呢?

这就是具体技术范畴,高可用、高可靠、可扩展、可维护…

除了系统本身稳定性,还需要APM,以及主动或被动系统探针

还有一种就是资产的稳定,各个服务不单软件还有硬件,是否都有人管控


其次就是人员稳定,每个人都得有backup,包括CTO本人,这主要是讲在master临时有事时,不至于事项被中断

还有更高阶的,企业文化是否吸引人才、配套人才发展机制

透明性

透明性,也有几方面

公司战略透明性,公司每月会开通告性会议,这已经不单单是研发部门,各个一级部门都要向一线员工去宣讲公司战略,各部门最近落地产出,虽然这成本有点高,但有价值,也防止中层管理信息隔断

团队财务透明,每个团队都在不停赶时间,不管是项目还是活动,那么怎么分配资源?是因为公司战略需要,还是因为项目负责人嗓门大。不管是拉新,拉营收,项目运营状态如何?投入产出比如何,需要从运营端同步到研发端,不仅激发一线研发热情,还能知道项目负责人是不是真的只是嗓门大

独立性

团队人员独立,常碰到一个团队A承接了紧急项目,去别的团队B借人,人刚借走,团队B也来大活了,去要回人还是再去借人?人开发完回来了,却常被对方人拉去联调,修bug

独立性也配合了稳定性

团队财务独立,公司整体是向上发展的,算总帐帐面都不错,其实好些团队财务是严重透支的,与上现的透明性相呼应

最近看到篇好文章《IO多路复用》,记得早期学习时,也去探索过select、poll、epoll的区别,但后来也是没有及时记录总结,也忘记了,学习似乎就是在记忆与忘记中徘徊,最后在心中留下的火种,是熄灭还是燎原就看记忆与忘记间的博弈

socket与io一对兄弟,有socket地方必然有io,io数据也大多来源于socket,回顾这两方面的知识点,大致梳理一下

socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议

除了TCP协议(三次握手、四次挥手)知识点外,再就是各阶段与java api对应的方法

三次握手关联到两个方法:服务端的listen()与客户端的connect()

两个方法的共通点:TCP三次握手都不是他们本身完成的,握手都是内核完成的,他们只是通知内核,让内核自动完成三次握手连接

不同点:connect()是阻塞的,listen()是非阻塞的

三次握手的过程细节:

  • 第一次握手:客户端发送 SYN 报文,并进入 SYN_SENT 状态,等待服务器的确认;
  • 第二次握手:服务器收到 SYN 报文,需要给客户端发送 ACK 确认报文,同时服务器也要向客户端发送一个 SYN 报文,所以也就是向客户端发送 SYN + ACK 报文,此时服务器进入 SYN_RCVD 状态;
  • 第三次握手:客户端收到 SYN + ACK 报文,向服务器发送确认包,客户端进入ESTABLISHED 状态。待服务器收到客户端发送的 ACK 包也会进入ESTABLISHED 状态,完成三次握手

io

IO中常听到的就是同步阻塞IO,同步非阻塞IO,异步非阻塞IO;也就是同步、异步、阻塞、非阻塞四个词组合体,可从名字上看就不大对,既然同步,应该都是阻塞,怎么会有同步非阻塞?不知道哪位先贤的学习总结却流传深远

还有些把non-blocking IO与NIO都混淆了

对于IO模型,最正统的应该来自Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models”

  • Blocking I/O
  • Non-Blocking I/O
  • I/O Multiplexing
  • Asynchronous I/O

在理解这四种常见模型前,先简单说下linux的机制,可以更方便理解IO,在《堆外内存》中提到linux的处理IO流程以及Zero-Copy技术,算是IO模型更深入的知识点

应用程序发起的一次IO操作实际包含两个阶段:

  • 1.IO调用阶段:应用程序进程向内核发起系统调用
  • 2.IO执行阶段:内核执行IO操作并返回
    • 2.1. 准备数据阶段:内核等待I/O设备准备好数据
    • 2.2. 拷贝数据阶段:将数据从内核缓冲区拷贝到用户空间缓冲区

对于阻塞与非阻塞,讲的是用户进程/线程与内核之间的切换;当内核数据没有准备好时,用户进程就得挂起

对于同步与异步,重点在于执行结果是否一起返回,IO就是指read,send是否及时获取到结果

大致分析一下,同步异步、阻塞非阻塞的两两组合其实是把宏观与微观进行了穿插,从应该程序角度获取结果是同步或异步,而IO内部再细分了阻塞与非阻塞

由上文所述:IO操作分两个阶段 1、等待数据准备好(读到内核缓存) ,2、将数据从内核读到用户空间(进程空间)。 一般来说1花费的时间远远大于2。 1上阻塞2上也阻塞的是同步阻塞IO; 1上非阻塞2阻塞的是同步非阻塞IO,NIO,Reactor就是这种模型; 1上非阻塞2上非阻塞是异步非阻塞IO,AIO,Proactor就是这种模型。

同步阻塞IO(Blocking IO)

因为用户态被阻塞,等待内核数据的完成,所以需要同步等待结果

同步非阻塞IO(Non-blocking IO)

用户态与内核不再阻塞了,但需要不停地轮询获取结果,浪费CPU,这方式还不如BIO来得痛快

IO多路复用

Reactor模式,意为事件反应,操作系统的回调/通知可以理解为一个事件,当事件发生时,进程/线程对该事件作出反应。Reactor模式也称作Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程/线程

对于IO多路复用,里面再有的细节就是一个优化过程,select,poll,epoll

AIO

Proactor模式,Reactor可理解为“来了事件我通知你,你来处理”,而Proactor是“来了事件我处理,处理完了我通知你”。这里“我”是指操作系统,“你”就是用户进程/线程

四种模型对比

对于IO模型的优化进程,一是操作系统的支持,减少系统调用,用户态与内核的切换;二是机制的变换,从命令式到响应性的转变


高性能架构

只温习Socket/IO知识太无趣了,我们要温故知新,升华一下,从架构角度谈一谈

从常规服务处理业务流程讲:request -> process -> response

站在架构师的角度,当然需要特别关注高性能架构的设计。高性能架构设计主要集中在两方面:

  1. 尽量提升单服务器的性能,将单服务器的性能发挥到极致。
  2. 如果单服务器无法支撑性能,设计服务器集群方案。

除了以上两点,最终系统能否实现高性能,还和具体的实现及编码相关。但架构设计是高性能的基础,如果架构设计没有做到高性能,则后面的具体实现和编码能提升的空间是有限的。形象地说,架构设计决定了系统性能的上限,实现细节决定了系统性能的下限。

单服务器高性能的关键之一就是服务器采取的并发模型,并发模型有如下两个关键设计点:

  • 服务器如何管理连接
  • 服务器如何处理请求

以上两个设计点最终都和操作系统的 I/O 模型及进程模型相关。

  • I/O 模型:阻塞、非阻塞、同步、异步
  • 进程模型:单进程、多进程、多线程

传统模式PPC&TPC

PPC,即Process Per Connection,为每个连接都创建一个进程去处理。此模式实现简单,适合服务器连接不多的场景,如数据库服务器

TPC,即Thread Per Connection,为每个连接都创建一个线程去处理。线程创建消耗少,线程间通信简单

这两种都是传统的并发模式,使用于常量连接的场景,如数据库(常量连接海量请求),企业内部(常量连接常量请求)

至于是进程还是线程,大多与语言特性相关,Java语言由于JVM是一个进程,管理线程方便,故多使用线程,如Netty。C语言进程和线程均可使用,如Nginx使用进程,Memcached使用线程。

不同并发模式的选择,还要考察三个指标,分别是响应时间(RT),并发数(Concurrency),吞吐量(TPS)。三者关系,吞吐量=并发数/平均响应时间。不同类型的系统,对这三个指标的要求不一样。

三高系统,比如秒杀、即时通信,不能使用

三低系统,比如ToB系统,运营类、管理类系统,一般可以使用

高吞吐系统,如果是内存计算为主的,一般可以使用,如果是网络IO为主的,一般不能使用。

Reactor&Proactor

对于传统方式,显示只能适合常量连接常量请求,不能适应互联网场景

如双十一场景下的海量连接海量请求;门户网站的海量连接常量请求;

引入线程池也是一种手段,但也不能根本解决,如常量连接海量请求的中间件场景,线程虽然轻量但也有得消耗资源,终有上限

Reactor,意为事件反应,操作系统的回调/通知可以理解为一个事件,当事件发生时,进程/线程对该事件作出反应。Reactor模式也称作Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程/线程。

可以看到,I/O多路复用技术是Reactor的核心,本质是将I/O操作给剥离出具体的业务进程/线程,从而能够进行统一管理,使用select/epoll去同步管理I/O连接。

Reactor模式的核心分为Reactor和处理资源池。Reactor负责监听和分配事件,池负责处理事件

如何高性能呢?就得IO多路复用,配合上进程、线程组合,就有:

  • 单Reactor 单进程 / 线程
  • 单Reactor 多线程
  • 多Reactor 单进程 / 线程(此实现方案相比“单 Reactor单进程”方案,既复杂又没有性能优势,所以很少实际应用)
  • 多Reactor 多进程 / 线程

单Reactor单线程

在这种模式中,Reactor、Acceptor和Handler都运行在一个线程中

单 Reactor 单进程的模式优点就是很简单,没有进程间通信,没有进程竞争,全部都在同一个进程内完成。

但其缺点也是非常明显,具体表现有:

  • 只有一个进程,无法发挥多核 CPU 的性能;只能采取部署多个系统来利用多核 CPU,但这样会带来运维复杂度,本来只要维护一个系统,用这种方式需要在一台机器上维护多套系统。
  • Handler 在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈

因此,单 Reactor 单进程的方案在实践中应用场景不多,只适用于业务处理非常快速的场景,目前比较著名的开源软件中使用单 Reactor 单进程的是 Redis

在redis中如果value比较大,redis的QPS会下降得很厉害,有时一个大key就可以拖垮

现在redis6.0版本后,已经变成多线程模型,对于大value的删除性能就提高了

单Reactor多线程

在这种模式中,Reactor和Acceptor运行在同一个线程,而Handler只有在读和写阶段与Reactor和Acceptor运行在同一个线程,读写之间对数据的处理会被Reactor分发到线程池中

单Reator多线程方案能够充分利用多核多 CPU的处理能力,但同时也存在下面的问题:

  • 多线程数据共享和访问比较复杂。例如,子线程完成业务处理后,要把结果传递给主线程的 Reactor 进行发送,这里涉及共享数据的互斥和保护机制。以 Java 的 NIO 为例,Selector 是线程安全的,但是通过 Selector.selectKeys() 返回的键的集合是非线程安全的,对 selected keys 的处理必须单线程处理或者采取同步措施进行保护。
  • Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈

多Reactor多线程

为了解决单 Reactor 多线程的问题,最直观的方法就是将单Reactor改为多Reactor

目前著名的开源系统 Nginx 采用的是多Reactor多进程,采用多Reactor多线程的实现有 Memcache 和 Netty

使用5W根因分析法(它又叫 5Why 分析法或者丰田五问法,具体是重复问五次“为什么”)检查一下对这块知识的学习程度

问题 1:为什么 Netty 网络处理性能高?

答:因为 Netty 采用了 Reactor 模式

问题 2:为什么用了 Reactor 模式性能就高?

答:因为 Reactor 模式是基于 IO 多路复用的事件驱动模式。

问题 3:为什么 IO 多路复用性能高?

答:因为 IO 多路复用既不会像阻塞 IO 那样没有数据的时候挂起工作线程,也不需要像非阻塞 IO 那样轮询判断是否有数据。

问题 4:为什么 IO 多路复用既不需要挂起工作线程,也不需要轮询?

答:因为 IO 多路复用可以在一个监控线程里面监控很多的连接,没有 IO 操作的时候只要挂起监控线程;只要其中有连接可以进行 IO 操作的时候,操作系统就会唤起监控线程进行处理。

问题 5:那还是会挂起监控线程啊,为什么这样做就性能高呢?

答:首先,如果采取阻塞工作线程的方式,对于 Web 这样的系统,并发的连接可能几万十几万,如果每个连接开一个线程的话,系统性能支撑不了;而如果用线程池的话,因为线程被阻塞的时候是不能用来处理其他连接,会出现等待线程的问题。其次,线上单个系统的工作线程数配置可以达到几百上千,这样数量的线程频繁切换会有性能问题,而单个监控线程切换的性能影响可以忽略不计。第三,工作线程没有 IO 操作的时候可以做其他事情,能够大大提升系统的整体性能。

Reference

五种IO模型透彻分析

IO模型

Scalable IO in Java

《从零开始学架构》