最近公司在推行项目标准化,意在把之前老旧的项目通过此次标准化将项目进行一次重构,提高项目的可维护性,可拓展性,可理解性。小组中正好有两三个项目也需要进行标准化改造,恰逢这次时机,重新翻阅了《阿里巴巴Java开发手册》和阅读了网上相关的文章,对项目分层有了进一步的理解与实践。

项目分层模型

三层模型

image.png
最早接触Web开发时,都是借助三层模型来进行开发,Web层来接收外部请求 【Web层接收请求,也可以处理简单的逻辑及参数校验】,然后将请求转发至Service层进行业务逻辑处理【Service层负责比较复杂的业务逻辑处理】,Service层需要与DB进行交互的则通过Dao层【负责与数据库交互】来支持。
以上这种开发模式针对比较小的项目时是比较合适的,代码层次比较清晰,也不会过多分层导致开发时加大开发量。但如果拿以上的分层结构来开发比较复杂庞大的项目时,就会暴露出一些不好的地方。
从上述说明来看,Service层承担了较大的责任,相当于几乎所有的业务逻辑开发都需要在Servcie层去完成,即使Web层能够为其分担一些简单的逻辑及参数校验,但久而久之,随着业务的扩大及功能的叠加也会造成单类过于庞大臃肿(也可以通过设计模式去改善既有代码);其次,我们一般事务都是加在Service层,如果Service的某个方法的逻辑过于复杂,也会导致长事务,事务嵌套的情况发生;

四层模型

image.png
该图源自于《阿里巴巴Java开发手册》工程结构中的应用分层,与上图不同的是 这张图增加了开放接口,通用处理层,外部接口或第三方平台 三个模块。下述内容为解释相关内容,同样引自《阿里巴巴Java开发手册》。

  • 开放接口:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;进行 网关安全控制、流量控制等。
  • 通用业务处理层:它有如下特征

    • 1) 对第三方平台封装的层,预处理返回结果及转化异常信息;
    • 2) 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;
    • 3) 与 DAO 层交互,对多个 DAO 的组合复用。
  • 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。

开放接口 外部接口或第三方平台比较好理解,就是对第三方提供接口和从第三方接口获取数据。
通用业务处理层 是 新增在Dao层和业务逻辑层之间的,但手册中也给了比较明显的特征描述:
第一条中的 【对第三方平台封装的层,预处理返回结果及转化异常信息】就是阐述了 Manager层可以作为第三方接口数据的接收层,在这层可以发起请求并对接口响应数据的处理,比如将第三方返回的json数据转化为应用中的实体,又或是将第三方返回的异常转化为应用中的异常,亦或是一些自定义的处理等等,那么在这是不会经过Dao层的;
第二条中的【对 Service 层通用能力的下沉,如缓存方案、中间件通用处理】这句话个人理解的意思就是比如有个AServcie和BServcie,AService需要调用BService的方法,那么BService的这个方法就可以作为Service层的通用能力进而下沉到Manager层,那这里面的逻辑可以是对数据的缓存处理,或者是调用中间件处理;
第三条中的【与Dao层交互,对多个Dao的组合复用】的意义就在于Service层调用Dao层时如果多个Dao之间是联动操作或者需要联表查询时,这个时候写在Service层会比较臃肿,那么将多个Dao操作放置在Manager层, 比如你要新增或删除数据时同时处理表A和表B,那么此时就可以将这两个操作放置在Manager层,如果上述没有其他数据库操作的话,那么甚至可以把事务放在Manager层,这样也可以缩短事务的占用时间;

在这里也可以对Manager层进行标明处理,比如命名时使用 XXXManager,注解时自定义 @Manager 注解,但也不要滥用Manager层,当逻辑复杂时可以使用,但逻辑简单时尽量不用,毕竟会增加层的复杂度;又或者是只对第三方平台的接口放置在Manager层处理;只要团队能达成共识,那么新增这一层将会是百利而无一害的。

分层领域模型

