引言

本文只是一家之言。

本文是一系列文章的缩略版本(完整版只写了个开头),尽量只讲具体的东西,如果有东西太干了,没有具体的“体感”,是作者的责任。

不喜欢看纯理论分析的可以跳到单一系统层次和模块设计(大多数人可能更加关注这一节,其实前面的部分更重要)

几个很干的原则

  • 解决复杂问题要用高级思维,不要用低级思维。
  • 蚂蚁/ebay 等若干家企业架构师四大原则 - 听过的可以往下跳:
    • 分治(其他所有原则都是从分治里衍生的)
    • 分层
    • 抽象
    • 演化
  • solid 5 原则很重要很重要 -很多人读过,很多人可能没有读过,温故知新很重要。
  • 注重过程质量,拿到结果质量。

业务系统为什么难写?

纯粹的业务驱动:技术的输入和决策完全来源于业务同事,甚至只受业务摆布的团队,架构容易混乱

  • 业务又不懂架构、业务又不懂功能点罗列的合理性,业务只会往技术团队身上扔需求。
    • 怎么把需求和实现分门别类是技术自己的事情
    • 但技术人员如果一直都很忙,没有自己的空闲时间或者对设计洁癖的坚持,慢慢地就会养成“把需求翻译成代码,然后往老的系统里面扔”(混乱根源 1)的坏习惯- 问题:翻译只是普通的低级思维,不能解决很复杂的问题。

不理解业务

  • 盲人摸象,一群瞎子摸大象,摸到耳朵的人以为大象是一把扇子;摸到肚子的人以为大象是一堵墙;摸到鼻子就以为大象是一根管子:
    • 在复杂业务面前,我们就是瞎子,甚至 pm 都是瞎子。很多时候我们没有机会摸完一头大象,就要做系统设计决策。因此,我们经常只把系统设计成一把扇子、一堵墙和一根管子(混乱根源 2)- 问题:定义问题太简单,设计解决方案太简单 。
      盲人摸象
    • 很多时候甚至一个 pm 认为耳朵是扇子,技术人员认为耳朵是一张饼,大家以为达成共识了,实际上概念是割裂的,大家也不愿探讨清楚(混乱根源 3)- 问题:技术不要觉得只懂技术就够了,实际上系统的业务越复杂,技术越不重要。
  • 一个系统刚刚设计的时候,最初的架构设计人员已经把层次都分好了。技术人员有惰性,大多数情况下,不会再在已经划分好的层次里面进行横竖切分,制造小模块和小层次(混乱根源 4)。大多数工程师能够习得的最趁手的抽象、最万能的抽象,就是一个 service 里的若干个函数,别的东西,大家什么都不会。所以大家只能把逻辑封装进这两个玩意儿里。
  • …………………………
  • 混乱是一切问题的根源,业务系统的熵值太高以后,新的功能不好加,旧的功能不好改,业务系统成为一个大箩筐,里面的东西就腐化了

以上问题的抽象解法

  • 技术团队要自己有技术驱动的工程师文化,大家志同道合,齐心合力:
    • 理解现有系统,定期架构 review,更新系统基线
    • 从业务输入出发,规划技术路径
    • 专门做技术改造来推动架构演化。
  • 学业务:
    • 学业务流程,多读领域故事,以业务输入为老师,请领域专家为老师。
    • 以对特定的领域有百科全书式的理解为目标-大家都不是搞金融的,一样可以成为金融架构师。
    • 注重需求分析和建模,要先有分析才出设计,而不是边设计边分析。
  • 注重设计:
    • 整体架构设计(这是由定期架构 review来决定的)。
    • 注重单一系统层次和模块设计,后面会专门讲。
    • 设计系统的时候要留有余地(OCP 法则),存有敬畏之心。我们应该只把我们摸到的地方设计成一把扇子、一堵墙和一根管子,其他东西留给以后摸到再设计和实现。
    • 要有穷举能力,要有归纳能力,要有演绎能力,要面向不确定性编程。

