代码的坏味道:可变的数据

一、背景

以Java语言为例,说到可变的数据,就要提到函数式编程,函数式编程主要有以下概念:

Java作为编程语言的老大哥之一,是在JDK8的时候引入了函数式编程,java是一门面向对象的编程语言,在以前调用函数的时候总是需要依赖于一个对象,经?;嵝闯瞿涿嗾庋拇耄?/p>

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello World");
    }
};

JDK8中引入了函数式编程接口和Lambda来简化代码:

Runnable runnable = () -> System.out.println("Hello World");

不可变性是函数式编程推崇的一个重要概念,保证数据的不可变性,从而可以让我们:开发更加简单、可回溯、测试友好,以及减少了任何可能的副作用,从而减少了Bug的出现。
但是JDK8对函数式编程的支持还不够完善,比如Collector的toXXX缺少生成不可变的集合,各种集合想要初始化一个不可变的对象也比较繁琐。当然这些问题在JDK11版本中得到了大幅度改进,不仅支持了类型推断,而且还支持了各种不可变对象的初始化,极大的简化了代码,比如:

// JDK7的常规初始化 ---------- 可变集合
List<String> list = new ArrayList<>();
list.add("test01");
list.add("test02");
list.add("test03");

// JDK7的匿名内部类初始化 --- 可变集合
List<String> list = new ArrayList<>() {{
    add("test01");
    add("test02");
    add("test03");
}};

// JDK8的Stream初始化 ------ 可变集合
List<String> list = Stream.of("test01", "test02", "test03").collect(Collectors.toList());

// JDK11的.of初始化 -------- 不可变集合
var list = List.of("test01", "test02", "test03");

// JDK11的stream初始化 ----- 不可变集合
var list = Stream.of("test01", "test02", "test03").collect(Colleactors.toUnmodifiableList());

// 借助工具类:Arrays ------- 不可变集合
List<String> list = new ArrayList<>(Arrays.asList("test01", "test02", "test03"));

// 借助工具类:Collections
List<String> readList = Collections.unmodifiableList(list);

// 借助工具类:Guava
ImmutableList<String> list = ImmutableList.of("test01", "test02", "test03");

通过上面各种集合初始化的对比,相信你也能发现,JDK11对不可变性的支持也日益完善,函数式编程的很多优秀的特性在java语言得到了实现,所以还在用jdk8的小伙伴还是尽早升级,要不然jdk17都要出来了。

二、不可控的可变数据

在第一版java语言的《重构》书中,还没有发现可变数据的影子,也难怪,这本书是在2010年出版的,jdk8是在2012年第一次发布,但是随着函数式编程在这些高级语言中的应用,在2019年第二版js语言的《重构》书中,在代码的坏味道中会发现多了可变数据这一条。

下面介绍两种好用的重构手法,来避免不可控的可变数据为我们带来的麻烦。

1. 移除设置函数(Remove Setting Method)

和读数据相比,修改数据是一项危险的操作,这也就是为什么在并发编程中会有各种复杂的锁机制来保证数据的一致性。对于项目中的Model来说,setter方法就是其对外暴露不可控因素的源头,其实我们完全可以避免使用setter,通过不可变的方式来替代。详情可见3.1的代码样例部分。

2. 编写不可变类

Java中最典型的不可变类就是String类,里面的各种方法,只要涉及到字符串的变化,不会再原字符串上进行修改,而是生成一个新的字符串返回。
想要编写不可变类,也不难,只要做到以下三点:

  • 所有字段只在构造函数中初始化
  • 若发生改变,就返回一个新对象
  • 编程纯函数

三、代码案例,如何避免代码中的可变性

接下来的代码都以jdk11版本的语法为例,部分代码为伪代码只为说明逻辑:

1. 基本数据类型、包装类和String

基本数据类型 :int、long、float、double、byte、short、boolean、char
包装类 :Integer、Long、Float、Double、Byte、Short、Boolean、Character
String 本身是不可变类

