《代码之丑》学习笔记08——缺乏封装:如何应对火车代码和基本类型偏执问题?

08 | 缺乏封装:如何应对火车代码和基本类型偏执问题?

在程序设计中,一个重要的观念就是封装,将零散的代码封装成一个又一个可复用的模块。任何一个程序员都会认同封装的价值,但是,具体到写代码时,每个人对于封装的理解程度却天差地别,造成的结果就是:写代码的人认为自己提供了封装,但实际上,我们还是看到许多的代码散落在那里。

1.火车残骸

String name = book.getAuthor().getName();

这段代码表达的是“获得一部作品作者的名字”。作品里有作者信息,想要获得作者的名字,通过“作者”找到“作者姓名”,这就是很多人凭借直觉写出的代码,不过它是有问题的。

你可以想一想,如果你想写出上面这段代码,是不是必须得先了解 Book 和 Author 这两个类的实现细节?也就是说,我们必须得知道,作者的姓名是存储在作品的作者字段里的。这时你就要注意了:当你必须得先了解一个类的细节,才能写出代码时,这只能说明一件事,这个封装是失败的。

解决这种代码的重构手法叫隐藏委托关系(Hide Delegate),说得更直白一些就是,把这种调用封装起来:

class Book {
  ...
  public String getAuthorName() {
    return this.author.getName();
  }
  ...
}


String name = book.getAuthorName();

火车残骸这种坏味道的产生是缺乏对于封装的理解,因为封装这件事并不是很多程序员编码习惯的一部分,他们对封装的理解停留在数据结构加算法的层面上。

比如说,有人编写一个新的类,第一步是写出这个类要用到的字段,然后,就是给这些字段生成相应的 getter,也就是各种 getXXX。很多语言或框架提供的约定就是基于这种 getter 的,就像 Java 里的 JavaBean,所以相应的配套工具也很方便。现在写出一个 getter 往往是 IDE 中一个快捷键的操作,甚至不需要自己手工敲代码。

要想摆脱初级程序员的水平,就要先从少暴露细节开始。 声明完一个类的字段之后,请停下生成 getter 的手,转而让大脑开始工作,思考这个类应该提供的行为。

在软件行业中,有一个编程的指导原则几乎就是针对这个坏味道的,叫做迪米特法则(Law of Demeter),这个原则是这样说的:

  • 每个单元对其它单元只拥有有限的知识,而且这些单元是与当前单元有紧密联系的;
  • 每个单元只能与其朋友交谈,不与陌生人交谈;
  • 只与自己最直接的朋友交谈。

这个原则需要我们思考,哪些算是直接的朋友,哪些算是陌生人。火车残骸般的代码显然就是没有考虑这些问题而直接写出来的代码。

2.基本类型偏执

public double getEpubPrice(final boolean highQuality, final int chapterSequence) {
  ...
}

这是我们上一讲用过的一个函数声明,根据章节信息获取 EPUB(一种电子书的格式) 的价格。也许你会问,这是一个看上去非常清晰的代码,难道这里也有坏味道吗?

没错,有。问题就出在返回值的类型上,也就是价格的类型上。

那么,我们在数据库中存储价格的时候,就是用一个浮点数,这里用 double 可以保证计算的精度,这样的设计有什么问题吗?

确实,这就是很多人使用基本类型(Primitive)作为变量类型思考的角度。但实际上,这种采用基本类型的设计缺少了一个模型。

虽然价格本身是用浮点数在存储,但价格和浮点数本身并不是同一个概念,有着不同的行为需求。比如,一般情况下,我们要求商品价格是大于 0 的,但 double 类型本身是没有这种限制的。

就以“价格大于 0”这个需求为例,如果使用 double 类型你会怎么限制呢?我们通常会这样写:


if (price <= 0) {
  throw new IllegalArgumentException("Price should be positive");
}

问题是,如果使用 double 作为类型,那我们要在使用的地方都保证价格的正确性,像这样的价格校验就应该是使用的地方到处写的。

如果补齐这里缺失的模型,我们可以引入一个 Price 类型,这样的校验就可以放在初始化时进行:


class Price {
  private long price;
  
  public Price(final double price) {
    if (price <= 0) {
      throw new IllegalArgumentException("Price should be positive");
    }
    
    this.price = price;
  }
}

这种引入一个模型封装基本类型的重构手法,叫做以对象取代基本类型(Replace Primitive with Object)。一旦有了这个模型,我们还可以再进一步,比如,如果我们想要让价格在对外呈现时只有两位,在没有 Price 类的时候,这样的逻辑就会散落代码的各处,事实上,代码里很多重复的逻辑就是这样产生的。而现在我们可以在 Price 类里提供一个方法:


public double getDisplayPrice() {
  BigDecimal decimal = new BigDecimal(this.price)return decimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
}

其实,使用基本类型和使用继承出现的问题是异曲同工的。大部分程序员都学过这样一个设计原则:组合优于继承,也就是说,我们不要写出这样的代码:


public Books extends List<Book> {
  ...
}

而应该写成组合的样子,也就是:


public Books  {
  private List<Book> books;
  ...
}

之所以有人把 Books 写成了继承,因为在代码作者眼中,Books 就是一个书的集合;而有人用 double 做价格的类型,因为在他看来,价格就是一个 double。这里的误区就在于,一些程序员只看到了模型的相同之处,却忽略了差异的地方。Books 可能不需要提供 List 的所有方法,价格的取值范围与 double 也有所差异

但是,Books 的问题相对来说容易规避,因为产生了一个新的模型,有通用的设计原则帮助我们判断这个模型构建得是否恰当,而价格的问题却不容易规避,因为这里没有产生新的模型,也就不容易发现这里潜藏着问题。

这种以基本类型为模型的坏味道称为基本类型偏执(Primitive Obsession)。这里说的基本类型,不限于程序设计语言提供的各种基本类型,像字符串也是一个产生这种坏味道的地方。

这里我稍微延伸一下,有很多人对于集合类型(比如数组、List、Map 等等)的使用也属于这种坏味道。之前课程里我提到过“对象健身操(出自《ThoughtWorks 文集》)”这篇文章,里面有两个与此相关的条款,你可以作为参考:

  • 封装所有的基本类型和字符串;
  • 使用一流的集合。

封装之所以有难度,主要在于它是一个构建模型的过程,而很多程序员写程序,只是用着极其粗粒度的理解写着完成功能的代码,根本没有构建模型的意识;还有一些人以为划分了模块就叫封装,所以,我们才会看到这些坏味道的滋生。

如果今天的内容你只能记住一件事,那请记住:构建模型,封装散落的代码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值