set 方法是坏味道?

news/2024/11/8 9:53:20/

1 满天Setter

public void approve(final long bookId) {...book.setReviewStatus(ReviewStatus.APPROVED);...
}

对作品进行审核:通过 bookId,找到对应的作品,接下来,将审核状态设置成审核通过。setter 往往是缺乏封装的一种做法。很多人在写代码时,写完字段就会利用 Lombok 生成 getter、setter。setter 同 getter 一样,反映的都是对细节的暴露。

这就意味着,你不仅可以读到一个对象的数据,还可以修改一个对象的数据。相比于读数据,修改是一个更危险操作。你不知道数据会在哪里被何人以什么方式修改,造成的结果是,别人的修改会让你的代码崩溃。与之相伴的还有各种并发问题。

可变的数据可怕,比可变的数据更可怕的是不可控的变化,暴露 setter 就是这种不可控的变化。把各种实现细节完全交给对这个类不了解的使用者去修改,没有人会知道他会怎么改,所以,这种修改完全不可控。

用一个函数替代 setter,也就是把它用行为封装起来:

public void approve(final long bookId) {...book.approve();...
}

通过在 Book 类里引入了一个 approve 函数,将审核状态封装。

class Book {public void approve() {this.reviewStatus = ReviewStatus.APPROVED;}
}

作为这个类的使用者,你并不需要知道这个类到底是怎么实现的。
这里的变化也变得可控。虽然审核状态这个字段还是会修改,但你所有的修改都要通过几个函数作为入口。有任何业务上的调整,都会发生在类内部,只要保证接口行为不变,就不会影响到其它代码。
有时你会说,我这 setter 只用在初始化过程,并不需要在使用的过程去调用:

Book book = new Book();
book.setBookId(bookId);
book.setTitle(title);
book.setIntroduction(introduction);

这种只在初始化中使用的代码,压根没必要以 setter 形式存在,真正需要的是一个有参数的构造函数:

Book book = new Book(bookId, title, introduction);

消除 setter ,有一种专门的重构手法,叫做移除设值函数(Remove Setting Method)。总之setter 完全没有必要存在。

Lombok 可以在编译的过程中生成相应代码,最大的优点是不碍眼。因为它的代码是在编译阶段生成的,所以,那些生成的代码在源码级别上是不存在的。下面就是一个例子:

@Getter
@Setter
class Book {private BookId bookId;private String title;private String introduction;
}

@Getter 表示为这个类的字段生成 getter
@Setter 表示生成 setter
因为@Setter的存在,其它代码还可以调用这个类的 setter,存在的问题并不会改变。
所以,一个更好的做法是禁用@Setter。lombok.config 配置禁用@Setter:

lombok.setter.flagUsage = error
lombok.data.flagUsage = error

这里除了@Setter,还禁用了@Data,这是 Lombok 中另外一个 Annotation,表示同时生成 getter 和 setter。既然我们禁用@Setter 是为了防止生成 setter,当然也要禁用@Data了。

2 可变的数据

反对 setter,一个重要原因是它暴露了数据。
暴露数据造成的问题在于数据的修改,进而导致出现难以预料的 Bug。在上面的代码中,我们把 setter 封装成一个个的函数,实际上是把不可控的修改限制在一个有限范围内。

进一步,如果数据压根不让修改,犯下各种低级错误的机会就进一步降低。没错,在这种思路下,可变数据(Mutable Data)就成了一种坏味道,这是 Martin Fowler 在新版《重构》里增加的坏味道,它反映着整个行业对于编程的新理解。

这种想法源自函数式编程,数据建立在不改变的基础上,如果需要更新,就产生一份新的数据副本,而旧有的数据保持不变。
随着函数式编程在软件开发领域中的地位不断提高,人们对于不变性的理解也越发深刻,不变性有效地解决了可变数据产生的各种问题。

所以,Martin Fowler 在《重构》第二版里新增了可变数据作为一种坏味道,这其实反映了行业的理解也是在逐渐推进的。不过,Martin
Fowler 对于可变数据给出的解决方案,基本上是限制对于数据的更新,降低其风险,这与我们前面提到的对 setter 的封装如出一辙。

解决可变数据,还有一个解决方案

3 编写不变类

函数式编程的不变性,其中的关键点就是设计不变类。String 类就是一个不变类,比如,如果我们把字符串中的一个字符替换成另一个字符,String 类给出的函数签名是这样的:

String replace(char oldChar, char newChar);

其含义是,这里的替换并不是在原有字符串上进行修改,而是产生了一个新的字符串。

那么,在实际工作中,我们怎么设计不变类呢?

  • 所有的字段只在构造函数中初始化
  • 所有的方法都是纯函数
  • 如果需要有改变,返回一个新的对象,而不是修改已有字段

回过头来看我们之前改动的“用构造函数消除 setter”的代码,其实就是朝着这个方向在迈进。如果按照这个思路改造我们前面提到的 approve 函数,同样也可以:

class Book {public void approve() {return new Book(..., ReviewStatus.APPROVED, ...);}
}

这里,我们创建出了一个“其它参数和原有 book 对象一模一样,只是审核状态变成了 APPROVED ”的对象。

