前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >火车残骸和基本类型偏执问题解决方案

火车残骸和基本类型偏执问题解决方案

作者头像
JavaEdge
发布2023-02-13 15:11:10
3670
发布2023-02-13 15:11:10
举报
文章被收录于专栏:JavaEdge

坏味道:缺乏封装。封装,将碎片式代码封装成可复用模块。但不同级别程序员对封装理解程度差异大,往往写代码的人认为自己提供了封装,但实际上,我们还是看到许多的代码散落在那里。

1 火车残骸

获得一篇博客作者名字:

代码语言:javascript
复制
String name = article.getAuthor().getName();

博客里有作者信息,想要获得作者名,通过“作者”找到“作者姓名”,这就是很多人凭借直觉写出的代码,这有问题!

是不是感觉自己无法理解封装了?

若你想写出上面这段,是不是得先了解Article、Author两个类的实现细节?即我们得知道,作者的姓名存储在作品的作者字段

这就是问题:当你须先了解一个类的细节,才能写代码,这只能说明这封装不优雅。

翻翻你手头的项目,这种在一行代码中有连续多个方法调用的情况是不是随处可见?

Martin Fowler 在《重构》中给这种坏味道起的名字叫过长的消息链(Message Chains),而有人则给它起了一个更为夸张的名字:火车残骸(Train Wreck),形容这样的代码像火车残骸一般,断得一节一节。

解决这种代码的重构方案叫隐藏委托关系(Hide Delegate),即把这种调用封装:

代码语言:javascript
复制
class Book {
  ...
  public String getAuthorName() {
    return this.author.getName();
  }
  ...
}

String name = book.getAuthorName();

2 产因

对封装理解不够,大部分人对封装仅停留在:数据结构+算法。 学习数据结构时,写代码都是拿到各种细节直接操作,但那是在做练习,不是工程。有人编写一个新类:

  • 第一步是写出这类要用的字段
  • 然后给这些字段生成各种 getXXX

很多语言或框架提供的约定就是基于这种 getter的,就像 Java 里的 JavaBean,相应配套工具lombok也很方便。让暴露越来越容易,封装反而无人在意了。

但要想成为架构师,就从少暴露细节开始。声明完一个类的字段后,请停下生成 getter 的手,思考类应该提供的行为。

3 迪米特法则

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

该原则需要思考:哪些是直接朋友、陌生人。 火车残骸代码就是没考虑这些问题,直接闷头写代码。写时一时爽,重构火葬场。

按迪米特法写代码,会不会让代码里有太多简单封装的方法?

有可能,不过,这也是单独解决这一个坏味道可能带来的结果。这种代码本质是缺乏对封装的理解,而一个好的封装需要基于行为。所以,把视角再提升,应考虑类应该提供哪些行为,而非简单地把数据换一种形式呈现就止步了。

有些内部 DSL 的表现形式也是连续的方法调用,但 DSL 是声明性的,在说做什么(What),而这里的坏味道是在说怎么做(How),二者抽象级别不同,不要混谈。

4 基本类型偏执

代码语言:javascript
复制
public double getEpubPrice(final boolean highQuality,
						   final int chapterSequence) {
  ...
}

根据章节信息获取 EPUB 价格。问题在返回值类型,即价格类型。在DB存储价格时,就是用一个浮点数,用 double 可保证计算的精度,这设计有问题?确实,这就是很多人使用基本类型(Primitive)作为变量类型思考的角度。但这种采用基本类型设计缺少一个模型。

虽价格本身用浮点数存储,但价格和浮点数本身不是同一概念,有着不同行为需求。一般要求商品价格大于 0,但 double 类型本身没这限制。

以“价格大于0”这个需求为例,使用 double 类型怎么限制?

代码语言:javascript
复制
if (price <= 0) {
  throw new IllegalArgumentException("Price should be positive");
}

如果使用 double 作为类型,那我们要在使用的地方都保证价格的正确性,像这样的价格校验就应该是使用的地方到处写。若补齐这缺失的模型,可引入一个 Price 类型,校验就可放在初始化时:

代码语言:javascript
复制
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 类里提供一个方法:

代码语言:javascript
复制
public double getDisplayPrice() {
  BigDecimal decimal = new BigDecimal(this.price);
  return decimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
}

使用基本类型和使用继承出现的问题异曲同工。大部分程序员都了解组合优于继承,即不要写出这样的代码:

代码语言:javascript
复制
public Books extends List<Book> {
  ...
}

而应该写成组合:

代码语言:javascript
复制
public Books  {
  private List<Book> books;
  ...
}
  • 把Books写成继承,是因为在开发者眼里,Books 就是一个书的集合
  • 有人用 double 做价格的类型,因为在他看来,价格就是一个 double

误区在于,一些程序员只看到模型相同之处,却忽略差异。Books 可能不需要提供 List 的所有方法,价格的取值范围与 double 也有差异。

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

这种以基本类型为模型的坏味道称为基本类型偏执(Primitive Obsession)。这基本类型,不限于程序设计语言提供的各种基本类型,像字符串也是。很多对集合类型(比如数组、List、Map 等等)的使用也属于这坏味道:

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

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

所以,真正要写好代码,要对软件设计有深入学习。

5 总结

与封装有关的坏味道:

  • 过长的消息链,或者叫火车残骸
  • 基本类型偏执。

火车残骸的代码就是连续的函数调用,它反映的问题就是把实现细节暴露了出去,缺乏应有的封装。重构的手法是隐藏委托关系,实际就是做封装。软件行业有一个编程指导原则,叫迪米特法则,可以作为日常工作的指导,规避这种坏味道的出现。

基本类型偏执就是用各种基本类型作为模型到处传递,这种情况下通常是缺少了一个模型。解决它,常用的重构手法是以对象取代基本类型,也就是提供一个模型代替原来的基本类型。基本类型偏执不局限于程序设计语言提供的基本类型,字符串也是这种坏味道产生的重要原因,再延伸一点,集合类型也是。

这两种与封装有关的坏味道,背后体现的是对构建模型了解不足,其实,也是很多程序员在软件设计上的欠缺。想成为一个更好的程序员,学习软件设计是不可或缺的。

构建模型,封装散落的代码。

6 怎样的封装算高内聚?

链式调用不一定都是火车残骸,比如:

  • builder模式,每次调用返回的都是自身,不牵涉到其他对象,不违反迪米特法则
  • java stream操作,就是声明性操作。

构建模型还有一个好处是加了一层抽象,屏蔽了外部变化,类似防腐层。 比如DDD中领域内只处理本领域的对象,使用其他领域的对象要先经过转换而非直接使用。

JavaBean,用MyBatis Genarater或Lombok生成都会有Setter方法,这样DB查询或接受参数时,数据自动映射到这个对象。如果不用setter,怎么赋值? 现在的数据库映射用的都是反射实现,与setter关系不大。

  1. 若你的编码方式是置顶向下的,且当前层都只面向意图定义空类和空函数。写出提倡的这种风格其实很正常。
  2. 结合1描述的编码方式。顶层类中不会有基础类型,每个属性的类型都会是一个面向意图的类来承接。顶层函数的实现部分只会有一个个函数,哪怕函数实现只有一行。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023/01/23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 火车残骸
    • 是不是感觉自己无法理解封装了?
    • 2 产因
    • 3 迪米特法则
    • 4 基本类型偏执
    • 5 总结
    • 6 怎样的封装算高内聚?
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档