识别代码的坏味道(四)

491 阅读7分钟

要想重构就需要先识别代码中存在的问题,然而问题有多种也分轻重缓急,所以Code Smell(代码坏味道)可以看作是应该首先动手解决的问题,因此重构过程可以抽象成如下简单的过程。

在之前的文章中介绍了常见的 22 种代码坏味道:

《识别代码中的坏味道(一)》

《识别代码中的坏味道(二)》

《识别代码中的坏味道(三)》

本本将介绍其他 3 个代码坏味道:

  1. 反复使用单个临时变量
  2. 无业务意义的临时变量
  3. 方法结果返回 null
  4. 多层条件嵌套
  5. 有副作用的查询方法
  6. 假设条件符合

01 反复使用单个临时变量

使用单个变量反复进行赋值操作。例如

int temp = level * score;

...

temp = temp * eight;
...

为什么反复使用单个临时变量是一种代码坏味道?

为单个变量在或许中多次在代码中经常见到的一种代码。这种代码的问题就在于变量所要表达的意思并不清楚,除非读完所有涉及到改变量赋值的地方,并把它们串联起来才能了解它到底是在处理什么逻辑。

这种风格的代码如果经常出现在代码中,无意在添加、修改、修复 bug 的时候都会降低代码的阅读速度,

如何解决反复使用单个临时变量这种代码坏味道?

通过非常简单的方法就可以解决上面的提到的语意化问题:为每次赋值创建一个独立的临时变量。重构后代码如下:

final int levelScore = level * score;

...
  
final int weightedScore = levelScore * weight;

...

将每个变量使用 final 关键字避免后续对其进行值的修改,同时为每个结算结果使用语意明确的变量名命名。

02 无业务意义的临时变量

变量作为某个业务逻辑的一部分,同时很难为变量找到合适的命名,且后续只是被使用了一次。

int tempScore = level * score + commentCount;
int score = tempScore * weight;
int finalScore = score + livenessScore;
...

为什么无业务意义的临时变量是一种代码坏味道?

  1. 临时变量只在函数中起作用,这非常容易导致长函数的出现。
  2. 过多的临时变量会导致不容易聚焦;
  3. 临时变量使用不当还造成函数难拆分。

如何解决无业务意义的临时变量这种代码坏味道?

如果业务中并没有相关的概念,那么就可以选择 Inline Temp(内联变量),用完整的表达时表示某个业务意义,例如

int weightedScore = (level * score + commentCount) * weight;
int score = weightScore + livenessScore;

如果Inline Temp(内联变量)有可能让表达式变得复杂,不容易快速揭示意图,因此可以使用 Extract Method (提炼函数)的方法,提炼一个单独的函数,并使用有意义的方法名表示意图。例如:

int weightedScore = getWeightedScore();
int score = weightedScore + livenessScore;
...

private int getWeightedScore() {
		return (level * score + commentCount) * weight;
}

提炼函数之后可以继续对 weightedScore 进行 Inline Temp 操作,减少局部变量,

int score = getWeightedScore() + livenessScore;

...

private int getWeightedScore() {
		return (level * score + commentCount) * weight;		
}

最后当读现在的代码的时候,很清楚知道 score 是由 weightedScore 和 livenessScore 组成。如果需要关注 weightedScore 的实现细节,可以查看 getWeightedScore() 方法的具体实现。

如果重构过程中发现某些提炼不符合预想的,可以通过 Inline Method(内联函数)还原提炼的函数,更换思路进行重构。

03 方法结果返回 null

在方法的返回值中返回 null 值。例如

public Order findOneBy(OrderState orderState) {
		if(Objects.nonNull(orderState)) {
				return orderRepository.findOneByOrderState(orderState);
		}
		
		return null;
}

为什么方法结果返回 null 是一种代码坏味道?

方法返回值返回 null,意味着方法者需要做出判断才能避免 NullPointerException,而重复的判断语句也会增加代码的复杂度,降低阅读代码的速度。

如果调用者没有注意到返回结果有可能为 null,很有可能导致 NullPointerException,增加 Bug。

