找回密码
 立即注册
首页 业界区 业界 别再被VO、BO、PO、DTO、DO绕晕!今天用一段代码把它们 ...

别再被VO、BO、PO、DTO、DO绕晕!今天用一段代码把它们讲透

窝酴 5 天前
大家好,我是晓凡。
前阵子晓凡的粉丝朋友面试,被问到“什么是VO?和DTO有啥区别?”
粉丝朋友:“VO就是Value Object,DTO就是Data Transfer Object……”
面试官点点头:“那你说说,一个下单接口里,到底哪个算VO,哪个算DTO?”
粉丝朋友有点犹豫了。
回来后粉丝朋友痛定思痛,把项目翻了个底朝天,并且把面试情况告诉了晓凡,下定决心捋清楚了这堆 XO 的真实含义。
于是乎,这篇文章就来了
今天咱们就用一段“用户下单买奶茶”的故事,把 VO、BO、PO、DTO、DO 全部聊明白。看完保准你下次面试不卡壳,写代码不纠结。
一、先放结论

它们都是“为了隔离变化”而诞生的马甲
缩写英文全称中文直译出现位置核心目的POPersistent Object持久化对象数据库 ↔ 代码一张表一行记录的直接映射DODomain Object领域对象核心业务逻辑层充血模型,封装业务行为BOBusiness Object业务对象应用/服务层聚合多个DO,面向用例编排DTOData Transfer Object数据传输对象进程/服务间精简字段,抗网络延迟VOView Object视图对象控制层 ↔ 前端展示友好,防敏感字段泄露一句话总结:
PO 管存储,DO 管业务,BO 管编排,DTO 管网络,VO 管界面。
下面上代码,咱们边喝奶茶边讲。
二、业务场景

用户下一单“芋泥波波奶茶”
需求:

  • 用户选好规格(大杯、少冰、五分糖)。
  • 点击“提交订单”,前端把数据发过来。
  • 后端算价格、扣库存、落库,返回“订单创建成功”页面。
整条链路里,我们到底需要几个对象?
三、从数据库开始:PO

PO是Persistent Object的简写
PO 就是“一行数据一个对象”,字段名、类型和数据库保持一一对应,不改表就不改它。
  1. // 表:t_order
  2. @Data
  3. @TableName("t_order")
  4. public class OrderPO {
  5.     private Long id;          // 主键
  6.     private Long userId;      // 用户ID
  7.     private Long productId;   // 商品ID
  8.     private String sku;       // 规格JSON
  9.     private BigDecimal price; // 原价
  10.     private BigDecimal payAmount; // 实付
  11.     private Integer status;   // 订单状态
  12.     private LocalDateTime createTime;
  13.     private LocalDateTime updateTime;
  14. }
复制代码
注意:PO 里绝不能出现业务方法,它只是一个“数据库搬运工”。
四、核心业务:DO

DO 是“有血有肉的对象”,它把业务规则写成方法,让代码自己说话。
  1. // 领域对象:订单
  2. public class OrderDO {
  3.     private Long id;
  4.     private UserDO user;      // 聚合根
  5.     private MilkTeaDO milkTea; // 商品
  6.     private SpecDO spec;      // 规格
  7.     private Money price;      // Money是值对象,防精度丢失
  8.     private OrderStatus status;
  9.     // 业务方法:计算最终价格
  10.     public Money calcFinalPrice() {
  11.         // 会员折扣
  12.         Money discount = user.getVipDiscount();
  13.         // 商品促销
  14.         Money promotion = milkTea.getPromotion(spec);
  15.         return price.minus(discount).minus(promotion);
  16.     }
  17.     // 业务方法:下单前置校验
  18.     public void checkBeforeCreate() {
  19.         if (!milkTea.hasStock(spec)) {
  20.             throw new BizException("库存不足");
  21.         }
  22.     }
  23. }
复制代码
DO 可以引用别的 DO,形成聚合根。它不关心数据库,也不关心网络。
五、面向用例:BO

