实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP么?

编程入门 行业动态 更新时间:2024-10-11 13:19:16

实战一(上):业务开发常用的基于贫血模型的MVC<a href=https://www.elefans.com/category/jswz/34/1771112.html style=架构违背OOP么?"/>

实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP么?

在前面几节课中,我们学习了面向对象的一些理论知识,比如,面向对象四大特性、接口和抽象类、面向对象和面向过程编程风格、基于接口而非实现编程和多用组合少用继承设计思想等等。接下来,我们再用四节课的时间,通过两个更加贴近实战的项目来进一步学习,如何将这些理论应用到实际的软件开发中。

据我了解,大部分工程师都是做业务开发的,所以,今天我们讲的这个实战项目也是一个典型的业务系统开发案例。我们都知道,很多业务系统都是基于MVC三层架构来开发的。实际上,更确切点讲,这是一种基于贫血模型的MVC三层架构开发模式。

虽然这种开发模式已经成为标准的Web项目的开发模式,但它却违反了面向对象编程风格,是一种彻彻底底的面向过程的编程风格,因此而被有些人称为。特别是领域驱动设计(Domain Driven Design,简称DDD)盛行之后,这种基于贫血模型的传统的开发模式就更加被人诟病。而基于充血模型的DDD开发模式越来越被人提倡。所以,我打算用两节课的时间,结合一个虚拟钱包系统的开发案例,带你赞底弄清楚这两种开发模式。

考虑到你有可能不太了解我刚刚提到的这几个概念,所以,在正式进入实战项目的讲解之前,我先带你搞清楚下面几个问题:

  • 什么是贫血模型?什么是充血模型?

  • 为什么说基于贫血模型的传统开发模式违反OOP?

  • 基于贫血模型的传统开发模式既然违反OOP,那又为什么如此流行?什么情况下我们应该考虑使用基于充血模型的DDD开发模式?

好了,让我们带着这些问题,正式开始今天的学习吧!

什么是基于贫血模型的传统开发模式?

我相信,对于大部分的后端开发工程师来说,MVC三层架构都不会陌生。不过,为了统一我们之间对MVC的认识,我还是带你一块来回顾一下,什么是MVC三层架构。

MVC三层架构中的M表示Model, V表示View, C表示Controller。它将整个项目分为三层:展示层、逻辑层、数据层。MVC三层开发架构是一个比较笼统的分层方式,落实到具体的开发层面,很多项目也并不会100%遵从MVC固定的分层方式,而是会根据具体的项目需求,做适当的调整。

比如,现在很多Web或者App项目都是前后端分离的,后端负责暴露接口给前端调用。这种情况下,我们一般就将后端项目分为

Repository层、Service层、Controller层。其中,Repository层负责数据访问,Service层负责业务逻辑,Controller层负责暴露接口。当然,这只是其中一种分层和命名方式。不同的项目、不同的团队,可能会对此有所调整。不过,万变不离其宗,只要是依赖数据库开发的Web项目,基本的分层思路都大差不差。

刚刚我们回顾了MVC三层开发架构。现在,我们再来看一下,什么是贫血模型?

实际上,你可能一直都在用贫血模型做开发,只是自己不知道而已。不夸张地讲,据我了解,目前几乎所有的业务后端系统,都是基于贫血模型的。我举一个简单的例子来给你解释一下。

