当前位置: 首页 > 科技观察

系统困境与软件复杂,为什么我们的系统这么复杂

时间:2023-03-20 16:57:11 科技观察

1.前言有一天,一位医生和一位土木工程师争论“谁是世界上最古老的职业”。医生说:“上帝用亚当的肋骨创造了夏娃,这是历史上第一次外科手术,所以最古老的职业应该是医生”,土木工程师说:“在创世记之前,上帝从人类世界的混沌中创造了天堂,这是较早的土木工程作业,所以最古老的职业应该是土木工程。”这时候,软件工程师拖着键盘走了出来,说道:“那你猜是谁制造的混乱?”建筑师不会轻易给100层楼加地下室,但我们经常做这样的事情,总会有人告诉你,“这个需求很简单”。把地雷埋在土里其实并不复杂,但我们经常面临的真实场景是:“给这个雷区加一个地雷”,谁也不知道雷区哪里有地雷。2、什么是复杂性我们一直在说系统很复杂,那么复杂性到底是什么?复杂性有多种定义,其中ThomasJ.McCabe于1976年提出的理性主义复杂性测度更具代表性。复杂认知与约翰·奥斯特豪特教授提出的感性学派。1.复杂性的理性衡量并不是一个新概念。早在20世纪70年代,软件就已经极其复杂,开发和维护的成本非常高。1976年,McCabe&Associates开始测试软件的结构,提出了McCabeCyclomaticComplexityMetric,我们也称之为McCabeCyclomaticComplexity。它通过多个维度来衡量软件的复杂性,以确定软件当前的开发/维护成本。圈复杂度代码状态可测试性成本维护成本圈复杂度1-10清晰/结构化可测试性高维护成本低圈复杂度10-20复杂可测试性中等维护成本圈复杂度20-30非常复杂可扩展低可测试性,高维护成本,高圈复杂度30,不可读不可测试,维护成本非常高2.感性认知复杂度高的代码肯定不是好代码,复杂度低的也不一定是好代码。JohnOusterhout教授认为,软件的复杂性可能更多是感性的,而不是理性的分析。Complexityisanythingthatmakessoftwarehadtounderstandortomodify--JohnOusterhout《A Philosophy of Software Design》译文:所谓复杂性就是任何让软件难以理解和修改的东西。50年后的今天,JohnOusterhout教授在《APhilosophyofSoftwareDesign》一书中提到了一个非常主观的观点。复杂性是使软件难以理解和修改的任何因素。模糊性和依赖性是造成复杂性的两个主要因素。模糊性产生了最直接的复杂性,使我们难以理解代码的含义。如果我们看不懂代码,就意味着我们更难去改变它。依赖导致复杂性不断传递,复杂性不断溢出最终导致系统无限腐败。一旦代码变成意大利面条,几乎无法修复,成本将成倍增加。3.复杂性的形式复杂系统往往有一些非常明显的特征。John教授将它们抽象为三类:Changeamplification、Cognitiveload和Unknownunknown。当这三个特征出现在我们的系统中时,就意味着我们的系统逐渐变得复杂了。症状1-Changeamplification变更放大:一个看似简单的变更,却需要修改多处不同的代码。--JohnOusterhout《A Philosophy of Software Design》译文:看似简单的变更,却需要修改多处不同的代码。变更放大指的是一个看似简单的变更,需要在很多不同的地方修改代码。典型代表就是Ctrl-CV代码开发。领域模型缺乏凝聚力和收敛性。当某个业务需要调整时,需要更改多个模块以适应业务发展。/***销售提货客户*/publicvoidpick(StringsalesId,StringcustomerId){//查询客户总数longcustomerCnt=customerDao.findCustomerCount(salesId);//查询销售库存longcapacity=capacityDao.findSalesCapacity(salesId);//判断是否超出限制if(customerCnt>=capacity){throwsnewBizException("capacityoverlimit");}//代码省略做customerpick}在CRM领域,业务员在接客户时,需要判断库存能力。代码确实符合要求。但随着业务的发展,签约客户应调整为不占用仓位。客户除了销售,还包括主管分配、线索分配、人工录入、数据购买等多个场景。如果没有存储域的模型集合,一个简单的逻辑调整就需要我们适配多个场景。匹配以满足要求。症状2-CognitiveloadCognitiveload:开发人员需要知道多少知识才能完成一项任务。--JohnOusterhout《A Philosophy of Software Design》译文:开发人员完成一项任务需要多少知识。认知负荷是指开发人员完成一项任务需要多少知识。在使用功能性框架时,我们希望它操作简单,而在部署复杂系统时,我们希望它结构清晰,这实际上是为了降低一个任务的成本。一味追求高端技术,设计复杂系统,增加学习和理解成本,都是本末倒置。TMF是整个星环的支柱,是业务中心可重用、可扩展架构的核心。但是TMF过于复杂,认知和学习成本非常高。我们日常生活中所面临的一些扩展需求,99%(或者应该说是100%)都不适合TMF。可能是通过一些设计模式或者是一些ifelse,可能更适合解决我们的问题。此外,还有一些简单的搜索场景使用Blink等流引擎,通过DDD搭建简单的后台系统,多个产品发布的状态机转换使用规则引擎等,这些都是复杂的认知负载。一种学位。症状3-UnknownunknownUnknownunknown:完成任务必须修改哪些代码并不明显--JohnOusterhout《A Philosophy of Software Design》译文:必须修改哪些代码才能完成任务。Unknownunknown是指必须修改哪些代码才能完成任务,或者开发人员必须获取哪些信息才能成功执行任务。该项目也是JohnOusterhout教授认为的最糟糕的复杂性形式。当您维护一个有20年历史的项目时,这种问题相对来说并不奇怪。一个500万行代码的应用程序,乱七八糟,缺乏文档,你无法控制,而且代码本身也没有明确表达它们应该解释的内容。这时,“未知的未知”出现了。你不知道改的那行代码能不能让程序正常运行,也不知道这行代码改了会不会引起新的问题。这时,我们才发现,只有上帝才能拯救那些“上帝”。4.为什么会出现复杂性?为什么软件越来越复杂?是否可以通过减少一些错误来避免一场灾难?回顾那些复杂的系统,我们可以发现很多导致系统崩溃的因素。想简单省事,不及时管理不合理的内容,缺乏匠心,忽视脏代码,缺乏技术能力,无法应对复杂系统缺乏交接和过渡,三无产品几乎不可能维护除了上面的内容,还有很多其他可以想到的原因。但我们发现他们似乎有一个共同的参照点——软件工程师,似乎所有复杂性的源头都是不合格的软件工程师,那么实际上,一些罪恶的根源是我们自己?1、统一的中国和分裂的欧洲大陆面积和中国差不多,为什么欧洲分裂,中国统一。有人说他们的文化不同,有人说语言不通是主要原因,有人说他们缺少一个秦始皇。其实我们回顾欧洲的历史,欧洲真的不缺少统一的帝国。罗马帝国曾将地中海作为自己的内海,拿破仑在全盛时期掌管着1300万平方公里的领土。欧洲曾经有过伟大的帝国,但没有一个是统一的。让我们再看看地图。事实上,世界上除了中国和俄罗斯,99%的国家都是小国。分裂是常态,团结是不正常的。马老师也说过,成功是偶然的,只有失败是必然的。只有极少数国家实现了大团结,所以我们不要问欧洲为什么分裂,而要问中国为什么统一。以此类推,我们的软件也是如此。复杂是常态,不复杂是不正常。2.软件的复杂性是一种本质属性,而不是偶然属性。GradyBooch在Object-OrientedAnalysisandDesignwithApplications中提出了这样一个概念。他认为软件的复杂性是与生俱来的,包括问题域的复杂性、管理开发过程的难度,以及通过软件可能实现的灵活性和表征。从这四个方面分析了离散系统行为问题。软件的发展必然伴随着复杂性,这是软件工程科学必须伴随的特性。一切事物,无一例外,都需要额外的能量和秩序来维持自身。我抽象地知道这是著名的热力学第二定律,它指出一切都在慢慢崩溃。——凯文·凯利额外的能量和秩序可以自我维持,无一例外。这就是众所周知的热力学第二定律,它指出所有事物都会慢慢分崩离析。凯文·凯利在《必然》中也提到了类似的观点。他认为世界上的一切都需要额外的能量和秩序来维持自身,而一切都在慢慢分崩离析。没有外力的注入,事情就会逐渐崩溃。这是世间万物的规律,不是我们做错了什么。五、软件架构治理的复杂性给软件系统注入的外力就是我们的软件架构和我们未来的每一行代码。软件架构有很多种,从最早的单体架构到后来的分布式架构,SOA、微服务、FaaS、ServiceMesh等等。所有的软件架构都是千变万化的,都是为了解决软件的复杂性。一、体系结构的本质编程范式是指程序的编程模式。软件架构发展到今天,只出现了三种编程范式(paradigm),即结构化编程、面向对象编程和函数式编程。结构化程序设计取消goto,去掉跳转语句,限制和规范程序控制权的直接传递,规范面向对象程序设计中指针的使用,限制和规范程序控制权的间接传递。函数式编程以lambda算法为核心思想,限制了程序中的赋值,规范了面向对象设计的五个原则S.O.L.I.D.依赖倒置限制了模块的依赖顺序,单一职责限制了模块的职责范围,接口隔离限制了接口提供的形式。软件的本质是约束。商品的代码不能写在订单字段,数据层的方法不能写在业务层。70年的软件开发并没有告诉我们该做什么,而是教会我们不该做什么。2.递增的复杂性软件的复杂性不会凭空消失,而是逐步增加。关于增量复杂性有3种观点:歧义创造复杂性,依赖传播复杂性复杂性通常不是由单一灾难造成的我们可以很容易地说服自己,当前变化带来的一点点复杂性没什么大不了的,它曾经是小李抱怨的我,说这段代码真恶心,弄了好久才看懂,而且代码很死板,就这个需求要改到这里,代码真是一团糟。我问他最后用它做了什么,他说,我又加了一块。三、关于战术编程的编程思考其实小李的做法并不是个人行为。可能我们在遇到复杂代码的时候都这样做过。约翰教授将这种编程方式称为“战术编程”。战术编程的主要特点是速度快,具有以下特点。当前必须是最快的,并且不会花费太多时间来找到最佳设计。每个编程任务都会引入一些复杂性。重构会减慢当前任务,所以保持最快@HSFProvider(serviceInterface=AgnDistributeRuleConfigQueryService.class)ID);结果模型result=newResultModel();if(StringUtils.isBlank(id)){result.setSuccess(false);result.setErrorMsg("id不能为空");返回结果}try{AgnDistributeRuleConfigagnDistributeRuleConfig=agnDistributeRuleConfigMapper.selectById(id);如果(agnDistributeRuleConfig==null){logger.error("agnDistributeRuleConfig为空");结果.setSuccess(false);result.setErrorMsg("agnDistributeRuleConfig为空");返回结果}this.filterDynamicRule(agnDistributeRuleConfig);BeanUtils.copyProperties(agnDistributeRuleConfig,agnDistributeRuleConfigDto);结果。设置成功(真);结果.setTotal(1);结果.setValues(agnDistributeRuleConfigDto);}catch(Exceptione){logger.error("queryAgnDistributeRuleConfigByIderror,",e);结果.setSuccess(false);result.setErrorMsg(e.getMessage());}返回结果;}}我们看上面的代码,是一段查询分发规则的业务逻辑。功能虽然可以,但其实有很多不规范的地方。Facade层定义了所有的逻辑-没有结构层级业务和技术不分离-接口信息和业务数据耦合Trycatchflyingeverywhere-缺乏统一的异常处理机制没有标准化的日志格式-日志格式混乱,但不可否认的是必须成为目前最快的。这是战术设计的特点之一,总是按照当前最快的交付计划推进,甚至很多组织都提倡这种工作方式,为了让职能更快地发挥作用,只注重短期利益而忽略长期利益价值。几乎每个软件开发组织都有至少一个将战术编程发挥到极致的开发人员:atacticaltornado。--JohnOusterhout《A Philosophy of Software Design》译:几乎每个软件开发组织都至少有一个将战术编程发挥到极致的开发人员:a战术龙卷风。开发商:战术龙卷风。将战术编程发挥到极致的人,被称为战术龙卷风。战术龙卷风腐蚀系统,换取当下最高效的解决方案(也许他自己不这么认为)。战术龙卷风还有以下特点:高产的程序员,没有人能比龙卷风更快完成任务总能在龙卷风过后留下毁灭的痕迹