track image
是时候想想该怎么删代码了

是时候想想该怎么删代码了

陈天陈天

武林外传里秀才怼上姬无命,来了一段关于「我是谁」的精彩逼问。

我是谁?我生从何来,死往何处,我为何要出现在这个世界上?我的出现对这个世界来说意味着什么,是世界选择了我还是我选择了世界?!我和宇宙之间有必然的联系吗?宇宙是否有尽头,时间是否有长短,过去的时间在那里消失,未来的时间又在何处停止,我在这一刻提出的问题还是还是你刚才听到的问题吗?

我们平时很少问自己这样愚蠢的问题。很多事情,我们是如此地习以为常,以至于非但自己看不到这样的问题,当别人问道时,反而纯纯地回一句:doesn't look like anything to me。

在很早前的一篇文章 技术债:the good, the bad, and the tao 里我问了几个问题:

  • 我们为什么要做 code review?可以不做 code review 么?不做会有很严重的问题么?

  • 在一个 team 里面,我们为什么要用同一种语言写代码?为什么程序员不能随意用自己想用的语言?

上面的问题,别说问了,想一想都是政治不正确。我们写代码遵循的很多流程,归根到底,都是为了软件能有更好的可维护性(maintainability) —— 毕竟,一段代码写下去,未来很久很久的时间,它们都是公司的资产:我们需要根据市场的需要,为其添加新功能,我们需要修改发现的 bug 和潜在的问题,我们还不得不不断提升其效率和能力,以便调和人民群众日益增长的需求和落后的生产力之间的矛盾 —— 这可是软件公司初级阶段的主要矛盾(高级阶段的都搞 AI 了)。所以,我们必须保证软件的可维护性 —— 而代码审核,同事间互相备份,使用同种语言开发都是服务于这个目的而存在的手段而已。那么问题来了:

为什么我们要维护代码?为什么不能重写代码?或者说,为什么我们要通过修改代码来维护代码,而不是通过删除和重写代码来达到维护代码的目的?

我们之所以不能,或者不愿这么做有很多原因:

  1. 添加一个新的功能同时保证不引入问题已经让人头大了 —— 要修改十几几十个文件,新增代码即便只有几百上千行,但涉及的代码成千上万行,都重写,写的过来么?风险太大

  2. 改动不算太大,没有必要重写啊

  3. 疯了,就算这么做是对的,老板肯定不会让我这么干

  4. 重写我自己的代码也就算了,重写别人的 —— 比我牛的我没底气;比我怂的我不忍心

  5. ...

这里面,除了第二个原因成立,其他其实都不太成立。第一个是架构的问题;三四是文化或者说认知的问题。架构问题如果深究,也是一种认知的问题。

因为我们认定了我们所写的代码,都不是可以随意抛弃(throwable)的代码,而是需要维护(maintainable)的代码;我们从不把代码看做阻碍我们前进的负债,而是沉淀下来的宝贵的资产。所以我们愿意日复一日地堆上千万行的代码,从不考虑有计划地删除。

同样的认知问题也发生在 imperative / functional programming 上。变量之所以被称之变量,是因为它能够被赋值,被修改。很多程序员无法想象如果变量只能被绑定,无法被修改,日子该如何过 —— 因为他们甚至不知道该如何处理最基础的控制流程:循环。你告诉他 high ordered function,tail recursion,他会一脸懵逼:it doesn't look like anything to me。

你也许会说 functional programming 和代码可随意抛弃是两种完全不同的认知,前者理性且有学术界的支撑,后者疯狂,近乎无稽之谈。让我们先抛开这个争论,假设确实有个疯子要我们设计一种架构,在这个架构下,我们引入的任何新的代码,都能够在未来的某个时刻被完全扔掉重写,而不会影响系统的其他功能。 我们该怎么做?

意大利面条式的架构肯定不行。别说把某个功能摘出来扔掉不影响功能了,光摘出来可能就已经让人竭尽全力了。

所以我们必须要模块化。每个模块各司其职,上帝的归上帝,凯撒的归凯撒。这样我们即便把凯撒抛弃了,上帝也不会活不下去。