单一系统层次和模块设计(大多数人可能更加关注这一节,其实前面的部分更重要)

软件工程师做抽象设计的时候,处理的是单元/模块/层次之间的关系,所以要对复杂系统具有横切和竖切的能力。

架构分层和分模块的演进

大家都知道系统很复杂, MVC 模式的样子:

MVC 模式

过了十几年,martin fowler 等人开始介绍所谓的《PresentationDomainDataLayering》 模式(有兴趣的同学可以读这本书《企业应用架构模式 [Patterns of Enterprise Application Architecture]》),导致了国内流行了十几年的所谓是三层架构、四层架构:

Presentation Model

我们团队现在的系统通常是什么样的?

工程规范之分包规范
工程规范之分包规范

一个烂系统通常是什么样的?

其实所有烂系统都没有高度,别人划分好模块和层次以后,里面都是流水账。特别是 service,上蹿下跳,复杂无比。

烂系统如流水账

好的系统给人的感觉

好的系统:结构分明、抽象恰当、职责明确、边界清晰。

好的系统如博士论文

坏的系统如流水账,好的系统如一本好书,局部来看是记叙文(可以使用 transaction script),全局来看是议论文(讨论 why、what、how)。

一个系统应该被设计成什么样?

没有标准答案,所以大家都乱来。

但合理的设计应该满足一个约束:系统是对业务问题的回答,系统能力能够支持用例

Step1 搞清楚业务用例和系统用例

当我们收到需求,一开始它是这样:

售卖重疾险用例

其实这是一个业务视角的业务用例,它背后是由若干系统用例支撑的:

售卖重疾险业务用例拆解为系统用例
售卖重疾险业务用例拆解为系统用例

而我们的系统中往往已经有过其他业务用例

新老业务和系统用例

两种业务用例共用一套系统用例

这个地方本来应该画流程图,但没时间不画了,默认用户故事的流程是:

  1. 生成投保单
  2. 定价
  3. 付款
  4. 出单

Step2 用例拆双层,能力也拆双层

如果用例能够分层(问题空间分层),系统能力也就可以分层(解空间分层),系统实际上就变成这样:

双层能力.drawio
双层能力

注意看,这里 service 层是一拆二的,很确切地把系统的内部能力按照用例分解为两层(先横切),然后按照领域进行领域划分(竖切)-这里是 eric evans 在 ddd 里推荐的一种拆法。

Step3 使用 OOP 来代替 POP,用命令对象来代替函数调用

业务用例层每个模块内可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 医疗险用户故事,旧代码
*/
medicalInsuranceProcess(Request) {
medicalInsuranceToubaodan(xx);
medicalInsurancePricing(xx);
medicalInsurancePay(xx);
medicalInsuranceIssued(xx);
}

/**
* 重疾险用户故事
*/
majorDiseaseInsuranceProcess(Request) {
majorIllnessInsuranceToubaodan(xx);
majorIllnessInsurancePricing(xx);
majorIllnessInsurancePay(xx);
majorIllnessInsuranceIssued(xx);
}

当然也有些人会只写一个 pay 方法,然后在 pay 方法里写 if-else,但这样写的代码一样烂,还是无限接近小学生代码。

如果使用抽象的方法来写,其实会写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 医疗险用户故事,旧代码
*/
medicalInsuranceProcess(Request) {
Context context = createContext(request);

toubaoDanActivity.execute(context);
pricingActivity.execute(context);
payAcivity.execute(context);
issueActivity.execute(context);
}

/**
* 重疾险用户故事
*/
medicalInsuranceProcess(Request) {
Context context = createContext(request);

toubaoDanActivity.execute(context);
pricingActivity.execute(context);
payAcivity.execute(context);
issueActivity.execute(context);
}

这样做的理由是:

标准的自顶向下设计,越高层的入口应该越抽象,只使用方法嵌套需要设计者的功力极高的功力,不适合大多数人。

