原文出自:https://juejin.cn/post/6903054491273625614
什么是重构
所谓重构是这样一个过程:在不改变代码外在行为的前提下,对源代码做出修改,以改进程序的内部结构,从而使代码变得易于理解,可维护和可扩展。本质上来说重构就是在代码写好之后改进它的设计。
重构的目的是什么
首先,重构是时刻保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。项目在演进,代码不停地在堆砌。如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。
❝ 劣质代码可能会影响后续优化的效率,从而进一步造成代码劣化;随着时间的推移,这种效应将会导致代码质量大幅下降。破窗效应 (The Broken Windows Theory) ❞
其次,优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。我们无法100%遇见未来的需求,也没有足够的精力、时间、资源为遥远的未来买单,所以,随着系统的演进,重构代码也是不可避免的。
何时需要重构
「第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构.
」
添加新功能时重构
「种一棵树最好的时间是十年前,其次是现在。」 重构的最佳时机就是在添加新的功能之前。再动手添加新功能之前,我们不妨先考虑一下,如果对现有的代码结构做些微调,是否会使加入新的功能变的容易的多。
❝ 如果你要给程序添加一个特性,但发现代码因缺乏良好的结构而不易于进行更改,那就先重构那个程序,使其比较容易添加该特性,然后再添加该特性 ❞
修改问题时重构
「扫去窗上的尘埃,才可以看到窗外的美景。」 修改一个问题时,我们需要先理解代码在做什么,然后才可以着手去修改。这段代码可能是别人写的,也可能时自己写的,但无论如何,当你觉得这段代码逻辑糟糕,需要花费几分钟才能明白其中的含义时,你就要想着如何去重构才可以使代码变的更加简洁直观
有计划的对代码重构
「找寻重构和开发进度中适合自己的平衡点」 但有些时候我们在准备重构的时会发现,之前的代码结构和依赖关系错综复杂,这时我们就要考虑当前是否有足够时间去很好的处理这些混乱的代码。尽管重构的目的是加快开发速度,但同时重构也会拖慢软件的开发进度。如果时间充足,那么当下就是进行重构最好的时机。当鱼和熊掌不可兼得的时候,应当保证软件的开发进度不受影响,其次才是进行重构。可以先把需要重构地方记录下来,整理出一个计划,在未来的一段时间内解决掉。
Code Review时重构
「处明者不见暗中一物,处暗者能见明中区事。」 Code Review有助于在开发团队中传播知识,也有助于让较有经验的开发者把知识传递给比较欠缺经验的人。Code Review对于编写清晰的代码也很重要,我写的代码也许对于我自己来说很清晰,但对于别人来说则不然。Code Review让更多人有机会提出有用的建议来对代码进行调整。三人行,则必有我师。
何时不应该重构
「有所为,有所不为。」 并非所有的糟糕代码都需要重构,如果你不需要使用到这段代码,那么就不必花心思去重构它。只有你需要理解其中的工作原理时,对其重构才有价值。当然如果重写比重构更容易,那么就不需要重构了。
如何保证重构后程序的正确性
保证代码正确性最好的方法就是进行「单元测试(Unit Testing)」 。当重构完成之后,如果新的代码仍然能通过单元测试,那就说明代码原有逻辑的正确性未被破坏,原有的外部可见行为未变。
测试驱动开发是非常完美的方案。但实际上大部分IT公司的程序由于种种原因并没有单元测试。这时需要一些工具用来帮助我们快速扫描代码中的问题。比如可以给代码增加Lint
语法检查,使用SonarQube
对代码进行质量和漏洞扫描,前端同学还可以使用TypeScript
等等。把这些代码自动扫描工具集成到CI里面,可以大幅度 减少出现bug的情况。目前我所在部门前端组的一系列产品包括项目,已经把这些功能集成在CI里面的,每次的代码更新,都会触发扫描代码的流程,CI失败就无法将代码合并到开发分支上面。
有了上述这些还不够,在重构完成之后,还要把改动部分的功能完整的自测一遍,以保证程序无误。当自测通过之后,就可以请测试同学来帮忙进行更加完整的测试流程。
❝ 为什么要进行这么严格的测试流程,因为要保证程序可靠性。如果一件事有可能出错,那么它一定会出错。❞
需要重构的Bad Code
糟糕的命名
整洁代码最重要的一环 就是好的名字,所以我们要深思熟虑如何给函数、模块、变量和类命名,使它们 能清晰地表明自己的功能和用法。
无意义的注释
学会只编写够用的注释,过犹不及,应当重视质量而不是数量
多层的if语句嵌套
if-else在程序设计中是不可避免的,作为程序员能做的就是减少嵌套,提升代码的可阅读性和质量
很酷却不宜理解代码
上面这种写法看起来是不是很酷,但是过一段时间再来看,你还能一眼看出这部分功能是做什么的吗?在我刚接触后端,使用python的时候写过这样的代码,结果就是在排查问题的时候相当头疼。代码写的别人看不懂并不厉害,而是写的谁都看的懂才是厉害。
❝ 调试在一开始就比编写程序困难一倍。因此,按照定义,如果你的代码写得非常巧妙,那么你就没有足够的能力来调试它。柯林汉定律 (Kernighan's Law) ❞
不必要的继承写法
继承虽然是面向对象的四大特性之一,使用继承可以解决代码复用的问题,但也有其缺点: 继承层次过深、过于复杂会影响到代码的可维护性。如果子类中有方法依赖于父类中的 方法或属性,那么当父类发生改变时,子类很可能会发生无法预知的错误。
而组合的方式是把类中所有的接口功能单独实现,然后使用这些单独的对象组合在一起,完成和目标类一致的功能。继承是用来表示类之间的 is-a
关系,而组合是一种 has-a
的关系。使用组合
接口
委托
的方式可以代替大多数的继承场景。
重构代码的设计原则
开闭原则 (The Open/Closed Principle)
❝ 实体应开放扩展并关闭修改。❞
实体(可以是类、模块、函数等)应该能够使它们的行为易于扩展,但是它们的扩展行为不应该被修改。
里氏替换原则 (The Liskov Substitution Principle)
❝ 可以在不破坏系统的情况下,用子类型替换类型。❞
如果组件依赖于类型,那么它应该能够使用该类型的子类型,而不会导致系统失败或者必须知道该子类型的详细信息。
依赖反转原则 (The Dependency Inversion Principle)
❝ 高级模块不应该依赖于低级实现。❞
更高级别的协调组件不应该知道其依赖项的详细信息。
接口隔离原则 (The Interface Segregation Principle)
❝ 不应强制任何客户端依赖于它不使用的方法。❞
组件的消费者不应该依赖于它实际上不使用的组件函数。
单一功能原则 (The Single Responsibility Principle)
❝ 每个模块或者类只应该有一项功能。❞
模块或者类只应该做一件事。实际上,这意味着对程序功能的单个小更改,应该只需要更改一个组件。例如,更改密码验证复杂性的方式应该只需要更改程序的一部分。
合成/聚合复用原则(Composite/Aggregate Reuse Principle)
❝ 量的使用合成和聚合,而不是继承关系达到复用的目的。❞
合成/聚合复用原则就是指在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用已有功能的目的。简而言之:要尽量使用组合/聚合关系,少用继承。
总结
如果文中有错误的地方,还望不吝赐教。写了这么多,其实是想表达一个观点:代码是写给人看的,所以要做到良好的编程风格,方便其他人阅读,维护。
如果你所在的团队代码感觉并不十分优雅的话,那就应当重构了。引用前面说到的一句话 「种一棵树最好的时间是十年前,其次是现在。
」