光有模块化也是不足够的。我们还得考虑分层。原子核和电子组成原子,若干原子组成分子,若干分子组成细胞,若干细胞组成组织,若干组织组成器官,若干器官组成生物体。每个层次各司其职,细胞不会过问原子中的电子是如何变态跃迁的。层次化能够保证在变化的系统中能有不变的,稳定的部分。在 ISO/OSI 结构中,物理链路层的变化被传输层屏蔽,而传输层的变化又被应用层掩盖地妥妥帖帖。程序员在发送一个 GET / 请求时,并不关心这个请求是经过 IPv4 还是 IPv6 传输的,更不需要迷失在 RJ45,fiber 这些多种多样的接口形态中。

分层还是不足够的,因为它虽然解开了纵向的耦合而忽视了横向的耦合。举个最简单的例子 —— 现在几乎所有的 web app 都拥抱 MVC,但几乎所有的经典 MVC framework 里做出来的 web app 都让你陷入横向耦合的陷阱,rails,django,phoenix,无一不是。如果让你设计一个博客系统,你会自然而然地从 Model 起,设计 Blog / Post / Comment / User 等基本的 model。然而,Post 跟 Blog 本可以无关, Blog 只是一组满足特定条件的 Post 的一个容器而已;Comment 也和 Post 无关,满足特定条件的 Comments 聚合起来,恰巧构成了 Post 的某个属性。因此,当你开始使用 rails generate model 的那一刻起,你的代码已经注定了有很强的横向耦合,难以将某个 Model 删除重写。有一天,当你发现 Post 需要用一个和 User 完全不同的 data store 存储时,你会发现,这几乎成了不可能完成的任务 —— 除非将整个系统重写。

所以我们还需要将功能和功能解耦,也就是服务化,通过接口或者说协议来约束双方的行为。拿刚才的博客系统来说,Post 应该对 User 而言完全是一个黑盒,User 无法触及 Post 的内部状态(使用什么存储方式),只能通过约定好的接口来获取 Post 的信息。这样的话,Post 和 User 可以在同一个数据库中,也可以在不同的数据库中。

服务化能够部分地让我们扔掉某个服务的代码完全重写,只要保证接口不变,就不会影响系统的其他功能。但它还不完全是最终的答案。我们从一个问题出发,走了这么远,已经可以心满意足了。Stephen Covey 劝诫我们:begin with the end in mind。如果我们在开始写代码的时候能够考虑到日后能被更加容易地删除,那么我们为此设计时会更加深思熟虑。我们会发现,写一段能够容易删除和重写的代码要比写一段容易维护的代码要难上很多。

川普在他的百日新政中,提到了这么一条:for every new federal regulation, two existing regulations must be eliminated. 且不管这有没有实现的可能,它都是很大胆和有远见卓识的想法。社会的效率之所以越来越低效,是跟法规(行政命令)只增不删或者增加的速度大于删除的速度,导致其臃肿繁复有极大的关系;软件系统之所以越来越庞大不堪,难以维护,也是跟我们只是不断添加和修改代码,代码的增加速度大于删除的速度有关,偏巧,增加的又多以业务逻辑为主 —— 要知道,很多数年前的业务逻辑相关的代码,可能已经和当下的商业环境大不相同,留着徒增烦恼,却不带来太多的价值。

对于这样的问题,生物界给出来的答案是新陈代谢。所谓新陈代解,就是生物体与外界环境之间的物质和能量交换以及生物体内物质和能量的自我更新过程。新陈代谢包括合成代谢(同化作用)和分解代谢(异化作用)。

同化作用是指生物体把从外界环境中获取的营养物质转变成自身的组成物质,并且储存能量的变化过程 —— 通过同化作用,程序员,软件系统中的叶绿素,用灵巧的双手把自己大脑中苦心孤诣钻研出来的成果转化成代码,汇入系统中;异化作用是指生物体能够把自身的一部分组成物质加以分解,释放出其中的能量,并且把分解的终产物排出体外的变化过程 —— 对程序员来说,就是对系统去芜存菁,定期不定期地删除代码中的渣滓,重写那些光是维护着就已经让人竭尽全力的代码。。。然而,我们却很少启动这个异化过程。

写这篇颇有争议(且尚未深思熟虑)的短文,并非想说代码维护不重要,重构不重要,只有重写才是王道,而是想抛出一个问题:有没有可能,我们在架构之初,就考虑到这个代码有可能成为一种负债,因此在设计上考虑到如何能轻松地将其删除或者替换?

文章被以下专栏收录
3 条评论
推荐阅读