别再被VO、BO、PO、DTO、DO绕晕?代码一讲就懂!

摘要:大家好,我是晓凡。 前阵子晓凡的粉丝朋友面试,被问到“什么是VO?和DTO有啥区别?” 粉丝朋友:“VO就是Value Object,DTO就是Data Transfer Object……” 面试官点点头:“那你说说,一个下单接口里,到底哪
大家好,我是晓凡。 前阵子晓凡的粉丝朋友面试,被问到“什么是VO?和DTO有啥区别?” 粉丝朋友:“VO就是Value Object,DTO就是Data Transfer Object……” 面试官点点头:“那你说说,一个下单接口里,到底哪个算VO,哪个算DTO?” 粉丝朋友有点犹豫了。 回来后粉丝朋友痛定思痛,把项目翻了个底朝天,并且把面试情况告诉了晓凡,下定决心捋清楚了这堆 XO 的真实含义。 于是乎,这篇文章就来了 今天咱们就用一段“用户下单买奶茶”的故事,把 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 就是“一行数据一个对象”,字段名、类型和数据库保持一一对应,不改表就不改它。 // 表:t_order @Data @TableName("t_order") public class OrderPO { private Long id; // 主键 private Long userId; // 用户ID private Long productId; // 商品ID private String sku; // 规格JSON private BigDecimal price; // 原价 private BigDecimal payAmount; // 实付 private Integer status; // 订单状态 private LocalDateTime createTime; private LocalDateTime updateTime; } 注意:PO 里绝不能出现业务方法,它只是一个“数据库搬运工”。 四、核心业务:DO DO 是“有血有肉的对象”,它把业务规则写成方法,让代码自己说话。 // 领域对象:订单 public class OrderDO { private Long id; private UserDO user; // 聚合根 private MilkTeaDO milkTea; // 商品 private SpecDO spec; // 规格 private Money price; // Money是值对象,防精度丢失 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 层。 @Service public class OrderBO { @Resource private OrderRepository orderRepository; // 操作PO @Resource private InventoryService inventoryService; // RPC或本地 @Resource private PaymentService paymentService; // 用例:下单 @Transactional public OrderDTO createOrder(CreateOrderDTO cmd) { // 1. 构建DO OrderDO order = OrderAssembler.toDO(cmd); // 2. 执行业务校验 order.checkBeforeCreate(); // 3. 聚合逻辑:扣库存、算价格 inventoryService.lock(order.getSpec()); Money payAmount = order.calcFinalPrice(); // 4. 落库 OrderPO po = OrderAssembler.toPO(order, payAmount); orderRepository.save(po); // 5. 返回给前端需要的数据 return OrderAssembler.toDTO(po); } } BO 的核心是编排,它把 DO、外部服务、PO 串成一个完整的业务动作。 六、跨进程/服务:DTO DTO 是“网络快递员”,字段被压缩成最少,只带对方需要的数据。 1)入口 DTO:前端 → 后端 @Data public class CreateOrderDTO { @NotNull private Long userId; @NotNull private Long productId; @Valid private SpecDTO spec; // 规格 } 2)出口 DTO:后端 → 前端 @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 片段。 @Data public class OrderVO { private String orderId; // 用字符串避免 JS long 精度丢失 private String productImage; // 带 CDN 前缀 private String priceText; // 已格式化为“¥18.00” private String statusTag; // 带颜色:green/red } VO 通常由前端同学自己写 TypeScript/Java 类,后端只负责给 DTO,再让前端 BFF 层转 VO。如果你用 Node 中间层或 Serverless,VO 就出现在那儿。 八、一张图记住流转过程 前端页面 │ JSON ▼ CreateOrderVO (前端 TS) │ 序列化 ▼ CreateOrderDTO (后端入口) │ BO.createOrder() ▼ OrderDO (充血领域模型) │ 聚合、计算 ▼ OrderPO (落库) │ MyBatis ▼ 数据库 返回时反向走一遍: 数据库 │ SELECT OrderPO │ 转换 OrderDTO │ JSON OrderVO (前端 TS 渲染) 九、常见疑问答疑 为什么 DO 和 PO 不合并? 数据库加索引、加字段不影响业务;业务改规则不改表结构。隔离变化。 DTO 和 VO 能合并吗? 小项目可以,但一上微服务或多端(App、小程序、管理后台),立马爆炸。比如后台需要用户手机号,App 不需要,合并后前端会拿到不该看的数据。 BO 和 Service 有什么区别? BO 更贴近用例,粒度更粗。Service 可能细分读写、缓存等。命名随意,关键看团队约定。 十、一句话背下来 数据库里叫 PO,业务里是 DO,编排靠 BO,网络走 DTO,前端看 VO。 下次面试官再问,你就把奶茶故事讲给他听,保证他频频点头。 本期内容到这儿就结束了 我是晓凡,再小的帆也能远航 我们下期再见 ヾ(•ω•`)o (●'◡'●)