关于后台问题的一些思考

关于后台问题的一些思考

1. 关于异常

  1. 对于异常应该且必须输出到日志中(不能捕获后不处理),但是不应该直接返回给前台;
  2. 返回给前台的应该是可读的简易信息,并且要注意不要暴露后台保密信息(包括类名、数据库名、文件名、行号等);
  3. 应给为每类异常建立专门的异常类,而不是直接new Exception();
  4. 良好的异常类的设计不应该原封不动的继承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

  1. 如果确实是由“意外情况”(数据库错误、网络错误等)造成返回null,应给直接抛出异常信息;对于能确定造成空指针异常的,应给封装空指针异常后再进行抛出;
  2. 如果只是因为查询结果为空,那么应该构建空集合进行返回,但是需要注意的是Collections.emptyList()或Collections.EMPTY_LIST,返回的空集合是一个特殊集合,只能进行常见集合的部分操作,使用时要特别注意;
  3. 最容易出现空指针异常的情况:数据库查询、链式调用(对象不为null,对象的某个属性为null)、Java8 Stream、循环(List中可能含有null元素)。

推荐阅读:避免Java应用中NullPointerException的技巧和最佳实践

3. 关于分包

建议为所有的异常类、配置类、工具类、常量类、枚举类对应的建立专门的包进行存放。

4. 关于分层

所谓的分包分层,不过是为了让各个类的职责更清晰,复用代码更方便。

5. 关于Entity、DO、DTO、VO之间的转换

  1. DTO(VO)是根据接口的定义和需要而创建的;
  2. Entity是根据数据库的设计而创建的;
  3. 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中加入业务方法,使其由贫血模型转变为充血模型。

我的理解是:

  1. 不要合并Service层和Entity,保留原来分层结构;
  2. 在Service层增加DOMAIN,用来取代BO、DO等实体类型,可以把上一条中提到的类型转换函数写在DOMAIN中,在DOMAIN中主要进行一些和实体关系紧密的验证和操作,以减轻service.impl中的函数复杂度;
  3. 实体转换顺序:Param(业务参数组合),Condition(分页排序等参数组合)->DOMAIN(->ENTITY)->VO;
  4. 增删改查,场景,事务等操作都放在Service层,在DOMAIN中只含有对域本身的操作,并且尽量把转换验证等操作都做成静态的,举例:如果在转化的过程中,需要引入其他service或dao,尽量通过作为函数参数引入,而不是类的成员变量,这样一来可以把所有的关联依赖都集中在Service中。DOMAIN中操作都是内存中进行,DOMAIN不可以操作数据库,所以DOMAIN中没有save()等方法,对DOMAIN的持久化应该在Service中进行;
  5. ENTITY和VO基本上都是贫血模型,可以按照数据库、持久化框架、前端界面等要求独立设计和演变;DOMAIN是充血模型,与业务相关,按照业务的要求进行演变;
  6. Service和DOMAIN都应该进行null判断,DOMAIN中的转换函数等本身有自身安全性要求,不能直接遇空报错;Service中的场景走向可能需要根据null判断来决定,切换场景、报错、返回空集合、返回null。

20191101更新,新的理解:

把更多的service逻辑转移到domain层,domain负责对象的存取和转换,service需要的实体对象都由domain层提供。domain应该对service层屏蔽缓存等概念。

  1. service层的代码尽量少而清晰,具体实现应该下沉到小的函数里。
  2. domain层主要负责对象组装和转换,domain层应该有自己的xxxDomain(相当于是对应域的service)。
  3. domain层下有自己的实体,以Model结尾(或者无需结尾后缀);有自己的service,以Domain结尾。
  4. service层与domain层的交互都通过Domain进行。
  5. model可以持有一个或多个entity(用entity初始化model),然后转化为其它dto、vo等对象。

再次整理:

最底层是数据库,dao(Repository)层。上一层是guava缓存,利用entity来初始化model,然后把model缓存下来。

  1. model中持有entity和各种转化好的dto,vo。dto和vo是在model的初始化函数里就完成了的,然后对外提供get函数。
  2. 尽量把领域可以处理的问题放到model里,service里只有业务逻辑。model里处理和该领域相关的逻辑。
  3. 如果需要其他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。

 

 

附录:

  1. 后台模型
    后台模型

  2. 后台目录
    后台目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值