《领域驱动设计》之软件复杂度

hello,大家好,我是聪聪。


最近闲暇时间看了下张逸的《领域驱动设计》,书中对于软件复杂度包含的一些内容,让我有些同感。目前接触的一些系统中都会有一些技术债坏味道在其中。

那么就在这里结合自身现有的一些系统设计,做一个随笔记录。

# 1.复杂系统

什么是复杂的系统呢?

由大量相互作用的部分组成的系统。与整个系统比起来,这些组成部分相对简单,没有中央控制,组成部分之间没有全局性通信,并且组成部分的相互作用导致了复杂行为。

这样的描述可以引入目前被大家进津津乐道的微服务架构云原生Service Mesh等等,专注于单一职责与功能的小型模块,利用模块化的方式组合出复杂的大型应用程序,各功能模块使用与语言无关的API进行相互通信。

根据上述对于复杂系统的定义,可以将其划分到不同职能模块的微服务中,也可以对于单一系统的细粒度理解。

函数、类、模块、组件和服务等,这些都是构成一个复杂系统元素。这些相对简单的元素之间的作用却导致了软件系统的复杂行为,从而提升软件系统的复杂度。

可以从理解能力维度预测能力维度两个方面来理解分析软件的复杂成因:

  • 理解能力维度
    • 简单的 Simple
    • 复杂的 Complicated
  • 预测能力维度
    • 有序的 Ordered
    • 复杂的 Complex
    • 混沌的 Chaotic

上述两个方面都蕴含了复杂,但是前者相对简单,可以理解为软件的被理解程度,从简单到复杂的进阶。

后者是与有序相对的,包含了软件的发展规律难以预测,是无序的,可以将其理解为复杂难测。

预测能力维度中混沌才是最高层次的复杂,压根就不可预测,可以理解为业务上理解、梳理复杂难以理解,业务发展上无序且快速。

那么,在剖析软件系统的复杂度时,我们可以从理解能力预测能力两个维度去探索复杂度的成因。

# 2.理解能力

是什么阻碍了你的理解能力呢?是陌生而产生的畏惧,还是庞大的规模产生的恐惧。

当项目组新来同学,需要快速理解整个项目架构、业务形态时,是否会迷失在错综复杂的系统调用、相互依赖的业务矩阵当中?

就像你来到一个陌生城市旅游时,是否会因为城市错综复杂的交通体系而迷失自我,倘若这座城市就是一个乡郊野外的村落,只有几间房屋和一条南北贯穿的街道,抬头便能将整个村落尽收眼底,你还会产生迷失之感吗?

因此,影响理解能力的第一要素便是:规模

# 2.1 规模

那么软件系统的规模是你我能够决定的吗?

当然不能,当业务高速发展,需求呈线性增长时,需求就像树木那样生长,一颗小树也会成为参天大树,只有到达某个时间节点时,才会趋于稳定。

当需求呈线性增长时,为了完成实现这些需求功能点时,软件系统的规模也会以近似的速度快速增长。

当然系统规模的快速扩张,不仅仅取决于需求的数量增长,还有需求功能点之间的依赖关系。需求不会独立存在,彼此之间相互影响、相互依赖,修改便会牵一发东全身。

  • 函数方法存在副作用,调用时可能对函数方法的返回结果做了隐含的假设。
  • 类的职责繁多,违背了单一职责原则。导致后续开发人员不敢轻易修改,同时也不情况修改会影响到哪些模块,持续熵增,
  • 热点代码频繁改动,职责不单一,逻辑层层包裹,没有清洗的边界划分。
  • 存在隐藏bug,诱发条件具备时便会引起雪崩式调用链瘫痪。
  • 业务场景包含了过多例外场景,每种例外场景存在不同的处理方式。
  • 同步逻辑、异步逻辑相互纠缠,无法清晰明了预知程序执行顺序。

随着软件规模的扩展,复杂度也在增长,这种增长非线性的,而是呈现更为陡峭的指数级增长,这实际上就是我们常说的软件的熵在产生副作用。我们无法规避熵增所带来的重击,软件的熵增可以理解为一个系统无序总量,当软件中无序增加时,便是我们常说的软件在腐烂

为什么我们无法避免软件熵增呢?

