您的位置:首页 > 酒世界酒世界

一文讲清DTO、VO、BO、PO、DO、DAO:各种XO对象与贫血富领域模型

2026-10-02人已围观

一文讲清DTO、VO、BO、PO、DO、DAO:各种"XO"对象与贫血/富领域模型

Java开发里最容易被一堆"XO"缩写绕晕的,莫过于DTO、VO、BO、PO、DO、DAO这些对象概念。它们名字相近,职责却大不相同。本文用通俗的语言,把这些对象的概念、区别、联系一次性讲透,并顺便把POJO、Java Bean、贫血模型、富领域模型这几个基础概念也梳理清楚。

先搞懂两个"老祖宗":POJO 与 Java Bean

在认识各种"XO"之前,得先弄清楚它们的共同底座——POJO和Java Bean。这两个概念是所有"XO"对象的起源。

POJO(Plain Old Java Object),字面意思是"单纯老旧的Java对象"。它的核心特征只有一个:不依赖任何框架,除了JDK之外不继承特定父类、不实现侵入性接口,就是一个干干净净的Java对象。POJO这个概念由Martin Fowler等人在2000年提出,初衷是为了消除EJB时代Entity Bean那种重框架绑定的复杂性。

?? POJO的关键在于"自由"——它在JDK下就能编译运行,没有应用服务器要求,没有必须继承的基类,没有强制实现的接口。

Java Bean则是POJO的一个"规范版"。一个类要想成为Java Bean,必须同时满足三个条件:

类是具体的、公共的

提供无参构造函数

为内部字段提供规范的 getter/setter 方法

框架之所以看重Java Bean,是因为可以通过反射访问对象的字段值,从而简化代码编写。比如JSP标签中使用Student类,如果不符合Java Bean规范,就无法实例化对象、无法设置firstName等字段值。

两者的关系是:POJO是更基础的概念,符合Java Bean规范的对象可视为POJO的特例;反过来,一个仅遵循Java Bean规范、不依赖任何框架的类,也可以认为是POJO。但Enterprise Java Bean由于依赖框架,绝不属于POJO。

需要澄清一个常见误解——POJO并非只有getter/setter的贫血对象。它的本质是一种设计模式,目的在于保持对象独立于框架,至于内部定义什么样的成员,并不是判定POJO的标准。

各种"XO"对象全景图

下面按照"数据从数据库到前端展示"的流转顺序,把各个对象捋一遍。

?? PO:持久化对象(Persistent Object)

PO对应数据库中表的字段,可以理解为数据库中的一条记录在Java对象里的映像。最形象的理解:一个PO就是数据库中的一条记录。

只要对象需要持久化,它就成为持久化对象——这与采用什么样的建模方法、什么样的设计方法没有关系。理想的领域模型对象应该是一个POJO,不依赖于除语言之外的任何框架。

?? DTO:数据传输对象(Data Transfer Object)

DTO用于在进程间传递数据,目的是减少方法调用的数量。在设计分布式应用时,应当尽可能设计粗粒度的服务,粗粒度服务可以减少服务调用的次数,从而减少不必要的网络通信,同时也能避免对分布式事务的支持。

举个具体例子:假设一张表有100个字段,那么对应的PO就有100个属性。但界面上只需要显示10个字段,客户端用Web Service获取数据时,没有必要把整个PO对象传到客户端——这时就可以用一个只有10个属性的DTO来传递结果,既精简了数据量,也不会暴露服务端表结构。

DTO最好设计为一个Java Bean:定义为公开的类,具有默认构造函数和getter/setter方法,这样有利于一些框架通过反射来创建与组装。它还应该是一个贫血对象,因为目的是传输数据,没有必要定义封装逻辑的方法,但可能需要为其定义转换方法。

??? VO:视图对象(View Object)

VO主要用于后端控制器服务和前端UI之间的数据传递,把某个指定页面(或组件)的所有数据封装起来。由于它服务于展示层,自然也属于DTO对象的范畴,我们可以沿用DTO模式来处理VO。

