代码重构

2022-07-03  本文已影响0人  cc_daily

为什么要重构代码

项目在不断演进过程中,代码不停地在堆砌。如果没有人为所有的代码的质量负责,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。

造成的原因

解决思路
通过持续不断的重构将代码中的问题代码清除掉

什么是重构

百度:重构(Refactoring)就是通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。
维基百科:代码重构(英语:code refactoring)指对软件代码做任何更动以增加可读性或者简化结构而不影响输出结果。

软件重构需要借助工具完成,重构工具能够修改代码同时修改所有引用该代码的地方。在极限编程的方法学中,重构需要单元测试来支持。
软件工程学里,重构代码一词通常是指在不改变代码的外部行为情况下而修改源代码,有时非正式地称为“清理干净”。在极限编程或其他敏捷方法学中,重构常常是软件开发循环的一部分:开发者轮流增加新的测试和功能,并重构代码来增进内部的清晰性和一致性。自动化的单元测试保证了重构不至于让代码停止工作。

重构既不修正错误,又不增加新的功能性。反而它是用于提高代码的可读性或者改变代码内部结构设计,并且删除死码,使其在将来更容易被维护。重构代码可以是结构层面抑或是语意层面,不同的重构手段施行时,可能是结构的调整或是语意的转换,但前提是不影响代码在转换前后的行为。特别是,在现有的程序的结构下,给一个程序增加一个新的行为可能会非常困难,因此开发人员可能先重构这部分代码,使加入新的行为变得容易。

根据重构的规模可以大致分为大型重构和小型重构。

大型重构

对顶层代码设计的重构,包括:系统、模块、代码结构、类与类之间的关系等的重构,重构的手段有:分层、模块化、解耦、抽象可复用组件等等。这类重构的工具就是我们学习过的那些设计思想、原则和模式。这类重构涉及的代码改动会比较多,影响面会比较大,所以难度也较大,耗时会比较长,引入bug的风险也会相对比较大。

小型重构

对代码细节的重构,主要是针对类、函数、变量等代码级别的重构,比如规范命名和注释、消除超大类或函数、提取重复代码等等。小型重构更多的是使用统一的编码规范。这类重构要修改的地方比较集中,比较简单,可操作性较强,耗时会比较短,引入bug的风险相对来说也会比较小。什么时候重构 新功能开发、修bug或者代码review中出现“不合理的代码”,我们就应该及时进行重构。持续在日常开发中进行小重构,能够降低重构和测试的成本。

不合理的代码

代码重复

方法过长

过大的类

逻辑分散

严重的情结依恋

数据混合/错误的使用基本类型

不合理的继承体系

过多的条件判断

临时变量过多

令人迷惑的暂时字段

纯数据类

不恰当的命名

过多/过少的注释

难以复用

难于变化

难于理解

难以测试

-分支、依赖较多,难以覆盖全面

好的代码

代码质量的评价有很强的主观性,描述代码质量的词汇也有很多,比如可读性、可维护性、灵活、优雅、简洁。这些词汇是从不同的维度去评价代码质量的。其中,可维护性、可读性、可扩展性又是提到最多的、最重要的三个评价标准。

要写出高质量代码,我们就需要掌握一些更加细化、更加能落地的编程方法论,这就包含面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。

如何重构

SOLID原则


solid.png

单一职责原则

一个类只负责完成一个职责或者功能,不要存在多于一种导致类变更的原因。

单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、松耦合。
但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

开放-关闭原则

添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。

开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。

很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的。特别是 23 种经典设计模式,大部分都是为了解决代码的扩展性问题而总结出来的,都是以开闭原则为指导原则的。最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态)。

里氏替换原则

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

子类可以扩展父类的功能,但不能改变父类原有的功能

父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。

接口隔离原则

调用方不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

依赖反转原则

指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。(可参考设计模式的适配器模式)

迪米特法则

又叫作最少知识原则(The Least Knowledge Principle),一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

合成复用原则

尽量使用合成/聚合的方式,而不是使用继承。

单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,告诉我们要对扩展开放,对修改关闭。

设计模式

软件开发人员在软件开发过程中面临的一般问题的解决方案。

简单归个类:

代码分层

模块结构说明

5.png

代码开发要遵守各层的规范,并注意层级之间的依赖关系。

命名规范

一个好的命名应该要满足以下两个约束:

如果你觉得一个类或方法难以命名的时候,可能是其承载的功能太多了,需要进一步拆分。

约定俗称的惯例

6.png