在 JDK 的演化中,我们可以看到一个很明显的趋势,新增的类越来越多地采用了不变类设计,比如,用来表示时间的类。
原来 Date 类里面还有各种 setter,而新增的 LocalDateTime 则一旦初始化就不会再修改。要操作这个对象,则会产生一个新对象:

LocalDateTime twoDaysLater = now.plusDays(2);

就目前的开发状态而言,想要完全消除可变数据很难做到,但可尽可能编写一些不变类。
区分类的性质。最核心要识别的对象分成两种,实体和值对象。实体对象要限制数据变化,而值对象就要设计成不变类。

函数式编程的本质,是对程序中的赋值进行了约束。基于这样的理解,连赋值本身其实都会被归入到坏味道的提示,这才是真正挑战很多人编程习惯的一点。

越来越多的语言中开始引入值类型,也就是初始化之后便不再改变的值,比如,Java 的 Valhalla 项目。像 Rust,缺省都是值类型,而如果你需要一个可以赋值的变量,反而要去专门声明。

Martin Fowler 在《重构》中还提到一个与数据相关的坏味道:全局数据(Global Data)。如果你能够理解可变数据是一种坏味道,全局数据也就很容易理解了,它们处理手法基本上是类似的。

4 总结

可变数据最直白的体现就是各种 setter:

  • 破坏了封装
  • 带来不可控的修改,给代码增添许多问题

解决它的一种方式就是移除设值函数(Remove Setting Method),将变化限制在一定的范围之内。

可变数据是《重构》第二版新增的坏味道,这其实反映了软件开发行业的一种进步,它背后的思想是函数式编程所体现的不变性。解决可变数据,一种方式是限制其变化,另一种方式是编写不变类。

在实践中,完全消除可变数据是很有挑战的。所以,一个实际的做法是,区分类的性质。值对象就要设计成不变类,实体类则要限制数据变化。

函数式编程的本质是对于赋值进行了约束,我们甚至可以把赋值作为一种坏味道的提示。很多编程语言都引入了值类型,而让变量成为次优选项。

限制可变的数据。


http://www.ppmy.cn/news/11415.html

相关文章

Android Studio实现一个新闻APP系统源码,仿网易,搜狐等新闻客户端,本科毕业设计必备项目

DavidTGNewsProject ##【Android】最新主流新闻app功能实现。仿网易,搜狐等新闻客户端 完整代码下载地址:Android Studio实现一个新闻APP系统源码 先给大家看一下效果图: 这个项目总体来说虽然不是特别难,但是确实非常常用的功能。是业余时间…

Linux设备树简析

1. 前言 限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。 2. 设备树的来源 在 Linux 中,每个设备驱动,管理一组设备数据,类似面向对象编程中类和其实例对象的关系。一段时…

OMNet++安装pydev

菜单栏->Help->Install New SoftWare->Add 输入 Pydev http://pydev.sourceforge.net/pydev_update_site/5.2.0链接为下载5.2.0版本&#xff0c;若jdk<1.8安装5.2.0之前的版本。\color{blue}{链接为下载5.2.0版本&#xff0c;若jdk<1.8安装5.2.0之前的版本。}链…

电力系统机组组合优化调度(IEEE14节点、IEEE30节点、IEEE118节点)(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f4dd;目前更新&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;电力系统相关知识&#xff0c;期刊论文&…

4.R语言【rehsape2包】介绍、melt( )、cast( )函数、其他使用技巧

b站课程视频链接&#xff1a; https://www.bilibili.com/video/BV19x411X7C6?p1 腾讯课堂(最新&#xff0c;但是要花钱&#xff0c;我花99&#x1f622;&#x1f622;元买了&#xff0c;感觉讲的没问题&#xff0c;就是知识点结构有点乱&#xff0c;有点废话&#xff09;&…

caoz的梦呓:谈谈信息不对称与To B业务的难点【推荐】

一、谈谈信息不对称 一直以来&#xff0c;大部分商业行为获利的一个重要基础是信息不对称。互联网时代一个巨大的谎言是&#xff0c;信息是公开透明的&#xff0c;但是很遗憾&#xff0c;这不是真的。 第一&#xff0c;一些非常具有价值的信息资源&#xff0c;并不在互联网流…

由数据范围反推算法复杂度以及算法内容

文章目录一、由数据范围反推算法复杂度以及算法内容二、数据范围三、其他知识点1. long 和 int 的大小跟系统位数有关2. memset 常用赋值一、由数据范围反推算法复杂度以及算法内容 一般 ACM 或者笔试题的时间限制是 1s 或 2s。在这种情况下&#xff0c;C 代码中的操作次数控制…

MacOS上效率神器 Alfred的基本用法

文章目录前言软件安装基础功能配置和使用基础配置基础功能搜索功能Web 搜索计算器词典查询和通讯录查询系统功能其它扩展Bookmarks 的应用设计 Workflows最后参考文章前言 工欲善其事必先利其器&#xff0c;Alfred 在 Mac 上所迸发的效率是前所未有的&#xff01; 作为 Mac 上…