《代码之丑》学习笔记07——滥用控制语句:出现控制结构,多半是错误的提示

07 | 滥用控制语句:出现控制结构,多半是错误的提示

这个坏味道就是滥用控制语句,也就是你熟悉的 if、for 等等,这个坏味道非常典型,但很多人每天都用它们,却对问题毫无感知。今天我们就先从一个你容易接受的坏味道开始,说一说使用控制语句时,问题到底出在哪。

1.嵌套的代码

在这里插入图片描述
考虑到篇幅,我就不用这么震撼的代码做案例了,我们还是从规模小一点的代码开始讨论:

public void distributeEpubs(final long bookId) {
  List<Epub> epubs = this.getEpubsByBookId(bookId);
  for (Epub epub : epubs) {
    if (epub.isValid()) {
      boolean registered = this.registerIsbn(epub);
      if (registered) {
        this.sendEpub(epub);
      }
    }                                            
  }
}

这是一段做 EPUB 分发的代码,EPUB 是一种电子书格式。在这里,我们根据作品 ID 找到要分发的 EPUB,然后检查 EPUB 的有效性。对于有效的 EPUB,我们要为它注册 ISBN 信息,注册成功之后,将这个 EPUB 发送出去。

代码逻辑并不是特别复杂,只不过,在这段代码中,我们看到了多层的缩进,for 循环一层,里面有两个 if ,又多加了两层。即便不是特别复杂的代码,也有这么多的缩进,可想而知,如果逻辑再复杂一点,缩进会成什么样子

这段代码之所以会写成这个样子,其实就是我在讲“长函数”那节课里所说的:“平铺直叙地写代码”。这段代码的作者只是按照需求一步一步地把代码实现出来了。从实现功能的角度来说,这段代码肯定没错,但问题在于,在把功能实现之后,他停了下来,而没有把代码重新整理一下。那我们就来替这段代码作者将它整理成应有的样子。

既然我们不喜欢缩进特别多的代码,那我们就要消除缩进。具体到这段代码,一个着手点是 for 循环,因为通常来说,for 循环处理的是一个集合,而循环里面处理的是这个集合中的一个元素。所以,我们可以把循环中的内容提取成一个函数,让这个函数只处理一个元素,就像下面这样:


public void distributeEpubs(final long bookId) {
  List<Epub> epubs = this.getEpubsByBookId(bookId);
  for (Epub epub : epubs) {
    this.distributeEpub(epub)}
}


private void distributeEpub(final Epub epub) {
  if (epub.isValid()) {
    boolean registered = this.registerIsbn(epub);
    if (registered) {
      this.sendEpub(epub);
    }
  }
}

这里我们已经有了一次拆分,分解出来 distributeEpub 函数每次只处理一个元素。拆分出来的两个函数在缩进的问题上,就改善了一点。第一个函数 distributeEpubs 只有一层缩进,这是一个正常函数应有的样子,不过,第二个函数 distributeEpub 则还有多层缩进,我们可以继续处理一下。

2.if 和 else

在 distributeEpub 里,造成缩进的原因是 if 语句。通常来说,if 语句造成的缩进,很多时候都是在检查某个先决条件,只有条件通过时,才继续执行后续的代码。这样的代码可以使用卫语句(guard clause)来解决,也就是设置单独的检查条件,不满足这个检查条件时,立刻从函数中返回。

这是一种典型的重构手法 :以卫语句取代嵌套的条件表达式(Replace Nested Conditional with Guard Clauses)。

我们来看看改进后的 distributeEpub 函数:

private void distributeEpub(final Epub epub) {
  if (!epub.isValid()) {
    return;
  }
  
  boolean registered = this.registerIsbn(epub);
  if (!registered) {
    return;
  }
  
  this.sendEpub(epub);
}

改造后的 distributeEpub 就没有了嵌套,也就没有那么多层的缩进了。你可能已经发现了,经过我们改造之后,代码里只有一层的缩进。当代码里只有一层缩进时,代码的复杂度就大大降低了,理解成本和出现问题之后定位的成本也随之大幅度降低。

函数至多有一层缩进,这是“对象健身操(《ThoughtWorks 文集》书里的一篇)”里的一个规则。前面讲“大类”的时候,我曾经提到过“对象健身操”这篇文章,其中给出了九条编程规则,下面我们再来讲其中的一条:不要使用 else 关键字

没错,else 也是一种坏味道,这是挑战很多程序员认知的。在大多数人印象中,if 和 else 是亲如一家的整体,它们几乎是比翼齐飞的。那么,else 可以不写吗?可以。我们来看看下面的代码:

public double getEpubPrice(final boolean highQuality, final int chapterSequence) {
  double price = 0;
  if (highQuality && chapterSequence > START_CHARGING_SEQUENCE) {
    price = 4.99;
  } else if (sequenceNumber > START_CHARGING_SEQUENCE
        && sequenceNumber <= FURTHER_CHARGING_SEQUENCE) {
    price = 1.99;
  } else if (sequenceNumber > FURTHER_CHARGING_SEQUENCE) {
    price = 2.99;
  } else {
    price = 0.99;
  }
  
  return price;
}

