CategoryResourceRepost/极客时间专栏/设计模式之美/设计原则与思想:面向对象/11 | 实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP吗?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

152 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="11 | 实战一业务开发常用的基于贫血模型的MVC架构违背OOP吗" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/94/28/949fe45c82caee3298905039cf341d28.mp3"></audio>
在前面几节课中,我们学习了面向对象的一些理论知识,比如,面向对象四大特性、接口和抽象类、面向对象和面向过程编程风格、基于接口而非实现编程和多用组合少用继承设计思想等等。接下来,我们再用四节课的时间,通过两个更加贴近实战的项目来进一步学习,如何将这些理论应用到实际的软件开发中。
据我了解大部分工程师都是做业务开发的所以今天我们讲的这个实战项目也是一个典型的业务系统开发案例。我们都知道很多业务系统都是基于MVC三层架构来开发的。实际上更确切点讲这是一种基于贫血模型的MVC三层架构开发模式。
虽然这种开发模式已经成为标准的Web项目的开发模式但它却违反了面向对象编程风格是一种彻彻底底的面向过程的编程风格因此而被有些人称为[反模式anti-pattern](https://zh.wikipedia.org/wiki/%E5%8F%8D%E9%9D%A2%E6%A8%A1%E5%BC%8F)。特别是**领域驱动设计**Domain Driven Design简称DDD盛行之后这种基于贫血模型的传统的开发模式就更加被人诟病。而基于充血模型的DDD开发模式越来越被人提倡。所以我打算用两节课的时间结合一个虚拟钱包系统的开发案例带你彻底弄清楚这两种开发模式。
考虑到你有可能不太了解我刚刚提到的这几个概念,所以,在正式进入实战项目的讲解之前,我先带你搞清楚下面几个问题:
- 什么是贫血模型?什么是充血模型?
- 为什么说基于贫血模型的传统开发模式违反OOP?
- 基于贫血模型的传统开发模式既然违反OOP那又为什么如此流行
- 什么情况下我们应该考虑使用基于充血模型的DDD开发模式
好了,让我们带着这些问题,正式开始今天的学习吧!
## 什么是基于贫血模型的传统开发模式?
我相信对于大部分的后端开发工程师来说MVC三层架构都不会陌生。不过为了统一我们之间对MVC的认识我还是带你一块来回顾一下什么是MVC三层架构。
MVC三层架构中的M表示ModelV表示ViewC表示Controller。它将整个项目分为三层展示层、逻辑层、数据层。MVC三层开发架构是一个比较笼统的分层方式落实到具体的开发层面很多项目也并不会100%遵从MVC固定的分层方式而是会根据具体的项目需求做适当的调整。
比如现在很多Web或者App项目都是前后端分离的后端负责暴露接口给前端调用。这种情况下我们一般就将后端项目分为Repository层、Service层、Controller层。其中Repository层负责数据访问Service层负责业务逻辑Controller层负责暴露接口。当然这只是其中一种分层和命名方式。不同的项目、不同的团队可能会对此有所调整。不过万变不离其宗只要是依赖数据库开发的Web项目基本的分层思路都大差不差。
刚刚我们回顾了MVC三层开发架构。现在我们再来看一下什么是贫血模型
实际上,你可能一直都在用贫血模型做开发,只是自己不知道而已。不夸张地讲,据我了解,目前几乎所有的业务后端系统,都是基于贫血模型的。我举一个简单的例子来给你解释一下。
```
////////// 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这样只包含数据不包含业务逻辑的类就叫作**贫血模型**Anemic Domain Model。同理UserEntity、UserVo都是基于贫血模型设计的。这种贫血模型将数据与操作分离破坏了面向对象的封装特性是一种典型的面向过程的编程风格。
## 什么是基于充血模型的DDD开发模式
刚刚我们讲了基于贫血模型的传统的开发模式。现在我们再讲一下另外一种最近更加被推崇的开发模式基于充血模型的DDD开发模式。
**首先,我们先来看一下,什么是充血模型?**
在贫血模型中,数据和业务逻辑被分割到不同的类中。**充血模型**Rich Domain Model正好相反数据和对应的业务逻辑被封装到同一个类中。因此这种充血模型满足面向对象的封装特性是典型的面向对象编程风格。
**接下来,我们再来看一下,什么是领域驱动设计?**
领域驱动设计即DDD主要是用来指导如何解耦业务系统划分业务模块定义业务领域模型及其交互。领域驱动设计这个概念并不新颖早在2004年就被提出了到现在已经有十几年的历史了。不过它被大众熟知还是基于另一个概念的兴起那就是微服务。
我们知道除了监控、调用链追踪、API网关等服务治理系统的开发之外微服务还有另外一个更加重要的工作那就是针对公司的业务合理地做微服务拆分。而领域驱动设计恰好就是用来指导划分服务的。所以微服务加速了领域驱动设计的盛行。
不过我个人觉得领域驱动设计有点儿类似敏捷开发、SOA、PAAS等概念听起来很高大上但实际上只值“五分钱”。即便你没有听说过领域驱动设计对这个概念一无所知只要你是在开发业务系统也或多或少都在使用它。做好领域驱动设计的关键是看你对自己所做业务的熟悉程度而并不是对领域驱动设计这个概念本身的掌握程度。即便你对领域驱动搞得再清楚但是对业务不熟悉也并不一定能做出合理的领域设计。所以不要把领域驱动设计当银弹不要花太多的时间去过度地研究它。
实际上基于充血模型的DDD开发模式实现的代码也是按照MVC三层架构分层的。Controller层还是负责暴露接口Repository层还是负责数据存取Service层负责核心业务逻辑。它跟基于贫血模型的传统开发模式的区别主要在Service层。
在基于贫血模型的传统开发模式中Service层包含Service类和BO类两部分BO是贫血模型只包含数据不包含具体的业务逻辑。业务逻辑集中在Service类中。在基于充血模型的DDD开发模式中Service层包含Service类和Domain类两部分。Domain就相当于贫血模型中的BO。不过Domain与BO的区别在于它是基于充血模型开发的既包含数据也包含业务逻辑。而Service类变得非常单薄。总结一下的话就是基于贫血模型的传统的开发模式重Service轻BO基于充血模型的DDD开发模式轻Service重Domain。
基于充血模型的DDD设计模式的概念今天我们只是简单地介绍了一下。在下一节课中我会结合具体的项目通过代码来给你展示如何基于这种开发模式来开发一个系统。
## 为什么基于贫血模型的传统开发模式如此受欢迎?
前面我们讲过基于贫血模型的传统开发模式将数据与业务逻辑分离违反了OOP的封装特性实际上是一种面向过程的编程风格。但是现在几乎所有的Web项目都是基于这种贫血模型的开发模式甚至连Java Spring框架的官方demo都是按照这种开发模式来编写的。
我们前面也讲过,面向过程编程风格有种种弊端,比如,数据和操作分离之后,数据本身的操作就不受限制了。任何代码都可以随意修改数据。既然基于贫血模型的这种传统开发模式是面向过程编程风格的,那它又为什么会被广大程序员所接受呢?关于这个问题,我总结了下面三点原因。
第一点原因是大部分情况下我们开发的系统业务可能都比较简单简单到就是基于SQL的CRUD操作所以我们根本不需要动脑子精心设计充血模型贫血模型就足以应付这种简单业务的开发工作。除此之外因为业务比较简单即便我们使用充血模型那模型本身包含的业务逻辑也并不会很多设计出来的领域模型也会比较单薄跟贫血模型差不多没有太大意义。
第二点原因是充血模型的设计要比贫血模型更加有难度。因为充血模型是一种面向对象的编程风格。我们从一开始就要设计好针对数据要暴露哪些操作定义哪些业务逻辑。而不是像贫血模型那样我们只需要定义数据之后有什么功能开发需求我们就在Service层定义什么操作不需要事先做太多设计。
第三点原因是思维已固化转型有成本。基于贫血模型的传统开发模式经历了这么多年已经深得人心、习以为常。你随便问一个旁边的大龄同事基本上他过往参与的所有Web项目应该都是基于这个开发模式的而且也没有出过啥大问题。如果转向用充血模型、领域驱动设计那势必有一定的学习成本、转型成本。很多人在没有遇到开发痛点的情况下是不愿意做这件事情的。
## 什么项目应该考虑使用基于充血模型的DDD开发模式
既然基于贫血模型的开发模式已经成为了一种约定俗成的开发习惯那什么样的项目应该考虑使用基于充血模型的DDD开发模式呢
刚刚我们讲到基于贫血模型的传统的开发模式比较适合业务比较简单的系统开发。相对应的基于充血模型的DDD开发模式更适合业务复杂的系统开发。比如包含各种利息计算模型、还款模型等复杂业务的金融系统。
你可能会有一些疑问这两种开发模式落实到代码层面区别不就是一个将业务逻辑放到Service类中一个将业务逻辑放到Domain领域模型中吗为什么基于贫血模型的传统开发模式就不能应对复杂业务系统的开发而基于充血模型的DDD开发模式就可以呢
实际上除了我们能看到的代码层面的区别之外一个业务逻辑放到Service层一个放到领域模型中还有一个非常重要的区别那就是两种不同的开发模式会导致不同的开发流程。基于充血模型的DDD开发模式的开发流程在应对复杂业务系统的开发的时候更加有优势。为什么这么说呢我们先来回忆一下我们平时基于贫血模型的传统的开发模式都是怎么实现一个功能需求的。
不夸张地讲我们平时的开发大部分都是SQL驱动SQL-Driven的开发模式。我们接到一个后端接口的开发需求的时候就去看接口需要的数据对应到数据库中需要哪张表或者哪几张表然后思考如何编写SQL语句来获取数据。之后就是定义Entity、BO、VO然后模板式地往对应的Repository、Service、Controller类中添加代码。
业务逻辑包裹在一个大的SQL语句中而Service层可以做的事情很少。SQL都是针对特定的业务功能编写的复用性差。当我要开发另一个业务功能的时候只能重新写个满足新需求的SQL语句这就可能导致各种长得差不多、区别很小的SQL语句满天飞。
所以在这个过程中很少有人会应用领域模型、OOP的概念也很少有代码复用意识。对于简单业务系统来说这种开发方式问题不大。但对于复杂业务系统的开发来说这样的开发方式会让代码越来越混乱最终导致无法维护。
如果我们在项目中应用基于充血模型的DDD的开发模式那对应的开发流程就完全不一样了。在这种开发模式下我们需要事先理清楚所有的业务定义领域模型所包含的属性和方法。领域模型相当于可复用的业务中间层。新功能需求的开发都基于之前定义好的这些领域模型来完成。
我们知道越复杂的系统对代码的复用性、易维护性要求就越高我们就越应该花更多的时间和精力在前期设计上。而基于充血模型的DDD开发模式正好需要我们前期做大量的业务调研、领域模型设计所以它更加适合这种复杂系统的开发。
## 重点回顾
今天的内容到此就讲完了,我们来一起回顾一下,你应该掌握的重点内容。
我们平时做Web项目的业务开发大部分都是基于贫血模型的MVC三层架构在专栏中我把它称为传统的开发模式。之所以称之为“传统”是相对于新兴的基于充血模型的DDD开发模式来说的。基于贫血模型的传统开发模式是典型的面向过程的编程风格。相反基于充血模型的DDD开发模式是典型的面向对象的编程风格。
不过DDD也并非银弹。对于业务不复杂的系统开发来说基于贫血模型的传统开发模式简单够用基于充血模型的DDD开发模式有点大材小用无法发挥作用。相反对于业务复杂的系统开发来说基于充血模型的DDD开发模式因为前期需要在设计上投入更多时间和精力来提高代码的复用性和可维护性所以相比基于贫血模型的开发模式更加有优势。
## 课堂讨论
今天课堂讨论的话题有两个。
1. 你做经历的项目中有哪些是基于贫血模型的传统的开发模式有哪些是基于充血模型的DDD开发模式呢请简单对比一下两者的优劣。
1. 对于我们举的例子中UserEntity、UserBo、UserVo包含的字段都差不多是否可以合并为一个类呢
欢迎在留言区写下你的答案,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。