阿里的Java编码规范曾提到过:类名必须遵循驼峰命名法,但“DTO/BO/PO”等可以例外。这几个“O”都是什么呢?有必要在代码里搞这么多“O”吗?如果一定要用,要怎样才能用好它们呢?

这里聊聊我的看法。 各种O 图片来自网络

这些O都是些什么?

子曰:必也正名乎。在聊它们之前,先聊聊它们的含义。

DTO

DTO是指Data Transfer Object,用来在网络上传输数据。

DTO并不关注接口协议——HTTP、MQ、Dubbo、gRPC、甚至更久远一些的WebService、RMI,也不关注序列化协议:HTTP、hession、gRPC、二进制流、JSON、XML等等。DTO只关注在数据交给协议底层之前的数据封装方式。

例如,我们会在Dubbo/Controler接口中使用DTO:

public interface UserFacade{
    public UserQueryResDto queryUser(UserQueryReqDto req);
}
public class UserControler{
    public UserQueryResDto queryUser(UserQueryReqDto req){
        // 略
    }
}

BO

BO基本上没有歧义,就是指Business Object,即业务对象。

业务对象有点像我们常说的领域模型,但是比领域模型的范围要小——BO往往只需要关注一项业务,而领域模型则是对全局的建模。也就是说,BO是领域模型的一个子集;而把所有BO组合起来,应该就能得到一个完整的领域模型了(莫名的想起了“把你们的力量联合起来,我就是地球超人”哈哈)。

考虑到一个业务中的数据关系可能相当复杂,BO也可以用ER图来描述(虽然这东西好像很少有人用了,即使用了也是用来描述数据库关系的,但是ER图其实很强的)。

例如,我们会在Business/Serivce接口中使用Bo:

public interface UserBusiness{
    public UserBo queryUser(UserBo user);
}
public interface UserService{
    public UserBo queryUserByName(String userName);
}

PO

PO也没什么歧义,指的就是persistent Object,即持久化对象。

PO的概念应该是伴随着ORM(Object Relational Mapping)出现的。看名字就知道,ORM的作用是把数据库中的关系型数据映射为系统中的数据对象,这里的“数据对象”就是指的PO。

一般情况下,一个PO类对应的就是一张数据库表;而一个PO实例对应的就是表中的一行数据。当然,面对1:n关系时,PO类与数据库表之间的关系也会更复杂。这里按下不表。

例如,我们会在Dao/Mapper接口中使用Po:

public interface UserDao{
    public UserPo selectUserByName(String userName);
}
public interface UserMapper{
    public UserPo selectUser(UserPo whereClause);
}

除了VO/BO/PO之外,还有一些常见的“O”,例如DO/VO/UFO/CTO等,还有几个不那么“O”的POJO/Model/Bean等。它们又是什么呢?

DO

DO是指Domain Object,即领域对象。Model则是指模特模型。他们俩与BO类似,都是用来描述领域模型的。

VO

关于VO是什么,现在有两种说法:一种认为VO是View Object的缩写,另一种则认为VO是指Value Object

但不管V是View还是Value,一般都认为VO主要用于在Web上传递数据。从这一点上看,View Object更加贴切一些。因为说到“View”,我们一般都会把它关联到MVC中的V——尽管数据库中也有“View”,但实际上很少会用到它。而说到MVC,我们一般都会把它与Web关联起来。因此,“View Object”一般是指在MVC中用于向View传递数据的类。

相比之下,Value Object一般是指像Integer/Long这种类,其核心在于表述一个“值”而非一个“引用”,其用途主要是对值进行比较、运算等操作。所以我更倾向于把VO解读成View Object

VO与DTO有相近、想通之处。它们就像装饰者模式和代理模式那样——看起来像鸭子,听上去像鸭子,吃起来也像鸭子,只是北京做的叫北京烤鸭,南京做的叫南京盐水鸭。例如,UserDto与UserVo可能会是这样的:

public class UserDto{
    private String carrer;
    // getter and setter
}
public class UserVo{
    private CarrerEnum carrer;
    // getter and setter
}

POJO

POJO的全称也有点歧义:有说叫Plain Ordinary Java Object的,也有说叫Plain Old Java Object的。而Bean呢?我没有找到它的通用中文译名,只知道任何一个Java类只要符合一定的规范,就可以称为JavaBean。与其它的“O”不同,POJO和Bean主要关注类的写法,而非类的应用场景。

有必要搞这么多的O吗?

规模较小的公司没有必要搞那么多的CEO/CFO/CIO/CTO;软件系统也是一样。

在一些简单情况——例如表模式中,我们可以一个O到底,从数据库、到中间处理、到数据传输、页面展示,都使用一个对象。

例如,我们有一张表t_uesr,同时有一个用户管理界面,只负责对这张表做增删改查处理。这时,我们即使写了不同的UserVo/UserBo/UserPo类,也会发现它们其实长得一模一样——因为这里的逻辑太简单了,只需要一个User类就可以了。把这样简单的事情搞复杂,非常的得不偿失。

public class UserController{
    public User query(String userName){
        return userService.queryUserByName(userName);
    }
}
public class UserServiceImpl implements UserService{
    public User queryUserByName(String userName){
        return userDao.selectUserByName(userName);
    }
}
public class UserDaoImpl implements UserDao{
    public User selectUserByName(String userName){
        // 略
    }
}
public class User{
    private String userName;
    private Integer age;
    private CarrerEnum carrer;
    // getters and setters
}

但是,如果业务逻辑变复杂了呢?

