算上实习的公司,现在是第四家公司了。头两家公司是传统行业,用的也是经典的三层架构,action,service,Dao。dao层负责持久化;service层负责业务逻辑,事务处理;action负责接收参数和返回数据。我相信只要是java程序员都这样玩过。也曾经疑惑过,为什么要一个简单的CRUD也要还要整俩接口。
还有一个问题是,我们通常会定义的一个实体类和数据库完全匹配,特别是用mybatis的时候,但是当我们返回前台的结果很可能和数据库不一样,这个时候可能需要定义额外的实体类用于匹配查询结果,曾经为了图方便,还用map玩过。
带着这些疑惑稀里糊涂的待了两家公司,终于来到了互联网公司。
同样还是针对一个表的CRUD,尼玛,分了四层,定义了四个实体类,当时我震惊了。后来有人告诉从格局上讲这是SOA、模块化思想,从接口来说,这是领域设计思想。当然架构上除了传统的struts2、spring、mybatis还有dubbo、zookeeper、redis等
我花了一段时间来体会这种架构,简单的说下吧。
如果是对UI显示,层次是这样滴, action , application service,domain service,dao。
如果是对外暴露(dubbo接口),export,application service,domain service,dao。
实体类分为,DTO,DO,VO还有Query(也相当于DTO)
当时项目对application service层简称 ao 层,对domain service层简称manager 层
dao层的作用,大家都知道;manager层用作处理当前领域的业务,它可能是单表,也可能是多表,通常就是当前项目所牵扯的主业务表的增删改查。ao 层就是跨领域层属于应用层,通常需要调其它服务的接口,action层就是接收参数和返回结果,没有任何逻辑。
然后实体类,DO是和数据库相匹配的实体,VO是和前台列表相匹配的实体,DTO是接口传输的实体,Query是接口查询条件的实体。
当时做的是CRM系统,我主要参与的就是商家、合同相关的业务。因为当时这个项目,是公司第一个模块化思想的项目,虽然不难做的也很头疼。
以合同查询来说吧,如果是PC端和手机端来调用的话,入口就是action,传递的参数会包装成Query。因为合同存有城市、商圈、店铺等id、还有附属协议等信息。最开始我直接是一个关联查询,直接通用mybatis匹配成VO,返回到前台。
被我的技术负责人给批了,正确的做法是这样的,我从数据库查出来的数据是个纯净的DO,这个DO经过service(当时我们是想在这一层用redis做缓存)到ao层,在ao层他会根据城市、商圈、店铺等id分别调不同的接口去查询相应的信息,然后在组装成VO,返回到action层。
因为当时对服务化这种思想不理解,觉得一条SQL就能解决的问题结果工作量瞬间陡增,还让我郁闷很久。
当有其他内部应用要调用这个接口,这个时候走的就是dubbo接口了,通过export接收参数,参数还是query,如果只是查询单独的合同信息,export会调用service,然后在export层将service返回的DO转成Dto。 如果需要通过这个接口查询合同的关联信息,比如店铺,商家,就调用AO层,
然后将AO层返回的对象转成DTO。
在DO转VO,或者DO转DTO的时候,通常是在VO、DTO定义一个静态工厂方法,通过该方法转换,保证业务代码的纯净。
这就是一个简单的查询接口,虽然开始觉得有些绕,后来也觉得蛮好,也接触了SOA和领域设计这种思想。这家公司待了没多久,公司C轮融资失败,为了缓解公司的经济危机,我也就跳槽了。然后我就来了这一家公司,一家C轮的电商公司。
还是SOA的思想,项目包结构依然分四层,export,facade,service,dao。实体类就定义了比较随意了,没有严格的规范。开始我觉得这四层跟我上家公司应该类似,只是名字换了而已。结果开发项目的时候却发现不一样,也让我对这种分层有了更深的认识。
export层:dubbo暴露的接口,实际功能基本上就是对应app的一个按钮。主要是做异常归集和接口暴露。
facade层:相当于一个门面,接口的主流程,理论上就是一条线,比较干净,抛一个自定义业务异常。
service层:接口的主逻辑,还有校验参数。通常一个接口参数校验应该越早越好,毕竟能早发现错误就早发现错误嘛,但是我们这边的规范是参数校验在service层。
dao层:不多说了。
因为dubbo接口是不能直接被app调用的,所以我们有个adaptor,算一个单独的项目,他会把app的http请求,转换成dubbo的入参,然后调我们的dubbo接口。
说是说的很顺畅,写的时候,我是很疑惑。
export层,它针对的是app的一个功能,对外暴露的接口复用性太差。
facade层,说是接口的业务流程,因为接口的主逻辑在service层,我看到很多人的代码,这一层就是调了一下service,导致这一层很薄,缺少存在的意义。
service层,参数校验,业务逻辑,调外部应用的接口,组装对象,太重了,完全不可复用。
dao层,我之前告诫公司的实习生,这里能复用就复用,然后公司的其他同事,却告诫他们能不复用就不复用。dao层的方法过多。
实体类,除了有和表关联的一个实体,入参和出参的实体只和app的功能相关,完全不可复用。
有一天和同事聊到这个项目,他是负责review 代码的一员,他说了句,"感觉你们写的代码都是一次性代码”,虽然我不知道他的理解跟我是否一样,但是我也是深有同感。当然真正开发的时候,不是一些理论所能概括全的。我总结一下自己做的项目,聊一聊自己的看法吧。
先谈一下实体类
DO,我之前的公司把它定义为何数据库对应的实体,其实它应该是个domain entity。它包含了数据库的所有字段,同时也包含了这个领域的其他关联对象,因为在一个领域内,可以使用sql的关联查询,不必在为了关联查询新建一个类。
DTO,数据传输对象,DO使我们从数据库得到的领域对象,而DTO往往也包含了多个领域的信息,所以我们需要转成DTO。
VO,其实它也属于DTO的一种。
Query,查询入参的后缀,同样属于DTO。
然后谈一下分层
action或者export:它负责入参和出参,对于普通的http应用它就是我们通常用struts2实现的action、用springMVC实现的controller。对于企业内部,可能不是使用http协议,但它依然负责某个应用的入参和出参。
ao或者facade:它的确是个门面,负责调用本领域的接口,和跨领域的接口
manager或者service:本领域的业务
dao:数据层。
其中Service和Dao层操作的对象理论上只应该有DO和query。
facade和export层操作的对象,理论上只应该有DTO,VO和Query。我们在facade层做对象的转换,DO转DTO,转VO。当然对象转换的操作最好交由具体的实体类操作。
因为service层和dao层是当前领域的业务,所以它们可能比较薄。业务最好保证一定的原子性,功能上保证松耦合,高复用。
facade层属于应用层,应用层要保证业务逻辑的干净,通用的业务,可以考虑模板。它的变化可能也比较快,当然如果service做的好的好的话,就是service的堆积了。
当然,好的架构师一点一点摸索,修改出来的,这里只是自己的一些总结。