重构技巧

  1. 多个方法代码重复、方法中代码过长或者方法中的语句不在一个抽象层级。
  2. 方法是代码复用的最小粒度,方法过长不利于复用,可读性低,提炼方法往往是重构工作的第一步。
  1. 把处理某件事的流程和具体做事的实现方式分开。把一个问题分解为一系列功能性步骤,并假定这些功能步骤已经实现。我们只需把把各个函数组织在一起即可解决这一问题。在组织好整个功能后,我们在分别实现各个方法函数。
public int discount(int inputVal, int quantity, int yearToDate) {
  if (inputVal > 50) inputVal -= 2;
  if (quantity > 100) inputVal -= 1;
  if (yearToDate > 10000) inputVal -= 4;
  return inputVal;
}

public int discount(int inputVal, int quantity, int yearToDate) { 
  int result = inputVal;
  if (inputVal > 50) result -= 2; 
  if (quantity > 100) result -= 1; 
  if (yearToDate > 10000) result -= 4; 
  return result; 
}
if ((platform.toUpperCase().indexOf("MAC") > -1) 
    && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0) {   
  // do something 
} 
  
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1; 
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1; 
final boolean wasResized = resize > 0; 
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {   
  // do something 
}

当存在这样一类条件表达式,它根据对象类型的不同选择不同的行为。可以将这种表达式的每个分支放进一个子类内的复写函数中,然后将原始函数声明为抽象函数。
当出现大量类型检查和判断时,if else(或switch)语句的体积会比较臃肿,这无疑降低了代码的可读性。 另外,if else(或switch)本身就是一个“变化点”,当需要扩展新的类型时,我们不得不追加if else(或switch)语句块,以及相应的逻辑,这无疑降低了程序的可扩展性,也违反了面向对象的开闭原则。

基于这种场景,我们可以考虑使用“多态”来代替冗长的条件判断,将if else(或switch)中的“变化点”封装到子类中。这样,就不需要使用if else(或switch)语句了,取而代之的是子类多态的实例,从而使得提高代码的可读性和可扩展性。很多设计模式使用都是这种套路,比如策略模式、状态模式。

  1. 非正常业务状态的处理,使用抛出异常的方式代替返回错误码
  2. 不要使用异常处理用于正常的业务流程控制
  3. 异常处理的性能成本非常高
  4. 尽量使用标准异常
  5. 避免在finally语句块中抛出异常
  6. 如果同时抛出两个异常,则第一个异常的调用栈会丢失
  7. finally块中应只做关闭资源这类的事情
  1. 某一段代码需要对程序状态做出某种假设,以断言明确表现这种假设。
  2. 不要滥用断言,不要使用它来检查“应该为真”的条件,只使用它来检查“一定必须为真”的条件
  3. 如果断言所指示的约束条件不能满足,代码是否仍能正常运行?如果可以就去掉断言

空引用的问题在Java中无法避免,但可以通过代码编程技巧(引入空对象)来改善这一问题。

根据单一职责原则,一个类应该有明确的责任边界。但在实际工作中,类会不断的扩展。当给某个类添加一项新责任时,你会觉得不值得分离出一个单独的类。于是,随着责任不断增加,这个类包含了大量的数据和函数,逻辑复杂不易理解。

此时你需要考虑将哪些部分分离到一个单独的类中,可以依据高内聚低耦合的原则。如果某些数据和方法总是一起出现,或者某些数据经常同时变化,这就表明它们应该放到一个类中。另一种信号是类的子类化方式:如果你发现子类化只影响类的部分特性,或者类的特性需要以不同方式来子类化,这就意味着你需要分解原来的类。

Java提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。自从Java8为接口增加缺省方法(default method),这两种机制都允许为实例方法提供实现。主要区别在于,为了实现由抽象类定义的类型,类必须称为抽象类的一个子类。因为Java只允许单继承,所以用抽象类作为类型定义受到了限制。

接口相比于抽象类的优势

  1. 现有的类可以很容易被更新,以实现新的接口。
  2. 接口是定义混合类型(比如Comparable)的理想选择。
  3. 接口允许构造非层次结构的类型框架。

接口虽然提供了缺省方法,但接口仍有有以下局限性:

  1. 接口的变量修饰符只能是public static final的
  2. 接口的方法修饰符只能是public的
  3. 接口不存在构造函数,也不存在this

可以给现有接口增加缺省方法,但不能确保这些方法在之前存在的实现中都能良好运行。
因为这些默认方法是被注入到现有实现中的,它们的实现者并不知道,也没有许可

上一篇 下一篇

猜你喜欢

热点阅读