// Controller+VO(View Object)
public class UserController{private UserService userService; //通过构造函数或者10c概架注入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组成了数据访问层, 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设计模式的概念,今天我们只是简单地介绍了一下。在下一节课中,我会结合具体的项目,通过代码来给你展示,如何基于这种开发模式来开发一个系统。

为什么基于贫血模型的传统开发模式如此受欢迎?

前面我们讲过,基于贫血模型的传统开发模式,将数据与业务逻辑分离,违反了0OP的封装特性,实际上是一种面向过程的编程风格。

但是,现在几乎所有的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开发模式,正好需要我们前期做大量的业务调研、领域模型设计,所以它更加适合这种复杂系统的开发.

辩证思考与灵活应用

还有两个问题值得讨论一下。第一个要讨论的问题是:在基于充血模型的DDD开发模式中,将业务逻辑移动到Domain中, Service类变得很薄,但在我们的代码设计与实现中,并没有完全将Service类去掉,这是为什么?或者说, Service类在这种情况下担当的职责是什么?哪些功能逻辑会放到Service类中?

区别于Domain的职责,Service类主要有下面这样几个职责。

1.Service类负责与Repository交流。在我的设计与代码实现中, VirtualWalletService类负责与Repository层打交道,调用Respository类的方法,获取数据库中的数据,转化成领域模型VirtualWallet,然后由领域模型VirtualWallet来完成业务逻辑,最后调用Repository类的方法,将数据存回数据库。

这里我再稍微解释一下,之所以让VirtualWalletService类与Repository打交道,而不是让领域模型VirtualWallet与Repository打交道,那是因为我们想保持领域模型的独立性,不与任何其他层的代码(Repository层的代码)或开发框架(比如Spring, MyBatis)耦合在一起.将流程性的代码逻辑(比如从DB中取数据、映射数据)与領域模型的业务逻辑解耦,让领域模型更加可复用。

2.Service类负责跨领域模型的业务聚合功能。VirtuniwalletService类中的transfer)转账函数会涉及两个钱包的操作,因此这部分业务逻辑无法放到VirtualWallet类中,所以,我们暂且把转账业务放到VirtualWalletService类中了。当然,虽然功能演进,使得转账业务变得复杂起来之后,我们也可以将转账业务抽取出来,设计成一个独立的领域模型。

3.Service类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日表、调用其他系统的RPC接口等都可以放到Service类中。

第二个要讨论问题是:在基于充血模型的DDD开发模式中,尽管Service层被改造成了充血模型,但是Controller层和Repository层还是贫血模型,是否有必要也进行充血領域建模呢?

答案是没有必要,Controller层主要负责接口的暴露, Repository层主要负责与数据库打交道,这两层包含的业务逻辑并不多,前面我们也提到了,如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄、看起来也很奇怪。

尽管这样的设计是一种面向过程的编程风格,但我们只要控制好面向过程编程风格的副作用,照样可以开发出优秀的软件。那这里的副作用怎么控制呢?

就拿Repository的Entity来说,即便它被设计成贫血模型,违反面向对象编程的封装特性,有被任意代码修改数据的风险,但Entity的生命周期是有限的。一般来讲,我们把它传递到Service层之后,就会转化成BO或者Domain来继续后面的业务逻辑。Entity的生命周期到此就结束了,所以也并不会被到处任意修改。

我们再来说说Controller层的vo,实际上VO是一种DTO (Data Transfer Object,数据传输对象)。它主要是作为接口的数据传编承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑、只包含数据。所以、我们将它设计成贫血模型也是比较合理的

重点回顾

今天的内容到此就讲完了,我们来一起回顾一下,你应该掌握的重点内容。

我们平时做Web项目的业务开发,大部分都是基于贫血模型的MVC三层架构,在专栏中我把它称为传统的开发模式。之所以称之为“传统”,是相对于新兴的基于充血模型的DDD开发模式来说的。基于贫血模型的传统开发模式,是典型的面向过程的编程风格。相反,基于充血模型的DDD开发模式,是典型的面向对象的编程风格。

不过,DDD也并非银弹。对于业务不复杂的系统开发来说,基于贫血模型的传统开发模式简单够用,基于充血模型的DDD开发模式有点大材小用,无法发挥作用。相反,对于业务复杂的系统开发来说,基于充血模型的DDD开发模式,因为前期需要在设计上投入更多时间和精力,来提高代码的复用性和可维护性,所以相比基于贫血模型的开发模式,更加有优势。

课堂讨论

今天课堂讨论的话题有两个。

1.你做经历的项目中,有哪些是基于贫血模型的传统的开发模式?有哪些是基于充血模型的DDD开发模式呢?请简单对比一下两者的优劣。

2.对于我们举的例子中,UserEntity、UserBo、UserVo包含的字段都差不多,是否可以合并为一个类呢?

更多推荐

实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP么?

本文发布于:2024-03-14 19:41:18,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1737188.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:架构   实战   模型   常用   业务

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!