访问量
访客数

如何重构一个程序

2023.04.20 阅读量

什么是重构

摘自《重构:改善既有代码的设计》

  • 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
  • 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。

重构的目的是使软件更容易被理解和修改。可以在软件内部做很多修改,但必须对软件可观察的外部行为只造成很小的变化,甚至不造成变化。与之形成对比的是性能优化,和重构一样,性能优化通常不会改变组件的行为,只会改变其内部结构。但是两者的出发点不同:性能优化往往使代码较难理解,但为了得到所需的性能你不得不这么做。

重构不会改变软件可观察的行为,重构之后的软件功能一往如此。

重构代码更像是整理代码,重构可以重构但是不要改变代码原本的功能,需要修改其内部结构,在不改变软件可观测行为的前提下,调整代码结构,当然这种修改肯定是有利的一面。提高软件的可理解性,降低变更成本。

为什么重构

首先需要明确的一点是,重构是一种经济行为,而不是道德上的要求,重构只有一个目的,就是让我们更快更好的开发代码。 为什么需要重构,是因为现有的代码不能够提高开发效率,有时不但不能提高,还会降低很多很多。

其次重构是保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。项目在演进,代码不停地在堆砌,如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。 当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。

当我们遇到这段代码实在是太恶心了,花了很长时间才看懂,并且代码非常僵硬,而正好这个需求需要改动到这里,代码真的就像一坨屎。而最后是怎么处理的,我们通常是又给它加了一坨。 我们为什么会这么去做?因为重构会减慢当前任务速度,所以我们总保持最快的写代码的速度。

那为什么这么做能干得又多又快?因为这样做将成本放到了未来。软件工程最大的成本在于维护,我们每一次代码的改动,都应该是对历史代码的一次整理,而不是单一的功能堆积。 这样虽然能赢得现在,但终将失去未来,而这个失败的未来或许需要全团队与它一起买单。这或许就是我们重构的最根本原因。

优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。所以当我们做一个需求一定要回过头整理代码,保证代码的质量。 傻子都能写出计算机可以执行的代码,只有能写出人类容易理解的代码的,才是优秀的程序员。

除此之外,重构对程序员本身还有其他意义:

  1. 更快速的定位问题,节省调试时间;
  2. 最小化变更风险,提高代码质量,减少修复事故的时间,提高ROI;
  3. 得到程序员同行的认可,更好的发展机会;

重构场景

因为重构是一种经济行为,所以重构的时候需要考虑成本,应该仅对必要的代码进行重构。 如果某个工作行为重复三次就可以认为未来也会存在重复,因此通过重构,将其封装为一个方法,使得下次工作更加高效,这其实是一种务实的做法。

举例,在添加一个新功能时,看之前的旧代码,如果有一些公共的功能可以提取出来,而这个功能,你新开发的功能又恰好用的上。 那么重构这个功能可以帮助你梳理、理解代码,程序如果如此迭代下去,长久下来会提高团队的开发效率。

在《重构:改善既有代码的设计》一书中提到持续重构的理念。重构本来就不是一件应该特别拨出时间来做的事,重构应该随时随地的进行。 重构不一定是需要大规模的展开的任务,重构应该是不断持续进行的,将任务拆解为多个具有完备性的任务,每周完成一个,每个任务的上线都不会引起问题,并使项目变得更好,这是一种持续重构的精神态度,是程序员最应该具有的工作习惯。

千万不要代码烂到一定程度之后才去重构。当代码真的烂到出现开发效率低、招了很多人天天加班、出活却不多、线上bug频发、领导发飙、中层束手无策、工程师抱怨不断、找bug难的时候,基本上重构也无法解决问题了。 我们要正确地看待代码质量和重构这件事情。技术在更新、需求在变化、人员在流动,代码质量总会在下降,代码总会存在不完美,重构就要持续的在进行。 我们应当时刻具有持续重构意识,才能避免开发初期就过度设计,避免代码维护的过程中质量的下降。

怎么重构

通读代码,分析现状,找到可重构代码。列出重构计划,确定重构目标,不要盲目的重构,明确的描述出重构后能达到的预期是什么。 重构计划中必须给出测试验证方案,保证代码的可测试性,保证重构前与重构后软件的行为一致。 将重构任务当作项目来管理,对指定任务的人明确排期和进度同步。这是重构大体上基本的步骤。

在重构项目时,我们可以借鉴分治思想。将重构任务拆分成每周都能见到效果的小任务,形成正反馈,定期开会同步进度,不断加强团队的重构意识。

下面是重构代码的一些常见问题和重构的一些建议。

规范代码命名