例如,对t_user中的用户,我们在查出表中的基本数据之外,还要根据t_topic表中的数据来计算其活跃度,并根据第三方服务返回的结果计算其声望值。这时,由于接口出参发生了变化,User类中就需要增加活跃度和声望值这两个字段;同时,为了计算着两个参数的值,User类中还需要增加t_topic表和第三方服务返回的一些中间数据。

由于User类承担了数据库映射的功能,而活跃度、声望值这两个字段与t_user表中任何列都关联不上,因此在User中增加这两个字段就很别扭;而且,t_topic和第三方服务的数据也可能需要放到User类中,但接口返回值中却不需要它们,这使得这几个字段在User类中也变得尴尬起来。

public class User{
    private String userName;
    private Integer age;
    private CarrerEnum carrer;
    // 活跃度和声望值,不映射数据库,但是需要在页面上展示。
    private Integer activity;
    private Integer prestige;
    // topic 和三方数据,不映射数据库也不需要在页面上展示,但是需要在中间计算时使用
   private List<TopicBo> topicList;
   private List<RateBo> rateList;
    // getters and setters
}

这种情况就像一个小公司做成了大企业,一个老板再也无法面面俱到了。这时就需要引入CEO/CFO/CIO/CTO等各种“O”来帮忙了。

怎样用好这些O?

业务变复杂了,系统的复杂度也变高了。而引入这么多的O,更进一步增加了系统的复杂度。要管理好越来越高的系统复杂度,最好的办法就是解耦合:用逻辑简单、结构复杂的模块代替结构简单、逻辑复杂的系统。

从解耦合的角度来看,使用DTO/BO/PO要注意一点:每个对象都限定在自己的功能范围内,不要越雷池一步。

DTO只用来处理接口入参/出参,在对接核心业务时,一定要把它转换成对应的BO。BO只负责在内存中处理数据,在做持久化之时,一定要把它转换成PO;在与第三方交互时,则要转换成对应的DTO。PO只负责处理持久化逻辑,获取到数据、交付给BO后,它就可以寿终正寝了。

public class UserController{
    public UserDto query(String userName){
        UserBo user = userService.queryUserByName(userName);
        // 把业务对象转为传输对象
        UserDto result = convert(user);
        return result;
    }
}
public class UserServiceImpl implements UserService{
    public UserBo queryUserByName(String userName){
        UserPo userPo = userDao.selectUserByName(userName);
        // 把持久化对象转为业务对象
        UserBo user = convert(userPo);
        // 从Topic表中获取Topic
        List<TopicBo> topicList = topicService.queryTopicByUser(user);
        user.setTopicList(topicList);
        // 从第三方服务获取Rate
        List<RateBo> rateList = rateFacade.queryRateByUser(user);
        user.setRateList(rateList);
        // 根据topic和rate计算activity和prestige:
        calculateActivity(user);
        calculatePresitige(user);
        return user;
    }
}
public class UserDaoImpl implements UserDao{
    public UserPo selectUserByName(String userName){
        // 略
    }
}
public class User{
    private String userName;
    private Integer age;
    private CarrerEnum carrer;
    // 活跃度和声望值,不映射数据库,但是需要在页面上展示。
    private Integer activity;
    private Integer prestige;
    // topic 和三方数据,不映射数据库也不需要在页面上展示,但是需要在中间计算时使用
   private List<Topic> issueList;
   private List<Rate> rateList;
    // getters and setters
}

像这样DTO/BO/PO各司其职,每一部分内的逻辑都很简单明了。而且,我们可以采取更加灵活、更加“开闭”的方式来扩展功能:

public class UserServiceImpl implements UserService{
    public UserBo queryUserByName(String userName){
        UserPo userPo = userDao.selectUserByName(userName);
        // 把持久化对象转为业务对象
        UserBo user = convert(userPo);
        return user;
    }
}
public class UserService4ActivityImpl implements UserService{
    public UserBo queryUserByName(String userName){
        UserBo user = delegate.queryUserByName(userName);
        // 从Topic表中获取Topic
        List<TopicBo> topicList = topicService.queryTopicByUser(user);
        user.setTopicList(topicList);
        calculateActivity(user);
        return user;
    }
}
public class UserService4PresitigeImpl implements UserService{
    public UserBo queryUserByName(String userName){
        UserBo user = delegate.queryUserByName(userName);
        // 从第三方服务获取Rate
        List<RateBo> rateList = rateFacade.queryRateByUser(user);
        user.setRateList(rateList);
        // 根据topic和rate计算activity和prestige:
        calculatePresitige(user);
        return user;
    }
}
public class UserService4SomethingIml implements UserService{
    public UserBo queryUserByName(String userName){
        UserBo user = delegate.queryUserByName(userName);
        // 进一步扩展新的需求。
        return user;
    }
}

高内聚、低耦合

写到最后又要开始碎碎念了。

高内聚、低耦合这两个词,从我做程序员的第一天就开始接触到。但是,为什么要高内聚、低耦合?怎样才算高内聚、低耦合?在Spring、微服务、Redis、NoSQL、大数据、AI等等高上大的“招式”一遍一遍刷新我们的技术栈的今天,还有人琢磨这些“内力”问题吗?对我们绝大多数人来说,日常工作是什么?是AI、大数据、NoSQL等等吗?何况,练好“内功”之后再学“招式”,不是更容易上手和掌握吗?

所谓:

举目望星河,低头趟沼泽。
拔足出泥淖, 乘桴海上歌。