在面向对象程序员的圈子里,getter和setter貌似是绕不过去的一对孪生兄弟。在一些本身就是为成为容器而生的数据类中,getter/setter可以说是与生俱来的。这些对象唯一用途是承载特定的数据,用于不同上下文中的数据包装和转换,生命周期也随着转换和传递的完成而终止。

比如程序员喜欢用的DTO (Data Transfer Object)、VO (View Object)、PO (Persistence Object)都属于这类对象。这些对象就是纯粹的数据类,数据类只需要提供取值和赋值的方法让用户能够交换数据即可,它们不承载任何业务,便不出现业务行为。

然而,很多时候,这对孪生兄弟会被过度呼唤,比如下面被祭起来而被IDE灰显的Dead Code:

image.png
setScore、setName两个setter就是灰显的Dead Code。

Talk is cheap,show me the code,来看一个真实保险系统的代码片段:

  1. public void cancelRiskInvestigationTask(RevertRiskInvestigationTaskRequestModel requestModel) {
  2. long taskId = Long.parseLong(requestModel.getRiskInvestigationTaskId());
  3. RiskInvestigationTask riskInvestigationTask = riskInvestigationTaskRepository.loadById(taskId)
  4. .orElseThrow(() -> new RevertTaskBlockException("没有找到该风险排查任务,无法撤销"));
  5. riskInvestigationTask.setTaskStatus(RiskInvestigationTaskStatusEnum.CANCELED);
  6. riskInvestigationTaskRepository.update(riskInvestigationTask);
  7. // ...
  8. }

注意,上述代码的riskInvestigationTask.setTaskStatus的调用就是熊出没的地方,它将riskInvestigationTask实体中的任务状态设置为取消(CANCELED)。很多人可能看到这里觉得没什么不妥,好像自己也一直这么干,程序仍然工作得好好的。

在Service类中去重置领域对象(RiskInvestigationTask)的状态确实是一种较为常见的做法,有的场景甚至需要先获取对象原来的状态,然后判断当前状态是否可以将其设置为CANCELED的状态。比如下面代码:

  1. public void cancelRiskInvestigationTask(RevertRiskInvestigationTaskRequestModel requestModel) {
  2. long taskId = Long.parseLong(requestModel.getRiskInvestigationTaskId());
  3. RiskInvestigationTask riskInvestigationTask = riskInvestigationTaskRepository.loadById(taskId)
  4. .orElseThrow(() -> new RevertTaskBlockException("没有找到该风险排查任务,无法撤销"));
  5. RiskInvestigationTaskStatusEnum originalStatus = riskInvestigationTask.getTaskStatus();
  6. if (originalStatus == RiskInvestigationTaskStatusEnum.PROCESSINIG) {
  7. riskInvestigationTask.setTaskStatus(RiskInvestigationTaskStatusEnum.CANCELED);
  8. }
  9. riskInvestigationTaskRepository.update(riskInvestigationTask);
  10. // ...
  11. }

上述的做法更多的是将RiskInvestigationTask当做一个纯数据容器在使唤,所有的业务逻辑都交给外层Service来做[1]。

补充点上下文,这个RiskInvestigationTask 是当前保险系统中的一个Entity,是DDD战术建模中定义的实体对象。

先上结论:这是一个不合理运用的坏味道。

不合理的点是什么呢?下面从三个视角来分析一下:

1. 代码坏味道。《重构》一书里面有个Feature Envy(特性依恋)的坏道:

某个函数为了计算某个值,从另一个对象那儿调用了几乎半打的取值方法,显而易见,这个函数更应该放在另一个对象那儿,因为大部分取值都来自那个对象的属性。

虽然这里没有半打取值方法,背后暴露的问题类似,都通过取得别人的数据来做运算和判断。

另外,这里面还散发另外一个坏味道 — Mutable Data(可变数据)。由于这个公开的setter存在,导致这个对象的某个属性可以在外部被随意更改,这是setter带来的副作用。

2. 对象封装。面向对象设计提倡数据跟对操作的数据行为绑定在一起,否则很可能破坏了对象的封装性。这里就破坏了封装性。

3. 领域行为。RiskInvestigationTask是一个领域对象Entity,如果在仔细琢磨一下这个领域对象承载的业务模型,这里面是不是缺少跟业务匹配的领域行为,比如业务上的风险排查任务取消,再比如任务审批等。

第1点从Clean Code的角度出发,通过代码坏味道来识别破坏对象封装的问题,也就是第2点所强调的。而我们常说对象的封装要将行为和数据绑定在一起,难免有点技术味过重。第3点则尝试回归到业务的视角,从业务本质出发,试图理解业务,通过合理的建模,并将业务操作通过领域对象的行为来表达。

划重点:但凡出现了setter的地方,都值得你停下来去思考几个问题:

  1. 为什么这里有setter? — 引起注意
  2. 它本身的定位是数据容器类吗? — 【若是,则没什么问题】
  3. 应该在外部获取它的数据来执行业务逻辑吗? — 【若用了领域模型,则不提倡】
  4. 是什么业务行为会导致这个状态发生这样的变化? — 【缺失了准确表达业务行为的领域对象行为】

通过以上的几个提问,最终发现是领域行为的缺失,于是为RiskInvestigationTask添加了一个领域行为cancel(),代码如下:

  1. public class RiskInvestigationTask {
  2. private RiskInvestigationTaskStatusEnum status;
  3. // ...
  4. public void cancel() {
  5. if (status == RiskInvestigationTaskStatusEnum.PROCESSINIG) {
  6. this.status = RiskInvestigationTaskStatusEnum.CANCELED;
  7. }
  8. }
  9. }

做了合理的行为抽象后,在外部Service层也就不需要获取其内部信息,外部只需要调用cancel()这个跟业务相匹配的领域行为:

  1. public void cancelRiskInvestigationTask(RevertRiskInvestigationTaskRequestModel requestModel) {
  2. long riskInvestigationTaskId = Long.parseLong(requestModel.getRiskInvestigationTaskId());
  3. RiskInvestigationTask riskInvestigationTask = riskInvestigationTaskRepository.loadById(riskInvestigationTaskId)
  4. .orElseThrow(() -> new RevertTaskBlockException("没有找到该风险排查任务,无法撤销"));
  5. riskInvestigationTask.cancel()
  6. riskInvestigationTaskRepository.update(riskInvestigationTask);
  7. // ...
  8. }

总结:setter虽有用,但别过度使唤。见到setter,希望你可以停下来问自己:你真的需要setter?

注释

  1. Martin Fowler在《企业应用架构模式中》提到的一种组织领域逻辑的方式:事务脚本。