从分而治之的思想到架构的设计

  • 2019 年 10 月 3 日
  • 筆記

辛巴当上了国王,他究竟要怎样才能管理好它的王国?

 

 

 

分治与总量控制

在上一篇文章里,我们得到两个信息:

  • 人类大脑的信息实时处理能力存在上限

  • 软件系统的复杂度远超人类大脑的复杂度处理上限

从而引出了人类解决大规模复杂问题的根本方法

  1. 分而治之

然而分而治之的需要基于一个前提进行

  1. 复杂度总量控制

因为绝大多数人类参与的问题中,分而治之都会引入额外的汇总求解成本。要尽量减少复杂度,有两个方面的思想层次的指导:

  • 子问题在人类可控范围内尽量的大

  • 每个子问题要高内聚,问题间要低耦合

子问题在人类可控范围内尽量大,则有助于减少子问题数量,因而减少合并处理子问题的成本。同时子问题不超过一个人处理能力的上限是因为,一旦超过一个人的能力范围,要多人合作解决同一个平面上问题的不同部分时,必然会因合作之间沟通等原因降低整体开发效率(1 + 1 < 2)

高内聚、低耦合是一体,但从不同角度描述的理念。高内聚,即所需完成的事情所需的资源在内部通过较少的代价即可取得,低耦合则是指代 输出给外部、或者从外部输入的资源尽可能的小。

在存在关联的子问题中,耦合并不可能消除。耦合这个词可能大多数程序员听起来有有点不好的意味,但耦合存在另外一个中性的名字——接口。所以,产生了一个接口就产生了一个耦合,接口参数越多,耦合就越强。

以上是对之前文章的回顾,本文后续将基于个人理解,讲述 分治的隔离级别,如何识别要拆分的点,以及解决如何拆的问题。

分治的隔离级别

分治时,拆分软件系统的形式很多,可以拆分成:

  • 不同的方法

  • 不同的类

  • 不同Package

  • 不同的moudule

  • 不同的jar

  • 不同的进程(微服务)

  • 不同的系统

以上形态拆分形式的的隔离级别越来越大(当然,还存在其他不同的拆分形式和粒度),我们又要如何选择拆分的形态呢?

这需要看我们当前对业务的理解程度。隔离级别越大,说明打通隔离越困难,修改调整越困难,但同时也意味着设计的架构越容易被遵守。因此当你坚信架构划分正确,不同部分需要尽可能分割时,可以采用基于进程甚至更高级别的隔离。

但对于业务变化依然很大,边界不清晰,难以把握全局时,采用不同进程隔离或者不同jar的隔离可能就不合适,因为架构依然可能还需要调整,如果已经拆分了进程、jar那么架构调整的难度将大幅增加。若不调整架构并要兼容之前架构未作适配的业务的话,那只能大幅增加模块间的耦合(接口)

因此个人觉得大规模的拆分,最理想的情况下,应该是在同一个jar的粒度下,先做好基于module/pakcage的拆分及重构,然后稳定运行一定程度后,如果仍然觉得有拆分的必要的话,再将其进行jar、进程级别的隔离。

当然上述的做法可能难以奏效,因为我们干很多事情都是运动式的,上面的做法看起来只是个内部重构,“很简单”,因此也难以争取到相关的时间、人力等资源来进行,因而我们的重构也许只能通过一次性“搞个大”的,冒着更高的开发风险与顶着可能不完善的架构来进行。

识别需要进行拆分的点

我们在写代码时,可能经常会遇到以下现象:

  • 一个方法长度过百行,甚至达到一千行 阅读者无法理解整个方法干了什么

  • 一个方法的参数超过7、8个甚至上十个,使用者不知道应该如何使用方法

  • 一个类注入上十个外部资源,进行单元测试时Mock操作时代价极大

  • 一个将来可能会变更的接口定义被外部直接引用调用数十遍,需要修改时难以下手,也不敢下手

  • 一个目录/package内放了数十上百个资源,当需要查找某个资源时,费眼神,费时间

  • 一个系统显式依赖于数十个上游系统,协调统筹这些系统工作,上游任意一个系统发生故障导致下游的bug产生时,都需要该系统排查及联系解决

  • 一个系统包含的逻辑很多,因此修改经常很多,一部分修改了,导致很大其他一部分要一起跟着测试、停机上线

  • ……

以上现象引起的问题的本质是,一个节点下直接对应的子节点过多,导致问题的规模超过了一个人的认知处理能力,从而需要多人在同一层级协作开发,而每个人都是一个内聚的个体,合作将会由于沟通、知识背景等原因而导致整体开发效率下降。上面举例的都是编程中的例子,实际上你可以推演到我们工作生活的方方面面。

因此,当一个正常的人因为问题的规模原因,而难以理解问题自身时,我们就需要对问题进行分解。

这就是所需拆分的点。

 

 

 

如何进行拆分

要进行拆分的话,我们首先要识别什么是内聚的。

 

 

 

插入图上一篇文章的图,假设这是从业务逻辑上看问题域的形状。那么中间那根高内聚低耦合的线是能够以最小的耦合切分两个问题域的。

