一周技术思考(第23期)-遗留代码是[定时炸弹]还是[地雷]

大家好,这里记录,我每周读到的技术书籍、专栏、文章以及遇到的工作上的技术经历的思考,不见得都对,但开始思考总是好的。

遗留代码是定时炸弹还是地雷

 

作为一名程序员,你职业生涯的大部分时间都要花在维护代码上。

 

可能,当你成为一名程序员的那一刻,你想的是挥刀秣马,励志要写出“经典永留存”的不朽代码篇章,但是,很不幸,老代码总是比新代码要多

 

即使你有了一个机会,可以从头开始开发一个全新的系统,但是呢,你也要记得:未来的某个时间点,你或其他人还得维护其代码。

 

软件生活就是这样,从了吧。

 

好的,那我去维护老代码,为了让我第二次、第三次看到这一段段老代码的时候,我的心情能够越来越好,我总得干点啥。

 

于是,想起了重构。

 

 

新志向:我维护过的代码要比我看到它时更优秀。

 

如何才能让它比当初更优秀呢。

 

那就要看看,它当时有哪些原因驱使你下定决心去重构的。首先,肯定不是它的功能不好使,“再烂”的代码只要能运行,在用户那里看到的就是实现了的功能,用户是看不到功能下面的代码的,所以,并不是它没有实现该有的功能。

 

那是什么呢,可读性差、代码神秘、难以理解等等,对吧,是这些原因,才诱使了你的决心。

 

重构的本质是改善了既有代码的设计,但,绝不是改变原先的功能。如果硬要说功能的话,那就“非功能的功能”。

 

重构的最要的目的是在不改变其功能的情况下使现有的代码更具可读性

 

马丁·福勒在《重构》这本书里给重构下的定义是:“重组现有代码实体、改变其内部结构而不改变其外部行为的规范式技术”。

 

记住,重构的时候,只是修改代码的架构行为,绝不是添加功能的时候。你说,我趁重构的时候,对业务功能做个优化吧,想法是好的,但,错误和故障,也会很快随之而来。

 

再次强调,重构的时候,不是添加功能的时候。

 

另外,在《重构》一书中,老马,还给了我们一些建议。

 

在开始重构之前,确保有良好的测试。

重构时,要采取简短而慎重的步骤进行。

 

记住,伟大的开发者都会编写可维护的代码,重构也是他们的必要技能之一

 

还有一点,需要提醒大家。

 

重构,和大多数事情一样,在问题很小的时候做起来更容易,要把它当作编码日常活动。

 

不过,虽然刚刚说了,重构是一件编码日常活动,但历史经验也告诉我们,对于那些历史遗留代码,我们不应该把它们当做定时炸弹,看见就要拆除它,这是不可取的,如果历史代码正常运行,一直也没有需求触及它,就不要动它。

 

而应该把遗留代码看成是地雷,等你趟到它的时候,再动它。也就是有需求过来了,在老代码上添加功能很费劲,维护起来成本也大,这时就要着手重构。

 

只重构需要修改的代码。

 

说完了遗留代码和重构,让我们开始今天的第二个话题,代码的变动率。

 

如何降低代码的变动率

 

什么是变动率?我先不告诉你,请先接着往下看。

 

我们写程序,或者叫做软件,从来不能,也不敢,说我写过的软件一定不会出问题,这个真是做不到。

 

软件区别于硬件的最大不同就是,软件是为变化而生的,硬件是为永恒而造的,因为软件的需求在变,而硬件的需求一旦成型之后,基本是不会变的。你想想一座桥梁、一幢房屋、一个线路板等等,它们在什么机会下会变化呢。

 

但是软件是做不到的,今天按照订单ID查询,明天按照用户ID聚合,都是时常发生的,而处在软件开发工作中的你,肯定深有体会。

 

这实际上是好事!不需要变更的软件通常是没有人使用的软件。

 

 

软件产品总是构建在不断发展和变化的设计基础之上,并由数量不断增长、能够正确运行的功能特性组成。

 

 

