hello,大家好,我是聪聪。
最近闲暇时间看了下张逸的《领域驱动设计》,书中对于软件复杂度包含的一些内容,让我有些同感。目前接触的一些系统中都会有一些技术债
、坏味道
在其中。
那么就在这里结合自身现有的一些系统设计,做一个随笔记录。
# 1.复杂系统
什么是复杂的系统呢?
由大量相互作用的部分组成的系统。与整个系统比起来,这些组成部分相对简单,没有中央控制,组成部分之间没有全局性通信,并且组成部分的相互作用导致了复杂行为。
这样的描述可以引入目前被大家进津津乐道的微服务架构
,云原生Service Mesh
等等,专注于单一职责与功能的小型模块,利用模块化的方式组合出复杂的大型应用程序,各功能模块使用与语言无关的API进行相互通信。
根据上述对于复杂系统
的定义,可以将其划分到不同职能模块的微服务中,也可以对于单一系统的细粒度理解。
函数、类、模块、组件和服务等,这些都是构成一个复杂系统元素。这些相对简单的元素之间的作用却导致了软件系统的复杂行为,从而提升软件系统的复杂度。
可以从理解能力维度
和预测能力维度
两个方面来理解分析软件的复杂成因:
- 理解能力维度:
- 简单的 Simple
- 复杂的 Complicated
- 预测能力维度:
- 有序的 Ordered
- 复杂的 Complex
- 混沌的 Chaotic
上述两个方面都蕴含了复杂
,但是前者相对简单,可以理解为软件的被理解程度,从简单到复杂的进阶。
后者是与有序相对的,包含了软件的发展规律难以预测,是无序的,可以将其理解为复杂难测。
在预测能力维度
中混沌才是最高层次的复杂,压根就不可预测,可以理解为业务上理解、梳理复杂难以理解,业务发展上无序且快速。
那么,在剖析软件系统的复杂度时,我们可以从理解能力
和预测能力
两个维度去探索复杂度的成因。
# 2.理解能力
是什么阻碍了你的理解能力呢?是陌生而产生的畏惧,还是庞大的规模产生的恐惧。
当项目组新来同学,需要快速理解整个项目架构、业务形态时,是否会迷失在错综复杂的系统调用、相互依赖的业务矩阵当中?
就像你来到一个陌生城市旅游时,是否会因为城市错综复杂的交通体系而迷失自我,倘若这座城市就是一个乡郊野外的村落,只有几间房屋和一条南北贯穿的街道,抬头便能将整个村落尽收眼底,你还会产生迷失之感吗?
因此,影响理解能力的第一要素便是:规模
。
# 2.1 规模
那么软件系统的规模是你我能够决定的吗?
当然不能,当业务高速发展,需求呈线性增长时,需求就像树木那样生长,一颗小树也会成为参天大树,只有到达某个时间节点时,才会趋于稳定。
当需求呈线性增长时,为了完成实现这些需求功能点时,软件系统的规模也会以近似的速度快速增长。
当然系统规模的快速扩张,不仅仅取决于需求的数量增长,还有需求功能点之间的依赖关系。需求不会独立存在,彼此之间相互影响、相互依赖,修改便会牵一发东全身。
- 函数方法存在副作用,调用时可能对函数方法的返回结果做了隐含的假设。
- 类的职责繁多,违背了单一职责原则。导致后续开发人员不敢轻易修改,同时也不情况修改会影响到哪些模块,持续熵增,
- 热点代码频繁改动,职责不单一,逻辑层层包裹,没有清洗的边界划分。
- 存在隐藏bug,诱发条件具备时便会引起雪崩式调用链瘫痪。
- 业务场景包含了过多例外场景,每种例外场景存在不同的处理方式。
- 同步逻辑、异步逻辑相互纠缠,无法清晰明了预知程序执行顺序。
随着软件规模的扩展,复杂度也在增长,这种增长非线性的,而是呈现更为陡峭的指数级增长,这实际上就是我们常说的软件的熵在产生副作用。我们无法规避熵增所带来的重击,软件的熵增可以理解为一个系统无序
总量,当软件中无序增加时,便是我们常说的软件在腐烂
。
为什么我们无法避免软件熵增呢?
源于我们构建软件时无法避免技术债,无论当前架构设计多么合理,技术决策多么优秀,随着软件的演进均会变得不堪一击,区别在于技术债多与少,以及后续利息有多高。
- 抛弃就得系统,丢弃历史技术债,重新设计一套符合现在业务架构决策的系统,但是必须背负新旧系统同时运行的负担、同时旧系统迁移所带来的成本。可能这就是后人面对
屎山
一样的设计时常说的说的历史原因所致
。 - 需要花费大量尽力、寻找熟悉系统人员进行评估、"设计"系统,在开发阶段详细确认所有系统影响点。由于经手人员加多、业务迭代较快,这份详细系统设计也仅仅是“熟悉系统人员”的一份回忆录而已。这也是也分技术债,但更多的是组织架构层面的。
- 不同业务存在不同分支,分支之间没有正相交,导致某一处的修改便会引入风险点;没有为业务准备合理、全面可行的单元测试,建立功能缺陷,导致系统运行风险增加。纷至沓来的技术债逐渐积累,积累到达临界点时,便会快速坍塌衰亡进入老年期,成为我们常说的“遗留系统”。繁重不堪,修改带来的收益远远小于风险,便无人敢动,只能说一句敬畏历史代码。
最近看了一个这样的历史原因
、遗留系统
标签的系统,在该系统上会经常发生各类生产事故,我称之为风险点的熵增爆发:
- 由于渠道名称命名无规范,经手人员随心所致。例如:一个渠道名称存在相同的业务含义,可以存在各类命名:
wechat
、WeChat
、WECHAT
。以至于后来判断逻辑上全部都得加上StringUtils._equalsIgnoreCase_()
这荒唐的逻辑,再或者将其全部转换为大写。试问:统一的逻辑为什么不能抽象出更加职责单一的领域呢? - 重复代码的急剧上升。如果需求的功能数量与代码行数之间出现了不成比例的关系,可以说明该系统的健康体征出现了异常。例如:代码行数庞大,并不能说明该系统复杂,可能是得了
肥胖症
,存在大量重复代码。
# 2.2 结构
同时,结构也是决定系统复杂度的一个关键因素。
结构之所以复杂,多数情况下是我们系统的质量属性所决定的。
例如:设计一个满足高性能、高并发需求的系统,你是否就需要考虑在系统中添加一系列中间件去满足该需求。引入缓存、并行处理、CDN、异步消息解耦削峰、异地容灾、分区可扩展。当然如果是一个海量数据进行处理,又得考虑数据如果分布式存储、如何多节点进行CPU内存高效利用执行,提高运算效率。
如果系统是单体架构,需求简单、业务领域职责单一。同时单体架构一定比微服务架构更加简单便于维护,那为什么还需要这么微服务、服务网格架构呢?因此还是回到了上面提到的质量属性
无论设计是优雅还是拙劣,系统结构都会因为设计而变得复杂,其中唯一的区别是:前者是主动地控制结构复杂度,而后者的复杂度变化是偶发性的,是一种技术债的产生,会随着系统规模的增大而产生的一种无序设计。
- 代码没有显而易见的入口路径。
- 不存在一致性,不存在风格,没有将各部分组织在一起的概念。系统当中的风格纯凭开发人员“喜好”、中间件使用方式也是“我想用这个中间件” ,“我在网上看到是这么使用的”,一盘散沙。
- 系统中的控制流让人觉得不舒服,无法预测或难以预测。
- 系统中有太多的“坏味道”,阅读代码时没有一种清新的感觉。
- 数据很少放在它应该放在的地方,而是放在了更方便使用它的地方。例如:直接在业务中进行注入配置属性,应当引入统一、有规则、有结构的配置类进行一并管理。
一个无序的系统,就像隔着毛玻璃观察事物,其中各类元素都变得模糊不清,充斥着各种技术债,在负责需求维护的开发人员中击鼓传花,谁也不愿意做那个踩坑,偿还历史技术债的人。
细节层面,代码浑浊不堪,违背了高内聚松耦合的设计原则,许多代码为了方便而方便,放错了位置;有些代码为了避免修改历史代码,“避免踩坑”,直接复制原有逻辑,直接出现新分支逻辑,导致大量重复代码块儿。
架构层面,缺乏系统、业务清晰边界。各种通信组件、调用依赖组件、底层架构组件相互纠缠在一起,缺乏统一管理,一旦修改某一点时往往时牵一发动全身。同一问题空间的解决方案是各式各样,让人烟花缭乱,散发这开发人员自我的喜好风格。
# 3.预测能力
了解更多内容,可以关注我的微信公众号,更多首发文章。