阿里研究员谷朴:警惕软件复杂度困局

简介:对于大型的软件系统如互联 分布式应用或企业级软件,为何我们常常会陷入复杂度陷阱识别复杂度增长的因素码开发以及演进的过程中需要遵循哪些原则将分享阿里研究员谷朴关于软件复杂度的思考:什么是复杂度、复杂度是如何产生的以及解决的思路。较长,同学们可收藏后再看。

如果我们只是写一段独立代码,不和其他系统交互,往往设计上要求不会很高,代码是否易于使用、易于理解、易于测试和维护,根本不是问题。而一旦遇到大型的软件系统如互联 分布式应用或者企业级软件,我们常常陷入复杂度陷阱,下图 the life of a software engineer 是我很喜欢的一个软件 cartoon,非常形象地展示了复杂度陷阱。

例如淘宝由一个单体 PHP 应用,经过 4、5 代架构不断演进,才到今天服务十亿人规模的电商交易平台。支付宝、Google 搜索、Netflix 微服务,都是类似的历程。

是不是一定要经过几代演进才能构建出来大型软件,就不能一次到位吗一个团队离开淘宝,要拉开架势根据淘宝交易的架构重新复制一套,在现实中是不可能实现的:没有哪个创业团队能有那么多资源同时投入这么多组件的开发,也不可能有一开始就朝着超级复杂架构开发而能够成功的实现。

既然 “软件设计和实现的本质是工程师相互通过写作来交流一些包含丰富细节的抽象概念并且不断迭代过程” (第三次强调了),那么,复杂度指的是软件中那些让人理解和修改维护的困难程度。相应的,简单性,就是让理解和维护代码更容易的要素。

“The goal of software architecture is to minimize the manpower required to build and maintain the required system.” Robert Martin, Clean Architecture.

因此我们将软件的复杂度分解为两个维度,都和人理解与维护软件的成本相关:

  • 第一,认知负荷 cognitive load :理解软件的接口、设计或者实现所需要的心智负担;
  • 第二,协同成本 Collaboration cost:团队维护软件时需要在协同上额外付出的成本。

我们看到,这两个维度有所区别,但是又相互关联。协同成本高,让软件系统演进速度变慢,效率变差,工作其中的工程师压力增大,而长期难以取得进展,工程师倾向于离开项目,最终造成质量进一步下滑的恶性循环。而认知负荷高的软件模块让程序员难以理解,从而产生两个后果:

  • 维护过程中易于出错,bug 率故障率高;
  • 更大机率 团队人员变化时被抛弃,新成员选择另起炉灶,原有投入被浪费,甚至更高糟糕的是,代码被抛弃但是又无法下线,成为定时炸弹。

2. 影响到认知负荷的因素

认知负荷又可以分解为:

  • 定义新的概念带来认知负荷,而这种认知负荷与概念和物理世界的关联程度相关;
  • 逻辑符合思维习惯程度:正反逻辑差异,逻辑嵌套和独立原子化组合。继承和组装差异。 

1)不恰当的逻辑带来的认知成本

看以下案例:

A. Code with too much nesting

B.  Code with less nesting

比较 A 和 B,逻辑是完全等价的,但是B的逻辑明显更容易理解,自然也更容易在 B 的代码基础上增加功能,且新增的功能很可能也会维持这样一个比较好的状态。

而我们看到 A 的代码,很难理解其逻辑,在维护的过程中,会有更大的概率引入 bug,代码的质量也会持续恶化。

2)模型失配:和现实世界不完全符合的模型带来高认知负荷

软件的模型设计需要符合现实物理世界的认知,否则会带来非常高的认知成本。我遇到过这样一个资源管理系统的设计,设计者从数学角度有一个非常优雅的模型,将资源账 用合约来表达(下图左侧),账户的 balance 可以由过往合约的累计获得,确保数据一致性。但是这样的设计,完全不符合用户的认知,对于用户来说,感受到的应该是账 和交易的概念,而不是带着复杂参数的合约。可以想象这样的设计,其维护成本非常之高。

一个典型的 unknown unknowns 是一部分代码存在这样的情况:

  • 代码缺乏充分的测试覆盖,一些重要场景依赖维护者手工测试;
  • 代码有隐藏 / 不易被发现的行为或者边界条件,与文档和接口描述并不符合。

对于维护者来说,改动这样的代码(或者是改动影响到了这样代码 / 被这样代码影响到了)时,如果按照接口描述或者文档进行,没发现隐藏行为,同时代码又缺乏足够测试覆盖,那么就存在未知的风险 unknown unknowns。这时出现问题是很难避免的。最好的方式还是要尽量避免我们的系统质量劣化到这个程度。

上线时,我们最大的噩梦就是 unknown unknowns:这类风险,我们无法预知在哪里或者是否有问题,只能在软件上线后遇到问题才有可能发现。其他的问题 尚可通过努力来解决(认知成本),而 unknown unknowns 可以说已经超出了认知成本的范围。我们最希望避免的也是 unknown unknowns。