如何解决方法返回 null 这种代码坏味道?

  1. 可以选择返回结果为 Optional,显式的告诉调用者应该对 null 值进行处理,从而避免 NullPointerException,Optional 提供的语法也能让代码更偏向于英语的自然语言来描述代码的后续调用。

    public Optional<Order> findOneBy(OrderState orderState) {
    		if (Objects.nonNull(orderState)) {
    				return orderRepository.findOnByOrderState(orderState);
    		}
    		return Optional.empty();
    }
    
  2. 使用 控对象模式,不过不推荐这种实现方式,因为这种方式虽然只能可以避免 NullPointerException 但是并没用方法的调用者变得简单,也不会可读性带来改善。

04 多层条件嵌套

条件表达式中不断潜逃新的条件表达式。例如

if (order.hadConfirmed()) {
		if (product.hadInventory()) {
				notifyTrasport(order);
		} else {
				notifyConsumer(order);
		}
} else {
		timer.wait(20, Timer.MINUTES);
}

为什么多层嵌套是一种代码坏味道?

上面的代码虽然可以正常运行,但是该段代码的后续维护成本却会逐渐增加,当条件越多、条件判断越复杂、嵌套越深读取代码代码的成本就越高。当添加和修改代码的时候,找到应该变动代码的位置也是不容易的。

如何解决多层嵌套这种代码坏味道?

可以使用卫语句来代替多层嵌套的表达时,卫语句指的是单独的检测条件。

例如上面的代码可以重构为如下

if (order.hadConfirmed() && product.hadInventory()) {
		notifyTransprot(order);
		return;
}

if (order.hadConfirmed() && product.hadInventory()) {
    notifyConsumer(order);
    return;
}

timer.wait(20, Timer.MINUTES);

如果判断消减较为复杂,则可以使用 Extract Method 来让判断条件更加具有语意。

05 有副作用的查询方法

一个方法方法名表明其为查询操作,但是方法体中除了查询操作还有其他操作(比如赋值操作)。

public List<Product> findAllProducts(Order order) {
		...
		List<Product> products = productRepository.findAll(order.getProductIds());
		order.setProducts(products);
		return products;
}

为什么有副作用的查询方法是一种坏味道?

  1. 方法名和实际的职责不完全相符,很有可能给方法调用者造成麻烦。
  2. 从方法调用者的代码时,并不会注意到 order 的属性被赋值过,因此会让阅读代码异常的困难。

如何解决有副作用的查询方法这种坏味道?

消除方法的副作用。

  1. 将副作用的代码剥离出来,被剥离出来的语句可以归置调用者中,

    public List<Product> findAllProducts(Order order) {
      	...
        return productRepository.findAll(order.getProductIds());
    }
    
    ...
    order.setProducts(productService.findAllProducts(order));
    ...
    
  2. 如果被剥离的语句更符合存在于服务提供者中,那么将有副作用的方法拆分成两个职责单一的方法。

    public List<T> findXXXX() {
    		...
    }
    
    public void setXXXXX(List<T> list) {
      	...
    }
    

06 假设条件符合

在代码中总是假设方法接收到的对象是不为null,或者假设其属性总是符合预期期望的。

public void verify(Order order) {
	
  	if (order.getProducts().isEmpty()) {
				...
		}
		...
}

为什么假设条件符合是一种代码坏味道?

原因很简单。假设条件符合意味着条件缺失时就会造成程序运行异常。虽然编写代码时书写很快,但是这种”快”可以贴切的理解成:从第一天就开始写Bug。

当遇到问题时会反复的读取代码来定位问题。

如何解决条件符合这种功能坏味道?

为假设符合的条件添加检测。JDK 或者一些工具类库都提供了简化检测的语法,使用这些语法能够让语意更加简单、清晰明了。

public void verify(Order order) {
		if (Objects.nonNull(oreder) && 
				Objects.nonNull(order.getProducts()) && 
				order.getProducts().isEmpty()) {
				...
		}
}

如果对象上某个方法返回的是一个自定义类型,为了后续让调用者能够轻松使用,并意识到存在 null 的情况,可以选择返回 Optional 结果。

总结

上面总结了 6 条日常代码中常见并容易被忽略的代码坏味道,消除这些坏味道能够有效的改善代码的可读性,同时能够避免问题的发生。