这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
1 前言
最近笔者完成了《重构》这本书的阅读
在移除设置函数这一章中, 笔者才发现, 工作中使用了太久的Lombok, 已经习惯于利用@Data注解实现Getter/Setter配置
那在Lombok中如何移除不必要的Setter呢?
从这一问题出发, 可以延申至"如何在Lombok下完成Java类的约束与重构?"
由于笔者能力有限, 暂时只能从以下几个方面进行总结:
- Java中的修饰符
- Lombok中的重构策略
- Lombok可能存在的问题
2 Java中的修饰符
Java中共有四种修饰符: public/default/protected/private
其中较为特殊的是default, 它和不写任何修饰符的意义相同
修饰符的可访问区间如下:
| 修饰符 | 类内部 | 子类 | 同一个包 | 其它 |
|---|---|---|---|---|
| public | √ | √ | √ | √ |
| default | √ | ? | √ | × |
| protected | √ | √ | × | × |
| private | √ | × | × | × |
其中default的子类可否访问是不明确的, 因为default访问条件是在同一包下, 若子类不在同一包下, 则无法访问
为什么要介绍修饰符呢?
一个良好的程序, 它的每个类的每个属性, 访问权限都应该是明确的
即便使用Lombok, 我们也不应用@Data解决一切的思路去面对经过自己手中的每一个类
无论是字段/方法, 还是Getter/Setter, 都应有它自己的访问权限控制
如果你能认同这一点, 下面的内容一定会对你有所帮助
3 Lombok中的重构策略
3.1 自封装字段
在这里要夸一下Lombok, 在此重构策略下, 它解放了程序员的双手和视觉观感
- 将类的属性修改为
private - 在类头部增加
@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类非常简单, 若是下面这几种情况呢:
- Person类共有20个参数, 其中12个需要在构造函数中使用
- Person类是某个类的子类, 构造函数需要调用父类, 父类的构造函数又需要8个传参
- 上述两种情况, 在开发收尾阶段突然调整Person类的构造规则,
之后你可能会疯, 如果有继承关系, 你改父类的时候会再疯一次
在工厂函数中, 推荐合理使用@Builder和@SuperBuilder
4 Lombok可能存在的问题
4.1 封装集合
当类中存在一个集合的属性时, 需要提起格外的警觉, 最好遵循以下原则:
- 无法在类的外部直接操作集合
- 无法在类的外部直接为该属性整体赋值
- 在类中实现集合所需要的操作
以数组为例, 上述三条原则对应的实现为:
- getter应返回数组的不可变副本
- 移除setter, 增加初始化函数, 且其内容应为遍历添加或深拷贝
- 增加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帮我们简洁了代码是好事
但对于集合和业务中的各种特殊需求, 我们应学会灵活变通
世界上没有最好的技术, 只有最优雅的使用方式