?? DTO与VO的本质区别:DTO代表服务层需要接收和返回的数据,VO代表展示层需要显示的数据。绝大部分应用场景下两者属性基本一致,但在设计层面概念上应该分开。

?? BO:业务对象(Business Object)

BO是用来描述业务概念的语义对象。在领域驱动设计中,业务对象的业务逻辑恰好也是关注的核心,可以认为领域驱动设计建立的领域模型皆是业务对象。一个BO可以封装多个PO,通常需要将BO转化成PO才能进行数据的持久化;反之,从数据库得到的PO,也需要转化成BO才能在业务层使用。

为了避免混淆,建议在领域驱动设计中不使用BO这个概念,直接用领域对象来表达。

?? DO:领域对象(Domain Object)还是数据对象(Data Object)?

DO是最容易引起歧义的一个缩写,目前业界存在两个主流版本:

来源 DO含义 等同概念

阿里巴巴开发手册 Data Object(数据对象) 等同于PO

领域驱动设计(DDD) Domain Object(领域对象) 等同于BO

这种双义性正是DO最棘手的地方。在DDD中,领域模型对象和业务对象(BO)是同一回事;而在数据层语境中,DO又是数据对象的简称。因此在团队协作中,一定要事先约定DO到底指哪一种,否则极易造成误解。

??? DAO:数据访问对象(Data Access Object)

DAO负责对持久化对象进行持久化,实现数据的访问。它是J2EE标准设计模式的接口之一,主要负责对数据的访问(注意:是对数据的访问,不是对数据库的访问)。

DAO可以持久化领域模型对象,但对领域模型对象的边界没有任何限制。

贫血模型 vs 富领域模型:对象里到底该不该放逻辑

理解了各种"XO"之后,一个更深层次的问题浮出水面:领域模型对象里,到底要不要放业务逻辑?

这就引出了著名的"贫血领域模型(Anemic Domain Model)"话题。这个术语由Martin Fowler创造,用来形容一种"不健康"的领域模型——它违背了面向对象设计的关键原则,即"数据与行为应该封装在一起"。

四种模型形态对照

为了更清晰地理解,业界通常把领域模型分为四种形态:

1. 失血模型

模型仅仅包含数据的定义和getter/setter方法,业务逻辑和应用逻辑都放到服务层中。这种类在Java中叫POJO,在.NET中叫POCO。

2. 贫血模型

贫血模型中包含了一些业务逻辑,但不包含依赖持久层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层中。可以看出,贫血模型中的领域对象是不依赖于持久层的。

3. 充血模型(富领域模型)

充血模型中包含了所有的业务逻辑,包括依赖于持久层的业务逻辑。所以,使用充血模型的领域层是依赖于持久层,简单表示就是 UI层→服务层→领域层→持久层。这才是符合面向对象设计思想的领域设计建模,在领域模型设计建模过程中,定义的实体与值对象都应该建立为富领域模型。

4. 胀血模型

把和业务逻辑不相干的其它应用逻辑(如授权、事务等)都放到领域模型中。

贫血模型的"病症"

当一个实体或值对象除了内部字段之外,就只有一系列的getter/setter方法,它就成为了贫血对象。贫血模型的坏处是多方面的:

影响了对象之间的协作方式

违背了"迪米特法则"与"信息专家模式"

导致了"特性依恋"坏味道的产生

破坏了封装,导致领域服务形成一种事务脚本的实现

完全和面向对象设计背道而驰,更像是一种面向过程设计

?? 贫血模型的根本问题在于:它引入了领域模型设计的所有成本(尤其是对象到数据库的O/R映射成本),却没有带来任何好处。

构建富领域模型的两个要求

要从贫血走向富领域,需要满足两点要求:

1. 领域模型仅仅封装领域逻辑,尽可能不掺杂访问外部资源如MyBatis、Redis、Kafka等的技术实现

2. 根据角色构造型分配职责,各司其职,共同协作

