| 读 A Philosophy of Software Design 有感,软件设计与架构复杂度,你是战术龙卷风吗?
前言
有一天,一个医生和一个土木工程师在一起争论“谁是世界上最古老的职业”。医生说:“上帝用亚当的肋骨造出了夏娃,这是历史上第一次外科手术,所以最古老的职业应该是医生”,土木工程师说:“在创世纪之前,上帝从混沌中创造了天堂与人间,这是更早之前的一次土木作业,所以最古老的职业应该是土木工程”。这时软件工程师拖着键盘走出来说,“那你认为,是谁创造了那片混沌?”
建筑师不会轻易给 100 层的高楼增加一个地下室,但我们却经常在干这样的事,并且总有人会对你说,“这个需求很简单”。到土里埋个地雷,这确实不复杂,但我们往往面临的真实场景其实是:“在这片雷区里加一个雷”,而雷区里哪里有雷,任何人都不知道。
什么是复杂性
我们一直在说系统很复杂,那到底什么是复杂性?关于复杂的定义有很多种,其中比较有代表的是 Thomas J. McCabe 在 1976 提出的理性派的复杂性度量,与 John Ousterhout 教授提出的感性派的复杂性认知。
理性度量
复杂性并不是什么新概念,早在上世纪 70 年代,软件就已经极其复杂,开发与维护的成本都非常高。1976 年 McCabe&Associates 公司开始对软件进行结构测试,并提出了 McCabe Cyclomatic Complexity Metric,我们也称之为 McCabe 圈复杂度。它通过多个维度来度量软件的复杂度,从而判断软件当前的开发/维护成本。
圈复杂度 | 代码状况 | 测性成本 | 维护成本 |
圈复杂度1 - 10 | 清晰/结构化 | 可测性高 | 维护成本低 |
圈复杂度10 - 20 | 复杂 | 可测性中 | 维护成本中 |
圈复杂度20 - 30 | 非常复杂 | 可测性低 | 维护成本高 |
圈复杂度30 | 不可读 | 不可测 | 维护成本非常高 |
感性认知
复杂度高的代码一定不是好代码,但复杂度低的也不一定就是好代码。John Ousterhout 教授认为软件的复杂性相对理性的分析,可能更偏感性的认知。
Complexity is anything that makes software hard to understand or to modify
译:所谓复杂性,就是任何使得软件难于理解和修改的因素。
John Ousterhout 《A Philosophy of Software Design》
50 年后的今天,John Ousterhout 教授在《A Philosophy of Software Design》书中提到了一个非常主观的见解:复杂性就是任何使得软件难于理解和修改的因素。
模糊性与依赖性是引起复杂性的2个主要因素,模糊性产生了最直接的复杂度,让我们很难读懂代码真正想表达的含义,无法读懂这些代码,也就意味着我们更难去改变它。而依赖性又导致了复杂性不断传递,不断外溢的复杂性最终导致系统的无限腐化,一旦代码变成意大利面条,几乎不可能修复,成本将成指数倍增长。
复杂性的表现形式
复杂的系统往往也有一些非常明显的特征,John 教授将它抽象为变更放大(Change amplification)、认知负荷(Cognitive load)与未知的未知(Unknown unknowns)这 3 类。当我们的系统出现这 3 个特征,说明我们的系统已经开始逐渐变得复杂了。
症状 1-变更放大
Change amplification: a seemingly simple change requires code modifications in many different places.
译:看似简单的变更需要在许多不同地方进行代码修改。
John Ousterhout 《A Philosophy of Software Design》
变更放大(Change amplification)指得是看似简单的变更需要在许多不同地方进行代码修改。比较典型的代表是 Ctrl-CV 式代码开发,领域模型缺少内聚与收拢,当需要对某段业务进行调整时,需要改动多个模块以适应业务的发展。
/**
* 销售捡入客户
*/
public void pick(String salesId, String customerId) {
// 查询客户总数
long customerCnt = customerDao.findCustomerCount(salesId);
// 查询销售库容
long capacity = capacityDao.findSalesCapacity(salesId);
// 判断是否超额
if(customerCnt >= capacity) {
throws new BizException("capacity over limit");
}
// 代码省略 do customer pick
}
在 CRM 领域,销售捡入客户时需要进行库容判断,这段代码也确实可以满足需求。但随着业务的发展,签约的客户要调整为不占库容。而客户除了销售捡入,还包括主管分发、leads 分发、手工录入、数据采买等多个场景,如果没对库容域做模型的收拢,一个简单的逻辑调整,就需要我们在多个场景做适配才能满足诉求。
症状 2-认知负荷
Cognitive load: how much a developer needs to know in order to complete a task.
译:开发人员需要多少知识才能完成一项任务。