在面向对象程序员的圈子里,getter和setter貌似是绕不过去的一对孪生兄弟。简单来讲,它俩一个是取值器(getter),一个是设值器(setter)。看一段代码就一目了然了:
public class Player {
private String name;
public Player(String name) {
this.name = name;
}
// getter
public String getName() {
return name;
}
// setter
public void setName(String name) {
this.name = name;
}
}
getter和setter就是两个普通的方法,这两个方法为了方便外界操纵Player
对象,获取Player
的属性值,以及重置Player
对象的属性值。
看着标题你可能心里开始嘀咕:“危言耸听了啊,不就是两个方法吗?怎么还能害人?我写这么多年的代码,都是无脑生成getter和setter的,而且都不用手写,IDE一键智能生成!”
好家伙,写了这么多年代码,你可曾想过为什么需要getter/setter?启蒙老师教的?书里讲的?年长的同事敦促的?IDE提倡的?……
不知道你中枪了没,我反正中枪了。半路出家的我在软件培训班中启蒙老师能行云流水般的生成getter/setter,而且还是纯手工的,当时我还觉得特酷。从此我染上了恶习,更巧合的是职场上遇到一帮跟我“同流合污”的程序员乐此不疲地吹捧getter/setter。甚至有很多技术书也在这么干,这让我坚定地不去怀疑getter/setter存在的价值,虽然有时候也不知有何价值,哪怕是被IDE灰显的Dead Code,也要把它祭在那里。就好比这段断码:
setScore、setName两个setter就是灰显的Dead Code。
setter害人不浅并不是说setter毫无价值,在一些本身就是为成为容器而生的数据类中,getter/setter可以说是与生俱来的。这些对象唯一用途是承载特定的数据,用于不同上下文中的数据包装和转换,生命周期也随着转换和传递的完成而终止。
现在程序员喜欢用的DTO (Data Transfer Object)、VO (View Object)、PO (Persistence Object)都属于这类对象。这些对象就是纯粹的数据类,数据类只需要提供取值和赋值的方法让用户能够交换数据即可,它们不承载任何业务,便不出现业务行为。
setter什么时候不合理呢?
Talk is cheap,Let me show you the code:
public void cancelRiskInvestigationTask(RevertRiskInvestigationTaskRequestModel requestModel) {
long riskInvestigationTaskId = Long.parseLong(requestModel.getRiskInvestigationTaskId());
RiskInvestigationTask riskInvestigationTask = riskInvestigationTaskRepository.loadById(riskInvestigationTaskId)
.orElseThrow(() -> new RevertTaskBlockException("没有找到该风险排查任务,无法撤销"));
riskInvestigationTask.setTaskStatus(RiskInvestigationTaskStatusEnum.CANCELED);
riskInvestigationTaskRepository.update(riskInvestigationTask);
// ...
}
注意,上述代码的riskInvestigationTask.setTaskStatus的调用就是熊出没的地方,它将riskInvestigationTask实体中的任务状态设置为取消(CANCELED
)。很多人可能看到这里觉得没什么不妥,好像自己也一直这么干,程序仍然工作得好好的。在Service类中去重置领域对象(RiskInvestigationTask
)的状态确实是很常见的做法,有的场景甚至需要先获取对象原来的状态,然后判断当前状态是否可以将其设置为CANCELED
的状态。比如下面代码:
public void cancelRiskInvestigationTask(RevertRiskInvestigationTaskRequestModel requestModel) {
long riskInvestigationTaskId = Long.parseLong(requestModel.getRiskInvestigationTaskId());
RiskInvestigationTask riskInvestigationTask = riskInvestigationTaskRepository.loadById(riskInvestigationTaskId)
.orElseThrow(() -> new RevertTaskBlockException("没有找到该风险排查任务,无法撤销"));
RiskInvestigationTaskStatusEnum originalStatus = riskInvestigationTask.getTaskStatus();
if (originalStatus == RiskInvestigationTaskStatusEnum.PROCESSINIG) {
riskInvestigationTask.setTaskStatus(RiskInvestigationTaskStatusEnum.CANCELED);
}
riskInvestigationTaskRepository.update(riskInvestigationTask);
// ...
}
上述的做法更多的是将RiskInvestigationTask
当做一个纯数据容器在使唤,所有的业务逻辑都交给外层Service来做。
补充点上下文,这个
RiskInvestigationTask
是一个Entity,是DDD战术建模中定义的实体,简单点就当它是一个面向对象设计的对象实体好了(数据 + 行为)。
先上结论:这是一个典型的不合理运用,注意不是错误,因为程序是可以工作的。
那不合理的点是什么呢?我从三个方面来掰扯一下不合理的地方:
1. 代码坏味道。《重构》一书里面有个Feature Envy(特性依恋)
的坏道:
某个函数为了计算某个值,从另一个对象那儿调用了几乎半打的取值方法,显而易见,这个函数更应该放在另一个对象那儿,因为大部分取值都来自那个对象的属性。
虽然这里没有半打取值方法,背后暴露的问题是类似的,都是通过取得别人的数据来做运算和判断。
如果说Feature Envy
你有点儿难以接受,说我强词夺理,我虽不情愿,也认了。那这里面还散发另外一个坏味道 — Mutable Data(可变数据)
。由于这个公开的setter存在,导致这个对象的某个属性可以在外部被随意更改,这就是setter带来的副作用。
2. 面向对象。面向对象设计提倡数据跟对操作的数据行为绑定在一起,否则很可能破坏了对象的封装性。这里就破坏了封装性。
3. 领域模型。RiskInvestigationTask
是一个领域对象Entity,如果在仔细琢磨一下,这里面是不是缺少什么领域行为,比如取消,比如通过审批等。
相比于前2点,第3点更难把握。如果前2点看成术(它们是具体可参考落地的指导建议),那么第3点就上升到道层面了,比如如何恰到好处的理解业务,对业务进行合理的建模,识别出恰当的领域行为,它很难有标准答案的,不可言说,说了也不一定会,除非认知到了。
那怎么办呢?绕了这么久,我好像在说:“这个很难,大家别努力了,听天由命吧。” 恰恰不是,都说苍蝇不叮无缝的蛋。既然是不好的设计,肯定有缝隙。让我们来跟苍蝇比一下敏感度,留意到这个缝隙,趁机修复漏洞。
划重点:它的缝隙就在那个setter上,但凡出现了setter的地方,都值得你停下来去思考几个问题:
- 为什么这里有setter? — 锁住缝隙
- 它本身的定位是数据容器类吗? — 拷问1 【大部分不是】
- 应该在外部获取他的数据来做逻辑判断吗? — 拷问2 【大部分会破坏封装性】
- 是什么业务行为会导致这个状态发生这样的变化? — 拷问3 【大部分因为缺失了领域行为】
通过以上的思考,最终很可能发现是领域行为的缺失,通过进一步合理的业务抽象,为RiskInvestigationTask
添加了一个领域行为cancel()
,代码如下:
public class RiskInvestigationTask {
private RiskInvestigationTaskStatusEnum status;
// ...
public void cancel() {
if (status == RiskInvestigationTaskStatusEnum.PROCESSINIG) {
this.status = RiskInvestigationTaskStatusEnum.CANCELED;
}
}
}
做了合理的抽象建模,在外部Service层也就不需要知道其内部信息了,外部只需要调用cancel()这个跟业务相匹配的领域行为:
public void cancelRiskInvestigationTask(RevertRiskInvestigationTaskRequestModel requestModel) {
long riskInvestigationTaskId = Long.parseLong(requestModel.getRiskInvestigationTaskId());
RiskInvestigationTask riskInvestigationTask = riskInvestigationTaskRepository.loadById(riskInvestigationTaskId)
.orElseThrow(() -> new RevertTaskBlockException("没有找到该风险排查任务,无法撤销"));
riskInvestigationTask.cancel()
riskInvestigationTaskRepository.update(riskInvestigationTask);
// ...
}
上述这样的代码是不是更整洁干净了呢? — 这个问题不用回答~ ~
总结,再划重点:setter虽有用,但要注意不要被setter给耽误了。啰嗦了这么多,不知道我有没有说清道明setter的误用之处。如果你中枪了,而且你也认为这枪中的冤枉,那赶紧拿起setter放大镜去搜罗你的码场吧,多长点心眼,总能躲掉几颗子弹,少点皮(加)肉(班)之苦。