// 在使用以上数据类型申请变量时, 应该尽量避免对同一个变量反复赋值
int i = toResult();
....
i = toAnotherResult();
// toAnotherResult()应该重新申请一个变量,不应该对以前的变量进行覆盖,并且不必要的变量应该进行Inline操作

还有一种情况在开发的时候会经常遇到:

// 第一种情况,if中只有一行赋值代码
String s1;
if(isRight(xxx)) {
    s1 = "test_01";
} else {
    s1 = "test_02";
}
use String s1 do something ...

// 第二种情况, if中内嵌了多行代码
String s2;
if(isRight(xxx)) {
    do something ...
    s1 = "test_01";
} else {
    do something ...
    s1 = "test_02";
}
use String s2 do something ...

上面这种情况应该在初始化的时候就给变量赋值。

// 第一种情况可以使用三目运算符来解决:
String s1 = isRight(xxx) ? "test_01" : "test_02";

// 第二种情况可以使用Extract Method(提炼函数)来解决:
String s2 = toStr(xxx);

private String toStr(xxx) {
    //使用卫语句简化if-else结构
    if(isRight(xxx)) {
        do something ...
        return "test_01";
    }
    do something ...
    return "test_02";
}

2. 构建不可变的集合

集合类型 :List、Map、Set, 下面List为例来说明

下列情况应当避免:

// 避免: 初始化可变列表
List<String> list = new ArrayList<>() {{
    add("test01");
    add("test02");
    add("test03");
}};

// 避免: 在一个方法中改变参数列表的长度
public void change(List<String> list) {
    list.add("test04");
}

// 避免: 在一个方法中改变参数列表的内部值
public void fill(List<Model> models) {
    models.foreach(model -> model.setType("new_model"));
}

构建不可变的列表:

// 初始化
var list_01 = List.of("test01", "test02", "test03", "");
var list_02 = List.of("test04", "test05", "test06", "");

// 通过stream来实现列表的合并和过滤,创建一个新的不可变集合
// 两个集合合并
var list_03 = Stream.concat(list_01.stream(), list_02.stream()).collect(Collectors.toUnmodifiableList());
// 多个集合合并
var list_04 = Stream.of(list1.stream(), list2.stream(), list3.stream()).flatMap(Function.identity()).collect(Collectors.toUnmodifiableList());
// 集合过滤
var list_05 = list_04.stream().filter(StringUtils::isNotEmpty).collect(Collectors.toUnmodifiableList());

// 集合改变内部的值生成一个新的集合,这里的model代表一个虚拟的对象
var models = List.of(model1, model2, model3);
var newModels = models.stream().map(model -> model.withType("new_model")).collect(Collectors.toUnmodifiableList());

总之: 集合搭配Stream可以进行任何变化生成新的不可变集合,没有副作用,非常的nice。

3. 构建不可变的model

@Setter是导致model可变的罪魁祸首,其实我们完全可以不使用setter来构建我们的model,可以在lombok中把setter相关的禁用掉。

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

我们可以用以下注释来构建不可见的model,并且通过staticName = "of"让model的构建更加函数式化。

// 声明
@With
@Getter
@Builder(toBuilder = true)
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor(staticName = "of")
public class Model() {
    private String id;
    private String name;
    private String type;
}

// 构建Model
var model_01 = Model.of("101", "model_01", "model");

// 构建空Model
var model_02 = Model.of();

// 构建指定参数的Model
var model_03 = Moder.toBuilder().id("301").name("model_03").build();

// 修改Model的一个值,通过@With来生成一个全新的model
var model_04 = model_01.withName("model_04");

// 修改多个值,通过@Builder来生成一个全新的model
var model_05 = model_01.toBuilder.name("model_05").type("new_model").build();

四、总结

编写代码时,时刻提醒自己:控制数据的可变性 ????????????????????????????

本文内容参考来源于:极客时间专栏《软件设计之美》《代码之丑》 | 书籍《重构

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容