【后端开发】(图解/实例)一文彻底讲清 DTO、VO、DO、PO、BO:别再在项目里乱用了
文章目录
- 前言
- 1 先把这几个概念搞清楚
- 1.1 DTO:Data Transfer Object,数据传输对象
- 1.2 VO:View Object,视图对象
- 1.3 BO:Business Object,业务对象
- 1.4 DO:Data Object,数据对象
- 1.5 PO:Persistent Object,持久化对象
- 1.6 Entity:实体
- 2 用一个小例子把概念传起来
- 3 别教条主义——这些概念不是必须全部用上
- 3.1 一个更真实的情况:大多数项目其实没这么复杂
- 3.2 一个很重要但常被忽略的点:命名不是最重要的
- 写在文后
前言
如果你写过后端项目,大概率经历过这样的代码:一个简单的“创建订单”接口,从 Controller 到 Service 再到数据库,来回传了好几种对象:
Controller 接收:CreateOrderRequestDTO
Service 处理:又组装了一个 OrderBO
持久化时:变成了 OrderDO
返回前端:又封装成 OrderDetailVO
甚至有时候还会看到:
OrderEntity
OrderPO
于是问题来了,为什么不能从头到尾只用一个 Order?
1 先把这几个概念搞清楚
废话不多说,先上图。
看完这张图,其实就已经能有个大概的想法了。
1.1 DTO:Data Transfer Object,数据传输对象
简单来说,DTO 就是负责传数据的。
它通常出现在接口边界上,用来接收请求、返回响应,或者在服务之间传递数据。
DTO 最关心的是:这份数据要传给谁,用哪些字段最合适。
比如注册用户时,前端传来用户名、密码、邮箱,这时就很适合用 DTO
publicclassRegisterUserDTO{privateStringusername;privateStringpassword;privateStringemail;}1.2 VO:View Object,视图对象
VO 在日常 Java 后端语境里,通常指 View Object,也就是返回给前端看的对象。
它和 DTO 最大的区别在于:DTO 更强调“传”,VO 更强调“看”。
比如数据库里用户表可能长这样:id username password gender create_time
但前端用户详情页真正需要的,也许是这样:用户 ID 用户名 性别文案 注册时间 不要密码
于是就可以定义一个 VO:
publicclassUserProfileVO{privateLonguserId;privateStringusername;privateStringgenderText;privateStringregisterTime;}顺带一提,在DDD语境里,VO 也可能是 Value Object(值对象),那是另一套概念。
本文说的 VO,统一指 View Object。
1.3 BO:Business Object,业务对象
BO 的重点是:业务。
DTO 不适合承载复杂业务逻辑,DO/PO 又更偏数据库,
那业务处理中间状态放哪里?这时 BO 就有意义了。
比如创建订单时,前端传来商品 ID、数量、优惠券 ID,
但真正进入业务处理后,系统可能还要补充:商品单价 原价总额 优惠金额 运费 最终应付金额 是否命中风控规则
这时候就可以定义一个 BO:
importjava.math.BigDecimal;importjava.util.List;publicclassOrderBO{privateLonguserId;privateList<Long>productIds;privateBigDecimaltotalAmount;privateBigDecimaldiscountAmount;privateBigDecimalfreightAmount;privateBigDecimalpayAmount;}关于BO 和 DTO 的区别?
- DTO 解决的是“怎么传”
- BO 解决的是“怎么处理业务”
例如:
publicclassCreateOrderDTO{privateLonguserId;privateList<Long>productIds;privateLongcouponId;}这是输入。
publicclassOrderBO{privateLonguserId;privateList<Long>productIds;privateBigDecimaltotalAmount;privateBigDecimalpayAmount;}这是业务处理中间态。前者更像“原材料”,后者更像“进入加工流程后的半成品”。
1.4 DO:Data Object,数据对象
DO 通常指和数据库记录对应的对象。
它最常出现的地方是持久化层,比如 MyBatis 的 Mapper、DAO、Repository。
DO 的目标很直接:方便和数据库字段打交道。
例如一张用户表 t_user,对应的 DO 可能长这样:
importjava.time.LocalDateTime;publicclassUserDO{privateLongid;privateStringusername;privateStringpassword;privateIntegergender;privateLocalDateTimecreateTime;}这个类的字段一般会比较贴近数据库表结构。
它不关心前端展示是否友好,也不关心业务表达是否自然,
它首先服务的是“存”和“查”。
DO 主要服务于数据库操作,一般不暴露给前端。很多项目里喜欢直接把 DO 返回给前端,这是不推荐的。因为 DO 往往会带出一些不该暴露的信息,比如密码、内部状态码、审计字段等信息。
1.5 PO:Persistent Object,持久化对象
PO 的中文一般叫“持久化对象”。
从实际开发看,PO 和 DO 经常是高度重合的。
很多项目里两者几乎就是一回事,只是团队叫法不同。
例如:
importjava.time.LocalDateTime;publicclassUserPO{privateLongid;privateStringusername;privateStringpassword;privateIntegergender;privateLocalDateTimecreateTime;}你会发现,它和刚才的 UserDO 没什么本质区别。
严格来说,DO 更强调“数据对象”,而 PO 更强调“持久化对象”
但在绝大多数普通后端项目里,你完全可以把它们理解为同一类东西。
真正要紧的,不是死抠这两个词,而是知道:
它们都偏数据库层,不应该直接承担接口展示职责。
1.6 Entity:实体
Entity 是最容易混淆的一个概念,因为它在不同语境下意思不完全一样。
第一种理解:ORM 实体
在很多 CRUD 项目里,Entity 就是 ORM 框架映射数据库表的实体类,本质上和 DO/PO 很接近。
比如:
importjakarta.persistence.Entity;importjakarta.persistence.Id;importjakarta.persistence.Table;importjava.time.LocalDateTime;@Entity@Table(name="t_user")publicclassUserEntity{@IdprivateLongid;privateStringusername;privateStringpassword;privateIntegergender;privateLocalDateTimecreateTime;}这种场景下,Entity 基本可以看作数据库实体类。
第二种理解:DDD 里的实体
在领域驱动设计里,Entity 不只是“映射数据库表的类”,
而是具有唯一标识、可变状态和生命周期的业务对象。
比如“订单”是实体,因为它有唯一订单号、状态会变化,而且生命周期明确。而“金额”通常不是实体,更像值对象。
在普通项目里:Entity 常常 ≈ DO/PO在 DDD 项目里:Entity 更强调业务身份和生命周期 ; DO/PO 更强调持久化存储
2 用一个小例子把概念传起来
还是举一个实际的例子,才能更让人明白他们之间的区分。
以“查询用户详情”为例:
1)前端传参:DTO
publicclassUserDetailQueryDTO{privateLonguserId;}2)数据库查询:DO / PO / Entity
publicclassUserDO{privateLongid;privateStringusername;privateStringpassword;privateIntegergender;}3)业务处理中间态:BO
publicclassUserBO{privateLonguserId;privateStringusername;privateIntegergender;privatebooleanvip;}4)返回前端:VO
publicclassUserDetailVO{privateLonguserId;privateStringusername;privateStringgenderText;privateStringvipTag;}一个很典型的流转过程就是:
UserDetailQueryDTO -> UserDO -> UserBO -> UserDetailVO
看到这里你会发现,它们并不是“几个长得差不多的类”,而是在同一个业务链路上,分别站在不同位置。
3 别教条主义——这些概念不是必须全部用上
看到这里,你大概率已经“懂了这些名词”。
但如果你下一步的想法是:
“以后每个项目我都要把 DTO、VO、BO、DO、PO、Entity 全都用一遍”
那其实是走向了另一个极端。
这一章想讲清楚一件更重要的事:
这些概念是工具,不是规范;是手段,不是目标。
3.1 一个更真实的情况:大多数项目其实没这么复杂
在很多实际项目里(尤其是校招生的项目、个人博客项目、简单业务系统),其实业务逻辑不复杂,数据模型也不多。
这种情况下,如果你一上来就强行拆 DTO / VO / BO / DO / PO / Entity,每一层都写一堆转换代码,每个接口都定义三四个对象,结果往往不是“设计优雅”,反而带来了代码量暴涨、可读性下降,修改成本变得很高,自己都维护不过来。
分层本来是为了解决复杂度,但在简单场景里,反而会制造复杂度。
比如一个简单的用户查询接口:
publicclassUserQueryDTO{privateLonguserId;}publicclassUserBO{privateLonguserId;privateStringusername;}publicclassUserDO{privateLongid;privateStringusername;}publicclassUserVO{privateLonguserId;privateStringusername;}然后在代码里疯狂转换:
UserBObo=convert(dto);UserDOdo=convert(bo);UserVOvo=convert(do);问题是:
- 这个场景真的需要 BO 吗?
- DTO 和 VO 的字段完全一样,有必要拆吗?
很多时候答案是:没必要。
当出现以下信号时,就该考虑拆分了:
1)前后端字段不一致
比如:
- 数据库存 gender = 1
- 前端需要 “男”
这时候 VO 就有价值。
2)存在敏感字段
比如:
- DO 里有 password
- 前端不应该看到
必须用 VO 做隔离。
3)业务逻辑开始复杂
比如:
- 涉及金额计算
- 多数据源组合
- 状态流转复杂
可以引入 BO,避免 Service 变成“大杂烩”。
3.2 一个很重要但常被忽略的点:命名不是最重要的
很多人会纠结到底叫 DO 还是 PO?到底叫 VO 还是 ResponseDTO?
其实这不是关键问题.真正重要的是要明白这个对象干什么用?它应该出现在哪一层??它能不能跨层乱用
你完全可以这样设计:
UserRequestUserResponseUserEntity
而不是强行套 DTO/VO/DO 的命名体系。名字可以不同,但职责不能混乱。
写在文后
期待您的一键三连!如果有什么问题或建议欢迎在评论区交流!
