[Java EE] 企业级Java Web应用的概念辨析: POJO(PO / DTO / VO) | BO/DO | DAO

概念不清,会很影响开发中的逻辑性和条理性,进而影响接口设计,代码编写的质量。
网络上大家对这些个概念的探究很多,但终究没有一个统一的说法。
不论哪家解释,我觉得最重要的是: 1)词汇之间的解释统一; 2)词汇之间的联系不冲突,能自圆其说,自成一体。
在自己初步探索后,个人更认同这样的理念(如下)。
若读者凑巧阅读到本文,欢迎一起探讨。

1 POJO := Plain Ordinary Java Object

POJO := Plain Ordinary Java Object

  • 定义:
  • 简单、朴素的Java对象。它是一个简单的 Java Bean,通常只包含属性、getter/setter 方法以及一些基本的逻辑。
  • POJO 是一个普通的 Java 对象,不依赖于任何框架或接口。
  • POJO 与 PO / DTO / VO 的关系?

一般的,POJO 默认指 PO(数据实体类)

POJO ⊇ { PO, DTO, VO }
  • 特点
  • 不依赖于任何框架(如 Spring、Hibernate)。

但一般会使用 lombok (@Data / @Builder / ...) 这类代码开发的提效工具库

  • 不实现任何特定的接口或不继承特定的类。
  • 通常用于表示简单的数据结构
  • Demo
public class User {
    private String name;
    private int age;

    // Getters and Setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

PO := Persistent Object

  • 定义:
  • 持久化对象(Persistent Object)

用于表示数据库中的一条记录,映射成Java对象

  • PO 是与数据库表直接映射的对象

PO仅用于表示数据库数据,无任何其它的数据操作

  • 通常用于 ORM(对象关系映射)框架中,如 Hibernate、MyBatis 等。
  • PO 的每个属性通常对应数据库表中的字段。
  • 特点:
  • 与数据库表结构一一对应。
  • 通常包含与数据库交互的注解或配置。
  • Demo:
@Entity
@Table(name = "user")
public class UserEntity {//或 User
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

    // Getters and Setters
}

DTO := Data Transfer Object

  • 定义
  • 数据传输对象(Data Transfer Object)
  • DTO 是用于在系统内不同微服务不同系统之间传输数据的对象,通常用于减少网络传输的数据量或简化数据传递。
    DTO通常用于【不同Web服务之间(RestFul / RPC调用 / WebService调用等)】
  • DTO 通常不包含业务逻辑
  • 特点
  • 用于在系统内不同微服务不同系统之间传输数据的对象。
  • 通常只包含数据字段和 getter/setter 方法。
  • 可以组合多个 POJO / PO 的数据。
    一个DTO可以对应多个从存储层返回的PO(Persistent Object)的json数组
    这里可以使用AutoMapper来进行自适配,减少两种对象适配的代码量。
  • Demo
public class UserDto {
    private String name;
    private String email;

    // Getters and Setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

VO := View Object(个人更认同) := Value Object

  • 定义
  • 视图对象/值对象
  • 通常用于业务层之间的数据传递,用于封装业务逻辑中的数据。

VO 对象通常是不可变的,且不包含业务逻辑

  • VO for request and response of users or controller layers

用于表示一个与前端界面交互的Java对象;用于展示层,它的作用是把某个指定页面(或页内组件)的所有数据封装起来。

  • 举例:
  • 展示层用户信息Dto传送过来男性显示成帅哥(客户端1),或者显示成靓仔(客户端2);将帅哥或者靓仔,转换成男性,以DTO形式请求服务端。
  • 视图模型VO可以对应客户端的网页显示,同样的DTO比如性别男,可以对应多个VO进行显示,即可以对应多个客户端,比如VO1把性别男显示成帅哥,VO2把性别男显示成靓仔等等。
  • VO VS PO:
  • PO: 纯数据库表记录的完全映射;
  • VO: 仅包含前端需要展示的数据/属性;(需要对前端/用户屏蔽部分字段;减小数据传输量)
  • 特点
  • 通常用于展示层与服务层之间的数据传输。
  • 不可变性,通常不包含业务逻辑。
  • Demo
public class UserVO {
    private String username;
    private String email;

    // Getters and Setters
}

QO := Query Object

  • 定义:
  • QO 是用于封装查询条件的对象,通常用于复杂查询场景。
  • QO 可以包含分页信息、排序规则、过滤条件等。
  • 特点:
  • 用于封装查询条件。
  • 通常用于 DAO 层或 Service 层。
  • Demo
public class UserQuery {
    private String username;
    private int page;
    private int size;
    private String sortBy;