既然,软件的需求在变化,我们的软件代码也肯定会随之变化、修改。我们不能保证程序不出问题,但是我们应该要有充足的理由和自信,喊出来,我们可以降低软件出问题的几率。

 

不能保证代码不出问题,但可以做到降低出错的几率。

 

如何才能让一段代码降低出错的几率呢。

 

那要看看发生错误的可能性有有哪些,如果一个程序模块承担了很多种职责,当其中一种职责被要求修改的时候,其它职责对应的代码块,很可能就会在不经意间遭到破坏。而且,如果这次修改这段代码的程序员还不是当初写这个模块的人,那么,这个”不经意“发生的可能性就会更大。

 

对,这其实就是我们说的单一职责。修改的理由越多,修改的机会就越多,修改的出错率就越大。

 

有多个修改理由的代码文件,一般也会承载了多项职责,代码量也较大。在这样的代码环境里面,经过修改之后,代码量会进一步增加。久而久之,这类代码就会像建在沙地上的楼阁一般,摇摇欲坠。

 

程序是由数据和逻辑组成的,因此呢,修改数据的理由和修改逻辑的理由都不应该是多个。

 

如果一个代码模块中的数据,只有当执行某一个方法的时候才会用到,这时你应该怎么办,你肯定要把这些数据移到这个方法里面,使其变成一个本地局部变量,另外,如果数据已经是局部变量了,再观察下围绕这些数据的相关逻辑是否要单独再抽离到一个新的方法中,如果你已经这么做了,那就说明你已经在践行单一职责原则了。

 

看完了数据,让我们再看逻辑。比如订单和库存,当订单发生变化的时候,库存肯定要一起变化,只不过是通知库存变化的通讯方式不同而已,可以同步,也可以异步,但是触发这个修改库存的动作肯定是要跟订单一起。但是订单和积分却不是一定要同时变化的,积分完全可以使用另外的一条“通道”来处理,如果你把订单和用户积分的逻辑放在了一起,那么肯定是要将其分开的。

 

我们的程序如果按照上面那种方式做了,也就是同时修改的代码和数据都放在了同一个地方,那么实际上,我们也可以说,它们的变动率是相同的。变动率相同意味着代码会在同一时间点被修改。

 

到这里,就可以回答这篇文章开头的问题了。

 

变动率:代码被修改的频率。

 

上面从程序的数据和逻辑两个角度去阐述了代码变动的方向或者具体位置,让我们再举一个实际的代码例子吧,谈一谈如何降低代码的变动率。

 

由于我在很多场合下,比如公司内部、公司外部分享的时候,但凡涉及到代码质量的时候我都会向大家提及SOLID原则,所以微信上也会经常有人问我,现实中这样的代码长啥样,其实,哪有那么神秘,对么,符合一定设计模式的代码,都有SOLID的气质。

 

因为我不能拿公司的业务代码举例,为了在我们这篇文章中,讲到这个例子,索性我帮大家到网上搜索一番,下面的代码的原文地址,我附在了文末的位置。

 

说,有一个需求要求实现:根据原材料加工生产出产品 A 和产品 B。

 

 

现在的这个工厂类,就可以满足了,如果你写成这样,然后上线,最终用户看到实现的功能,这并没有任何问题,也就是你从满足需求的角度来讲,你已经完成了任务了。

 

而且,你写上面这段代码的速度应该是很快的,上线速度也很快,业务方也很满意。

 

嗯,第一次:快

 

遗憾的是,你没有考虑到扩展性,没有考虑到以后需求的变化,没有考虑到再来类似需求,还要继续往这段代码上面追加代码。

 

有一天,工厂决定将 A 产品材料加工规则改变,B 产品不变。如果按照上面代码的逻辑,来满足这次需求,B 产品也会使用新的材料加工规则,但是,B本来是不需要变的,为了保证A规则改变,B规则不变,那你估计就要加判断,加隔离逻辑,你编写代码的“负担”就增加了吧,工作量也大了。

 