BO 是“场景大管家”,把多个 DO 攒成一个用例,常出现在 Service 层。
  1. @Service
  2. public class OrderBO {
  3.     @Resource
  4.     private OrderRepository orderRepository; // 操作PO
  5.     @Resource
  6.     private InventoryService inventoryService; // RPC或本地
  7.     @Resource
  8.     private PaymentService paymentService;
  9.     // 用例:下单
  10.     @Transactional
  11.     public OrderDTO createOrder(CreateOrderDTO cmd) {
  12.         // 1. 构建DO
  13.         OrderDO order = OrderAssembler.toDO(cmd);
  14.         // 2. 执行业务校验
  15.         order.checkBeforeCreate();
  16.         // 3. 聚合逻辑:扣库存、算价格
  17.         inventoryService.lock(order.getSpec());
  18.         Money payAmount = order.calcFinalPrice();
  19.         // 4. 落库
  20.         OrderPO po = OrderAssembler.toPO(order, payAmount);
  21.         orderRepository.save(po);
  22.         // 5. 返回给前端需要的数据
  23.         return OrderAssembler.toDTO(po);
  24.     }
  25. }
复制代码
BO 的核心是编排,它把 DO、外部服务、PO 串成一个完整的业务动作。
六、跨进程/服务:DTO

DTO 是“网络快递员”,字段被压缩成最少,只带对方需要的数据。
1)入口 DTO:前端 → 后端
  1. @Data
  2. public class CreateOrderDTO {
  3.     @NotNull
  4.     private Long userId;
  5.     @NotNull
  6.     private Long productId;
  7.     @Valid
  8.     private SpecDTO spec; // 规格
  9. }
复制代码
2)出口 DTO:后端 → 前端
  1. @Data
  2. public class OrderDTO {
  3.     private Long orderId;
  4.     private String productName;
  5.     private BigDecimal payAmount;
  6.     private String statusDesc;
  7.     private LocalDateTime createTime;
  8. }
复制代码
DTO 的字段命名常带 UI 友好词汇(如 statusDesc),并且绝不暴露敏感字段(如 userId 在返回给前端时可直接省略)。
七、最后一步:VO

VO 是“前端专属快递”,字段可能二次加工,甚至带 HTML 片段。
  1. @Data
  2. public class OrderVO {
  3.     private String orderId; // 用字符串避免 JS long 精度丢失
  4.     private String productImage; // 带 CDN 前缀
  5.     private String priceText; // 已格式化为“¥18.00”
  6.     private String statusTag; // 带颜色:green/red
  7. }
复制代码
VO 通常由前端同学自己写 TypeScript/Java 类,后端只负责给 DTO,再让前端 BFF 层转 VO。如果你用 Node 中间层或 Serverless,VO 就出现在那儿。
八、一张图记住流转过程
  1. 前端页面
  2.    │ JSON
  3.    ▼
  4. CreateOrderVO (前端 TS)
  5.    │ 序列化
  6.    ▼
  7. CreateOrderDTO (后端入口)
  8.    │ BO.createOrder()
  9.    ▼
  10. OrderDO (充血领域模型)
  11.    │ 聚合、计算
  12.    ▼
  13. OrderPO (落库)
  14.    │ MyBatis
  15.    ▼
  16. 数据库
复制代码
返回时反向走一遍:
  1. 数据库
  2.    │ SELECT
  3. OrderPO
  4.    │ 转换
  5. OrderDTO
  6.    │ JSON
  7. OrderVO (前端 TS 渲染)
复制代码
九、常见疑问答疑


  • 为什么 DO 和 PO 不合并?
    数据库加索引、加字段不影响业务;业务改规则不改表结构。隔离变化。
  • DTO 和 VO 能合并吗?
    小项目可以,但一上微服务或多端(App、小程序、管理后台),立马爆炸。比如后台需要用户手机号,App 不需要,合并后前端会拿到不该看的数据。
  • BO 和 Service 有什么区别?
    BO 更贴近用例,粒度更粗。Service 可能细分读写、缓存等。命名随意,关键看团队约定。
十、一句话背下来

数据库里叫 PO,业务里是 DO,编排靠 BO,网络走 DTO,前端看 VO。
下次面试官再问,你就把奶茶故事讲给他听,保证他频频点头。
本期内容到这儿就结束了
我是晓凡,再小的帆也能远航
我们下期再见 ヾ(•ω•`)o (●'◡'●)

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册