流程被完全抽象化了,流程的变动不再是函数套函数,而是对象套对象。对象是一种很适合嵌套和分隔的设计单元,它的多态比函数的重载强大得多。

中国人受贫血模型+事务脚本的设计模式影响太深,很多人都忘记了,23 种设计模式其实都是使用富血对象!在设计的过程中,要注重理解行为型设计模式里面组件沟通的思路,选择对象或者方法作为扩展点-使用方法作为扩展点比较危险,因为继承的耦合强于组合,总是倾向于桥接到更远端的对象是更好的选择,这就诞生了对象的层次。

Step4 引入策略,把变化拆解到领域原子能力颗粒度

有人会想,是不是每次新增一个业务用例,我重新派生一个 activity 就行了?

其实大可不必,新的用户故事本身只是对领域能力的重新编排,在每个 activity 中的变动点无非是:

  1. 使用新的不同的领域能力-这要求我们有新增领域能力的能力
  2. 编排不同领域能力的顺序-这就要求我们有新增领域顺序的策略

以定价为例,我们先提供不同的基础领域能力:

双层能力-细化领域

双层能力-细化领域.png

然后用策略模式把他们包装起来:

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
/**
* 重疾险用户故事
*/
majorIllnessInsuranceProcess(Request) {
context = createContext(request);

toubaoDanActivity.execute(xx);
pricingActivity.execute(xx);
payAcivity.execute(xx);
issueActivity.execute(xx);
}

// 分割线

public class PayActivity {

List<PayStrategy> strategies;

void pay(Context context) {
for (PayStrategy strategy : strategies) {
if (context.isActivated()) {
strategy.execute(context);
}
}
}
}

public class YinqiZhilianStrategy {
public void execute(Context context) {
// pre
yinqizhilianDelegate(request1);
yinqizhilianManager(request2);
// 其实有 manager 不应该直接接触 dao
yinqizhilianMDao(request3);
// post
}
}

假设我们有了这样的层次,则每次我们只要变动:

  1. activity 之间的任意顺序。
  2. acitivity 和 strategy 之间的关系。
  3. strategy 的种类。

就可以灵活自顶向下地复用现有的架构来以最小的改动支撑新的业务需求。

原本复杂的流程,简单翻译就是扁平的结构,如果我们通过横切竖切来制造多次抽象,逐渐填充细节,系统就有了高度。

strategy 还有额外的好处:它抹掉了不同领域的具体实现的具体差异,使得很多服务要调用远程服务、数据库所必须的连接性操作、适配性操作,和基础的,完全与用户故事无关的原子能力,完全被 strategy 封装得干干净净。上层应用调用一个 strategy,就像调用一个领域。

有意识地引入层次
有意识地引入层次

Step5 如何实现动态激活策略?引入业务身份和业务配置

其实怎么设计 context 和怎么写 context.isActivated() 是最难的。

一个基本的思路,每次用户提需求的时候填一个表:

1
2
3
4
5
6
-- 业务身份
1. 产品业务身份名称: majorIllnessInsuranceBizIdentity
-- 业务配置
2. xxx流程使用xxx 能力
3. xxx流程使用xxx 能力
4. xxx流程使用xxx 能力
  1. 2/3/4行存入数据库里。
  2. 上游业务系统的 请求进来的时候带入这个majorIllnessInsuranceBizIdentity。
  3. 请求进入 controller 的时候就把 context 里面的 xxx 能力、xxx 能力的映射表设为 true。
  4. 在每个 strategy 里 context.isActivated() 读这张配置表。

理论上流程标准化以后,系统扩展点应该全部存在数据库里面,上产品操作数据库激活扩展点集合。

这就是分层架构 + 标准化流程建模 + 面向不确定性编程思想。蚂蚁的保险使用繁星策略,context 横跨多个系统;阿里淘系使用星环系统,context 可以横跨多个 bu。

整洁架构、六边形架构和洋葱架构

清晰架构(01): 融合 DDD、洋葱架构、整洁架构、CQRS…(译)

洋葱架构
整洁架构
合理的控制流