软件工程的核心是控制复杂度

2019-04-20#观点

离职有一段时间,还是过得挺开心的。终于有大把的时间可以做些喜欢的事。通过一段时间的学习沉淀,突然领悟到了一点东西。在此记录一下。

今天起来脑子里突然浮现出来2个关键字: 软件工程控制复杂度 。这2个关键字在脑海中闪过,牵连起近期学到的知识,似乎一切都揭示了问题的本质——软件工程的核心就是控制复杂度。

复杂度是什么

在谈论复杂度之前,我们先谈谈熵。等一下,熵又是什么?

熵最初是热力学里面提出的概念,举个栗子。

我有一杯冷水,我有一杯热水。现在我把热水加到冷水里面。最终水的温度,将趋于恒定。

现在我想把冷水和热水分开,重新装入原来得杯子,很明显这是不可能的,这个过程是不可逆的。

整个系统趋近于无序。为什么是无序,因为整个系统内,已经没有热量传递了,全是温水往哪里传递热量?该系统将处于一个混沌的状态,死气成成。

热量的传递无时无刻不再进行,我们的宇宙最终会变成一个恒定无序的状态,里面的东西没有任何意义。

回归到信息论中来,熵被用来度量系统的随机性(不确定性)。又到了抛硬币时间,我有1个硬币抛一下,你有一半的概率猜中是正面还是反面。现在我有1万个硬币,你几乎不可能猜对了。

熵的概念应用到软件工程中其实非常容易理解。

我写了1行代码,你肯定很容易理解,知道代码是干嘛用的。

我写了10行的时候,你简单阅读一下就能理解。

我写了100行的时候,你可能要画个图,梳理一下流程才能理清楚。

...

我写了100000 行代码(bug一堆,我还没写注释,我溜了),接盘的程序员天天问候我家人,这是再正常不过的事情了。

现在有个百万行代码的项目,整个系统bug一堆,但是没有人敢动代码,这像不像一杯恒定的温水,死气成成,再无创新的可能呢?开发人员无法理解其意义,项目也就没有了意义。也就谈不上扩展和维护了。

软件的熵总是倾向于最大化的,程序员们称之为“软件腐烂”。 —— 《程序员修炼之道》

怎么控制复杂度

没有麦克斯韦妖,也没有银弹。我们这些凡夫俗子是怎么解决复杂度问题的呢?下面我们做这样的定义

软件工程(项目) = 软件系统 + 开发团队

**软件自成一个系统,开发软件的人是另外一个系统。**从一定程度上来说,软件工程中的复杂度跟代码量成正比,跟开发人员数量成正比。**封闭系统总是熵增的,**还好软件工程 是2个系统,开发团队可以介入软件系统,减少其熵。

开发团队是人,人是不断进化的,任何外部的信息都会影响到开发团队,所以开发团队是开放的系统。也就是说一个软件项目是一个开放的系统。所以复杂度还是可以控制的,至于控制效果的如何,就要看开发团队的水平。

两大法宝

  • 聚——抽象
  • 把相似的问题,抽象成一类问题,实现这类问题的公共解。代码量减少,代码复用,熵减。
  • 总复杂度降低
  • 散——分治
  • 把复杂度高的问题分解成复杂度低的问题,使得问题容易处理。熵并不会减少,相反可能会增加。
  • 总的复杂度并没有降低,而是单元复杂度降低

在分治中,如果找了相似的子结构,又可以通过抽象,把相似子结构的部分合并。这样就是最好的解法了。

软件行业兴起这么多年来,前人总结了大量的经验来控制复杂度。我举几个简单的例子来说明他们为什么能控制复杂度。

  • 在变量命名中,见名知意是一条很重要的准则。
  • 名称即索引,好的名称便于检索。你掌握了名称也就掌握了它。
  • 这一条既不属于抽象也不属于分治,但是它放在了第一条,因为代码是会被阅读的。名字共识是开发人员协作的基础,没有名字也就谈不上控制。
  • 函数复用
  • 功能相同的函数封装成通用函数,减少代码量。
  • 黑盒封装
  • 屏蔽内部细节,暴露接口。
  • 不管你的代码写的多烂,把过程封装成函数,暴露等着别人调用即可,他人也无需关心盒子内的实现。这让开发人员得以抽身去关注更高层次的问题,构建更加复杂的系统。
  • 模块化
  • 代码量日益增长,模块化组织代码。
  • 每种语言都会提供模块化的支持,这是必然的。
  • 大模块划分成小模块,分而治之的思想在这里得到体现。
  • 模块化也可以理解为高层次的黑盒封装。
  • 在语言层面,有模块化的支持。在框架层面也有,例如 Django 会按照 app 来组织代码。这些都是标配。
  • 如今前端开发自成体系,模块化是必然的。但最开始 JS 是不支持模块化的。后来 node 自己引入了模块实现。再后来,模块化在 ES6 中得到官方支持。模块化是控制复杂度必不可少的工具。
  • 微服务
  • 微服务的思想依然是分治
  • 开发团队拆分,业务代码拆分。大系统拆分为小系统。
  • 单元复杂度降低,但随之而来的是协作问题。

