项⽬架构之传统三层架构和领域模型三层架构
摘要: 本⽂对项⽬采⽤的架构进⾏介绍,并说明每⼀个模块在架构中扮演的⾓⾊。
本⽂⾪属于新⼿图⽂教程《IDEA+Maven搭建Spring+SpringMVC+Mybatis项⽬⾻架》
下⼀篇:图⽂⽰范IDEA创建Maven项⽬和⼦模块
⼀、⼯程结构
本系列⽂章所⽰范的项⽬基于传统三层架构进⾏分层,基于⼯作职责和Maven结构进⾏模块划分。本⽂将对传统三层架构和对应的领域模型架构、以及每个模块的职责进⾏简单的说明。下图即⽰范项⽬的模块结构:
⼯程结构
⼆、架构之传统三层架构
传统三层架构是⼀种软件架构,是⼀种典型的、基于贫⾎模型的、⾯向过程的JavaWeb分层⽅式。该架构分为以下三个层次:
数据访问层(DAL - Data Access Layer)即对包括数据库在内的数据源进⾏操作的部分。
业务逻辑层(BLL - Business Logic Layer)即对业务数据进⾏逻辑处理的部分。
表现层(UI - User Interface)即与⽤户交互的部分。
分层的⽬的是为了解耦和明确责任。开发⼈员可以只关⼼⾃⼰所负责的那⼀层,因为他只需要知道上⼀层提供了哪些接⼝,从⽽利⽤这些接⼝进⾏编程。⽽上⼀层的开发⼈员在不改变接⼝的情况下,可以任意地替换具体的实现,从⽽实现松耦合。
相⽐更传统的架构,三层架构有着明显的优势,但也有不可忽视的缺点。初期的JavaWeb在JSP内同时进⾏数据库读写、业务逻辑处理和页⾯渲染,简单⽽暴⼒。⽽今的架构,在JSP之上增加了⼀个处理业务逻辑的中间层和⼀个封装了数据库操作的数据访问层,毫⽆疑问造成了代码量的⼤幅度上升和效率的下降。
本项⽬中,Mybatis承担数据访问的责任;SpringMVC包揽了页⾯渲染和请求调度;Spring的IoC和AOP组成了整个项⽬的⽀架;Spring 的事务控制为业务逻辑层的⼀致性提供了强有⼒的保障。
不同的语⾔、不同的框架对三层架构有不同的演绎,但殊途同归,业务数据的流向是⼀致的。下⾯以⼀个点菜的例⼦来⽰范这⼀点:
三层架构数据流⽰意图
三、架构之领域模型架构
领域模型的概念源于2004年出版的经典著作《Domain-Driven Design –Tackling Complexity in the Heart of Software》(《领域驱动设计:软件核⼼复杂性应对之道》,简称DDD)。所谓领域,即软件所关注的主题区域:
每个软件程序的⽬的都是为了执⾏某项活动,或是满⾜⽤户的某种需求。⽤户会把软件程序应⽤于某个主题区域,这个区域就是软件的领域。⼀些领域涉及物质世界,例如机票预订程序的领域中包括飞机乘客在内。有些领域则是⽆形的,例如会计程序的⾦融领域。
——摘⾃《领域驱动设计》第⼀章
该著作提出,设计软件分为两个步骤:
由领域专家、开发⼈员、设计⼈员就某⼀领域进⾏交流,从中发现领域概念,并根据需要,划定边界,将边界内的概念抽象为领域模型。 由领域模型驱动软件设计,⽤代码来实现该模型。
基于此,DDDSample官⽹提出了另⼀种三层架构(参考链接):
领域模型三层架构
如图,该架构分为界⾯(Interfaces)、应⽤(Application)和领域(Domain)三层,以及⼀个基础设施(Infrastructure),是⼀种基于充⾎模型的、⾯向对象的分层⽅式。其各层职责如下:
1. 界⾯层(Interface)
负责所有与外部系统的交互,包括WebService、RMI或REST等。包括外观(Facade)、装配(Assembler)和数据传输对象(DTO)三类组件:
★ DTO组件
因为领域对象不适合暴露给⽤户,因此需要在返回给⽤户之前,重新封装为DTO,只暴露我们希望暴露的内容。同时,DTO还有减少请求的次数、简化传输对象、避免代码重复等作⽤。
★ Assembler组件
正如其字⾯上的含义,Assembler是⼀个装配⼯⼈,负责DTO与领域对象的转换。
★ Facade组件
Facade是外观模式的践⾏者,作⽤与传统三层架构的Controller类似,负责将⼀个或多个service⽅法组合起来,然后封装为⼀个接⼝提供为外部系统。换句话说,他负责将外部请求委派给⼀个或多个service进⾏处理。他本⾝不处理任何业务逻辑。
1. 应⽤层(Application)
应⽤层的主要组件就是Service,其粒度与传统三层架构的service⼀致。差别在于,传统三层架构的service层负责业务逻辑的处理,⽽领域模式三层架构的service只负责将业务委派给领域对象进⾏处理。
2. 领域层(Domain)
这⼀层是整个软件的核⼼,⼏乎包括了所有的业务逻辑。他包括了Entity(实体)、Value Object(值对象)、Domain Event(领域事件)和Repository(仓储)等领域组件。下⾯以时下很⽕的共享⾃⾏车为例来简单解释这⼏个组件:
★ Entity/Value Object
实体是⼀个在业务领域有着唯⼀标识的对象。实体有属性和状态,有业务⾏为,其业务⾏为会影响他的属性和状态。⽽值对象呢,⽤于描述没有唯⼀标识的对象。
举个栗⼦,当我们希望对每⼀辆共享⾃⾏车进⾏管理时,他应该被设计为实体,有唯⼀编号作为标识,有颜⾊、重量、价格、品牌等属性,有位置状态、使⽤状态,有租赁⾏为。当其被租赁时,其位置状态和使⽤状态可能发⽣改变。
同样是共享⾃⾏车,如果我们的系统只为了统计各地区各品牌⾃⾏车的使⽤情况,即我们只关⼼他是什么,⽽不关⼼他是谁,那么他应该被设计为值对象。
★ Domain Event
简单的说,实体触发事件,实体绑定事件。⽤户的租赁⾏为会触发租赁事件;⽽⾃⾏车绑定了租赁事件,当事件发⽣时,⾃⾏车的使⽤状态发⽣改变。
★ Repository
仓储类似于传统三层架构的DAO接⼝,但只是接⼝,不包括实现。
1. 基础设施(Infrastructure)
作为基础设施,Infrastructure负责给三层架构提供⽀持。所有与具体平台、框架相关的实现都会在这⼀层实现,以免影响三层架构职责的纯粹性、以及污染领域模型。对象持久化的具体实现也放在基础设施⾥。
四、传统三层架构 VS 领域模型架构
从领域模型架构各层的职责可以看出,他和传统三层架构最⼤的差别在于,领域模型架构的业务逻辑包含在领域模型⾥,⽽传统三层架构的业务逻辑在Service层。为了实现这⼀点,领域模型还引⼊了在Javascript和ActionScript中常见的事件机制;⽽传统三层架构中,领域模型的属性和⾏为严格分离,变成了POJO和Service。
个⼈认为,两种架构的出发点是相同的,⼀样是先挖掘领域概念,然后建模,再根据模型进⾏设计。差别在于,当业务逻辑的复杂程度在单个开发⼈员或单个团队的把控能⼒范围之内时,采⽤⾯向过程的传统三层架构可以很快地完成建模⼯作,并开始业务逻辑的设计;⽽当业务逻辑复杂到⼀定程度时,则有必要花更多的时间⽤在建模上,去抽取模型的⾏为,去设计和关联模型事件,以期在后续迭代中,开发⼈员只需⾯对⼀个个可以清晰地理解的领域对象,⽽不是⼀坨动辄上千⾏的某业务⾏为的逻辑代码。
综上考虑,⼤部分web系统可以采⽤传统三层架构。
五、模型的形态
不同的架构、不同的层、不同的应⽤场景中有着不⼀样的建模需求,因此表述相同概念的模型可能会有不同的“形态”,例如:
充⾎模型 - 领域模型架构中包含了领域逻辑和领域属性的领域模型。
失⾎模型 - 传统三层架构中只有get/set⽅法,没有业务逻辑的POJO对象。
贫⾎模型 - 类似充⾎模型,但是不包括持久化相关逻辑。
PO - Persistant Object,持久化对象,即DAO从JDBC取出来的对象。传统三层架构中,PO即POJO组件中的对象,存在于DAO和Service之间。
DO - Domain Object,领域对象。领域模型架构中,PO从数据库取出来后,有⼀个“重建”的概念,即根据数据还原实体,这个被还原的实体就是DO,存在于DAO和Service之间。
DTO - Data Transfer Object,数据传输对象。上⾯在领域模型架构的界⾯层提过。对传统三层架构来说,该对象存在于Service和Controller之间。PO到DTO的转换可以在service实现,也可以在controller实现。本教程在Service进⾏转换。
VO - View Object,视图对象。Controller在返回DTO给视图时,可能还需要包括状态信息—例如操作成功/失败的状态吗、提⽰⽂本等—这时就需要在DTO外⾯再包⼀层,即View Object。该对象存在于Controller和Web之间,由Controller进⾏装配。
六、MVC模式
跟架构密切相关的另⼀个词汇是MVC模型,即Model-View-Controller模式。MVC模式是⼀种设计软件的模式,不是⼀种架构。在传统三层架构中,MVC的理念被应⽤在表现层:View提交请求数据给Controller,Controller返回数据⽤于渲染View,两者之间以Model(VO -ViewModel)的形式进⾏通信。如下图:
mvc模式⽰意图
七、模块说明
注:下⽂中提到的Parent(⽗模块)和Aggregator(聚合器)概念会在后续章节讲解。
1. study-parent:pom
他是⼀个Parent - ⽗模块(被其他模块配置为parent)
配置基本参数供其他模块继承
管理整个项⽬所需要的第三⽅依赖及其版本
他是⼀个Aggregator - 聚合器(配置modules并包含了其他模块)
配置build信息、配置maven插件,规范项⽬构建⾏为
根据不同的运⾏环境配置多套profile
该项职能可以下放到study-blog
即此处不配置modules节点,让common和plugin分别构建,blog相关包统⼀构建。
2. study-common:jar
放置⼯具类,例如Base64加密解密、MD5加密解密、读写Cookie等。来源包括:
1. 从成熟的java开源项⽬的common/util包获得⼯具类
2. 平时根据个性化需求进⾏收集⼯具类
开源项目3. ⾃⼰写⼯具类
4. 也可以使⽤Guava这样的成熟类库。
放置⾃定义基础类。例如⾃定义的BaseException及其⼦类UploadException、⾃定义的BaseInterceptor及其⼦类EventInterceptor、Controller基类、Model基类等。
放置与第三⽅依赖相关的个性封装或实现。例如Mybatis多数据源的实现、Excel通⽤导⼊导出的实现、对httpClient⽅法的个性封装、对shiro的个性封装等。
放置硬编码的参数。例如⾃定义常量,⾃定义枚举等等。可配置的参数放在study-web模块的xml或者properties⽂件中。
3. study-plugin:jar
这个模块主要是第三⽅服务的实现类。例如调⽤短信接⼝、实现登录等。
第三⽅服务相关的参数可以硬编码在代码内,也可以放在study-web模块的resources中,交由spring进⾏初始化。
1. study-blog:pom
blog相关模块的Parent - ⽗模块(被blog相关模块配置为parent)
统⼀管理⼦模块的依赖。
该项职能可以上升到study-parent。
即⼦模块的parent改为study-parent,直接继承study-parent的属性。
blog相关模块的Aggregator - 聚合器(配置modules并包含了其他模块)
聚合⼦模块,统⼀构建。
⼀个⽹站除了Blog可能还包括了OA、CMS等⼦系统,每个⼦系统都有各⾃的pojo、mapper、service和controller,容易混乱。
5. study-blog-pojo:jar
POJO即Plain Old Java Objects,简单的JavaBean,即对对象实体的封装,只有属性和get/set⽅法。POJO中的实体还可以称为
PO(Persistant Object - 持久化对象)。我们把内存中的临时数据存储到存储设备上变成永久数据的过程叫做持久化。对于需要进⾏持久化操作的对象,我们称之为持久化对象。Hibernate中把持久化对象的⽣命周期分成三种:
临时状态 - 刚创建出来,还未持久化的状态。此时该对象不在session缓存中,数据库中也没有相应的记录。
持久化状态 - 已经被持久化,且在session缓存中的状态。对该状态下对象的修改会由Hibernate同步反映到数据库中。
游离状态 - 已经持久化但不在session缓存中的状态。对该状态下对象的修改不会反映到数据库中,除⾮⼿动调⽤相关⽅法使之回到持久化状态。
然⽽Mybatis并未对对象的⽣命周期进⾏管理,更不会有持久化状态。但我们根据是否已进⾏持久化操作,将对象状态粗分为临时状态和游离状态。
1. study-blog-mapper:jar
Mybatis将它的DAO对象(Data Access Object - 数据访问对象)称为Mapper - 映射器,每个映射器包含了⼀个Mapper接⼝类和⼀个XML映射配置⽂件。其中,XML映射配置⽂件包含了SQL代码和映射定义信息(也可以通过注解直接写在接⼝类中,但不推荐)。
Mybatis的映射器都放在这个模块⾥。该模块只负责数据的CRUD操作,不管业务逻辑。数据访问对象不限于数据库数据源。
1. study-blog-controller:jar
controller的作⽤常被概括为“取调转”:
取 - 取出页⾯发来的数据。
当变量较多时考虑打包成⼀个VO(View Object - 表现层对象,不是Value Object - 值对象,⽤于controller与view交互)。
调 - 调⽤service⽅法,完成具体的业务逻辑。
转 - 转发到下⼀个controller或页⾯。
8. study-blog-service:jar
service就是三层架构的业务逻辑层,也有biz(business的缩写)这种叫法。业务逻辑层是三层架构中最为厚重的⼀层,所有跟具体业务相关的环节都在这⾥完成。
另外,在soa架构中,业务逻辑层会细分为biz和service:
biz是对业务逻辑的更细粒度划分,但⼜有别于DAO对数据库的原⼦性操作,⽬的在于抽象出业务逻辑的公共部分,更好地实现代码复⽤。 service即对外暴露的接⼝。借⽤悠然⼤神的例⼦,biz层的许多东西是不适合对外的,好⽐厨师炒菜,你看了炒菜的过程估计吃不下去。但是让service层包装⼀下,让漂亮的服务员端上来就好多了。
9. study-web:war
放置所有⼦系统相关的jsp和前端静态资源。
放置所有resources。mapper⾥边的xml不在此列。
如果把不同⼦系统的web模块分成不同的模块(例如study-blog-web和study-cms-web),打包时需要通过maven插件进⾏合并,较为复杂。
本⽂就先介绍到这⾥。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论