我们都知道,很多业务系统都是基于 MVC 三层架构来开发的。实际上,更确切点讲,这是一种基于贫血模型的 MVC 三层架构开发模式。虽然这种开发模式已经成为标准的 Web 项目的开发模式,但它却违反了面向对象编程风格,是一种彻彻底底的面向过程的编程风格,因此而被有些人称为反模式(antipattern)。
特别是领域驱动设计(Domain Driven Design,简称 DDD)盛行之后,这种基于贫血模型的传统的开发模式就更加被人诟病。而基于充血模型的 DDD 开发模式越来越被人提倡。
MVC架构(大前提)
MVC 三层开发架构是一个比较笼统的分层方式,落实到具体的开发层面,很多项目也并不会 100% 遵从 MVC 固定的分层方式,而是会根据具的项目需求,做适当的调整。
现在很多 Web 或者 App 项目都是前后端分离的,后端负责暴露接口给前端调用。 这种情况下,我们一般就将后端项目分为 Repository 层、Service 层、Controller 层。其中,Repository 层负责数据访问,Service 层负责业务逻辑,Controller 层负责暴露接口。当然,这只是其中一种分层和命名方式。不同的项目、不同的团队,可能会对此有所调整。不过,万变不离其宗,只要是依赖数据库开发的 Web 项目,基本的分层思路都大差不差。
- V :View(展示层):HTML
- C(#) :Controller(逻辑层)
- Controller : 前端交互,暴露接口 ↑
- Service(#) :核心业务逻辑
- Dao :数据库交互,数据存取 ↓
- M : Model(数据层):MySQL
1.传统开发模式(贫血模型)
1.1 贫血模型
数据与逻辑分离 --> 面向过程编程
1.2 传统开发模式
分层模型(全贫)
- Controller + VO
- Service + BO 完全数据逻辑隔离,~O只做数据结构;破坏了面向对象封装特性,是典型面向过程的编程风格
- Dao + Entity
1.3 开发流程
SQL驱动
我们接到一个后端接口的开发需求的时候,就去看接口需要的数据对应到数据库中,需要哪张表或者哪几张表,然后思考如何编写 SQL 语句来获取数据。
之后就是定义 Entity、BO、VO, 然后模板式地往对应的 Repository、Service、Controller 类中添加代码。业务逻辑包裹在一个大的 SQL 语句中,而 Service 层可以做的事情很少。
SQL 都是针对特 定的业务功能编写的,复用性差。当我要开发另一个业务功能的时候,只能重新写个满足新需求的 SQL 语句,这就可能导致各种长得差不多、区别很小的 SQL 语句满天飞。
所以,在这个过程中,很少有人会应用领域模型、OOP 的概念,也很少有代码复用意识。 对于简单业务系统来说,这种开发方式问题不大。但对于复杂业务系统的开发来说,这样的 开发方式会让代码越来越混乱,最终导致无法维护。
1.4 代码示例
// 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) //
// 业务逻辑都在Service中
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; }
}
// BO只存储数据,无业务逻辑
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;
}
2.DDD 开发模式(充血模型)
2.1 充血模型:
领域模型(Domain)处理大部分逻辑,Service只做少部分逻辑 --> 数据逻辑融合 --> 面向对象编程
2.2 DDD开发模式
分层模型(贫+充)
- Controller + VO(贫血):因为VO是作前端传输数据载体,不应该有逻辑
- Service < Domain(领域模型)
- Service 类负责与 Dao 交流
- 将Entity转化为Domain,由 Domain(领域模型)完成大部分业务逻辑
- 保证Domain不与其他层(Dao…)或框架(Spring…) 耦合在一起
- Service 类负责跨领域模型的业务聚合功能。
- Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消 息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中
- Dao + Entity(贫血):Entity主要用于数据库字段映射,若加入业务逻辑有被任意代码修改的风险
2.3 开发流程
领域驱动。
在这种开发模式下,我们需要事先理清楚所有的业务,定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发,都基于之前定义好的这些领域模型来完成。
我们知道,越复杂的系统,对代码的复用性、易维护性要求就越高,我们就越应该花更多的 时间和精力在前期设计上。而基于充血模型的 DDD 开发模式,正好需要我们前期做大量的 业务调研、领域模型设计,所以它更加适合这种复杂系统的开发。
2.4 代码示例
篇幅原因,代码示例详见下一篇文章…
3.总结
3.1 贫血模型比充血模型更受欢迎
面向过程编程风格有种种弊端,比如,数据和操作分离之后,数据本身的操作就不受限制了。任何代码都可以随意修改数据。既然基于贫血模型的这种传统开发模式 是面向过程编程风格的,那它又为什么会被广大程序员所接受呢?关于这个问题,下面三点原因:
- 第一点原因是,大部分情况下,我们开发的系统业务可能都比较简单,简单到就是基于 SQL 的 CRUD 操作,所以,我们根本不需要动脑子精心设计充血模型,贫血模型就足以应 付这种简单业务的开发工作。除此之外,因为业务比较简单,即便我们使用充血模型,那模型本身包含的业务逻辑也并不会很多,设计出来的领域模型也会比较单薄,跟贫血模型差不 多,没有太大意义。
- 第二点原因是,充血模型的设计要比贫血模型更加有难度。因为充血模型是一种面向对象的编程风格。我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑。而不是 像贫血模型那样,我们只需要定义数据,之后有什么功能开发需求,我们就在 Service 层定 义什么操作,不需要事先做太多设计。
- 第三点原因是,思维已固化,转型有成本。基于贫血模型的传统开发模式经历了这么多年, 已经深得人心、习以为常。你随便问一个旁边的大龄同事,基本上他过往参与的所有 Web 项目应该都是基于这个开发模式的,而且也没有出过啥大问题。如果转向用充血模型、领域驱动设计,那势必有一定的学习成本、转型成本。很多人在没有遇到开发痛点的情况下,是不愿意做这件事情的。
3.2 什么项目使用DDD开发模式
- 基于贫血模型的传统的开发模式,比较适合业务比较简单的系统开发。
- 基于充血模型的 DDD 开发模式,更适合业务复杂的系统开发。比如,包含各种利息 计算模型、还款模型等复杂业务的金融系统。