技术面问题:事务脚本模式/MVC三层开发架构面向过程,DDD面向对象
库表驱动开发:事务脚本模式
熟悉MVC的开发们就会突然发现,平时写的就是Transaction Script.
事务脚本模式(Transaction Script)可以简称TS模式,这个模式本身的核心思想就是名称中的两个词:即,事务与脚本。
①、事务可以理解为实际需要执行的一段原子业务;
②、脚本则是指的一组原子业务的编排方式。而通常来说脚本的编排会直接映射到用户的一个行为动作上。
或者说:
①、事务--通常指执行的业务
②、脚本--指一组系统执行的操作(也就是脚本)与每个用户操作在逻辑上关联起来。
事务脚本模式可能是最简单的业务逻辑模式,它完全是过程式的。
按照模式,TS没有包含任何面向对象设计的成分,通过TS建模的任何逻辑都是通过IF,WHILE和FOR等语言语法元素表达。
事务脚本本身可以理解是用户动作的一次任务编排,而触发的方式一般直接与用户的行为建立联系。由于使用的是脚本的编排方式,所以事务的组织是面向过程的。
而对于事务脚本模式,不同关联用户动作的脚本所编排的事务之间一般是相互隔离的,即事务之间不会影响,但不同的脚本之间可以编排相同的事务,即事务之间可以复用。
事务脚本模式的本质是事务与事务的编排。
一次用户的动作对应的是一次业务请求,请求后便需要一层来进行事务的编排,也就是脚本层。
脚本层是对于事务本身的编排,所以我们还需要一层事务层。事务针对的是数据库的多次操作,所以还有一层数据库层。
于是就有了:事务层,脚本层层,数据库层的三层模型。
而这与经典的三层模型是等价的,当然如果你的框架结构中有Handler层或者Manager层的话,脚本层或者事务层的对应关系是可以进行调整的。
而对于事务脚本的实现模式上就可以发现,其中是完全不需要任何面向对象的设计模式。所以在进行事务脚本设计模式建模的时候,其中的任何逻辑都可以通过if,else,for,while等元素表达,所以事务脚本模式会造成技术和业务混合,业务和业务混合。
基于贫血模型的传统MVC三层架构
①、什么是三层架构
MVC三层架构中的M表示Model,V表示View,C表示Controller。它将整个项目分为三层:展示层,逻辑层,数据层。
MVC三层开发架构是一个比较笼统的分层方式,落实到具体的开发层面,很多项目也并不会100%遵从MVC固定的分层方式,而是会根据具体的项目需求,做适当的调整。比如:现在很多Web或者APP项目都是前后端分离,后端负责暴漏接口给前端调用。这种情况下,我们一般将后端的项目分为Repository层,Service层,Controller层。其中,Repository层负责数据访问,Service层负责业务逻辑,Controller层负责暴漏接口。当然,这只是其中一分层和命名方式。
不同的项目,不同的团队,可能会对此有所调整,不过,万变不离其宗,只要是依赖数据库开发的Web项目,基本的分层思路都差不多。
MVC对应于DDD中的贫血模型
目前几乎所有的业务后端系统,都是基于贫血模型的。
那么什么是贫血模型呢?
贫血模型所有的业务逻辑都不包含在实体对象内,而是放在Business Logic层。
贫血模型优点是系统的层次结构清楚,各层之间单向依赖,Client ->(Business Facade)->Business Logic ->Data Access Object.
可见,领域对象几乎只作为传输介质之用,不会影响到层次的划分。这就是传统的数据驱动开发。
在使用Spring的使用,通常暗示着你使用了贫血模型,我们把Domain类用来单纯地存储数据,Spring管不着这些类的注入和管理,Spring关心的逻辑层(比如单例的被池化了的Business Logic层)可以被设计成singleton 的bean。假如我们这里逆天而行,硬要在Domain类中提供业务逻辑方法,那么我们在使用Spring构造这样的数据bean的时候就遇到许多麻烦,比如:bean之间的引用,可能引起大范围的bean之间的嵌套构造器的调用。
举个例子:
// Controller+VO(View Object) public class UserController { private UserService userService; // 通过构造函数或者 IOC 框架注入 public UserVo getUserById(Long userId) { UserBo userBo = userService.getUserById(userId); UserVo userVo = [...convert userBo to userVo...]; return userVo; }}public class UserVo {// 省略其他属性、get/set/construct 方法 private Long id; private String name; private String cellphone;}// Service+BO(Business Object) //public class UserService { private UserRepository userRepository; // 通过构造函数或者 IOC 框架注入 public UserBo getUserById(Long userId) { UserEntity userEntity = userRepository.getUserById(userId); UserBo userBo = [...convert userEntity to userBo...]; return userBo; }} public class UserBo {// 省略其他属性、get/set/construct 方法 private Long id; private String name; private String cellphone;} // Repository+Entity //public class UserRepository { public UserEntity getUserById(Long userId) { //... }}public class UserEntity {// 省略其他属性、get/set/construct 方法 private Long id; private String name; private String cellphone;}
我们平时开发web后端项目的时候,基本上都是这么组织代码的
①、UserEntity和UserRepository组成了数据访问层
②、UserBo和UserService组成了业务逻辑层
③、UserVo和UserController在这里属于接口层。
从代码中,我们可以发现,UserBo是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑,业务逻辑集中在UserService中,我们通过UserService来操作UserBo。
换句话来说,Service层的数据和业务逻辑,被分割为BO和Service两个类,像UserBo这样,只包含数据,不包含业务逻辑的类,就叫做贫血模型。
同理,UserEntity,UserVo都是基于贫血模型设计的。这种贫血模型将数据与业务分离,破坏了面向对象的封装特性,是一种典型的面向过程的编程风格。
什么是基于充血模型的DDD开发模式
充血模型,数据和对应的业务逻辑被封装到同一个类中,因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
实际上,基于充血模型的DDD开发模式实现的代码,也可以按照三层架构分层的。
Controller层还是负责暴漏接口,Repository层还是负责数据存取,Service层负责核心业务逻辑。它根基于贫血模型的传统开发模式的区别主要在Service层。
①、在基于贫血模型的传统开发模式中,Service层包含Service类和BO类两部分,BO是贫血模型,只包含数据,不包含具体的业务逻辑,业务逻辑集中在Service类中。
②、在基于充血模型的DDD开发模式中,Service层包含Service类和Domain类两部分。Domain就相当于贫血模型中的BO,不过,Domain与BO的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑,而Service类变得非常单薄。
总结一下的话就是:
①、基于贫血模型的传统的开发模式,重Service轻BO;面向过程的思维方式,面向过程的架构方式
②、基于充血模型的DDD开发模式,轻Service重Domian,面向对象的思维方式,面向对象的架构方式。
和贫血模型相比,充血模型层次结构把大多业务逻辑放在Domain Object里面,Business Logic只是简单封装部分业务逻辑以及控制事务,权限等,这样层次结构就变为Client->Business Facade ->Business Logic ->Domain Object->Data Access Object.
它的优点是面向对象,Business Logic符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑太过沉重。
贫血模型是指领域对象里只有get和set方法(POJO),所有的业务逻辑都不包含在内而是放在Business Logic层。
@Data@ToStringpublic class User { private Long id; private String username; private String password; private Integer status; private Date createdAt; private Date updatedAt; private Integer isDeleted; public boolean isActive(User user){ return user.getStatus().equals(StatusEnum.ACTIVE.getCode()); } public void setUsername(String username){ return username.trim(); }}
贫血模型在失血模型基础之上,领域对像的包含一些状态变化,但是停留在内存层面,不关心数据持久化。
为什么基于贫血模型的传统开发模式如此受欢迎?
基于贫血模型的传统开发模式,将数据与业务逻辑分离,违反了OOP的封装特性,实际上是一种面向过程的编程风格。
但是,现在几乎所有的Web项目,都是基于这种贫血模型的开发模式,甚至连java Spring框架的官方demo,都是按照这种开发模式来编写的。
而面向过程编程风格有很多弊端,比如:数据和操作分离之后,数据本身的操作就不受限制了。然和代码都可以随意修改数据,既然基于贫血模型的这种传统开发模式是面向过程编程风格的,那么它又为什么被广大程序员所接受呢?
原因如下:
①、第一点原因是:大部分情况下,我们开发的系统业务可能都比较简单,简单到就是基于SQL的CRUD操作,所以,我们根本不需要动脑子精心设计充血模型,贫血模型就足以应付这种简单业务的开发工作。除此之外,因为业务比较简单,即便我们使用充血模型,那模型本身包含的业务逻辑也并不会很多,设计出来的领域模型也会比较单薄,跟贫血模型差不多,没有太大意义。
②、第二点原因是,充血模型的设计要比贫血模型更有难度。因为充血模型是一种面向对象的编程风格,我们从一开始就要设计好针对数据暴漏哪些操作,定义哪些业务逻辑而不是像贫血模型那样,我们只需要定义数据,之后有什么功能开发需求,就在Service层定义什么操作,不需要事先做太多设计。对于业务部复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的DDD开发模型有点大材小用,无法发挥作用。相反,对于业务复杂的系统来说,基于充血模型的DDD开发模式,因为前期需要在设计上投入更多的时间和精力,来提高代码的复用性和维护性,所以相比基于贫血模型的开发模式,更加有优势。
③、第三点原因是,思维已固化,转型有成本,基于贫血模型的传统开发模式经历了这么多年,已经深得人心,习以为常。如果转向用充血模型,领域驱动设计,那势必有一定的学习成本,转型成本,很多人在没有遇到开发痛点的情况下,是不愿意做这件事情的。