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

大家好,我是晓凡。

前阵子晓凡的粉丝朋友面试,被问到“什么是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 管界面。

下面上代码,咱们边喝奶茶边讲。


二、业务场景

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

需求:

  1. 用户选好规格(大杯、少冰、五分糖)。
  2. 点击“提交订单”,前端把数据发过来。
  3. 后端算价格、扣库存、落库,返回“订单创建成功”页面。

整条链路里,我们到底需要几个对象?


三、从数据库开始: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 渲染)

九、常见疑问答疑

  1. 为什么 DO 和 PO 不合并?
    数据库加索引、加字段不影响业务;业务改规则不改表结构。隔离变化。

  2. DTO 和 VO 能合并吗?
    小项目可以,但一上微服务或多端(App、小程序、管理后台),立马爆炸。比如后台需要用户手机号,App 不需要,合并后前端会拿到不该看的数据。

  3. BO 和 Service 有什么区别?
    BO 更贴近用例,粒度更粗。Service 可能细分读写、缓存等。命名随意,关键看团队约定。


十、一句话背下来

数据库里叫 PO,业务里是 DO,编排靠 BO,网络走 DTO,前端看 VO。

下次面试官再问,你就把奶茶故事讲给他听,保证他频频点头。

本期内容到这儿就结束了

我是晓凡,再小的帆也能远航

我们下期再见 ヾ(•ω•`)o (●’◡’●)

Java 企业级开发中,VO(View Object)、BO(Business Object)、DTO(Data Transfer Object)和 PO(Persistent Object)是用于不同层级数据建模的核心对象类型。它们分别服务于不同的目的,并在系统分层架构中承担特定的角色。 ### VO(View Object) VO 是面向前端或用户界面的数据模型,主要用于展示层与用户的交互。它通常封装了从后端获取的业务数据,并以适合前端展示的方式进行组织。VO 可以包含格式化后的数据、聚合多个业务对象的信息,也可以屏蔽敏感字段以增强安全性。例如,在一个订单管理系统中,订单详情页面可能需要将订单信息、客户信息和商品信息整合到一个 VO 中呈现给前端。 ### BO(Business Object) BO 是封装核心业务逻辑的对象,通常位于业务逻辑层。它由一个或多个 PO 组合而成,承载了具体的业务规则和操作。例如,在电商系统中,一个 `OrderBO` 可能包含订单的基本信息、用户信息以及商品列表,并提供如“计算总价”、“检查库存”等业务方法。BO 的设计往往更贴近业务领域模型,有助于提升代码的可维护性和复用性。 ### DTO(Data Transfer Object) DTO 主要用于服务接口之间的数据传输,特别是在远程调用或微服务架构中,用于减少网络请求次数并优化数据交换效率。DTO 不包含任何业务逻辑,仅作为数据容器使用。例如,在一个分布式系统中,订单服务通过 RPC 调用用户服务获取用户信息时,可以定义一个 `UserDTO` 来封装所需的数据字段,确保传输的数据结构清晰且最小化。 ### PO(Persistent Object) PO 是与数据库表结构一一对应的对象,通常用于持久化层(DAO 层)与数据库交互。PO 的属性直接映射到数据库表字段,不包含业务逻辑。例如,`UserPO` 对应数据库中的 `user` 表,其属性包括 `id`, `username`, `password` 等。PO 的主要职责是保证数据准确地存入数据库或从数据库读取。 ### 数据流转与转换 在一个典型的 Java Web 应用中,数据在各层之间流动的过程如下: 1. **持久化层**:通过 DAO 查询数据库返回 `UserPO`。 2. **业务逻辑层**:将 `UserPO` 转换为 `UserBO`,并在其中执行业务逻辑。 3. **服务接口层**:将 `UserBO` 转换为 `UserDTO`,供远程调用或跨模块使用。 4. **表现层**:将 `UserDTO` 或其他业务数据组装成 `UserVO`,返回给前端展示。 这种分层设计不仅提高了系统的解耦程度,还增强了灵活性和可维护性。例如,当数据库表结构调整时,只需修改 PO 和 DAO 层,而不影响上层业务逻辑或接口调用。 ### 示例代码POJO 与对象转换 ```java // PO 示例 public class UserPO { private Long id; private String username; private String password; // getter/setter } // BO 示例 public class UserBO { private Long id; private String username; public String getMaskedUsername() { return username.substring(0, 2) + "****"; } // getter/setter } // DTO 示例 public class UserDTO { private Long id; private String username; // getter/setter } // VO 示例 public class UserVO { private String maskedUsername; private String formattedCreateTime; // getter/setter } ``` 在实际开发中,可以通过工具类(如 Dozer、MapStruct 或手动赋值)实现这些对象之间的转换。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员晓凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值