代码坏味道:可变的数据

呈现形态:

  • (1)缺失行为,暴露细节

可变数据最直白的体现就是各种 setter。存在的问题:setter 一方面破坏了封装,同时把一个类的内部行为,即实现细节暴露了出来;另一方面它会带来不可控的修改,给代码增添许多问题。

解决方式:引入行为,封装细节,对应有一种重构手法叫移除设值函数(Remove Setting Method),将变化限制在一定的范围之内。

  • (2)可变的数据

可变数据是《重构》第二版新增的坏味道,它背后的思想是函数式编程所体现的不变性。

解决可变数据,一种方式是限制其变化,另一种方式是编写不变类。

  • (3)全局数据

编程规则:

限制变化,尽可能编写不可变类。

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

坏味道示例:

满天飞的setter
public void approve(final long bookId) {
    ...
    book.setReviewStatus(ReviewStatus.APPROVED);
    ...
}

上面这种操作,意味着你不仅可以读到一个对象的数据,而且还可以修改。而相比于读数据,修改是一个更危险的操作。可修改就意味着可变,而可变的数据会带来许多问题,简言之,你不知道数据会在哪里被何人以什么方式修改,造成的结果是,别人的修改会让你的代码崩溃。

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

缺乏封装再加上不可控的变化,在我个人心目中,setter 几乎是排名第一的坏味道。

上述代码优化重构之后:

public void approve(final long bookId) {
    ...
    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 的形式存在,真正需要的是一个有参数的构造函数。当构造方法的参数过多时,可考虑使用builder模式来重构。

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

下面是 lombok.config 的配置,通过它,我们就可以禁用 @Setter 了:

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

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

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

那么,这个思路再进一步的话,如果我们的数据压根不让修改,犯下各种低级错误的机会就进一步降低了。这种想法源自函数式编程这种编程范式。在函数式编程中,数据是建立在不改变的基础上的,如果需要更新,就产生一份新的数据副本,而旧有的数据保持不变。

在实际工作中,我们怎么设计不变类呢?要做到以下三点:

  • 所有的字段只在构造函数中初始化;

  • 所有的方法都是纯函数;

  • 如果需要有改变,返回一个新的对象,而不是修改已有字段。

针对代码生成工具,如果它生成的是一个算法,比如编译器生成器,我可以去用,因为我只要关注接口,不需要关注细节。如果生成的是模型,我倾向于不用它,因为我要关注其中的细节。如果封装它,可能就失去了代码生成的意义。

我常常说到软件设计的品位,生成大量 setter 代码的工具,本身就是设计品位不佳的体现。基于一个品位不佳的工具谈改进,这可能不是我关注的方向。

你可能感兴趣的:(代码坏味道:可变的数据)