Lombok下的类重构

1,074 阅读5分钟

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

1 前言

最近笔者完成了《重构》这本书的阅读

在移除设置函数这一章中, 笔者才发现, 工作中使用了太久的Lombok, 已经习惯于利用@Data注解实现Getter/Setter配置

那在Lombok中如何移除不必要的Setter呢?

从这一问题出发, 可以延申至"如何在Lombok下完成Java类的约束与重构?"

由于笔者能力有限, 暂时只能从以下几个方面进行总结:

  1. Java中的修饰符
  2. Lombok中的重构策略
  3. Lombok可能存在的问题

2 Java中的修饰符

Java中共有四种修饰符: public/default/protected/private

其中较为特殊的是default, 它和不写任何修饰符的意义相同

修饰符的可访问区间如下:

修饰符类内部子类同一个包其它
public
default?×
protected××
private×××

其中default的子类可否访问是不明确的, 因为default访问条件是在同一包下, 若子类不在同一包下, 则无法访问

为什么要介绍修饰符呢?

一个良好的程序, 它的每个类的每个属性, 访问权限都应该是明确的

即便使用Lombok, 我们也不应用@Data解决一切的思路去面对经过自己手中的每一个类

无论是字段/方法, 还是Getter/Setter, 都应有它自己的访问权限控制

如果你能认同这一点, 下面的内容一定会对你有所帮助

3 Lombok中的重构策略

3.1 自封装字段

在这里要夸一下Lombok, 在此重构策略下, 它解放了程序员的双手和视觉观感

  1. 将类的属性修改为private
  2. 在类头部增加@Data注解

例如我们创建Account类:

@Data
public class Account {

    private String id;

}

在取值设置的时候只能调用get/set方法:

public class Test {

    public static void main(String[] args) {
        Account account = new Account();
        // id属性无法直接访问
        account.id = "test-01";
        account.setId("test-01");
        account.getId();
    }

}

3.2 移除设置函数

当某个字段在创建之后不允许改变时, 我们应取消它的设置函数

继续夸Lombok, 在此重构策略下, 我们只需要将字段变量设置为final:

@Data
public class Account {

    private final String id;

}
public class Test {

    public static void main(String[] args) {
        Account account = new Account("test-01");
        // setId方法不存在
        account.setId("test-01");
    }

}

3.3 工厂模式取代构造函数

当创建对象不仅仅是做简单的构建动作时, 我们应使用工厂函数

而Lombok中的Builder为工厂函数提供了一定程度的遍历

假设我们创建一个Person类, 拥有gender属性, 值为0代表女性, 值为1代表男性:

@Data
public class Person {

    public Person(int gender) {
        this.gender = gender;
    }

    private int gender;

}

现在我们创建两个特别的工厂函数: 创建男性和创建女性:

@Data
public class Person {

    public Person(int gender) {
        this.gender = gender;
    }

    private int gender;

    public static Person createFemale() {
        return new Person(0);
    }

    public static Person createMale() {
        return new Person(1);
    }

}

我们用Builder来改造下Person类:

@Data
@Builder
public class Person {

    private int gender;

    public static Person createFemale() {
        return Person.builder().gender(0).build();
    }

    public static Person createMale() {
        return Person.builder().gender(1).build();
    }

}

乍一看好像没什么太大区别, 主要原因是我们的Person类非常简单, 若是下面这几种情况呢:

  1. Person类共有20个参数, 其中12个需要在构造函数中使用
  2. Person类是某个类的子类, 构造函数需要调用父类, 父类的构造函数又需要8个传参
  3. 上述两种情况, 在开发收尾阶段突然调整Person类的构造规则, 之后你可能会疯, 如果有继承关系, 你改父类的时候会再疯一次

在工厂函数中, 推荐合理使用@Builder@SuperBuilder

4 Lombok可能存在的问题

4.1 封装集合

当类中存在一个集合的属性时, 需要提起格外的警觉, 最好遵循以下原则:

  1. 无法在类的外部直接操作集合
  2. 无法在类的外部直接为该属性整体赋值
  3. 在类中实现集合所需要的操作

以数组为例, 上述三条原则对应的实现为:

  1. getter应返回数组的不可变副本
  2. 移除setter, 增加初始化函数, 且其内容应为遍历添加或深拷贝
  3. 增加add和remove方法

在使用Lombok的@Data时, 上述原则是无法实现的, 我们需要进行调整

继续使用刚才的Person类, 假设每个人都有学习的课程courses, 根据封装集合原则, 在使用Lombok的情况下进行改造:

@Data
public class Person {

    @Setter(AccessLevel.NONE)
    private List<String> courses = new ArrayList<>();

    public List<String> getCourses() {
        return Collections.unmodifiableList(this.courses);
    }

    public void initCourses(List<String> courses) {
        assert this.courses.isEmpty() : "课程数组不为空, 无法初始化";
        this.courses.addAll(courses);
    }

    public void addCourse(String course) {
        this.courses.add(course);
    }

    public void removeCourse(String course) {
        this.courses.remove(course);
    }

}

注意: 在initCourses方法中加入了assert关键字, 此功能在运行Java程序时默认是关闭的, 需要手动增加VM参数开启

实际使用效果如下:

public class Test {

    public static void main(String[] args) {
        // 初始化后, courses为[]
        Person person = new Person();
        // setCourses方法不存在
        person.setCourses(Arrays.asList("test-01", "test-02"));
        // ["test-01", "test-02"]
        person.initCourses(Arrays.asList("test-01", "test-02"));
        // assert关键字报错, 无法调用两次初始化
        person.initCourses(Arrays.asList("test-01", "test-02"));
        // ["test-01", "test-02", "test-03"]
        person.addCourse("test-03");
        // ["test-01", "test-02"]
        person.removeCourse("test-03");
        // 报错, getter方法获取的是只读副本, 无法调用add/remove等
        person.getCourses().add("test-04");
    }

}

5 总结

笔者一直坚信, 偷懒是进步的必要条件

但此处的偷懒是指在代码数量/手指按键次数/心智负担等方面, 学习和思考是不能偷懒的

Lombok帮我们简洁了代码是好事

但对于集合和业务中的各种特殊需求, 我们应学会灵活变通

世界上没有最好的技术, 只有最优雅的使用方式