VO、BO、PO、DTO、DO含义整理
一、先放结论
它们都是“为了隔离变化”而诞生的马甲
| 缩写 | 英文全称 | 中文直译 | 出现位置 | 核心目的 |
|---|
| PO | Persistent Object | 持久化对象 | 数据库 ↔ 代码 | 一张表一行记录的直接映射 |
| DO | Domain Object | 领域对象 | 核心业务逻辑层 | 充血模型,封装业务行为 |
| BO | Business Object | 业务对象 | 应用/服务层 | 聚合多个DO,面向用例编排 |
| DTO | Data Transfer Object | 数据传输对象 | 进程/服务间 | 精简字段,抗网络延迟 |
| VO | View Object | 视图对象 | 控制层 ↔ 前端 | 展示友好,防敏感字段泄露 |
一句话总结: PO 管存储,DO 管业务,BO 管编排,DTO 管网络,VO 管界面。
下面上代码,咱们边喝奶茶边讲。
二、业务场景
用户下一单“芋泥波波奶茶”
需求:
- 用户选好规格(大杯、少冰、五分糖)。
- 点击“提交订单”,前端把数据发过来。
- 后端算价格、扣库存、落库,返回“订单创建成功”页面。
整条链路里,我们到底需要几个对象?
三、从数据库开始:PO
PO是Persistent Object的简写 PO 就是“一行数据一个对象”,字段名、类型和数据库保持一一对应,不改表就不改它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| kotlin
体验AI代码助手 代码解读 复制代码 @Data @TableName("t_order") public class OrderPO { private Long id; private Long userId; private Long productId; private String sku; private BigDecimal price; private BigDecimal payAmount; private Integer status; private LocalDateTime createTime; private LocalDateTime updateTime; }
|
注意:PO 里绝不能出现业务方法,它只是一个“数据库搬运工”。
四、核心业务:DO
DO 是“有血有肉的对象”,它把业务规则写成方法,让代码自己说话。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| csharp
体验AI代码助手 代码解读 复制代码 public class OrderDO {
private Long id; private UserDO user; private MilkTeaDO milkTea; private SpecDO spec; private Money price; private OrderStatus status;
public Money calcFinalPrice() { Money discount = user.getVipDiscount(); Money promotion = milkTea.getPromotion(spec); return price.minus(discount).minus(promotion); }
public void checkBeforeCreate() { if (!milkTea.hasStock(spec)) { throw new BizException("库存不足"); } } }
|
DO 可以引用别的 DO,形成聚合根。它不关心数据库,也不关心网络。
五、面向用例:BO
BO 是“场景大管家”,把多个 DO 攒成一个用例,常出现在 Service 层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| scss
体验AI代码助手 代码解读 复制代码@Service public class OrderBO {
@Resource private OrderRepository orderRepository; @Resource private InventoryService inventoryService; @Resource private PaymentService paymentService;
@Transactional public OrderDTO createOrder(CreateOrderDTO cmd) {
OrderDO order = OrderAssembler.toDO(cmd);
order.checkBeforeCreate();
inventoryService.lock(order.getSpec()); Money payAmount = order.calcFinalPrice();
OrderPO po = OrderAssembler.toPO(order, payAmount); orderRepository.save(po);
return OrderAssembler.toDTO(po); } }
|
BO 的核心是编排,它把 DO、外部服务、PO 串成一个完整的业务动作。
六、跨进程/服务:DTO
DTO 是“网络快递员”,字段被压缩成最少,只带对方需要的数据。
1)入口 DTO:前端 → 后端
1 2 3 4 5 6 7 8 9 10 11 12 13
| kotlin
体验AI代码助手 代码解读 复制代码@Data public class CreateOrderDTO { @NotNull private Long userId; @NotNull private Long productId; @Valid private SpecDTO spec; }
|
2)出口 DTO:后端 → 前端
1 2 3 4 5 6 7 8 9 10 11 12
| kotlin
体验AI代码助手 代码解读 复制代码@Data public class OrderDTO { private Long orderId; private String productName; private BigDecimal payAmount; private String statusDesc; private LocalDateTime createTime; }
|
DTO 的字段命名常带 UI 友好词汇(如 statusDesc),并且绝不暴露敏感字段(如 userId 在返回给前端时可直接省略)。
七、最后一步:VO
VO 是“前端专属快递”,字段可能二次加工,甚至带 HTML 片段。
1 2 3 4 5 6 7 8 9 10 11
| typescript
体验AI代码助手 代码解读 复制代码@Data public class OrderVO { private String orderId; private String productImage; private String priceText; private String statusTag; }
|
VO 通常由前端同学自己写 TypeScript/Java 类,后端只负责给 DTO,再让前端 BFF 层转 VO。如果你用 Node 中间层或 Serverless,VO 就出现在那儿。
八、一张图记住流转过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| scss
体验AI代码助手 代码解读 复制代码前端页面 │ JSON ▼ CreateOrderVO (前端 TS) │ 序列化 ▼ CreateOrderDTO (后端入口) │ BO.createOrder() ▼ OrderDO (充血领域模型) │ 聚合、计算 ▼ OrderPO (落库) │ MyBatis ▼ 数据库
|
返回时反向走一遍:
1 2 3 4 5 6 7 8 9 10 11
| java
体验AI代码助手 代码解读 复制代码数据库 │ SELECT OrderPO │ 转换 OrderDTO │ JSON OrderVO (前端 TS 渲染)
|
九、常见疑问答疑
- 为什么 DO 和 PO 不合并? 数据库加索引、加字段不影响业务;业务改规则不改表结构。隔离变化。
- DTO 和 VO 能合并吗? 小项目可以,但一上微服务或多端(App、小程序、管理后台),立马爆炸。比如后台需要用户手机号,App 不需要,合并后前端会拿到不该看的数据。
- BO 和 Service 有什么区别? BO 更贴近用例,粒度更粗。Service 可能细分读写、缓存等。命名随意,关键看团队约定。
十、一句话背下来
数据库里叫 PO,业务里是 DO,编排靠 BO,网络走 DTO,前端看 VO。