结合团队实践,一种折中建议是:实体采用贫血模型,让实体和领域服务共同构成领域模型。这样可以使实体具备业务知识,但又不至于太过臃肿。

各对象在分层架构中的协作关系

把上述各种对象放到经典的分层架构中,调用顺序如下:

1. 客户端浏览器发起HTTP/JSON请求

2. 服务器端过滤器过滤请求,做安全、编码、session管理等处理,拦截器拦截请求做记录日志、访问统计等操作,并装载上下文信息

3. 控制器接收前端传过来的DTO对象,并调用本地服务或者远程服务

4. Service分为接口与实现两部分。Service服务接口接受控制层的远程或本地调用,接收DTO对象,调用本地BO中的业务逻辑,并将BO返回的业务结果(DTO对象)返回给控制层

5. BO分为接口与实现两部分。BO服务接口接受Service的本地调用,接收DTO对象,实现业务逻辑,使用DO对象调用DAO来访问数据库,将DAO返回DO对象转换为DTO对象,返回给Service

6. DAO层接受BO层的调用,接收DO对象,实现具体的SQL逻辑,访问数据库,并将返回的数据模型返回给BO

7. 数据访问层实现数据持久化工作,将数据库的库表结构与java对象做映射

可以看到,DTO贯穿了控制器、Service、BO之间的数据传输;DO/PO专注于数据访问层;BO承载业务逻辑。各司其职,协作顺畅。

五个常见问答

Q1:既然DTO和VO属性大多一致,为什么还要分两个?

A:这是实现层面与设计层面的区别。在实现层面,对于绝大部分应用场景来说,确实没必要多此一举,DTO和VO可以合二为一。但在设计层面,概念上应该存在VO和DTO,因为两者有着本质的区别:DTO代表服务层需要接收的数据和返回的数据,而VO代表展示层需要显示的数据。如果强行合并,会在设计意图上造成混淆。

Q2:DO到底指Domain Object还是Data Object?

A:这取决于团队约定。阿里巴巴开发手册中,DO(Data Object)等同于PO;而在DDD领域驱动设计中,DO(Domain Object)等同于BO。由于两者容易混淆,建议在项目启动时明确约定DO的含义,并在代码规范文档中固定下来。

Q3:POJO一定是Java Bean吗?

A:不一定。POJO强调不依赖框架,而Java Bean额外要求有默认无参构造函数和规范getter/setter。一个POJO如果遵循了Java Bean的设计规范,可以成为一个Java Bean;但POJO本身并不需要强制具备无参构造函数或属性存取方法。反过来,一个Java Bean如果仅仅遵循了设计规范,并没有依赖任何框架,也可以认为是一个POJO。

Q4:为什么贫血模型被诟病?

A:因为它违背了面向对象设计的关键原则——"数据与行为应该封装在一起"。贫血模型中的对象除了内部字段之外就只有一系列的getter/setter方法,导致:影响对象间协作方式、违背迪米特法则与信息专家模式、产生特性依恋坏味道、破坏封装、使领域服务形成事务脚本实现。它引入了领域模型设计的所有成本,却没有带来面向对象的好处。

Q5:在实际项目中,实体应该用贫血模型还是富领域模型?

A:结合团队实践,一种稳妥的建议是:实体采用贫血模型,实体和领域服务共同构成领域模型。这样既可以让实体具备一定的业务知识,又不至于让单个实体类变得太过臃肿。同时要记住构建富领域模型的两个要求:领域模型仅仅封装领域逻辑,尽可能不掺杂访问外部资源的技术实现;根据角色构造型分配职责,各司其职,共同协作。

回到原点看,各种"XO"对象的本质,是为了在软件分层中各司其职:PO/DO贴近数据存储,DTO/VO服务于数据传输与展示,BO/DO承载业务逻辑,DAO专注数据访问。而POJO与Java Bean则是它们的共同底座——一个追求框架无关的自由,一个提供反射友好的规范。理解了这条主线,再看各种"XO"就不会再混淆了。