源于我们构建软件时无法避免技术债,无论当前架构设计多么合理,技术决策多么优秀,随着软件的演进均会变得不堪一击,区别在于技术债多与少,以及后续利息有多高。

  • 抛弃就得系统,丢弃历史技术债,重新设计一套符合现在业务架构决策的系统,但是必须背负新旧系统同时运行的负担、同时旧系统迁移所带来的成本。可能这就是后人面对屎山一样的设计时常说的说的历史原因所致
  • 需要花费大量尽力、寻找熟悉系统人员进行评估、"设计"系统,在开发阶段详细确认所有系统影响点。由于经手人员加多、业务迭代较快,这份详细系统设计也仅仅是“熟悉系统人员”的一份回忆录而已。这也是也分技术债,但更多的是组织架构层面的。
  • 不同业务存在不同分支,分支之间没有正相交,导致某一处的修改便会引入风险点;没有为业务准备合理、全面可行的单元测试,建立功能缺陷,导致系统运行风险增加。纷至沓来的技术债逐渐积累,积累到达临界点时,便会快速坍塌衰亡进入老年期,成为我们常说的“遗留系统”。繁重不堪,修改带来的收益远远小于风险,便无人敢动,只能说一句敬畏历史代码。

最近看了一个这样的历史原因遗留系统标签的系统,在该系统上会经常发生各类生产事故,我称之为风险点的熵增爆发:

  • 由于渠道名称命名无规范,经手人员随心所致。例如:一个渠道名称存在相同的业务含义,可以存在各类命名:wechatWeChatWECHAT。以至于后来判断逻辑上全部都得加上StringUtils._equalsIgnoreCase_()这荒唐的逻辑,再或者将其全部转换为大写。试问:统一的逻辑为什么不能抽象出更加职责单一的领域呢?
  • 重复代码的急剧上升。如果需求的功能数量与代码行数之间出现了不成比例的关系,可以说明该系统的健康体征出现了异常。例如:代码行数庞大,并不能说明该系统复杂,可能是得了肥胖症,存在大量重复代码。

image.png

# 2.2 结构

同时,结构也是决定系统复杂度的一个关键因素。

结构之所以复杂,多数情况下是我们系统的质量属性所决定的。

例如:设计一个满足高性能、高并发需求的系统,你是否就需要考虑在系统中添加一系列中间件去满足该需求。引入缓存、并行处理、CDN、异步消息解耦削峰、异地容灾、分区可扩展。当然如果是一个海量数据进行处理,又得考虑数据如果分布式存储、如何多节点进行CPU内存高效利用执行,提高运算效率。

如果系统是单体架构,需求简单、业务领域职责单一。同时单体架构一定比微服务架构更加简单便于维护,那为什么还需要这么微服务、服务网格架构呢?因此还是回到了上面提到的质量属性

无论设计是优雅还是拙劣,系统结构都会因为设计而变得复杂,其中唯一的区别是:前者是主动地控制结构复杂度,而后者的复杂度变化是偶发性的,是一种技术债的产生,会随着系统规模的增大而产生的一种无序设计

  • 代码没有显而易见的入口路径。
  • 不存在一致性,不存在风格,没有将各部分组织在一起的概念。系统当中的风格纯凭开发人员“喜好”、中间件使用方式也是“我想用这个中间件” ,“我在网上看到是这么使用的”,一盘散沙。
  • 系统中的控制流让人觉得不舒服,无法预测或难以预测。
  • 系统中有太多的“坏味道”,阅读代码时没有一种清新的感觉。
  • 数据很少放在它应该放在的地方,而是放在了更方便使用它的地方。例如:直接在业务中进行注入配置属性,应当引入统一、有规则、有结构的配置类进行一并管理。

一个无序的系统,就像隔着毛玻璃观察事物,其中各类元素都变得模糊不清,充斥着各种技术债,在负责需求维护的开发人员中击鼓传花,谁也不愿意做那个踩坑,偿还历史技术债的人。

细节层面,代码浑浊不堪,违背了高内聚松耦合的设计原则,许多代码为了方便而方便,放错了位置;有些代码为了避免修改历史代码,“避免踩坑”,直接复制原有逻辑,直接出现新分支逻辑,导致大量重复代码块儿。

架构层面,缺乏系统、业务清晰边界。各种通信组件、调用依赖组件、底层架构组件相互纠缠在一起,缺乏统一管理,一旦修改某一点时往往时牵一发动全身。同一问题空间的解决方案是各式各样,让人烟花缭乱,散发这开发人员自我的喜好风格。

# 3.预测能力


了解更多内容,可以关注我的微信公众号,更多首发文章。 wechat