    // Getters and Setters
}

2 BO/DO := Business Object / Domain Object

BO/DO := Business Object / Domain Object

  • 定义:
  • 业务对象(Business Object) / 业务对象/领域对象
  • BO 是封装业务逻辑的对象,通常包含业务规则和逻辑。
  • BO 可以组合多个 PO 或 DTO,并对其进行操作。
BO 包含了业务逻辑,常封装了对DAO,RPC等的调用;
可进行PO,VO,DTO之间的转换;
通常属于业务层,但要区别于直接对外提供服务的服务层;
BO提供基本业务单元的基本业务操作,在设计上属于被服务层业务流程调用的对象,一个业务流程可能需要调用多个BO来完成
  • 特点:
  • 包含业务逻辑。
  • 可以组合多个 PO 或 DTO。
  • 通常用于 Service 层。
  • Demo
public class UserBO {
    private UserPO userPO;
    private List<OrderPO> orders;

    public double calculateTotalOrderAmount() {
        return orders.stream().mapToDouble(OrderPO::getAmount).sum();
    }

    // Getters and Setters
}

3 DAO

Database Access Object

  • 定义:
  • 数据库访问对象(Database Access Object)
  • DAO 是用于封装对数据源的访问的对象,通常用于抽象和封装对数据库的 CRUD 操作。

DAO 层是数据访问层,负责与数据库交互。
DAO 对象访问数据库,包括增删查改等操作;同POJO一起使用

  • 特点:
  • 面向 Service层,提供对数据库的访问接口。
  • 隐藏数据库操作的细节。
  • 通常与 PO 或 Entity 一起使用。
  • Demo
public interface UserDAO {
    UserPO getUserById(Long id);
    void saveUser(UserPO user);
    void updateUser(UserPO user);
    void deleteUser(Long id);
}

// 实现类
public class UserDAOImpl implements UserDAO {
    @Override
    public UserPO getUserById(Long id) {
        // 数据库查询逻辑
    }

    @Override
    public void saveUser(UserPO user) {
        // 数据库保存逻辑
    }

    @Override
    public void updateUser(UserPO user) {
        // 数据库更新逻辑
    }

    @Override
    public void deleteUser(Long id) {
        // 数据库删除逻辑
    }
}

J 提问环节

  • 假定: 我们有一个面试系统,数据库中存储了很多面试题,通过 web 和 API 提供服务。可能会做如下的设计:
  • 数据库表:面试题
  • PO:
  • VO:
  • DTO:
  • DAO:
  • BO:

Y 最佳实践 for Java Web 应用

来源网络,并非权威,仅供参考

基础 Java Bean

  • BaseObject

Controller 层 : 与 前端视图层(VO) 或 面向第三方应用的传输层(DTO)

  • 本层操纵 Java Bean 的类一般命名: XxxController (主流/推荐)

  • BasePage
  • BaseRequest
  • BaseResponse
  • PageRequest
  • PageResponse
  • XxxRequest 及 XxxPageRequest

如: GetConfigRequest(根据API进行一一定义) / SetConfigRequest

API 命名规范 (仅使用1种)

增 : createXxx [√] / addXxx [x] / appendXxx [√]
删 : deleteXxx [√]
查 : exportXxx / getXxx [√] / findXxx [x] / queryXxx[x] 
改 : setXxx[] / updateXxx[]

复数/批量 : ...List[√] / ...s[x]
  • XxxPageResponse(XxxDto/XxxVo) 及 XxxResponse(XxxDto/XxxVo)

  • 场景总结

  • BaseRequest(非分页请求场景)
  • PageRequest(分页请求场景)
  • XResponse<PageResponse> (分页响应场景 / 非分页响应场景) 【通用响应/推荐】
  • XResponse<BaseResponse>(非分页响应场景)

Service 层 : 处理业务逻辑、将 PO 与 VO / BO / DO / DTO 互转

  • 本层操纵 Java Bean 的类一般命名: XxxService / XxxServiceImpl (主流/推荐)

Service 层的协作模式

  • 模式1: Service + DAO

DAO中只做CRUD及类似的简单操作(称之为功能点,不包含业务逻辑)
Service中通过调用一个或多个DAO中的功能点来组合成为业务逻辑.
Service的数量应该由功能模块来决定。

在这种模型中业务逻辑是放在Service中的,事务的边界也应该在Service中控制. 
当然,直接在Service中控制事务会引入非业务逻辑的代码,幸好Spring的AOP可以解决这个问题,这也是引入Spring的原因之一.
如果说到缺点,就在于对某些对象的操作就是简单的CRUD,Service层显得累赘
  • 模式2: Service + BO, 而 BO = DAO + 业务方法

在原先DAO的基础上添加业务方法,形成BO对象。
需要注意的是: BO中的业务方法往往是针对一个实体对象的,如果需要跨越多个实体对象,则方法应该放在Service中。
当然小规模的应用中,没有Service,完全是DAO或BO也是可以接受的。

  • 参考文献

场景:PO 转 VO / DTO / BO (必读)

方案1:PO 作为 VO 的构造器入参(组合式)

UML关系:组合关系

案例

// 实体类(PO类)
public class Goods {
    private Long id;
    private String name;
    private BigDecimal price;
    private String description;
    // 其他属性和方法...
}

// VO类通过组合实现
public class GoodsVO {
    private Long id;
    private String name;
    private BigDecimal price;

    // 构造方法或工厂方法,将实体类转换为VO类
    public GoodsVO(Goods goods) {
        this.id = goods.getId();
        this.name = goods.getName();
        this.price = goods.getPrice();
    }

    // Getter 和 Setter 方法...
}

通过组合,我们实现了 VO 类对实体类的定制化展示,同时保留了灵活性,使得 VO 类的设计不受实体类的限制。
但会重复编写大量的 set 和 get 代码,对象之间频繁复制

方案2:继承方式的VO设计(继承式)【不推荐】

  • VO继承 PO 实体类,转而调用实体类的部分属性,可以达到复用相同属性的效果。

如果你决定使用继承,下面是一个简单的示例。
在这个例子中,GoodsVO 类继承自 Goods 类,通过继承,GoodsVO 类拥有了 Goods 类的所有属性和方法。

// VO类通过继承实现
public class GoodsVO extends Goods {
    // 新增或覆盖需要展示的属性
    private String displayInfo;

    // 构造方法或工厂方法,将实体类转换为VO类
    public GoodsVO(Goods goods) {
        // 调用父类构造方法,复制基本属性
        super.setId(goods.getId());
        super.setName(goods.getName());
        super.setPrice(goods.getPrice());
        super.setDescription(goods.getDescription());

        // 初始化VO类特有的属性
        this.displayInfo = "Additional information for display";
    }

    // Getter 和 Setter 方法...
}
  • 通过继承,我们可以在 GoodsVO 类中新增或覆盖需要展示的属性,实现对特定场景的定制化。

但需要注意的是,继承关系通常带来类之间的紧密耦合,可能会限制类的扩展性和灵活性

  • 这种方式生成的文档很不清晰,每个接口都有一堆没返回给前端的字段;业务中的额外字段会污染VOPOJO(PO) 承担的责任太重了,

方案3:开源的Bean转换工具 MapStruct 【推荐】

参见文档:

方案4:开源的Bean转换工具 MapStruct-Plus 【推荐】

参见文档

方案5:开源的Bean转换工具 Spring BeanUtils

参见文档

原则上,不推荐用于做Java数据对象的转换工具;
但其工具类不仅限于做数据对象的转换功能,还有一些其他不错的方法(按需使用即可)

方案5:开源的Bean转换工具 Apache Commons BeanUtils

参见文档

Repository(DAO) 层 : 定义、访问、操纵数据实体(PO/Entity)

  • 本层的 Java Bean,主要通过包路径(xx.entity) 和 ORM的注解(如:@Entity)区分。

  • 本层操纵 Java Bean 的类一般命名: XxxRepository (主流/推荐) / XxxMapper / XxxDAO

R FAQ

Q:由“为什么VO不能继承PO?” 引出的为什么组合优于继承?

Q:为什么组合优先于继承?DTO / VO / DO 的真正意义?

  • 不少工程师在个人项目中喜欢直接使用各种 model 来继承 po,单纯的因为可以直接省去写各种重复 gettersetter 的步骤,并且不需要使用各种对象拷贝工具(如:MapStruct / MapStruct-Plus / Spring 的 BeanUtils.copyProperties等)。
    但是,搞vodopo,还有其他各种 Xo,是正为了解藕它们之间的联系,而继承却是建立它们之间的耦合关系,确实是会限制类的扩展性和灵活性

比如:当表结构做了增减,do 变了 vo 也变了,你的接口返回项也会变化,线上的项目容易翻车(虽然我觉得表结构不会那么轻易变化),也不利于节约流量信息安全,会返回很多不必要的字段给前端,容易被不怀好意的人推测出数据库的字段。

  • 面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承

同样地,在《阿里巴巴Java开发手册》中有一条规定:谨慎使用继承的方式进行扩展,优先使用组合的方式实现。

X 参考文献

posted @ 2020-09-15 13:11  千千寰宇  阅读(600)  评论(0)    收藏  举报