POJO教条
Java工程里,你经常看到这样的类名:*PO、*DO、*DTO、*VO、*BO。我从网上找到它们的定义:
• PO:数据持久化对象 (Persistent Object, PO),与数据库结构一一映射,它是数据持久化过程中的数据载体。
• DO:领域对象( Domain Object, DO),微服务运行时核心业务对象的载体, DO 一般包括实体或值对象。
• DTO:数据传输对象( Data Transfer Object, DTO),用于前端应用与微服务应用层或者微服务之间的数据组装和传输,是应用之间数据传输的载体。
• VO:视图对象(View Object, VO),用于封装展示层指定页面或组件的数据。
它们在系统中的位置分别是:
定义与配图原文链接:https://fanlv.fun/2021/02/08/ddd/#1-11-PO%E3%80%81DO%E3%80%81DTO%E3%80%81VO
你可能对其中某个定义有异议,但是不是影响本文所写的内容。
这些定义是从哪来的呢?
虽然我没有证据,但是,个人认为这些类名源自:POJO(Plain Old Java Object)定义。POJO的定义来自Martin Fowler2000年时的一次演讲。
Martin Fowler似乎有没有直接说明:POJO除了包含字段、getter和setter方法,不包含任何其它业务逻辑。这样的技术规则。
但是,我遇到的几乎所有的Java开发人员、架构师都这样认为。
甚至有人把它当成真理,连和他讨论的余地都没有。这让我有一种身处”地心说“年代的感觉。
反问POJO教条
在这里,我提出疑问:
不论是什么POJO,它终究是一个Java类。我们为什么要强制要求一个Java类除了字段和getter和setter方法,不包含任何其它业务逻辑?这是一个技术限制,还是一个业务限制?
除了技术限制,我想不到任何理由。
什么样的技术强制要求我们一个Java类不允许包含业务逻辑呢?
案例
业务背景介绍
我们需要实现一个类似status.slack.com页面的功能。它能显示每个系统的可用性。如下图:![[slack-status.png]]
代码案例
这个页面的数据的结构如下:
public class SystemStatusVo {
private String systemName;
// 系统当前的状态
private String status;
// issueMap用于存储各个Issue类型及其数量
private Map<String, Integer> issueMap = new HashMap<>();
当我们发现该系统存在严重级别的告警通知时,我们认为该系统处于故障状态。
所以,我们需要根据数据库中的数据来判断一个系统是否存在严重级别的告警通知。
”告警通知“在数据库中的表结构如下:
CREATE TABLE alerts (
...
-- status可能是值:firing, resolved等
status varchar(16) NULL,
-- 告警通知的级别
severity varchar(16) NULL,
-- er
system varchar(32) NULL
...
);
alerts
表中一行数据代表一条告警通知。是List结构。而前端显示时,使用的的Map结构。
alerts
表对应的实体类的定义:
@Entity
@Table(name = "alerts")
public class Alert {
// ...
@Column(name = "status", length = 16)
private String status;
@Column(name = "severity", length = 16)
private String severity;
@Column(name = "system", length = 32)
private String system;
// ... getter setter
我们将Alert的List结构转成SystemStatusVo的Map结构的逻辑称了x逻辑。
现在摆在我们面前的问题是:x逻辑应该放在哪个类里?
按POJO的拥护者的说法:x逻辑决不能放在Alert和SystemStatusVo这两个类中,而放在一个叫SystemStatusService
的类中。
抱歉了,我持有不同的观点。我认为就是应该放在这两个类中的。
Alert类中,我们增加如下方法:
/**
* 统计每个system下的每个severity的个数
*/
public static List<AlertRepository.SystemSeverityCountDto> groupSeverityCountBySystem(String status) {
// InstanceFactory的作用是获取一个AlertRepository的实现类的实例
AlertRepository alertRepository = InstanceFactory.getInstance(AlertRepository.class);
return alertRepository.groupSeverityCountBySystem(status);
}
AlertRepository代码如下:
public interface AlertRepository {
//...
List<SystemSeverityCountDto> groupSeverityCountBySystem(String status);
// 内部类。因为它目前和AlertRepository是强耦合的
class SystemSeverityCountDto {
private String system;
private String status;
// 每个告警级别的通知的数量
private int num;
private String severity;
// getter setter
}
}
// ...
而AlertRepository的实现类其实就是执行一个group的sql。然后,将结构放入到List<SystemSeverityCountDto>
中。
P.S. 如果使用的是NoSQL,可能是另一个写法了。
按POJO拥护者的说法,我们污染了DO类和PO类。
接下来,我们看一下Controller类的实现:
@GetMapping(value = "/status")
public ModelAndView status(...) {
// ...
List<AlertRepository.SystemSeverityCountDto> systemSeverityCountDtos =
Alert.groupSeverityCountBySystem("firing");
// 将dto转成vo
TreeSet<SystemStatusVo> systemStatusVos = SystemStatusVo.buildBy(systemSeverityCountDtos);
//... 前端直接使用systemStatusVos进行渲染
}
SystemStatusVo.buildBy
的作用是将SystemSeverityCountDto List转成自己所需要的结构。
所以,SystemStatusVo的最终代码如下:
public class SystemStatusVo {
private String systemName;
// 系统当前的状态
private String status;
// issueMap用于存储各个Issue类型及其数量
private Map<String, Integer> issueMap = new HashMap<>();
public static TreeSet<SystemStatusVo> buildBy(List<AlertRepository.SystemSeverityCountDto> dtoList) {
// ... 这部分逻辑,我们可以很轻松对其进行单元测试
}
// ...
案例之后
并不是说我们要完全去除POJO,案例中的SystemSeverityCountDto就是一个典型的POJO。
但是,我心中并没有”POJO除了包含字段、getter和setter方法,不包含任何其它业务逻辑“这样的教条。
在某天,如果业务需要,我们就会给它加上业务逻辑,如:
class SystemSeverityCountDto {
private String system;
private String status;
// 每个告警级别的通知的数量
private int num;
private String severity;
// 新加的业务逻辑。判断是否critical级别
public boolean isCriticalSeverity(){
return "critical".equals(severity);
}
// getter setter
}
DDD下没有POJO
当我们在设计一个类的时候,是什么驱动我们进行设计的?
如果是领域知识,那么,在这个类的级别上,你进行的是领域驱动设计(DDD)类的行为。
如果是POJO教条,那你就不是领域驱动设计。因为POJO教条是技术规则,不是领域。
当我们承认这点后,那些POJO的类后缀,就是多余的了。
这就是DDD没有POJO的原因。
(完)
好文推荐: