2.4.5 getter 与setter
在实例方法中有一类特殊的方法,即getter 与setter 方法,它们一般不包含任何业务逻辑,仅仅是为类成员属性提供读取和修改的方法,这样设计有两点好处:
(1)满足面向对象语言封装的特性。尽可能将类中的属性定义为private,针对属性值的访问与修改需要使用相应的getter 与setter 方法,而不是直接对public 的属性进行读取和修改。
(2)有利于统一控制。虽然直接对属性进行读取、修改的方式和使用相应的getter 与setter 方法在效果上是一样的,但是前者难以应对业务的变化。例如,业务要求对某个属性值的修改要增加统一的权限控制,如果有setter 作为统一的属性修改方法则更容易实现,这种情况在一些使用反射的框架中作用尤其明显。
因此,在类成员属性需要被外部访问的类中,getter 与setter 方法是必备的。除特殊情况需要增加业务逻辑外,它们仅仅是对成员属性的访问和修改操作,其承载的信息价值比较低,所以,建议在类定义中,类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter 方法。
最典型的getter 与setter 方法使用是在POJO(Plain Ordinary Java Object,简单的Java 对象)类中。在本书中,POJO 专指只包含getter、setter、toString 方法的简单类,常见的POJO 类包括DO(Dada Object)、 BO(Business Object)、DTO(Data Transfer Object)、VO(View Object)、AO(Application Object)。POJO 作为数据载体,通常用于数据传输,不应该包含任何业务逻辑。因此,在POJO 类中,getter 与setter不但是重要的组成部分,更是与外界进行信息交换的桥梁。getter 与setter 方法定义参考示例如下:
public class TicketDO {
private Long id;
// 目的地
private String destination;
// getter 方法,要求:直接返回相应属性值,不增加业务逻辑
public Long getId() {
return id;
}
public String getDestination() {
return destination;
}
// 参数名称与类成员变量名称一致,定义中this. 成员名= 参数名,尽量不增加业务逻辑
public void setId(Long id) {
this.id = id;
}
public void setDestination(String destination) {
this.destination = destination;
}
}
getter 与setter 方法的定义非常简单,正因如此,工程师们会放松对它们的警惕,导致在实际应用中因为不当操作出现问题。下面来罗列那些易出错的getter 与setter方法定义方式:
(1) getter/setter 中添加业务逻辑。问题出现时,程序员的惯性思维会忽略getter/setter 方法的嫌疑,这会增加排查问题的难度。如下示例代码,在getData() 中增加了逻辑判断,修改了原属性值,如出现属性值不一致的情况,这里可能会是程序员最后被排查到的地方。
public Integer getData() {
if (condition) {
return this.data + 100;
} else {
return this.data - 100;
}
}
(2)同时定义isXxx() 和getXxx()。在类定义中,两者同时存在会在iBATIS、JSON 序列化等场景下引起冲突。比如,iBATIS 通过反射机制解析加载属性的getter方法时,首先会获取对象所有的方法,然后筛选出以get 和is 开头的方法,并存储到类型为HashMap 的getMethods 变量中。其中key 为属性名称,value 为getter 方法。因此isXxx() 和getXxx() 方法只能保留一个,哪个方法被后存储到getMethods 变量中,就会保留哪个方法,具有一定的随机性。所以当两者定义不同时,会导致误用,进而产生问题。
与此相关的某个故障中,负责故障回顾的同事在内网发了一篇贴子,标题为“珍爱生命,远离有毒getter”,即在某个POJO 类中,Boolean 属性既有getXxx(),又有isXxx()。isXxx() 的逻辑存在错误,在修复故障时,想当然地在getXxx() 方法中进行修正,但无法修复故障,因为事实上调用的是isXxx()。排查了完整的调用链路,问题才回到POJO 类本身上。这个过程花费了大量的排查精力和故障处理的宝贵时间。
(3)相同的属性名容易带来歧义。在编程过程中,应该尽量避免在子父类的成员变量之间、不同代码块的局部变量之间采用完全相同的命名。虽然这样定义是合法的,但是要避免。这样使用非常容易引起混淆,在使用参数时,难以明确属性的作用域,最终难以分清到底是父类的属性还是子类的属性。扩展开来,对于非setter/getter的参数名称也要避免与成员变量名称相同。
public class ConfusingName {
public int alibaba;
// 反例:非setter/getter 方法的参数名称,不允许与本类成员变量同名
public void get(String alibaba) {
if(true) {
final int taobao = 15;
...
}
for (int i = 0; i < 10; i++) {
// 在同一方法体中,不允许与其他代码块中的taobao 命名相同
final int taobao = 15;
...
}
}
}
class Son extends ConfusingName {
// 反例:不允许与父类的成员变量名称相同
public int alibaba;
}
2.4.6 同步与异步
同步调用是刚性调用,是阻塞式操作,必须等待调用方法体执行结束。而异步调用是柔性调用,是非阻塞式操作,在执行过程中,如调用其他方法,自己可以继续执行而不被阻塞等待方法调用完毕。异步调用通常用在某些耗时长的操作上,这个耗时方法的返回结果,可以使用某种机制反向通知,或者再启动一个线程轮询。反向通知方式需要异步系统和各个调用它的系统进行耦合;而轮询对于没有执行完的任务会不断地请求,从而加大执行机器的压力。
异步处理的任务是非时间敏感的。比如,在连接池中,异步任务会定期回收空闲线程。举个现实中的例子,在代码管理平台中,提交代码的操作是同步调用,需要实时返回给用户结果。但是当前库的代码相关活动记录不是时间敏感的,在提交代码时,发送一个消息到后台的缓存队列中,后台服务器定时消费这些消息即可。
某些框架提供了丰富的异步处理方式,或者是把同步任务拆解成多个异步任务等。