嗯,第二次:慢

 

再有一天,工厂决定对两个规则都变化,而且还增加了很多细则,如果你不做任何额外的隔离性工作,只好硬着头皮在这个文件里面继续“深耕”,由于这次,规则变化幅度大,在上一次变化的基础上又增加了规则细节,你这次的编码量也会更大。

 

嗯,第三次:更慢

 

于是,你回顾了这几次需求变化过程,得到的开发速度的结论是:快、慢、更慢

 

好了,上面那是一个没有追求的程序员干的事情。

 

那么一个有追求的程序员,是按照下面这样做得,朝着单一职责的方向前进,于是有了下面的代码设计。

 

 

我们将原先的 ProductFactory 类设计成了接口,并通过实现该接口生成 了AProductFactory类和 BProductFactory 类,它们分别负责生产 产品A 和 产品B。

 

另外,通过以上代码,我们可以发现无论任何一个产品改动的时候,也不会影响到其他产品的生产,即便后续新增产品,也可以横向添加 XProductFactory 来生产产品。

 

呀,不对啊,上文不是说,要举例说明代码变动率的吗,对,你没有发现么,如果按照这样的方式来书写代码,在产品A的规则变化的时候,负责产品B的代码都不会改变吗

 

到了这里,现在已经比我们早先看到的代码好多了。但,还是有一点小遗憾,ProductFactory 依旧不是职责单一的。

 

有追求的程序员,肯定还会继续前进。

 

为了让职责,单一的更彻底,我们将两个职责设计成两个接口。

 

 

 

通过实现 IProductFactory,IMaterialProcessing 两个接口来实现材料加工类和产品工厂类,通过对应的产品工厂类生成产品。这样,我们[认为]便没有了因需求不同而导致职责混淆的烦恼了。

 

已经很完美了,是么。确实,这样的代码环境,足以秒杀早先的那种代码设计。

 

不过,有的同学可能会说,一个类即可实现的功能,变成了多个类去实现,这样岂不是更复杂了吗。的确,我们的文件数量是变多了。但,像上面那个例子中的情景,你真的想在一个文件里面生产所有的产品吗

 

我们有SOLID五个原则,而单一职责又是这些原则中最基础的一个,没有单一职责,其它原则实践起来,就无从谈起,关于他们之间的关系,大家可以翻看一周思考系列中的其它文章来阅读。

 

在这里,也提醒大家注意,下面这段描述。

 

切记!切记!我们在利用单一职责原则的时候,要防止掉落到一个模块应该只做一件事的陷阱中。一个模块只做一件事也确实是一个原则,但这仅仅是一个面向底层实现细节的设计原则,并不是单一职责的全部。

 

于是,Bob大叔在《架构整洁之道》中对单一职责的定义做了最终的更新和描述上的完善:

 

任何一个软件模块都应该只对某一类行为者负责。

 

所以,你认为上述的代码,还有哪些修改的空间吗,这个问题留给你阅后思考。

 

 

 

 恭喜你,又完成一次思考。

 

 

参考资料:

《软技能2:软件开发者职业生涯指南》、《软件开发本质论》、《修改软件的艺术》

文中代码来自https://cloud.tencent.com/developer/article/1452054

文中彩色插图来自《软件开发本质论》

忆往期:

架构六大思维养成记

你好,我是前台,再给你引荐下XY台

工作十几年,开了上千个会,该说说了

一个年老代程序员午后谈谈架构和架构师

如何让软件姓“软”

一文一点 | 给你一份实现业务复用的指南

这个假期我通过【得到】得到了什么

一文一点 | 什么才是复用的最高等级

一文一点 | 系统从高可用到高不可用都经历了什么

4000字8分钟带你理解Serverless架构

考虑系统扩展性时仅仅理解AKF立方体是不够的

全面详解互联网企业开放API的 “守护神”

从HTTP/1.1到HTTP/2,让WEB性能更上一层楼

我的第6个京东618

上班十年后我发现可以这样边工作边学习

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值