但软件系统不是平面,是多维立体的,不同维度看到的形状不一样,这根线在其他维度的投影,如在组织架构上看,可能就不是最优的。

因此我们可以知道,一个软件系统的内聚并不是简单一维的,从不同角度观察得到的内聚拆分结果 很可能是一致的,也有可能是正交的,但也有可能是非正交得此失彼的。

那看待软件系统的内聚,可以有哪些视角(维度)呢?

它至少包含以下内容:

  • 业务逻辑视角

    • 业务流程视角

    • 业务稳定程度视角

    • 业务关注点视角

    • 业务对象视角

    • 业务契约视角

  • 性能视角

    • 计算效率视角

    • 存储完整视角

    • 网络通讯效率视角

  • 人员组织架构视角

    • 沟通协调成本视角

    • 权责利益视角

  • 系统安全视角

  • ……

由于篇幅原因,本文仅讨论在业务逻辑视角下的逻辑拆分。

业务流程视角

我们的业务流程通常会分段组成,如

  • 客户注册

  • 客户登录

  • 客户浏览

  • 客户交易

  • 订单派送/执行

  • 订单后评价

而这些流程是业务领域的术语,其天然就是内聚的,若不是内聚的,则会在真实世界里阶段与阶段间出现过多的耦合,导致效率变慢。

因此我们很大程度上可以参考实际业务的流程来划分应用程序的流程,毕竟软件是对真实世界的模拟与辅助。(至于软件反向协助业务完善其运行逻辑及抽象,则是另外一个故事了,我们这里不谈)

因此在分段的业务流程中,我们可以将 注册登录 作为一个组件,而后续的浏览交易等内容可以作为一个组件。当然后续业务继续变得复杂时,我们还可以根据上面讨论的继续拆分,把浏览、交易继续分开。

业务稳定程度视角

大多数业务都存在一些易变的逻辑,也存在一些不稳定的逻辑,按照逻辑稳定程度我们可以对其分层:

  • 渠道逻辑

  • 应用逻辑

  • 共享服务中心逻辑

  • 技术框架逻辑

  • 物理资源管控逻辑

(以上仅为其中一种分层结构,可以有其他形式的分层)

我们借用 阿里巴巴 谢纯良 分享的一幅图

 

 

 

 

上面这幅图可以体现的是通用稳定逻辑的下沉,达到所谓中台火力支援的效果。中台的核心理念其实是特别地朴素的,但任何事情体量大了,实施起来就会特别难。并且中台也不仅仅是软件系统的事情,还涉及组织架构等方面内容。

业务关注点视角

在我们购买一件商品进行交易的过程中,实际上有很多逻辑进行了处理:

  • 主交易流程的关注点:放入购物车,下订单,支付,发货等等

  • 积分相关系统的关注点:交易成功后要根据交易金额加积分,如果最终交易回滚了我们要回滚积分

  • 营销系统相关的关注点:这个交易是否从我们的我们的营销活动进入,我们的营销活动是否促成了这笔交易

  • 库存系统的关注点:这笔订单我应该在什么时候冻结库存,这笔订单我应该在什么时候释放库存,这笔订单我应该在什么时候扣减库存,我应该扣哪个仓储的库存

  • ……

如果把诸如此类的逻辑拍平放到一个平面的逻辑中,那么理解将会特别地困难,

  • 一个原因是代码将会变得特别地长

  • 另外一个原因理解这个代码将需要掌握更多方面的业务知识

  • 还有就是程序员的思维需要在不同的业务上下文中切换,这也会增加理解的难度。

但我们能经常看到代码并没有按照关注点分离,而是将所有的逻辑都堆到一起。

对于此类代码,在方法复杂度已经超越了一个人的理解能力范围后,则需要对其按照业务关注点分离。主流程不应该感知各类分支流程的存在,主流程就应该只关注主流程视角的业务,如 交易主流程 只看 购物车、下订单、支付、发货等,而依附于该主流程的 积分、营销等则不应该在主流程中显式出现(可以通过消息,类SPI等方式隐式联动),因为这会加重主流程的认知负担。

 

 

业务对象视角

在我们的业务系统里,经常喜欢使用事务脚本的模式(有另外一个带偏见的名字——贫血模型)的模式来进行开发。这种模式是一种简单高效的模式,直到业务进化到特别复杂的程度。

为什么事务脚本不适用于特别复杂的逻辑?其实这就是我们本文一直讨论的问题,太复杂从而一个人难以把控。事务脚本以数据库记录为粒度来组织逻辑,并且通常不同类型的数据库记录之间缺少封装隔离,因此众多类型的记录以及其相互作用关系在会同一个层级里直接暴露到一个人眼前。这就相当于一个图书馆里的书,没有按分类放到不同区域的不同书架上,而是直接一堆堆到了你面前一样。

因此我们在应对复杂逻辑的编码时,需要更高层级的封装,就像要整理图书,将图书划分到不同区域一样,这样我们才能理清图书馆的书的分布,从而找到对应的书。

那如何归类封装我们系统里的数据记录以及相关的数据处理逻辑呢?