7)认知成本低要不易出错,而不是无脑“简化”

从认知成本角度来说,我们还要认识到,衡量不同方案/写法的认知成本,要考虑的是不易出错,而不是表面上的简化:表面上简化可能带来实质性的复杂度上升。

例如,为了表达时间段,可以有两种选择:

在上面这个例子里面,我们都知道,应该选用第二个方案,即采用 Duration 作 time period,而不是 int:尽管 Duration 本身需要一点点学习成本,但是这个模式可以避免多个时间单位带来的常见问题。

3. 影响协同成本的因素

协同成本则是增长这块模块所需要付出的协同成本。什么样的成本是协同成本p>

  • 增加一个新的特性往往需要多个工程师协同配合,甚至多个团队协同配合;
  • 测试以及上线需要协调同步。

1)系统模块拆分与团队边界

在微服务化时代,模块/服务的切分和团队对齐,更加有利于迭代效率。而模块拆分和边界的不对齐,则让代码维护的复杂度增加,因这时新的特性需要在跨多个团队的情况下进行开发、测试和迭代。

另外一个角度,则是:

Any piece of software reflects the organizational structure that produces it.

或者就是我们常说的“组织架构决定系统架构”,软件的架构最后会围绕组织的边界而变化(当然也有文化因素),当组织分工不合理时,会产生重复的建设或者冲突。

2)服务之间的依赖,Composition vs Inheritance / Plugin

软件之间的依赖模式,常见的有 Composition 和 Inheritance 模式,对于 local 模块/类之间的依赖还是远程调用,都存在类似模式。

复杂度的恶化到一定程度,一定进入有诸多 unknown unknowns 的程度。好的工程师一定要能识别这样的状态:可以说,如果不投入力气去做一定的重构/改造,有过多 unknown unknowns 的系统,很难避免失败的厄运了。

这张图是要表明,软件演进的过程,是一个“不由自主”就会滑向过于复杂而无法维护的深渊的过程。如何要避免失败的厄运文章的篇幅不容许我们展开讨论如何避免复杂度,但是首要的,对于真正重要的、长生命周期的软件演进,我们需要做到对于复杂度增量零容忍。

5. Good enough vs Perfect

软件领域,从效率和质量的折中,我们会提“Good enough”即可。这个理论是没错的。只不过现实中,我们极少看到“overly good”,因为过于追求 perfection 而影响效率的情况。大多数情况下,我们的系统是根本没做到 Good enough。

对复杂度增长的对策

API 设计最佳实践思考:https://developer.aliyun.com/article/701810

有人会说,项目交付的压力才是最重要的,不要站着说话不腰疼。实际呢为绝对不是这样。多数情况下,我们要对复杂度增长采用接近于“零容忍”的态度,避免“能用就行”,原因在于:

  • 复杂度增长带来的风险(unknown unknowns、不可控的失败等)往往是后知后觉的,等到问题出现时,往往 legacy 已经形成一段时间,或者坑往往是很久以前埋的;
  • 当我们在代码评审、设计评审时面临一个个选择时,每一个 Hack、每一个带来额外成本和复杂度的设计似乎都显得没那么有危害:就是增加了一点点复杂度而已,就是一点点风险而已。但是每一个失败的系统的问题都是这样一点点积累起来的;
  • 破窗效应 Broken window:一个建筑,当有了一个破窗而不及时修补,这个建筑就会被侵入住认为是无人居住的、风雨更容易进来,更多的窗户被人有意打破,很快整个建筑会加速破败。这就是破窗效应,在软件的质量控制上这个效应非常恰当。所以,Don’t live with broken windows (bad designs, wrong decisions, poor code) :有破窗尽快修。

零容忍,并不是不让复杂度增长:我们都知道这是不可能的。我们需要的是尽力控制。因为进度而临时打破窗户也能接受,但是要尽快补上。

当然文章一开始就强调了,如果所写的业务代码生命周期只有几个月,那么多半在代码变得不可维护之前就可以下线了,那可以不用关注太多,能用就行。

最后,作为 Software engineer,软件是我们的作品,希望大家都相信:

  • 真正的工程师一定在意自己的作品:我们的作品就是我们的代码。工匠精神是对每个工程师的要求;
  • 我们都可以带来改变:代码是最公平的工作场地,代码就在那里,只要我们愿意,就能带来变化。

云原生微服务框架哪家强h1>

近期阿里云原生团队联合 X-lab 开放实验室发布《2020 年微服务领域开源数字化 告》,通过 2020 年 1 月到 6 月的 GitHub 日志进行统计,分析微服务框架项目以及 Spring Cloud 项目的 GitHub 开发者行为日志,分析了开源微服务框架活跃度。

点击查看 告全文:https://developer.aliyun.com/article/770673

原文链接:https://developer.aliyun.com/article/771010p>

文章知识点与官方知识档案匹配,可进一步学习相关知识云原生入门技能树首页概览8829 人正在系统学习中

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2020年7月22日
下一篇 2020年7月22日

相关推荐