很多同学创建一个项目之后,就迫不及待的上手开写了。项目代码不像一些框架代码一样可以随意的去写,但一般都是采用MVC的模式进行开发。很悲催的是,Java中Web开发的这些目录名称,到现在还是一团乱麻,你需要自己去规划。
什么Controller、Service、Dao等,但其实这种划分方式弊端很多! 本文将先介绍两种典型的分层结构,然后稍微借鉴一下DDD的思想,谈一下我在项目中常用的目录结构。本篇文章非常的实用,将探讨怎样做一个应对大型项目的目录划分。
清晰的目录结构,能够辅助其他同学轻而易举的了解项目的功能模块,在项目中保持整体一致的约定也是一个非常好的习惯。如果再加上一个扩展性,那目录划分就是重中之重。
有两种典型的分类方式,但也有很多细节。
1. 最简单的MVC
我们平常最熟悉的,就是MVC结构。这种结构很流行,写简单项目很方便,但是会产生严重的耦合问题、Service爆炸问题,数千、上万行的代码是家常便饭。
- Model(模型)表示应用程序核心(比如数据库记录字段)。
- View(视图)显示数据(数据库记录)。
- Controller(控制器)处理输入(写入数据库记录)。
在项目划分上,就类似下面的目录结构。
1.1 模型
domain是DDD中一个非常宽泛的概念。不过,我们平常就当作数据库对应的Java 类使用了(没什么错)。在实际操作中,它还可能有下面几种名字,在普通项目中区别不大,你最好在项目中保持相同的意义来避免歧义。
- entity 这个意义比较明显,就是实体的意思,最常用。比如JPA的Entity注解
- model模型的意思,一般用来在不同系统之间交互。但如果你的模型非常简单,直接用entity来表示也是可以的
- domain 这个范围有点大,甚至会包含领域内service。如果你对DDD的概念不是很熟悉,那就玩上面几种
对于简单的项目,我通常在项目中使用entity来表示和数据库的交互。在JPA之类的ORM中,也是做相关处理的。比如javax.persistence.Entity注解。你要明白的是,Spring Data其实取了一个比较折衷的点,把很多东西揉在一起了。
1.2 Dao
dao层叫数据访问层,全称为data access object,属于一种比较底层,比较基础的操作。在一些其他框架中,还会叫别的名字。
- mapper 这个一般是Mybaits之类的框架所生成的目录,通常是一些接口。
- repository 仓库的意思,在jpa中经常用。
Dao应该满足最小封装原则,理论上只涉及一句SQL的执行。如果有多个数据的存取动作,需要封装在Service中,并用事务进行管理(虽然这么说,但repository在DDD中,是不和具体的数据库打交道的)。
1.3 service和controller
这个没什么好说的,基本上所有重要的逻辑都在这里完成。service用于逻辑处理,controller用于接口暴露。
2. 根据功能组织
大多数情况下,我们使用上面的这种划分模式,能够很好的完成工作。比如,所有的数据处理,都放在Dao层,所有的逻辑处理,都放在Service层。
这在小项目中相安无事,但如果项目中,有成百上千个Entity,这些目录中的文件就会爆炸,以至于最后无法维护。
另外一个问题就是,仅仅一个简单的功能,就可能分散在多个package下的多个文件中,大型项目维护变得困难。
我们有另外一个思路,就是根据功能进行分组。比如下面的截图。
我们把相似功能,放在modules下的单个文件夹中。如果这个功能模块比较大,我么可以在功能模块下,再进行分层设计。
比如上图,有一个商品服务,我们单独给它分配了一个目录空间goods,然后在里面又划分了dao、entity等目录;但对于Service和Controller,我们简单的放在了外层,可以看到在模块内的分配是比较灵活的。
这么做的好处是显而易见的。功能变的非常的集中,各个package之间的内容互不影响。
3. 还是不够优雅
其实,即使我们这样划分了,项目仍然会面临很大的挑战(很多DDD的书籍,会大量讨论各层的交互)。
下面分享一个我在平常使用的分层模式,兼顾高内聚和低耦合,有着良好的扩展性。
- config,最外层的一些全局配置,比如web配置,消息队列配置等
- system,全局的工具和依赖功能,在DDD中叫做基础设施(但在非DDD实践的项目中名称太怪异了)
- auth,权限认证模块,比如JWT或者Spring Sercurity,这部分的设计要独立,以便后续抽离到Zuul之类的网关
- bc,在DDD中是限界上下文的意思(Bounded Context),我们也可以直接叫模块,这些模块有着严格的界限,可以根据请求量,拆分成相应的微服务。在上图,crm、images、order等等,都可以抽离成独立的微服务
我们再来看一下每个模块之内的结构。
- 和传统的MVC类似。不过,为了屏蔽变化,兼顾扩展性,我们增加了更多的内容。
- persitence,持久层,具体使用JPA还是Mybatis,这个是无关紧要的。我们的目标,就是尽量的弱化持久层的实现,将变化封装在Domain层中
- persitence/dao,具体的持久层接口,比如MyBatis的Mapper文件,或者JPA的Repository
- domain层,具体的业务层,你可以认为是一堆Getter、Setter的Bean。我们尽量会把大多数验证类和变化封装在这里(可以大体认为是DDD中的充血模型)
- controller,具体的Rest接口层。但不同的是,有很多不同的请求和返回,我们封装成了Request和Response,用来接受提交的数据,对返回数据进行瘦身等
- application,应对传统的service层,除了在application能够调用Dao,其他层是没有权利调用Dao的
- api,和application的功能是相同的。只不过,api的接口,指的是模块之间可以相互调用的接口。除了api暴露的这些接口,bc之间的类和接口,默认彼此是不可见的
- util,不通用的util,会放在模块内部,而不是抽离出公共的util
除了要解决目录方面的问题,我们还要把数据的流向给规划清楚。
一个上层的应用,是可以通过API接口直接调用下层服务的。比如,订单系统访问商品基础信息的数据;反之却不可以,比如商品基础信息模块访问订单系统的接口。
低层想要对高层的数据产生变化,就只能通过消息模块,将变更发布出去,其他的模块就可以订阅这些变化。
小结
综上所述,xjjdog认为,如果你的项目,可能会比较大,单纯的使用分层的package,并不是一个好的习惯。
你可能对这种后台管理类的项目驾轻就熟,有很多有用的模版,它们都是简单的MVC分层。这应付一些外包项目,干一些一锤子买卖的时活,或许没什么问题,但一旦是比较大的长期项目,这种分层的目录接口就显现出它的弊端。
这是因为:项目的短期风险,是工期问题;而长期风险,是扩展问题。随着访问量的增加,还有低耦合高内聚的需求增加,如何快速的应对需求,减少BUG,将会是制约项目发展的最主要因素。
作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。