前人已经给了我们答案,那就是面向对象编程(并不是用了面向对象编程语言就是在写着面向对象的代码)。面向对象编程语言提供了我们实现基于面向对象思想系统的必要支持,接下来的就是看我们怎么使用语言的特性了。我们应该如何高内聚低耦合地划分对象呢?

领域驱动设计的思想给我们提供了方法论支持,其建议代码里的对象实现要与我们的业务模型设计绑定,而业务模型的设计需要开发与业务一起完成。在一起完成的过程中:

  • 业务告知开发其希望达到效果

  • 开发从业务的陈述过程中,领悟业务的思维模式

  • 开发将业务思维模式里的概念表达于对象模型中

  • 开发将设计的对象模型与业务一起走查各种业务场景以持续完善模型

  • 在完善模型设计的过程中开发甚至能完善业务脑海中不合适不恰当的思维

如此一来,我们得到的对象设计模型就是与业务领域视角一致的,在开发时我们绑定模型与实现,以对象模型驱动开发,这就是我所理解的领域驱动设计一词的含义。

 

 

(实际上领域驱动设计一书中带来的一套面向对象分层方法论在缺失上面的开发、业务交互设计模型的过程也能使用)

业务契约视角

上面的视角都是从如何识别内聚角度出发的,而业务契约视角则是从维护耦合不变的角度出发的。

契约即接口、即耦合。

我们要有了内聚的模块,才能决定耦合的接口,但一旦耦合的接口确定后,我们不应该轻易变动接口,应该尽可能地将逻辑划分的耦合的一端来处理。

举个大家都很熟悉的例子,就是组装台式电脑。我们组装电脑的时候,不同厂商的不同 型号部件(CPU、显卡、内存、硬盘等等)大部分都可以组装到不同厂商的不同型号主板里。而之所以兼容性这么好,主板和各个零部件都能各自独立演化,这与固定的接口标准有莫大的关系。

我们软件也一样,如果可以的话,尽量保持接口不变,逻辑尽量归纳到接口支持范围内,而不是去突破接口。

 

 

(左中右三类部件只要契约/接口不变,其就能各自独立的演化)

架构的设计

什么是架构呢?

架构是一种设计,一种指导设计的设计,一种为了达到某种目的、效果而对子设计进行限制和规范的设计。

其通常表现为

  1. 在一个整体下,划分不同的组件,规划限定不同组件的职责,设定不同组件的协作形式

架构可大可小,有不同的层级。

  • 指导不同类之间协作关系的是架构

  • 指导不同模块之间协作关系的是架构

  • 指导不同系统之间协作关系的是架构

  • 知道人与人之间协作关系的也是架构(组织架构)

所以大家看出来了没有?我们又回到了分治与高内聚低耦合的思想。只要我们高内聚低耦合的心法熟记于心,我们至少是自己写的代码方法的架构(当然,如果你说我的方法不拆分,一次几百行撸到底那我无F可说)。

由高内聚低耦合内功心法演化而出来的外功五花八门,我们很容易就陷入到了各种神奇的商业名词中难以自拔,如 微服务、中台、云原生、FAAS等等概念中。但如果我们融会贯通了内功之后,我们可以看到,上面的种种名词仅仅是在解决特定问题时,用分治的方法加以高内聚低耦合指导得到的其中一种解决方案。

我们收敛一下,上面主要谈了在业务逻辑层次如何识别内聚,如何进行拆分,这些实际上是指导我们进行业务逻辑方面架构的方法论,你能很好地运用这些方法时,我们的架构能力就从方法层级至少提到了到模块级。

当然作为一名优秀的架构师,业务逻辑并不是其唯一考虑的方面,我们之前就已经说了,内聚的维度有很多,因此,一个合理设计的软件系统,还要考虑组织架构内聚、性能内聚等等方面的因素。

总结

  • 人类的认知处理能力有上限,因此对于超过处理上限的问题人类必须对其进行分治处理

  • 而拆分大多数情况下是存在合并消耗的,会使得总体问题变得更复杂

  • 因此我们要控制复杂度的总量,使其总复杂度尽可能少

  • 控制复杂度总量其中一个方面是在人类可控范围内子问题尽可能的大,以减少合并处理问题的消耗

  • 控制复杂度总量的另外一个方面是合理拆分,使得合并损耗最小

  • 合理拆分的指导思想就是高内聚低耦合

  • 高内聚低耦合从不同的角度去看可能得到的结果可能是不一样的

  • 从业务逻辑角度实现高内聚低耦合的形式有

    • 按业务流程拆分

    • 按业务稳定程度拆分

    • 按业务关注点拆分

    • 按业务对象视角拆分

    • 按业务契约视角拆分

  • 掌握了合理的拆分技巧,你就是你自己的架构师

     

关于作者

在两家排名前三的股份制商业银行及互金创业公司工作过,目前在一家互联网银行工作。做过业务,做过中间件,做过架构,也带过小团队。

欢迎添加个人微信skyesx探讨技术相关问题,请备注名字+公司,谢谢。

 

微信号

 

公众号

 

 

 

 

(所以辛巴要如何管理它的王国?我想它应该要先学学编程吧?!)