关于后台问题的一些思考
1. 关于异常
- 对于异常应该且必须输出到日志中(不能捕获后不处理),但是不应该直接返回给前台;
- 返回给前台的应该是可读的简易信息,并且要注意不要暴露后台保密信息(包括类名、数据库名、文件名、行号等);
- 应给为每类异常建立专门的异常类,而不是直接new Exception();
- 良好的异常类的设计不应该原封不动的继承Exception,直接调用super函数,而应该在异常类内部构造能够更加清楚的表示异常信息的Message。
public class AccountUnderflowException extends Exception {
private static final long serialVersionUID = -6299588017190080876L;
private Account account;
private BigDecimal amount;
public AccountUnderflowException(Account account, BigDecimal amount) {
this.account = account;
this.amount = amount;
}
public String getMessage() {
return String.format("Not allowed to debit '%s' from account '%s'", amount, account);
}
}
注:代码出自http://download.csdn.net/download/angly/8206155
2. 关于返回值为null
- 如果确实是由“意外情况”(数据库错误、网络错误等)造成返回null,应给直接抛出异常信息;对于能确定造成空指针异常的,应给封装空指针异常后再进行抛出;
- 如果只是因为查询结果为空,那么应该构建空集合进行返回,但是需要注意的是Collections.emptyList()或Collections.EMPTY_LIST,返回的空集合是一个特殊集合,只能进行常见集合的部分操作,使用时要特别注意;
- 最容易出现空指针异常的情况:数据库查询、链式调用(对象不为null,对象的某个属性为null)、Java8 Stream、循环(List中可能含有null元素)。
推荐阅读:避免Java应用中NullPointerException的技巧和最佳实践
3. 关于分包
建议为所有的异常类、配置类、工具类、常量类、枚举类对应的建立专门的包进行存放。
4. 关于分层
所谓的分包分层,不过是为了让各个类的职责更清晰,复用代码更方便。
5. 关于Entity、DO、DTO、VO之间的转换
- DTO(VO)是根据接口的定义和需要而创建的;
- Entity是根据数据库的设计而创建的;
- DO是根据业务需要而创建的。
一般情况下,可以采用装饰器模式,让DO继承自DTO,并持有Entity,并且包含fromEntity()、fromEntitys()和toEntity()、toEntitys()等转换函数。因为DO持有Entity,所以在service层所做的操作都可以透传到Entity,需要持久化时,只需要采用转化函数转化为Entity进行持久化操作。而且因为DO继承了DTO,所以虽然接口中定义的是DTO,但是service层却可以直接返回DO而不用转化。
如果想让DO变为一个充血模型,那么可以把持久化操作放到DO中,service层只负责业务逻辑。这样DO中就连上述的转换函数也不需要了,对service层来说,Dao层完全是透明的。
6. 关于DDD(领域驱动设计)
按照官方的说法是:相当于把现有的Service层和Entity合并到一起,在Entity中加入业务方法,使其由贫血模型转变为充血模型。
我的理解是:
- 不要合并Service层和Entity,保留原来分层结构;
- 在Service层增加DOMAIN,用来取代BO、DO等实体类型,可以把上一条中提到的类型转换函数写在DOMAIN中,在DOMAIN中主要进行一些和实体关系紧密的验证和操作,以减轻service.impl中的函数复杂度;
- 实体转换顺序:Param(业务参数组合),Condition(分页排序等参数组合)->DOMAIN(->ENTITY)->VO;
- 增删改查,场景,事务等操作都放在Service层,在DOMAIN中只含有对域本身的操作,并且尽量把转换验证等操作都做成静态的,举例:如果在转化的过程中,需要引入其他service或dao,尽量通过作为函数参数引入,而不是类的成员变量,这样一来可以把所有的关联依赖都集中在Service中。DOMAIN中操作都是内存中进行,DOMAIN不可以操作数据库,所以DOMAIN中没有save()等方法,对DOMAIN的持久化应该在Service中进行;
- ENTITY和VO基本上都是贫血模型,可以按照数据库、持久化框架、前端界面等要求独立设计和演变;DOMAIN是充血模型,与业务相关,按照业务的要求进行演变;
- Service和DOMAIN都应该进行null判断,DOMAIN中的转换函数等本身有自身安全性要求,不能直接遇空报错;Service中的场景走向可能需要根据null判断来决定,切换场景、报错、返回空集合、返回null。
20191101更新,新的理解:
把更多的service逻辑转移到domain层,domain负责对象的存取和转换,service需要的实体对象都由domain层提供。domain应该对service层屏蔽缓存等概念。
- service层的代码尽量少而清晰,具体实现应该下沉到小的函数里。
- domain层主要负责对象组装和转换,domain层应该有自己的xxxDomain(相当于是对应域的service)。
- domain层下有自己的实体,以Model结尾(或者无需结尾后缀);有自己的service,以Domain结尾。
- service层与domain层的交互都通过Domain进行。
- model可以持有一个或多个entity(用entity初始化model),然后转化为其它dto、vo等对象。
再次整理:
最底层是数据库,dao(Repository)层。上一层是guava缓存,利用entity来初始化model,然后把model缓存下来。
- model中持有entity和各种转化好的dto,vo。dto和vo是在model的初始化函数里就完成了的,然后对外提供get函数。
- 尽量把领域可以处理的问题放到model里,service里只有业务逻辑。model里处理和该领域相关的逻辑。
- 如果需要其他service可以直接问spring要,而不是@Autowired。可以写一个SpringUtil来获取spring容器中的类。
这里的model就是所谓的领域对象。
再次整理:
领域可以是分层的,一个大的领域下可以有多个小领域。大领域中可能含有构建小领域所需的一些数据,所以有的时候大领域也可以充当小领域的工厂。(工厂模式,优于把大领域当作参数传给小领域的构造函数)
再次整理:
领域对象,持有entity,生成dto。领域对象与实体是有对应关系的,领域对象变了,entity会对应发生变化,反之亦然。领域对象与dto是工厂和产品的关系,产品一旦被生产好就和工厂没有关系了,产品之间也不应该互相影响(不应该指向一个对象)。
- get表示获取某个属性
- to表示从本对象吐出另一个对象(也可以表示生成另一对象)
举例说明:
// 正确:
public class XxxDomain {
private XxxEntity xxxEntity;
public XxxDomain(XxxEntity xxxEntity) {
this.xxxEntity = xxxEntity;
}
public XxxDomain(XxxDTO xxxDTO) {
this.xxxEntity = new XxxEntity();
BeanUtils.copyProperties(xxxDTO, xxxEntity);
}
public XxxEntity getXxxEntity() {
return xxxEntity;
}
public XxxDTO toXxxDTO() {
XxxDTO xxxDTO = new XxxDTO();
BeanUtils.copyProperties(xxxEntity, xxxDTO);
return xxxDTO;
}
}
// 错误:
public class XxxDomain {
private XxxEntity xxxEntity = new XxxEntity();
private XxxDTO xxxDTO = new XxxDTO();
public XxxDomain(XxxEntity xxxEntity) {
this.xxxEntity = xxxEntity;
BeanUtils.copyProperties(xxxEntity, xxxDTO);
}
public XxxDomain(XxxDTO xxxDTO) {
this.xxxDTO = xxxDTO;
BeanUtils.copyProperties(xxxDTO, xxxEntity);
}
public XxxEntity toXxxEntity() {
return xxxEntity;
}
public XxxDTO toXxxDTO() {
return xxxDTO;
}
}
7. 关于命名
1. 第一层:表现层,对外服务层,User Interface layer
如果是面向前端网站,包可以用controller、api,类可以用**Controller、**Api;
如果是面向服务,包可以用facade、application、api,类可以用**Facade、**ApiService、**Api;
作为参数传入这层的实体类,一般放在query、condition包下,可以用**Param(业务参数组合)、**Condition(分页排序等参数组合)、**Form(表单参数);
作为返回值传出这层的实体类,一般放在model、vo、dto等包下,可以用**VO(面向前端)、**DTO(面向服务)。
2. 第二层:业务层,业务逻辑层,Business Logic Layer
一般包名 service、business(简写biz),类名可用**Service、**Business(Biz);
这层的实体,一般放在domain包下,为所谓的BO、DOMAIN等从现实生活中提取的对应的业务对象,是在系统中操作起来最方便的实体类,并且负责连接和转换第一层和第三次的实体对象,建议使用不带后缀实体名。
3. 第三层:持久层,数据访问层,Data access layer
一般包名可用dao、repository等,类名可用**Dao、**Repository等;
这层是实体,一般放在entity包下,可以用**Entity。
另外,包名一般为:com.公司/产品线.项目名.各层分类.功能分类,如com.taobao.cainiao.controller.address下面放着和地址相关的接口。
再另外,每层一般都有接口和实现,应该分包放置,接口包命名见上,实现包在接口包的基础上+.impl,实现类在接口的基础上+Impl。
8. 关于发布
如果是面向前端的项目一般打成一个包即可;
如果是面向服务的项目一般打成两个包:
一个包是第一层的接口和实体,可以命名为***-api:
另一个包是剩下的部分,可以命名为***-service。
附录:
-
后台模型
-
后台目录