【转】阿里研究员:警惕软件复杂度困局

阿里研究员谷朴:警惕软件复杂度困局 **简介:** 对于大型的软件系统如互联网分布式应用或企业级软件,为何我们常常会陷入复杂度陷阱?如何识别复杂度增长的因素?在代码开发以及演进的过程中需要遵循哪些原则?本文将分享阿里研究员谷朴关于软件复杂度的思考:什么是复杂度、复杂度是如何产生的以及解决的思路。较长,同学们可收藏后再看。 ![1.png](https://ucc.alicdn.com/pic/developer-ecology/aa880b82b14540b588cd595bcc03ad81.png "1.png") 作者 |张瓅玶(谷. 阅读详情

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

原创 谷朴 阿里技术 8月24日

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

 

文末福利:免费下载《2020年微服务领域开源数字化报告》。

 

写在前面

 

软件设计和实现的本质是工程师相互通过“写作”来交流一些包含丰富细节的抽象概念并且不断迭代过程。

 

另外,如果你的代码生存期一般不超过6个月,本文用处不大。

 

一  软件架构的核心挑战是快速增长的复杂性

 

越是大型系统,越需要简单性。

 

大型系统的本质问题是复杂性问题。互联网软件,是典型的大型系统,如下图所示,数百个甚至更多的微服务相互调用/依赖,组成一个组件数量大、行为复杂、时刻在变动(发布、配置变更)当中的动态的、复杂的系统。而且,软件工程师们常常自嘲,“when things work, nobody knows why”。

 

图源:https://divante.com/blog/10-companies-that-implemented-the-microservice-architecture-and-paved-the-way-for-others/

 

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

 

图源:http://themetapicture.com/the-life-of-a-software-engineer/ 

 

做为一个有追求的软件工程师,大家肯定都思考过,我手上的项目,如何避免这种似乎难以避免的复杂度困境?

 

然而对于这个问题给出答案,却出乎意料的困难:很多的文章都给出了软件架构的设计建议,然后正如软件领域的经典论著《No silver bullet》所说,这个问题没有神奇的解决方案。并不是说那么多的架构文章都没用(其实这么方法多半都有用),只不过,人们很难真正去follow这些建议并贯彻下去。为什么?我们还是需要彻底理解这些架构背后的思考和逻辑。所以我觉得有必要从头开始整理这个逻辑:什么是复杂度,复杂度是如何产生的,以及解决的思路。

 

二  软件的复杂度为什么会快速增长?

 

要理解软件复杂度会快速增长的本质原因,需要理解软件是怎么来的。我们首先要回答一个问题,一个大型的软件是建造出来的,还是生长出来的?BUILT vs GROWN,that is the problem.

 

1  软件是长出来的,不是建造出来的

 

软件不是建造出来的,甚至不是设计出来的。软件是长出来的。

 

这个说法初看上去和我们平时的认识似乎不同,我们常常谈软件架构,架构这个词似乎蕴含了一种建造和设计的意味。然而,对于软件系统来说,我们必须认识到,架构师设计的不是软件的架构,而是软件的基因,而这些基因如何影响软件未来的形态则是难以预测,无法完全控制。

 

为什么这么说?所谓建造和“生长”差异在哪里?其实,我们看今天一个复杂的软件系统,确实很像一个复杂的建筑物。但是把软件比作一栋摩天大楼却不是一个好的比喻。原因在于,一个摩天大楼无论多么复杂,都是事先可以根据设计出完整详尽的图纸,按图准确施工,保证质量就能建造出来的。然而现实中的大型软件系统,却不是这么建造出来的。

 

 

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

 

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

 

 

也就是说,软件的动态“生长”,更像是上图所画的那样,是从一个简单的“结构”生长到复杂的“结构”的过程。伴随着项目本身的发展、研发团队的壮大,系统是个逐渐生长的过程。

 

2  大型软件的核心挑战是软件“生长”过程中的理解和维护成本

 

复杂软件系统最核心的特征是有成百上千的工程师开发和维护的系统(软件的本质是工程师之间用编程语言来沟通抽象和复杂的概念,注意软件的本质不是人和机器沟通)。如果认同这个定义,设想一下复杂软件是如何产生的:无论最终多么复杂的软件,都要从第一行开始开发。都要从几个核心开始开发,这时架构只能是一个简单的、少量程序员可以维护的系统组成架构。随着项目的成功,再去逐渐细化功能,增加可扩展性,分布式微服务化,增加功能,业务需求也在这个过程中不断产生,系统满足这些业务需求,带来业务的增长。业务增长对于软件系统迭代带来了更多的需求,架构随着适应而演进,投入开发的人员随着业务的成功增加,这样不断迭代,才会演进出几十,几百,甚至几千人同时维护的复杂系统来。

 

大型软件设计核心要素是控制复杂度。这一点非常有挑战,根本原因在于软件不是机械活动的组合,不能在事先通过精心的“架构设计”规避复杂度失控的风险:相同的架构图/蓝图,可以长出完完全全不同的软件来。大型软件设计和实现的本质是大量的工程师相互通过“写作”来交流一些包含丰富细节的抽象概念并且相互不断迭代的过程[2]。稍有差错,系统复杂度就会失控。

 

所以说了这么多是要停留在形而上吗?并不是。我们的结论是,软件架构师最重要的工作不是设计软件的结构,而是通过API,团队设计准则和对细节的关注,控制软件复杂度的增长。

 

  • 架构师的职责不是试图画出复杂软件的大图。大图好画,靠谱的系统难做。复杂的系统是从一个个简单应用 一点点长出来的。

 

  • 当我们发现自己的系统问题多多,别怪“当初”设计的人,坑不是一天挖出来的。每一个设计决定都在贡献复杂度。

 

三  理解软件复杂度的维度

 

1  软件复杂度的两个表现维度:认知负荷与协同成本

 

我们分析理解了软件复杂度快速增长的原因,下面我们自然希望能解决复杂度快速增长这一看似永恒的难题。但是在此之前,我们还是需要先分析清楚一件事情,复杂度本身是什么?又如何衡量?

 

代码复杂度是用行数来衡量么?是用类的个数/文件的个数么?深入思考就会意识到,这些表面上的指标并非软件复杂度的核心度量。正如前面所分析的,软件复杂度从根本上说可以说是一个主观指标(先别跳,耐心读下去),说其主观是因为软件复杂度只有在程序员需要更新、维护、排查问题的时候才有意义。一个不需要演进和维护的系统其架构、代码如何关系也就不大了(虽然现实中这种情况很少)。

 

 

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

 

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

 

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

 

  • 第一,认知负荷 cognitive load :理解软件的接口、设计或者实现所需要的心智负担。

 

  • 第二,协同成本Collaboration cost:团队维护软件时需要在协同上额外付出的成本。

 

我们看到,这两个维度有所区别,但是又相互关联。协同成本高,让软件系统演进速度变慢,效率变差,工作其中的工程师压力增大,而长期难以取得进展,工程师倾向于离开项目,最终造成质量进一步下滑的恶性循环。而认知负荷高的软件模块让程序员难以理解,从而产生两个后果:(1) 维护过程中易于出错,bug 率故障率高;(2) 更大机率 团队人员变化时被抛弃,新成员选择另起炉灶,原有投入被浪费,甚至更高糟糕的是,代码被抛弃但是又无法下线,成为定时炸弹。

 

2  影响到认知负荷的因素

 

认知负荷又可以分解为:

 

  • 定义新的概念带来认知负荷,而这种认知负荷与 概念和物理世界的关联程度相关。

 

  • 逻辑符合思维习惯程度:正反逻辑差异,逻辑嵌套和独立原子化组合。继承和组装差异。 

 

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

 

看以下案例[7]:

 

A.  Code with too much nesting

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
response = server.Call(request) if response.GetStatus() == RPC.OK:  if response.GetAuthorizedUser():    if response.GetEnc() == 'utf-8':      if response.GetRows():        vals = [ParseRow(r) for r in                response.GetRows()]        avg = sum(vals) / len(vals)        return avg, vals      else:        raise EmptyError()    else:      raise AuthError('unauthorized')  else:    raise ValueError('wrong encoding')else:  raise RpcError(response.GetStatus())

 

B.  Code with less nesting

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
response = server.Call(request) if response.GetStatus() != RPC.OK:  raise RpcError(response.GetStatus())
if not response.GetAuthorizedUser():  raise ValueError('wrong encoding')
if response.GetEnc() != 'utf-8':  raise AuthError('unauthorized') if not response.GetRows():  raise EmptyError()
vals = [ParseRow(r) for r in        response.GetRows()]avg = sum(vals) / len(vals)return avg, vals

 

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

 

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

 

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

 

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

 

 

(3)接口设计不当

 

以下是一个典型的接口设计不当带来的理解成本。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
class BufferBadDesign {
  explicit Buffer(int size);// Create a buffer with given sized slots  void AddSlots(int num);// Expand the slots by `num`  // Add a value to the end of stack, and the caller need to  // ensure that there is at least one empty slot in the stack before  // calling insert  void Insert(int value);
  int getNumberOfEmptySlots(); // return the number of empty slots}

 

希望我们的团队不会设计出这样的模块。这个问题可以明显看到一个接口设计的不合理带来的维护成本提升:一个Buffer的设计暴露了内部内存管理的细节(slot维护),从而导致在调用最常用接口 “insert”时存在陷阱:如果不在insert前检查空余slot,这个接口就会有异常行为。

 

但是从设计角度看,维护底层的Slot的逻辑,也外部可见的buffer的行为其实并没有关联,而只是一个底层的实现细节。因此更好的设计应该可以简化接口。把Slot数量的维护改为内部的实现逻辑细节,不对外暴露。这样也完全消除了因为使用不当带来问题的场景。同时也让接口更易于理解,降低了认知成本。

 

  •  
  •  
  •  
  •  
  •  
  •  
class Buffer {    explicit Buffer(int size); // Create a buffer with given sized slots    // Add a value to the end of buffer. New slots are added     // if necessary.     void Insert(int value);}

 

事实上,当我们发现一个模块在使用时具备如下特点时,一般就是难以理解、容易出错的信号:

 

  • 一个模块需要调用者使用初始化接口才能正常行为:对于调用者来说,需要调用初始化接口看似不是大的问题,但是这样的模块,带来了多种后患,尤其是当存在多个参数需要设置,相互关联关系复杂时。配置问题应该单独解决(比如通过工厂模式,或者通过单独的配置系统来管理)。

 

  • 一个模块需要调用者使用后做清理/ finalizer才能正常退出。

 

  • 一个模块有多种方式让调用者实现完全相同的功能:软件在维护过程中,出现这种状况可能是因为初始设计不当后来修改设计 带来的冗余,也可能是设计原版的缺陷,无论如何这种模块,带着强烈的“坏味道”。

 

完全避免这些问题很难,但是我们需要在设计中尽最大努力。有时通过文档的解释来弥补这些问题是必要的,但是好的工程师/架构师,应该清醒的意识到,这些都是“坏味道”。

 

(4)一个简单的修改需要在多处更新

 

简单修改涉及多处更改也是常见的软件维护复杂度因素,而且主要影响的是我们的认知负荷:维护修改代码时需要花费大量的精力确保各处需要修改的地方都被照顾到了。

 

最简单的情形是代码当中有重复的“常数”,为了修改这个常数,我们需要多处修改代码。程序员也知道如何解决这一问题,例如通过定义个constant 并处处引用避免magic number。再例如网页的风格/色彩,每个页面相同配置都重复设置同样的色彩和风格是一种模式,而采用css模版则是更加易于维护的架构。这在架构原则中对应了数据归一化原则(Data normalization)。

 

稍微复杂一些的是类似的逻辑/或者功能被copy-paste多次,原因往往是不同的地方需要稍微不同的使用方式,而过去的维护者没有及时refactor代码提取公共逻辑(这样做往往需要更多的时间精力),而是省时间情况下选择了copy-paste。这就是常说的 Don't repeat yourself原则:

 

Every piece of knowledge must have a single, unambiguous, authoritative representation within a system[8] 

 

(5)命名

 

软件中的API、方法、变量的命名,对于理解代码的逻辑、范围非常重要,也是设计者清晰传达意图的关键。然而,在很多的项目里我们没有给Naming /命名足够的重视。

 

我们的代码一般会和一些项目关联,但是需要注意的是项目是抽象的,而代码是具体的。项目或者产品可以随意一些命名,如阿里云喜欢用中国古代神话(飞天、伏羲、女娲)命名系统,K8s也是来自于希腊神话,这些都没有问题。而代码中的API、变量、方法不能这样命名。

 

一个不好的例子是前一段我们的Cluster API 被命名为Trident API(三叉戟),设想一下代码中的对象叫Trident时,我们如何理解在这个对象应该具备的行为?再对比一下K8s中的资源:Pod, ReplicaSet, Service, ClusterIP,我们会注意到都是清晰、简单、直接符合其对象特征的命名。名实相符可以很大程度上降低理解该对象的成本。

 

有人说“Naming is the most difficult part of software engineering[9][10]”,或许也不完全是个玩笑话:Naming的难度在于对于模型的深入思考和抽象,而这往往确实是很难的。

 

需要注意的是:

 

(a)Intention vs what it is

 

需要避免用“是什么”来命名,要用“for what / intention”。“是什么”来命名是会很容易将实现细节。比如我们用 LeakedBarrel做rate limiting,这个类最好叫 RateLimiter,而不是LeakedBarrel:前者定义了意图(做什么的),后者 描述了具体实现,而具体实现可能会变化。再比如 Cache vs FixedSizeHashMap,前者也是更好的命名。

 

(b)命名需要符合当前抽象的层级

 

首先我们软件需要始终有清晰的抽象和分层。事实上我们Naming时遇到困难,很多就是因为软件已经缺乏明确的抽象和分层带来的表象而已。

 

(6)不知道一个简单特性需要在哪些做修改,或者一个简单的改动会带来什么影响,即unknown unknowns 

 

在所有认知复杂度的表现中,这是最坏的一种,不幸的是,所有人都曾经遇到过这样的情况。

 

 

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

 

  • 代码缺乏充分的测试覆盖,一些重要场景依赖维护者手工测试。

 

  • 代码有隐藏/不易被发现的行为或者边界条件,与文档和接口描述并不符合。

 

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

 

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

 

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

 

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

 

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

 

  •  
  •  
  •  
  •  
// Time period in seconds.void someFunction(int timePeriod); // time period using Duration. void someFunction(Duration timePeriod);

 

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

 

3  影响协同成本的因素

 

协同成本则是增长这块模块所需要付出的协同成本。什么样的成本是协同成本?(1)增加一个新的特性往往需要多个工程师协同配合,甚至多个团队协同配合;(2) 测试以及上线需要协调同步。

 

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

 

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

 

另外一个角度,则是:

 

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

 

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

 

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

 

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

 

 

上图左侧是Inheritance(继承或者是扩展模式),有四个团队,其中一个是Framework团队负责框架实现,框架具有三个扩展点,这三个扩展点有三个不同的团队实现插件扩展,这些插件被Framework调用,从架构上,这是一种类似于继承的模式。

 

右侧是组合模式(composition):底层的系统以API服务的方式提供接口,而上层应用或者服务通过调用这些接口来实现业务功能。

 

这两种模式适用于不同的系统模型。当Framework偏向于底层、不涉及业务逻辑且相对非常稳定时,可以采用inheritance模式,也即Framework被集成到团队1,2,3的业务实现当中。例如RPC framework就是这样的模型:RPC底层实现作为公共的base 代码/SDK提供给业务使用,业务实现自己的RPC 方法,被framework调用,业务无需关注底层RPC实现的细节。因为Framework代码被业务所依赖,因此这时业务希望Framework的代码非常稳定,而且尽量避免对framework层的感知,这时inheritance是一种比较合适的模型。

 

然而,我们要慎用Inheritance模式。Inheritance模式的常见陷阱:

 

(a)要避免出现管理倒置

 

即Framework层负责整个系统的运维(framework团队负责代码打包、构建、上线),那么会出现额外的协同复杂度,影响系统演进效率(设想一下如果Dubbo的团队要求负责所有的使用Dubbo的应用的打包、发布成为一个大的应用,会是多么的低效)。

 

(b)要避免破坏业务逻辑流程的封闭性

 

Inheritance模式如果使用不当,很容易破坏上层业务的逻辑抽象完整性,也即“扩展实现1”这个模块的逻辑,依赖于其调用者的内部逻辑流程甚至是内部实现细节,这会带来危险的耦合,破坏业务的逻辑封闭性。

 

如果你所在的项目采用了插件/Inheritance模式,同时又出现上面所说的管理倒置、破坏封闭性情况,就需要反思当前的架构的合理性。

 

而右侧的Composition是更常用的模型:服务与服务之间通过API交互,相互解耦,业务逻辑的完整性不被破坏,同时框架/Infra的encapsulation也能保证。同时也更灵活,在这种模型下,Service 1, 2, 3 如果需要也可以产生相互调用。

 

另外《Effective Java》一书的Favor composition over inheritance有很好的分析,可以作为这个问题的补充。

 

(3)可测试性不足带来的协同成本

 

交付给其他团队(包括测试团队)的代码应该包含充分的单元测试,具备良好的封装和接口描述,易于被集成测试的。然而因为 单测不足/模块测试不足,带来的集成阶段的复杂度升高、失败率和返工率的升高,都极大的增加了协同的成本。因此做好代码的充分单元测试,并提供良好的集成测试支持,是降低协同成本提升迭代效率的关键。

 

可测试性不足,带来协同成本升高,往往导致的破窗效应:上线越来越靠运气,unknown unknowns越来越多。

 

(4)文档

 

降低协同成本需要对接口/API提供清晰的、不断保持更新一致的文档,针对接口的场景、使用方式等给出清晰描述。这些工作需要投入,开发团队有时不愿意投入,但是对于每一个用户/使用方,需要依赖钉钉上的询问、或者是依靠ATA文章(多半有PR性质或者是已经过时,没有及时更新,毕竟ATA不是产品文档),协同成本太高,对于系统来说出现bug/使用不当的几率大为增加了。

 

最好的方式:(1)代码都公开;(2)文档和代码写在一起(README.md, *.md),随着代码一起提交和更新,还计算代码行数,多好。

 

4  软件复杂度生命周期

 

 

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

 

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

 

5  Good enough vs Perfect

 

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

 

四  对复杂度增长的对策

 

每一份新的代码的引入,都在增加系统的复杂度:因为每一个类或者方法的创建,都会有其他代码来引用或者调用这部分代码,因而产生依赖/耦合,增加系统的复杂度(除非之前的代码过度复杂unncessarily complex,而通过重构可以降低复杂度),如果读者都意识到了这个问题,并且那些识别增加复杂度的关键因素对于大家有所帮助,那么本文也就达到了目标。

 

而如何Keep it simple,是个非常大的话题,本文不会展开。对于API设计,在[5]中做了一些总结,其他的希望后续有时间能继续总结。

 

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

 

  • 复杂度增长带来的风险(unknown unknowns、不可控的失败等)往往是后知后觉的,等到问题出现时,往往legacy已经形成一段时间,或者坑往往是很久以前埋的。

 

  • 当我们在代码评审、设计评审时面临一个个选择时,每一个Hack、每一个带来额外成本和复杂度的设计似乎都显得没那么有危害:就是增加了一点点复杂度而已,就是一点点风险而已。但是每一个失败的系统的问题都是这样一点点积累起来的。

 

  • 破窗效应Broken window:一个建筑,当有了一个破窗而不及时修补,这个建筑就会被侵入住认为是无人居住的、风雨更容易进来,更多的窗户被人有意打破,很快整个建筑会加速破败。这就是破窗效应,在软件的质量控制上这个效应非常恰当。所以,Don't live with broken windows (bad designs, wrong decisions, poor code) [6]:有破窗尽快修。

 

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

 

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

 

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

 

  • 真正的工程师一定在意自己的作品:我们的作品就是我们的代码。工匠精神是对每个工程师的要求。

 

  • 我们都可以带来改变:代码是最公平的工作场地,代码就在那里,只要我们愿意,就能带来变化。

 

Reference

 

[1]John Ousterhout, A Philosophy of software design

[2]Frederick Brooks, No Silver Bullet - essence and accident in software engineering

[3]Robert Martin, Clean Architecture

[4]https://medium.com/monsterculture/getting-your-software-architecture-right-89287a980f1b 

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

[6]Andrew Hunt and David Thomas, The pragmatic programmer: from Journeyman to master

[7]https://testing.googleblog.com/2017/06/code-health-reduce-nesting-reduce.html

[8]https://en.wikipedia.org/wiki/Don%27t_repeat_yourself

[9]http://www.multunus.com/blog/2017/01/naming-the-hardest-software/

[10]https://martinfowler.com/bliki/TwoHardThings.html

复杂性真相:软件开发中的潜在陷阱与解决策略 系统复杂性是指系统的构建、运作和维护过程中涉及到的多个因素、组件、关系和交互的综合程度。认识软件的复杂性之前,需要找到合适的方法来度量一个软件系统是否复杂,以及复杂度有多少。 阅读详情

相关推荐

国央企创新负责人如何通过平台实现科研成果的快速识别与转化?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

如何写出简洁的 CQRS 代码?

导读:命令和查询责任分离(CQRS)是指将数据存储的读取和更新操作分开的一种模式。实施 CQRS 据称可以提高性能、可扩展性和安全性。迁移到 CQRS 模式所创造的灵活性,使系统能够随着...

weixin_45583158的博客 579

PotPlayer自用皮肤

自用 PotPlayer 皮肤,仅微调了字体大小与颜色,是按个人习惯调整,未做适配性调整。

警惕软件复杂度困局

简介:对于大型的软件系统如互联网分布式应用或企业级软件,为何我们常常会陷入复杂度陷阱?如何识别复杂度增长的因素?在代码开发以及演进的过程中需要遵循哪些原则?本文将分享阿里研究员谷朴关于软件复杂度的思考:什么是复杂度、复杂度是如何产生的以及解决的思路。较长,同学们可收藏后再看。 写在前面 软件设计和实现的本质是工程师相互通过“写作”来交流一些包含丰富细节的抽象概念并且不断迭代过程。 另外,如果你的代码生存期一般不超过6个月,本文用处不大。 一 软件架构的核心挑战是快速增长的复杂性 越是...

AlbenXie的博客 779

2-shell及常用命令-续.pdf

2-shell及常用命令-续.pdf

cocos2dx lua + protobuf

vs 2015 cocos2dx 3.10  lua  protobuf1. 下载 protobuf 源码     https://github.com/bnnswyp/protobuf/tree/master/protobuf-2.5.0    解压后的文件列表如下所示         其中 vsprojects 文件夹为 protobuf 的vs 工程2. 打开 protobuf.sln 编译...

u014486706的博客 2163

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

导读:对于大型的软件系统如互联网分布式应用或企业级软件,为何我们常常会陷入复杂度陷阱?如何识别复杂度增长的因素?在代码开发以及演进的过程中需要遵循哪些原则?本文将分享阿里研究员谷朴关于软件复杂度的思考:什么是复杂度、复杂度是如何产生的以及解决的思路。较长,同学们可收藏后再看。 写在前面 软件设计和实现的本质是工程师相互通过“编码”来交流一些包含丰富细节的抽象概念并且不断迭代过程。 另外,如果你的代码生存期一般不超过6个月,本文用处不大。 一 软件架构的核心挑战是快速增长的...

一个天秤座的架构师 2万+

41-dify案例分享-基于database插件实现Text2sql的数据库查询图表工作流

本文介绍基于 Text2SQL 的 Dify 工作流实现过程。先阐述 Text2SQL 概念、步骤及应用领域。接着说明工作流制作,包括用 chatflow 加开场白、获取系统时间,配置 LLM 生成 SQL,用 SQL Execute 工具查询,经 LLM 汇总结果,代码生成 ECharts 数据,条件分支判断图表类型输出。最后介绍验证测试、提供体验地址,还提及借助工具生成建表和初始化数据 SQL 语句。

个人技术 1万+

阿里研究员(P10):警惕软件复杂度困局!

写在前面软件设计和实现的本质是工程师相互通过“写作”来交流一些包含丰富细节的抽象概念并且不断迭代过程。另外,如果你的代码生存期一般不超过6个月,本文用处不大。1.软件架构的核心挑战是快速...

chengjun_java的专栏 667

AD620仪表放大器模组实战:从调零到信号放大的完整指南

本文详细介绍了AD620仪表放大器模组的实战应用,从调零到信号放大的完整指南。AD620模组以其专业级性能和易用性成为微弱信号放大的首选,集成了AD620芯片、LM358运放和7660电源转换芯片。文章提供了调零技巧、增益设置方法以及单端与差分输入的正确连接方案,并通过心电信号采集和称重传感器等实际案例展示了其广泛应用。

weixin_29290379的博客 376

C++控制台实现简单的注册登录

C++控制台实现简单的注册登录 #include<iostream> #include<string> #include <vector> #include <conio.h> void Display();//调用display显示switch()界面 void Register();//注册 void Enter();//登录 using nam...

乔路非 2万+

工作笔记2

阿里研究员:警惕软件复杂度困局 https://developer.aliyun.com/article/770886?spm=a2c6h.12873581.0.0.bada6446EVSvaJ&groupCode=alitech Java编程技巧:如何实现参数的输入输出? https://developer.aliyun.com/article/771556?spm=a2c6h.12873581.0.0.bada6446EVSvaJ&groupCode=alitech ..

AlbenXie的博客 245

lammps案例:聚乙烯和石墨烯界面pull out模拟

大家好,我是小马老师。 本文介绍lammps模拟计算界面结合力的一个案例。 本案例参考论文《The interfacial mechanical properties of functionalized graphene- polymer nanocomposites》,对论文作者表示感谢。 模型共分2层,上层为聚乙烯模型,下次为单层石墨烯,模拟过程共分3部分:建模、弛豫、拉伸。 (1)建模 使用MS建立聚乙烯和石墨烯模型,具体方法可参考之前的推文,使用build layer方法建立聚乙烯和石墨烯组合模型,

lammps_jiayou的博客 3494

2020年度盘点出炉!技术好文一口气读完

简介:温故知新。不知不觉间,今天已经是2020年的最后一天了。2020注定是不平凡的一年。突如其来的疫情,让我们的工作和生活都发生了重大变化。作为技术人,我们用技术抗疫,快速上线的健康码、AI医生、远程办公......用技术,为抗疫贡献出自己的一份力量。这一年,阿里技术依然一如既往为同学们送上满满的技术干货,向所有开发者分享阿里工程师在技术上的学习收获、经验心得,分享阿里巴巴在电商业务上的实践探索、技术沉淀,以及在前沿科技领域的创新和突破。辞旧迎新之际,阿里妹为大家盘点这一年的年度好文,希望同学们温故知新,

阿里云开发者 7245

YOLOv12改进策略【小目标改进】| 添加专用于小目标的检测层 附YOLOv1~YOLOv12的检测头变化详解

在目标检测领域,小目标检测一直是一个具有挑战性的问题。算法以其高效快速的特点受到广泛关注,然而在面对小目标时,仍存在一些局限性。本文将介绍如何在中添加小目标检测层,以提高对小目标的检测能力。原始模型结构如下:详细内容参考:YOLOv12原始模型结构介绍 二、有效特征层对应的检测头类别 2.1 P3/8 - small检测头 原始模型中的对应的检测头主要用于检测相对较小的目标。其特征图大小相对较大,空间分辨率较高。 适合检测尺寸大概在到像素左右的目标。 2.2 P4/16 - medium检测头 这个检

Limiiiing的博客 1741

用 CL_SALV_TREE 把层级数据做成一棵会呼吸的 ABAP ALV 树:从类继承扫描到事件交互的完整拆解

本文介绍了SAP ABAP中使用CL_SALV_TREE实现树形ALV报表的方法。该技术将层级数据(如类继承关系)以可展开/折叠的树形结构展示,解决了传统ALV Grid无法直观显示层次结构的问题。核心实现采用"数据表+层级表"分离的设计模式:数据表存储节点业务信息,层级表维护父子关系。通过add_node方法动态构建树形结构,支持设置节点文本、图标和事件。文中提供了完整示例代码,演示了如何递归扫描ABAP类继承关系并构建树形ALV,这种技术特别适用于展示类族谱、权限树等层级数据,能显著

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 206

redis数据库价格_2020年企业级redis价格

阿里云数据库Redis正式上线Tair持久存储系列 提供大规格命令级持久化能力的云上Redis服务2020年9月20日,阿里云云数据库Redis正式上线Tair(又称阿里云Redis企业版)持久存储系列,该系列包含两种产品形态:持久内存型和容量存储型。该系列产品是Tair系列继性能增强型和混合存储型后又一力作,其兼容原生Redis的数据结构和接口,并具备更大容量规格、抖动更低且命令级数据持久化.....

weixin_30946627的博客 634

手把手教你学Simulink--基于Simulink的开关磁阻电机电流斩波控制仿真建模

特点:定子和转子均由普通硅钢片叠压而成,没有永磁体或绕组在转子上。工作原理:利用转子旋转时不同位置的磁阻变化产生转矩。控制挑战:由于SRM的高度非线性和磁饱和效应,需要复杂的控制算法来优化性能。利用Simscape Electrical中的模块构建SRM的物理模型,配置参数如电阻、电感等。matlab深色版本% 添加SRM电机模块根据实际需求调整电机参数。通过上述步骤,我们简要介绍了如何基于Simulink进行开关磁阻电机的电流斩波控制仿真建模。

MHD0815的博客 306

视频号首次直播:关于职业规划那些事!

蓝色关注,回复“1”获取知名公司程序员和产品职级见字如面,我是军哥。上周六(11月21)晚上九点,我在我的视频号(军哥聊科技)做了一次一小时的直播,最高在线40人+,总共300人+来看过...

chengjun_java的专栏 502

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

职场正在淘汰那些 1 年经验用了 N 年的人

蓝色关注,回复“1”获取知名公司程序员和产品职级见字如面,我是军哥。经常听到有朋友感叹,自己30多岁了,工作经验丰富,但找不到太好的工作,还经常被身边的95后(所谓的后起之秀)超越。在看...

chengjun_java的专栏 199

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

上一篇: 【转】GDB调试程序及分析core
下一篇: [好文分享]C++11内存模型
LinuxG_G
博客等级 码龄15年 58粉丝 176原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值