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");
}
}
在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<AgnDistributeRuleConfigDto> queryAgnDistributeRuleConfigById(String id) {
logger.info("queryAgnDistributeRuleConfigById id=" + id);
ResultModel<AgnDistributeRuleConfigDto> result = new ResultModel<AgnDistributeRuleConfigDto>();
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,但不规范的地方其实非常多
- Try catch 满天飞 - 缺少统一异常处理机制
但不可否认,他一定是当前最快的。这就是战术设计的特点之一,永远按当前最快速交付的方案进行推进,甚至很多组织鼓励这种工作方式,为了使功能更快运作,只注重短期收益而忽略长期价值。战术龙卷风