代码命名要见名知意,避免增加认知负荷,还要考虑命名对整体架构的影响,要与整体的风格保持一致。 以下是一些命名建议:

  • 命名尽量不要使用缩写,因为意义不明确,除非缩写是外界公认的,比如:ENCH
  • 在Java中较为适用的是驼峰命名法,如: helloWordHelloWord,但是这种方法如果命名过长也不能很直观的展示信息,所以尽量不要起太长的名字,否则什么方法都不管用。 如果一个方法的名字过长,那么很可能这个方法不止做了一件事,这时候我们需要将它拆分;
  • 接口和类的名称首字母应该大写,如MyClass/MyInterface;方法的名称首字母小写,如myMethod,常量的命名全部大写,同时用蛇形命名法,单词的分割用下划线隔开,如MY _CONSTANT
  • 方法的命名用动词,类的命名用名词,属性的命名用名词,接口的命名用形容词或动词,抽象类的命名应以Abstract/Base为前缀,实现类命名要以impl结尾,异常类以Exception结尾;

降低复杂度与依赖传递

所谓的复杂性,就是任何使得软件难于理解和修改的因素。 模糊性与依赖性是引起复杂性的两个主要因素,模糊性产生了最直接的复杂度,让我们很难读懂代码真正想表达的含义,无法读懂这些代码,也就意味着我们更难去改变它。 而依赖性又导致了复杂性不断传递,不断外溢的复杂性最终导致系统的无限腐化,一旦代码变成意大利面条,几乎不可能修复,成本将成指数倍增长。

我们可以找到很多导致系统复杂度高的原因:

  • 想简单图省事,没有及时治理不合理的内容。
  • 缺少匠心追求,对肮脏代码视而不见。
  • 技术能力不够,无法应对复杂系统。

除了上述内容外,还可以想到很多理由,但我们发现它们好像有一个共同的指向点就是软件工程师,似乎所有复杂的源头就是软件工程师的不合格导致,所以其实一些罪恶的根因是我们自己?

软件复杂才是常态,不复杂才不正常。软件的复杂性是固有的,包括问题域的复杂性、管理开发过程的困难性、通过软件可能实现的灵活性与刻画离散系统行为的问题,这四个方面来分析了软件的发展一定伴随着复杂,这是软件工程这门学科必然伴随的一个特性。 所以所有的软件架构万变不离其宗,都在解决软件的复杂性。

世间万物都需要额外的能量和秩序来维持自身,无一例外。没有外部力量注入事物就会逐渐崩溃,这是世间万物的规律,而不是我们哪里做得不对。 所以我们才需要对系统进行维护甚至重构,来降低系统的复杂度,保障系统的正常运行及降低维护的成本。

避免过度设计

过度设计就是增加复杂度的一种。

在初学编程的时候,只管埋头写程序,浑浑噩噩的进行开发,但是很快便发现事先做好设计可以节省返工的成本。 许多人把设计看作软件开发的关键环节,而把编程看成是机械式的劳动,他们认为设计就像画工程图纸而编码就像施工。因此需要把更更多得精力放在预先设计上,以免日后修改。

但是当过分的考虑程序未来所要面对的需求时,将陷入过度设计的陷阱,为了当下用不上的能力,而使程序变得复杂,这些设计很可能是现在未来都不需要的。

过度设计是一种负面的设计,所以我们要合理的进行程序设计。要避免过度设计,把握好设计的尺度,适度设计。下面是避免过度设计的几点建议:

  • 将注意力集中在应用程序的核心功能上,满足用户的基本需求。避免在边缘功能上过度设计和开发,这些功能可能会增加程序的复杂性和难度,尽量采用简单的解决方案,而不是过度复杂的设计。
  • 设计的目的之一是为产品和用户带来更多的价值和服务,因此必须保证所有的设计都有其存在的“价值”,这个“价值”可以是为用户带来更好的体验,也可以是是为产品带来更多的收益。任何没有“价值”的设计就是“过度设计”,那样可以直接删除。

总之要明确,设计是产品实现用户需求的一种手段,不是最终的目的。不能为了设计而设计,添加一些乱七八糟的功能,那样产品的用户体验也是很差的。在设计中应当确保应用程序的核心功能得到优先关注,避免过度边缘化。

没有时间重构

没有时间重构,这是重构所面临最多的借口,是自己也是团队的借口。

为此,必须要明确重构是经济行为而不是一种道德行为,重构使得开发效率变得更高,因此仅对必要的代码进行重构,某个工作行为如果重复三次就可以认为未来也会存在重复,因此通过重构使得下次工作更加高效,这是一种务实的作法。 重构不一定是需要大规模的展开的任务,重构应该是不断持续进行的,将任务拆解为多个具有完备性的任务,每周完成一个,每个任务的上线都不会引起问题,并使项目变得更好,这是一种持续重构的精神态度,是程序员最应该具有的工作习惯。

如果你在给项目添加新的特性,发现当前的代码不能高效的完成这个任务,并且同样的任务出现三次以上,那么这时你应该先重构,再开发新特性。

发表评论