您现在的位置是:亿华云 > IT科技
系统困境与软件复杂度,为什么我们的系统会如此复杂
亿华云2025-10-08 20:56:49【IT科技】7人已围观
简介一、前言有一天,一个医生和一个土木工程师在一起争论“谁是世界上最古老的职业”。医生说:“上帝用亚当的肋骨造出了夏娃,这是历史上第一次外科手术,所以最古老的职业应该是医生”,土木工程师说:“在创世纪之前
一、系统前言
有一天,困境一个医生和一个土木工程师在一起争论“谁是软件世界上最古老的职业”。医生说:“上帝用亚当的复杂肋骨造出了夏娃,这是度为的系历史上第一次外科手术,所以最古老的什们职业应该是医生”,土木工程师说:“在创世纪之前,此复上帝从混沌中创造了天堂与人间,系统这是困境更早之前的一次土木作业,所以最古老的软件职业应该是土木工程”。这时软件工程师拖着键盘走出来说,复杂“那你认为,度为的系是什们谁创造了那片混沌?”
建筑师不会轻易给100层的高楼增加一个地下室,但我们却经常在干这样的此复事,并且总有人会对你说,系统“这个需求很简单”。到土里埋个地雷,这确实不复杂,但我们往往面临的站群服务器真实场景其实是:“在这片雷区里加一个雷”,而雷区里哪里有雷,任何人都不知道 。
二、什么是复杂性
我们一直在说系统很复杂,那到底什么是复杂性?关于复杂的定义有很多种,其中比较有代表的是Thomas J. McCabe 在1976提出的理性派的复杂性度量,与John Ousterhout 教授提出的感性派的复杂性认知。
1.理性度量
复杂性并不是什么新概念,早在上世纪70年代,软件就已经极其复杂,开发与维护的成本都非常高。1976年McCabe&Associates公司开始对软件进行结构测试,并提出了McCabe Cyclomatic Complexity Metric,我们也称之为McCabe圈复杂度。它通过多个维度来度量软件的复杂度,从而判断软件当前的开发/维护成本。
圈复杂度
代码状况
测性成本
维护成本
圈复杂度1 - 10
清晰/结构化
可测性高
维护成本低
圈复杂度10 - 20
复杂
可测性中
维护成本中
圈复杂度20 - 30
非常复杂
可测性低
维护成本高
圈复杂度30
不可读
不可测
维护成本非常高
2.感性认知
复杂度高的代码一定不是亿华云计算好代码,但复杂度低的也不一定就是好代码。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.
-- John Ousterhout 《A Philosophy of Software Design》
译:开发人员需要多少知识才能完成一项任务。
认知负荷(Cognitive load)是指开发人员需要多少知识才能完成一项任务。使用功能性框架时,我们希望它操作简单,部署复杂系统时,我们希望它架构清晰,其实都是降低一项任务所需的成本。盲目的追求高端技术,设计复杂系统,增加学习与理解成本都属于本末倒置的一种。
TMF是整个星环的支柱,也是业务中台面向可复用可扩展架构的核心。但TMF太过复杂,认知与学习成本非常高,我们日常中所面临的一些扩展诉求99%(或者应该说100%)都不适合TMF,可能通过一些设计模式或者就是一些if else,可能更适合解决我们的问题。
除此之外,还包括一些简单搜索场景却用到了blink等流式引擎,简单后台系统通过DDD进行构建,几个商品发布的状态机转换用上了规则引擎等等,都属于认知负荷复杂度的一种。
症状3-未知的未知
Unknown unknowns: it is not obvious which pieces of code must be modified to complete a task
-- John Ousterhout 《A Philosophy of Software Design》
译:必须修改哪些代码才能完成任务。
未知的未知(Unknown unknowns)是指必须修改哪些代码才能完成任务,或者说开发人员必须获得哪些信息才能成功地执行任务。这一项也是John Ousterhout教授认为复杂性中最糟糕的一个表现形式。
当你维护一个有20年历史的项目时,这种问题的出来相对而言就没那么意外。由于代码的混乱与文档的缺失,导致你无法掌控一个500万行代码的应用,并且代码本身也没有明显表现出它们应该要阐述的内容。这时“未知的未知”出现了,你不知道改动的这行代码是否能让程序正常运转,也不知道这行代码的改动是否又会引发新的问题。这时候我们发现,那些“上帝类”真的就只有上帝能拯救了。
四、为什么会产生复杂性
那软件为什么越来越复杂,是不是减少一些犯错就能避免一场浩劫呢?回顾那些复杂的系统,我们可以找到很多因素导致系统腐化。
想简单图省事,没有及时治理不合理的内容缺少匠心追求,对肮脏代码视而不见技术能力不够,无法应对复杂系统交接过渡缺失,三无产品几乎无法维护除了上述内容外,还可以想到很多理由。但我们发现他们好像有一个共同的指向点 - 软件工程师,似乎所有复杂的源头就是软件工程师的不合格导致,所以其实一些罪恶的根因是我们自己?
1.统一的中国与分裂的欧洲
欧洲大陆面积大体与中国相当,但为什么欧洲是分裂的,而中国是统一的。有人说他们文化不一样,也有人说他们语言不通是主要原因,也有人说他们缺一个秦始皇。其实我们回顾欧洲的历史,欧洲还真不缺一个大一统的帝国。罗马帝国曾经让地中海成为自己的内海,拿破仑鼎盛时期掌管着1300万平方公里的领地。欧洲也曾出现过伟大的帝国,但都未走向统一。
我们再观察地图,其实除了中国、俄罗斯以外,全世界99%的国家都是小国。分裂才是常态,统一才不正常。马老师也曾说过,成功都有偶然性只有失败才存在必然。只有极少国家才实现了大一统,所以我们不应该问为什么欧洲是分裂的,而应该问为什么中国是统一的。类比到我们的软件也同样如此,复杂才是常态,不复杂才不正常。
2.软件固有的复杂性
The Complexity of software is an essential property, not an accidental one.
-- Grady Booch 《Object-Oriented Analysis and Design with Applications》
译:软件的复杂性是一个基本特征,而不是偶然如此。
Grady Booch在 Object-Oriented Analysis and Design with Applications 中提出这样一个观念,他认为软件的复杂性是固有的,包括问题域的复杂性、管理开发过程的困难性、通过软件可能实现的灵活性与刻画离散系统行为的问题,这4个方面来分析了软件的发展一定伴随着复杂,这是软件工程这本科学所必然伴随的一个特性。
Everything, without exception, requires additional energy and order to maintain itself. I knew this in the abstract as the famous second law of thermodynamics, which states that everything is falling apart slowly.
-- Kevin Kelly 《The Inevitable》
译:世间万物都需要额外的能量和秩序来维持自身,无一例外。这就是著名的热力学第二定律,即所有的事务都在缓慢地分崩离析。
Kevin Kelly在 The Inevitable 也有提过类似的观点,他认为世间万物都需要额外的能量和秩序来维持自身,所有的事物都在缓慢地分崩离析。没有外部力量的注入事物就会逐渐崩溃,这是世间万物的规律,而非我们哪里做得不对。
五、软件架构治理复杂度
为软件系统注入的外力就是我们的软件架构,以及我们未来的每一行代码。软件架构有很多种,从最早的单体架构,到后面的分布式架构、SOA、微服务、FaaS、ServiceMesh等等。所有的软件架构万变不离其宗,都在致力解决软件的复杂性。
1.架构的本质
编程范式指的是程序的编写模式,软件架构发展到今天只出现过3种编程范式( paradigm ),分别是结构化编程,面向对象编程与函数式编程。
结构化编程取消 goto 移除跳转语句,对程序控制权的直接转移进行了限制和规范面向对象编程限制 指针 的使用,对程序控制权的间接转移进行了限制和规范函数式编程以 λ演算法 为核心思想,对程序中的赋值进行了限制和规范面向对象的五大设计原则 S.O.L.I.D。依赖倒置限制了模块的依赖顺序、单一职责限制模块的职责范围、接口隔离限制接口的提供形式。
软件的本质是约束。商品的代码不能写在订单域,数据层的方法不能写在业务层。70年的软件发展,并没有告诉我们应该怎么做,而是教会了我们不该做什么。
2.递增的复杂性
软件的复杂性不会凭空消失,并且会逐级递增。针对递增的复杂性有3个观点:
模糊性创造了复杂,依赖性传播了复杂复杂性往往不是由单个灾难引起的我们可以容易地说服自己,当前变更带来的一点点复杂性没什么大不了曾经小李跟我抱怨,说这段代码实在是太恶心了,花了很长时间才看懂,并且代码非常僵硬,而正好这个需求需要改动到这里,代码真的就像一坨乱麻。我问他最后是怎么处理的,他说,我给它又加了一坨。
3.编程思维论
战术编程
其实小李的这种做法并非是一个个体行为,或许我们在遇到复杂代码时都曾这样苟且过,John教授这种编程方法称之为“战术编程”。战术编程最主要的特点是快,同时具备如下几个特点。
当前一定是最快的不会花费太多时间来寻找最佳设计每个编程任务都会引入一些复杂度重构会减慢当前任务速度,所以保持最快速度@HSFProvider(serviceInterface = AgnDistributeRuleConfigQueryService.class)
public class AgnDistributeRuleConfigQueryServiceImpl implements AgnDistributeRuleConfigQueryService {
@Override
public ResultModel queryAgnDistributeRuleConfigById(String id) {
logger.info("queryAgnDistributeRuleConfigById id=" + id);
ResultModel result = new ResultModel();
if(StringUtils.isBlank(id)){
result.setSuccess(false);
result.setErrorMsg("id cannot be blank");
return result
}
try {
AgnDistributeRuleConfigDto agnDistributeRuleConfigDto = new AgnDistributeRuleConfigDto();
AgnDistributeRuleConfig agnDistributeRuleConfig = agnDistributeRuleConfigMapper.selectById(id);
if(agnDistributeRuleConfig == null){
logger.error("agnDistributeRuleConfig is null");
result.setSuccess(false);
result.setErrorMsg("agnDistributeRuleConfig is null");
return result
}
this.filterDynamicRule(agnDistributeRuleConfig);
BeanUtils.copyProperties(agnDistributeRuleConfig, agnDistributeRuleConfigDto);
result.setSuccess(true);
result.setTotal(1);
result.setValues(agnDistributeRuleConfigDto);
} catch (Exception e) {
logger.error("queryAgnDistributeRuleConfigById error,", e);
result.setSuccess(false);
result.setErrorMsg(e.getMessage());
}
return result;
}
}我们看上面这段代码,是一段查询分发规则的业务逻辑。虽然功能能够work,但不规范的地方其实非常多
Facade层定义全部逻辑 - 未做结构分层业务与技术未做分离 - 耦合接口信息与业务数据Try catch 满天飞 - 缺少统一异常处理机制没有规范化的日志格式 - 日志格式混乱但不可否认,他一定是当前最快的。这就是战术设计的特点之一,永远按当前最快速交付的方案进行推进,甚至很多组织鼓励这种工作方式,为了使功能更快运作,只注重短期收益而忽略长期价值。
战术龙卷风
Almost every software development organization has at least one developer who takes tactical programming to the extreme: a tactical tornado.
-- John Ousterhout 《A Philosophy of Software Design》
译:几乎每个软件开发组织都有至少一个将战术编程发挥到极致的开发人员:战术龙卷风。
将战术编程发挥到极致的人,叫战术龙卷风。战术龙卷风以腐化系统为代价换取当前最高效的解决方案(或许他自己并未觉得)。战术龙卷风也有如下几个特点:
是一位多产的程序员,没人比龙卷风更快完成任务总能留下龙卷风后毁灭的痕迹很赞哦!(536)