...

对于控制复杂度,大师们精于此道,他们给出了很多建议(最佳实践)。你若不准寻这些建议,你就是团队里面的捣蛋鬼,你写的代码再多,也只是暂时实现了功能,但是引入的却是难以消除的熵。

控制复杂度的方式还有很多,各种经典的书中都有提到,例如《程序员修炼之道》、《代码大全》等等。书中提出了很多原则,很多方法,以及各种最佳实践。他们告诉你如何写最好的代码,而这些代码为什么好,因为它们本质上都控制了复杂度/单元复杂度,当复杂度降下来,代码可读性、可扩展性就越好。整个项目“腐烂”的速度就越慢,生命周期就越长。

在代码之外减少变量

软件工程中每引入一个变量,熵就会随之增加。这里的变量不仅仅指代码中的变量,一个开发人员是一个变量,开发环境也是变量,语言框架亦是变量。很多时候我们引入变量,是为了快。但是《人月神话》告诉我们事情不是想象的那么简单。

尽可能地减少变量,或者实在不行就管理变量。

  • 开发环境,测试环境,生产环境。每个环境都是一个变量。所以有了docker,有了容器,把他们抽象成一个环境。
  • 后端开发,前端开发,每个开发都是一个变量。所以有了全栈,能少一个是一个。
  • 需求,开发,测试,部署,运维。每一个环节都是一个变量。当每个环节都有不同的人时,复杂度成指数爆炸。所以有了 devops,有了全周期。
  • 版本1.0,版本2.0。每一个版本都是一个变量。但是我们又无法消除版本。所以我们有了版本控制,有了 svn,有了 git。

用汇编也能写一个淘宝,但是没人会这样做。软件工程要求快,但也要保证其质量,所以诞生了越来越多的技术/思想/开发模式,来解决复杂度的问题,同时提高开发效率。

从交接文档看软件熵增

项目完结之后,软件系统趋于稳定。这个时候整个软件系统看起来是一个封闭系统。根据定义,封闭系统总是熵增的,那这个时候软件系统的复杂度应该是增加的,怎么理解这一点呢?

我们来看这样一个问题,一个项目已经完结,在线上跑了很久了。这个时候PM说要加个新功能。

  1. 完结后的一天PM说要加新功能,没问题,加。
  2. 完结后的一年PM说要加新功能,???。

我们都知道1简单,2困难。这里简单和困难的定义是,开发人员需要做多少功才能开始开发新功能。前者做的功少,后者做的功多。为什么呢?即使是完结的项目,它的熵也在增加。可是一时间我们竟想不出是哪里增加了,代码又没变动。是的,代码没有变动,但是人变了。开发人员没有对软件系统做功了。

软件工程(项目) = 软件系统+开发人员

回到定义,一个封闭的系统总是熵增的。需要外部对其做功才能使其稳定。项目文档,开发人员对项目的熟悉程度(记忆),等等都是外部对软件系统做的功。文档遗失,开发人员对项目的熟悉程度降低等等这些因素,都会使得完结的项目熵增。

如果要使得1天后加功能和1年后加功能同样简单。需要开发人员始终保持对项目的熟悉程度。即开发人员需要做功,维持熵稳定。如果有一天开发人员离职了,文档可以辅助新的开发人员熟悉项目。人的记忆力会降低,但是文字不会。文字可以跨越时空交流。这就是为什么离职需要写交接文档。

新成员加入后,为了实现新功能,需要先熟悉系统。而这个熟悉的过程就是在做功。文档是前辈做的功,这样新成员可以少做功。复杂度越低,新成员做的功就越少。对于企业而言,做功的代价就是花钱。

写在最后

在写这篇文章之前,我先以 软件工程 控制复杂度 为关键字检索了一遍,搜到下面这篇文章。

https://zhuanlan.zhihu.com/p/21885300

大家的结论都是——软件工程的核心是控制复杂度。

在文章写道一半时,我又搜索了一遍,找到了下面这篇文章。

https://www.infoq.cn/article/7vtun90yxpht-0hgdf72

第二篇中提到了**控制复杂性有两个策略。**跟我想的简直是一模一样,哈哈哈,这大概就是英雄所见略同吧。

读完《信息简史》后,看问题地角度就不一样了,在这里强烈安利一下 《信息简史》这本书。本篇文章很大程度上是受信息论的启发。用熵的观点去看待软件工程中的各种问题,与 SICP、《程序员修炼之道》、Rust文档中的观点,以及自己的从业经验相互印证,使我得出了这个结论。

当我以结论检索时,发现这个结论已经存在了。这让我很开心,知识果然都是共通的。