这是一个根据 EPUB 信息进行定价的函数,它的定价逻辑正如代码中所示。

  • 如果是高品质书,而且要是章节序号超过起始付费章节,就定价 4.99;
  • 对一般的书而言,超过起始付费章节,就定价 1.99;
  • 超过进一步付费章节,就定价 2.99。
  • 缺省情况下,定价 0.99。

就这段代码而言,如果想不使用 else,一个简单的处理手法就是让每个逻辑提前返回,这和我们前面提到的卫语句的解决方案如出一辙:

public double getEpubPrice(final boolean highQuality, final int chapterSequence) {
  if (highQuality && chapterSequence > START_CHARGING_SEQUENCE) {
    return 4.99;
  } 
  
  if (sequenceNumber > START_CHARGING_SEQUENCE
        && sequenceNumber <= FURTHER_CHARGING_SEQUENCE) {
    return 1.99;
  } 

  if (sequenceNumber > FURTHER_CHARGING_SEQUENCE) {
    return 2.99;
  } 
  
  return 0.99;

对于这种逻辑上还比较简单的代码,这么改造还是比较容易的,而对于一些更为复杂的代码,也许就要用到多态来改进代码了。不过在实际项目中,大部分代码逻辑都是逐渐变得复杂的,所以,最好在它还比较简单时,就把坏味道消灭掉。这才是最理想的做法。

3.重复的 Switch

通过前面内容的介绍,你会发现,循环和选择语句这些你最熟悉的东西,其实都是坏味道出现的高风险地带,必须小心翼翼地使用它们。接下来,还有一个你从编程之初就熟悉的东西,也是另一个坏味道的高风险地带。我们来看两段代码:

public double getBookPrice(final User user, final Book book) {
  double price = book.getPrice();
  switch (user.getLevel()) {
    case UserLevel.SILVER:
      return price * 0.9;
    case UserLevel.GOLD: 
      return price * 0.8;
    case UserLevel.PLATINUM:
      return price * 0.75;
    default:
      return price;
  }
}


public double getEpubPrice(final User user, final Epub epub) {
  double price = epub.getPrice();
  switch (user.getLevel()) {
    case UserLevel.SILVER:
      return price * 0.95;
    case UserLevel.GOLD: 
      return price * 0.85;
    case UserLevel.PLATINUM:
      return price * 0.8;
    default:
      return price;
  }
}

这两段代码,分别计算了用户在网站上购买作品在线阅读所支付的价格,以及购买 EPUB 格式电子书所支付的价格。其中,用户实际支付的价格会根据用户在系统中的用户级别有所差异,级别越高,折扣就越高。

显然,这两个函数里出现了类似的代码,其中最类似的部分就是 switch,都是根据用户级别进行判断。事实上,这并不是仅有的根据用户级别进行判断的代码,各种需要区分用户级别的场景中都有类似的代码,而这也是一种典型的坏味道: 重复的 switch(Repeated Switch)。

之所以会出现重复的 switch,通常都是缺少了一个模型。所以,应对这种坏味道,重构的手法是:以多态取代条件表达式(Relace Conditional with Polymorphism)。具体到这里的代码,我们可以引入一个 UserLevel 的模型,将 switch 消除掉:

interface UserLevel {
  double getBookPrice(Book book);
  double getEpubPrice(Epub epub);
}


class RegularUserLevel implements UserLevel {
  public double getBookPrice(final Book book) {
    return book.getPrice();
  }
  
  public double getEpubPrice(final Epub epub) {
    return epub.getPrice();
}


class GoldUserLevel implements UserLevel {
  public double getBookPrice(final Book book) {
    return book.getPrice() * 0.8;
  }
  
  public double getEpubPrice(final Epub epub) {
    return epub.getPrice() * 0.85;
  }
}


class SilverUserLevel implements UserLevel {
  public double getBookPrice(final Book book) {
    return book.getPrice() * 0.9;
  }
  
  public double getEpubPrice(final Epub epub) {
    return epub.getPrice() * 0.85;
  }
}


class PlatinumUserLevel implements UserLevel {
  public double getBookPrice(final Book book) {
    return book.getPrice() * 0.75;
  }
  
  public double getEpubPrice(final Epub epub) {
    return epub.getPrice() * 0.8; 

有了这个基础,前面的代码就可以把 switch 去掉了:

public double getBookPrice(final User user, final Book book) {
  UserLevel level = user.getUserLevel()
  return level.getBookPrice(book);
}


public double getEpubPrice(final User user, final Epub epub) {
  UserLevel level = user.getUserLevel()
  return level.getEpubPrice(epub);
}

我们都知道,switch 其实就是一堆“ if…else” 的简化写法,二者是等价的,所以,这个重构手法,以多态取代的是条件表达式,而不仅仅是取代 switch。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值