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

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

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


1 火车残骸


获得一篇博客作者名字:


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


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


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

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


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


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


Martin Fowler 在《重构》中给这种坏味道起的名字叫过长的消息链(Message

Chains),而有人则给它起了一个更为夸张的名字:火车残骸(Train Wreck),形容这样的代码像火车残骸一般,断得一节一节。


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


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 基本类型偏执


public double getEpubPrice(final boolean highQuality,

        final int chapterSequence) {

 ...

}

根据章节信息获取 EPUB 价格。问题在返回值类型,即价格类型。在DB存储价格时,就是用一个浮点数,用 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 等等)的使用也属于这坏味道:


封装所有的基本类型和字符串

使用流的集合

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


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


5 总结


与封装有关的坏味道:


过长的消息链,或者叫火车残骸

基本类型偏执。

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


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


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


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


6 怎样的封装算高内聚?


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


builder模式,每次调用返回的都是自身,不牵涉到其他对象,不违反迪米特法则

java stream操作,就是声明性操作。

构建模型还有一个好处是加了一层抽象,屏蔽了外部变化,类似防腐层。

比如DDD中领域内只处理本领域的对象,使用其他领域的对象要先经过转换而非直接使用。


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

现在的数据库映射用的都是反射实现,与setter关系不大。


若你的编码方式是置顶向下的,且当前层都只面向意图定义空类和空函数。写出提倡的这种风格其实很正常。

结合1描述的编码方式。顶层类中不会有基础类型,每个属性的类型都会是一个面向意图的类来承接。顶层函数的实现部分只会有一个个函数,哪怕函数实现只有一行。

目录
相关文章
|
存储 编译器 C语言
类与对象(随心记录一)
类与对象(随心记录一)
108 1
|
安全 Java 编译器
《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(异常 | 泛型)
《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(异常 | 泛型)
《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(异常 | 泛型)
|
前端开发 JavaScript 物联网
带你读书之“红宝书”:第五章 基本引用类型⑥
带你读书之“红宝书”:第五章 基本引用类型⑥
71 0
带你读书之“红宝书”:第五章 基本引用类型⑥
|
前端开发
带你读书之“红宝书”:第五章 基本引用类型③
带你读书之“红宝书”:第五章 基本引用类型③
86 0
带你读书之“红宝书”:第五章 基本引用类型③
|
存储 前端开发
带你读书之“红宝书”:第五章 基本引用类型④
带你读书之“红宝书”:第五章 基本引用类型④
85 0
带你读书之“红宝书”:第五章 基本引用类型④
|
前端开发
带你读书之“红宝书”:第五章 基本引用类型②
带你读书之“红宝书”:第五章 基本引用类型②
63 0
带你读书之“红宝书”:第五章 基本引用类型②
|
安全 前端开发
带你读书之“红宝书”:第五章 基本引用类型⑤
带你读书之“红宝书”:第五章 基本引用类型⑤
79 0
带你读书之“红宝书”:第五章 基本引用类型⑤
|
开发框架 .NET 编译器
自定义类型(跑路人笔记2)
自定义类型(跑路人笔记1)
自定义类型(跑路人笔记2)
|
编译器 C++
自定义类型(跑路人笔记1)
自定义类型(跑路人笔记)
自定义类型(跑路人笔记1)