浅谈面向对象编程与面向过程编程以及DDD充血Model
先抛出开头一个问题:一直在说OOP面向对象编程,你现在写的代码真的是OOP吗?你确定你写的不是PO(面向过程)的代码?
面向对象编程和面向过程编程
解答上面的问题,首先来回顾一下什么是面向对象编程和面向过程编程,回顾前先说一下一个误区。
-
误区: 面向对象编程语言 不等于 面向对象编程
可能会有人说我用的Java怎么就被说成写面向过程?
- Java是一种面向对象编程语言,面向对象编程语言只是提供易于面向对象编程的语法,例如:extends实现继承、interface、abstract class实现多态等,让你更容易地面向对象编程
- 面向对象编程是一种编程开发制品的思想,面向对象编程的四项基本原则:继承、抽象、封装、多态,也就是说如果你的代码符合这个原则范式,那么你就是面向对象编程。举个例子,Linux内核是用c编写的,但是很多地方都会有面向对象的身影,例如:VFS文件系统,调度系统等等。再多说一下,思想是通用的,是思考的方式(模式),思想来源于现实也反映现实,计算机体系很多思想也是来源于现实,例如遗传算法、神经网络等等
- 那么能够用Java一面向对象编程语言写面向过程的代码是不是就很合理呢?
-
面向对象编程和面向过程编程的区别
- 面向对象是以类为组织代码的基本单元,面向过程是以过程(或方法)组织代码的基本单元
- 主要特点:数据和方法分离
- 而面向对象也具有上面说的四种基本原则:继承、抽象、封装、多态
违背OOP的贫血模型(反模式anti-pattern)
你写的Service层十有八九是POP!
回忆一下MVC架构,MVC分别就是数据层Model、展示层View、控制层Contoller。
而自从前后端分离之后,后端又可以分成Repostory数据层、Service业务逻辑层、Controller控制器层。
- Repository层主要负责各个DB的读取以及对象转换(在相对简单项目各种ORM或许就能完成需求,可是在复杂场景ORM自身或许有点力不从心,例如复杂ERP、电商等等,就会是各种的SQL乱飞)
- Service层负责主要业务逻辑
- Controller层负责暴露Restful接口(有可能有的非Restful)。
那么,一个项目主要逻辑主要在Service层,而目前项目的Service几乎都是POP!为什么这样说呢?
先看看一段基于MVC后端代码(例子可能会有不规范地方):
public class UserController {
private UserService userService;
public UserVo getUserById(...){
UserBo userBo = userService.getUserById(..);
UserVo userVo = convertToVo(userBo);
return userVo;
}
}
@Data//lombok注解
public class UserVo{
private String userId;
private String userName;
...
}
public class UserService{
public UserBo getUserById(...){
UserDo userDo = userRepository.getUserById();
UserBo userBo = convertToBo(userBo);
return userBo;
}
}
@Data//lombok注解
public class UserBo{
private String userId;
private String userName;
...
}
public class UserRepository {
public UserDo getUserById(..) {
.....read through db/cache
return userDo;
}
}
@Data//lombok注解
public class UserDo{
private String userId;
private String userName;
...
}
这应该目前最常用的代码架构,即使可能会有些公司有适当改造,但是大同小异
- Controller+VO
- Service+BO(有的甚至直接使用DO/Entity)
- Repository+DO
贫血模型
先抛开Repository层和Controller层,看我们先把关注点放到代码量最多的Service层的BO
不知道到这里对比上面说过的大家有没有发现以下几点:
- BO并不包含任何的业务逻辑,只是包含数据,这不是正正是面向过程编程的特征?
- 而像Bo这种不包含任何业务逻辑的类,通常就被称为贫血模型
- 而贫血模型将数据和操作分离,破坏了面向对象的封装特性,就是一种典型的面向过程的风格。
Bo滥用lombok真的好吗?
可能会有同学注意到我们特意标明了一个lombok注解。然后回问一句Bo这个类不是使用了getter和setter从而具有封装的思想吗?那么使用lombok为所有的成员变量设置getter和setter真的好吗?
一般使用lombok设置所有的setter和getter的理由是:以后可能会用到啊,现在顺手定义无伤大雅,而且以后使用更方便。
再来看一个购物车例子:
public class ShoppingCart{
@Setter
@Getter
private int itemCount;
@Setter
@Getter
private int totalPrice;
@Getter
private List<ShoppingCartItem> items = new ArrayList<>();
public void addItem(ShoppingCartItem item) {
items.add(item);
itemsCount++;
totalPrice += item.getPrice();
}
}
-
这是一个购物车类,有三个属性,itemCount,totalPrice,items三个属性
itemCount,totalPrice分别设置了setter和getter,那么跟public有什么区别?是否有些自欺欺人?
-
而items没有定义setter,而是封装了addItem。可是这样真的没问题吗?来看一段访问代码
public class CartServie{ public void addItem(ShoppingCartItem item) { ShoppingCart shoppingCart = 通过id拿出shoppingCart; shoppingCart.getItems.add(item); } }
- 如果多个同学共同开发,没有留意到addItem,很可能写出这样的代码,直接通过getItems拿到内存中的items的对象,往里面塞item对象,整个shoppingCart类数据是否不一致了?
面向对象封装的定义:通过权限访问控制,隐藏内部数据,外部仅通过类提供的有限接口访问、修改内部数据。不应该暴露不该又的setter方法,甚至一些容器的getter方法。
如果我又想使用list的getter方法怎么办呢?其实java提供了Collections.unmodifiedList()方法,请同学自行查阅。
充血模型
既然有贫血模型,那么肯定会有充血模型,bingo,充血模型也是存在的。
- 充血模型:数据和业务逻辑封装到同一个类,而且是符合面向对象编程的。
这里不得不说一下DDD领域驱动设计,DDD的核心概念其实就是利用充血模型开发,DDD其实早诞生于2004年,只不过蹭了一波微服务热度。
众所周知,微服务总是离不开调用链、监控、API网关等服务治理的工具(微服务的交错复杂特点带来的,这里顺带提一下混沌工程,用于检测分布式系统潜在漏洞的科学),微服务的另一个难点就是服务拆分,而DDD刚好可以填上这一块难点。
实际上,基于充血模型DDD开发的代码,还是基于MVC架构,与传统模式贫血模型开发的区别在于service层
-
充血模型,service层包含domain类和service类,但是区别在于domain类既包含数据也包含业务逻辑,service的功能相对被削弱。
换句话说,充血模型DDD开发模式重domain轻service,而贫血模型重service轻BO
为什么贫血模型那么流行?
不知道有没有人发现Spring官方的demo也是使用贫血模型?
-
那么贫血模型模型开发为什么那么流行?
-
SQL-Driven开发
目前大部分开发都是基于数据库,也就是常说的SQL-Driven开发。开发前,只需要定义好相对应的表,然后拿来需求往service类上堆代码就可以。充血模型开发前需要精心设计,而且如果需求简单,其实贫血模型和充血模型并无两样。贫血模型开发速度更快,其实这当中个人觉得会有一定敏捷开发的原因,现代互联网公司迭代快,有的甚至太过于追求开发交付速度。
-
充血模型开发有难度
目前国内大部分开发者其实更符合码农的角色,java developer来说基于spring打天下,基于贫血模型,定义好数据,直接读取在service类处理一下就可以返回。然而充血模型在设计期需要花费不少精力设计,明确每个类的“职责”,需要暴露哪些业务逻辑(符合GRASP),开发成本高。
-
思维固化
世界上最难改变的两者之一就是思想,人不是纯理性的动物,否则就变成冷冰冰的机器。大部分的developer已经习惯了贫血模型的开发,潜意识也是很恐怖的,实用派就会觉得我基于贫血模式开发的业务系统也没出过问题,何必要增加改变成本?当业务上没有遇到痛点,是很难发生改变的。
-
什么项目适合充血模型DDD?
最后,重申一点,DDD不是银弹,不要盲目追求DDD,简单的业务系统或者特定的业务场景有时更适合贫血模型开发。
这让我回想起当初滥用parallelStream差点把应用拖垮,《Java并发编程艺术》第一章也不是教大家如何去更好使用并发容器或者工具,而是说明一个情况,不是所有场景都适用多线程,上下文切换也有很大消耗,正如AIO NIO不一定优于BIO,Redis除了收发请求Reactor和持久化的fsync线程,执行引擎使用的是单线程。
那么什么项目适合充血模型DDD?
- 复杂业务系统,例如包含各种利息计算模型、还款模型的金融业务系统
- 当你发现你的Service已经臃肿不堪,不能再往上堆代码需要重构的时候可以考虑
- 当使用DDD重构时候,请务必熟悉业务架构,业务驱动开发,即使你对DDD概念再深入,不熟悉业务架构是设计不出一流的DDD架构的
最后
充血模型不能只是代码层面去思考,更需要思维转变
- 贫血模型,上面说到的SQL-Driven,接到需求,看接口需要的数据对应在哪张表,利用SQL语句读出来然后做一些业务逻辑,然后模版式地往Repository、Service和Controller类添加代码。过程中,很少人使用OOP,领域模型的概念,甚至有的连代码复用也没有
- 充血模型,先理清楚有哪些业务,业务领域模型的职责(成员变量和方法),领域模型其实更类似可复用的业务中间层。新功能开发可以基于各个领域对象完成。而不再是基于BO往上堆代码(基于数据库开发,很容易造成service臃肿)。
本文章是学习OOP的总结,有不当之处请指出批评,本文参考以下并不限于:
王争老师《设计模式之美》
Craig Larman《UML和模式应用》