上述内容把项目进行分层之后,每一层之间的数据传输就是下一个需要解决的问题,有的项目会直接使用数据库所对应的对象来作为各个层之间的传输对象,这样的好处就是不必维护那么多的对象,但带来的问题就是这个对象过于沉重,一点点的修改都可能带来bug甚至线上故障。
分层领域模型希望在每一层都有自己对应的领域模型,《阿里巴巴Java开发手册》有如下规约:

  • DO(Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
  • BO(Business Object):业务对象。由 Service 层输出的封装业务逻辑的对象。
  • AO(Application Object):应用对象。在 Web 层与 Service 层之间抽象的复用对象模型, 极为贴近展示层,复用度不高。
  • VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
  • Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止 使用 Map 类来传输。

每一层都会有输入和输出两个方向,那么每一层的入参和响应都有对应的模型,如果说过度追求每一层之间的转换,也是比较麻烦的。
比如下图是我刚工作时画的领域模型传输过程:入参时使用DTO作为传输对象 到Mapper层(Dao层)后续转换为PO对象来查询DB,输出时使用PO作为查询结果的存储对象,然后再Mapper层使用Vo(Value Object),最后到Controller层时Vo(View Object)来作为最终响应客户端的对象。整个流程看下来从请求到响应需要3-4次的转换,这样在平时的开发过程中还是比较耗时耗力的。
image.png
其实具体怎么划分这些层的领域模型,又或者说怎么命名这些领域模型,只要团队中能够达成一致,具体怎么做都是可以接受的。毕竟规范代码是为了方便以后代码维护,代码复用,提升开发效率,如果说一昧的追求代码分层模型而导致开发效率的降低,也是一种得不偿失。
这里介绍下个人的领域分层模型处理时,Web层提供给终端显示层时的入参统一命名为:XXX Dto,统一响应为 XXX Vo;对外提供接口时和请求外部接口时的统一入参命名为:XXX Request,统一响应为 XXX Response。Dto 可以从Web层直接请求到Dao层,Dao层查询响应之后为Entity实体层,若查询的时为连表则返回Bo对象,或需要在Manager层组合数据时也使用Bo对象来进行返回,最后在Service层封装为Vo或者Response之后返回。
image.png

框架实践

上一节内容说到我们如果过度追求每一层的转换内容,那么开发效率也会大大降低。但是,众所周知Java的强大之处就是在于其日积月累下强大的生态,各式各样的开发框架应接不暇,比如对于Bean的转换就有 BeanUtils,BeanCopier,Dozer,Orika,MapStruct等各种框架。当然经过网上各式各样的文章测评,目前最安全,高效的框架当属MapStruct。【详情可参考链接 5种常见Bean映射工具的性能比对

介绍

image.png
该图取自于mapstruct官网,从描述上就可以看出mapstruct在基于代码生成在编译时生成Bean的映射处理器,这样即高效又安全,可以在编辑时期就确定是否有报错,大大降低了程序在运行时出现错误的可能性。

使用

MapStruct的使用也是比较方便的,具体的使用方式在官方文档上都有详细的说明 mapstruct1.5.3文档,有详细的文字描述与示例供参考使用。

下面介绍几个常见的用法:

  • 当转换之间的对象属性相同时,无需再定义@Mapping来标识字段之间的映射关系,当需要字段映射关系时刻通过@Mapping(source = "fieldA,target="fieldB")来映射;
  • 当不需要某个字段的映射时,可通过@Mapping(target = "id", ignore = true)来忽略;
  • 当某个字段为常量时,可通过@Mapping(target="payType",constant = 1)来标识该字段使用常量1来填充;
  • 当某个字段需要通过变量获取时,可通过 @Mapping(target="time",expression = "java(new java.util.Date())"来表示time字段为为每次new Date生成的时间,java()则表示是java代码的表达式;
  • 个人在使用mapstruct时会定义一个BaseConverter ,当其他一些常用的转换器时可以直接继承该Converter,而不需要每次都写对应的方法,@Mapper(componentModel = "spring") 则表示注入到Spring容器中,可通过@Resource@Autowired 来注入;

    public interface BaseConverter<Entity, Vo, Dto> {
    
      /**
       * Entity --> Vo
       */
      Vo entity2Vo(Entity entity);
    
      /**
       * Entity --> Vo
       */
      List<Vo> entity2Vo(List<Entity> entityList);
    
      /**
       * Dto --> Entity
       */
      Entity dto2Entity(Dto dto);
    }
    
    @Mapper(componentModel = "spring")
    public interface AConverter extends BaseConverter<A, AVo, ADto> {
    
    }
    
    @Mapper(componentModel = "spring")
    public interface BConverter extends BaseConverter<B, BVo, BDto> {
    
    }

    总结

    项目的分层如何分,项目的领域模型如何设计其实并不重要,并没有一个标准的规范去践行,每个团队在摸索中都会有合适自己的实践,归根到底都是为了一个目的:让项目的代码高内聚低耦合,更整洁清晰易懂,让新来的开发能够根据规范快速熟悉项目,让当前身在团队中的开发的效率提升,那么这件事的意义就达到了,只要能够在团队中达成一致,开发人员都能够坚持实行,那么对于团队和开发来说,都是一件利大于弊的好事。

    参考链接

    我项目中的代码是如何分层的?
    阿里巴巴Java开发手册
    同事问我代码结构中 Manager层是干什么的
    5种常见Bean映射工具的性能比对


Ekko
2 声望0 粉丝