什么是面向过程编程
“面向过程”(Procedure Oriented,简称PO)是一种以过程为中心的编程思想。其原理就是将问题分解成一个一个详细的步骤,然后通过函数实现每一个步骤,并依次调用。
面向过程,其实核心就是“过程”两个字。过程指的是解决问题的步骤,即先干什么、后干什么、再干什么、然后干什么……
面向过程编程是一种编程范式或编程风格。它以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如继承、多态、封装),仅支持面向过程编程。
什么是面向对象编程
面向对象编程中有两个非常重要、非常基础的概念,那就是类(class)和对象(object)。
面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石 。
面向对象编程语言是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
面向对象编程相比较面向过程而言,多了很多巨大的优势:更加能够应对大规模复杂程序的开发;风格的代码更易复用、易扩展、易维护;语言更加人性化、更加高级、更加智能。
- 对于大规模复杂程序的开发,程序的处理流程并非单一的一条主线,而是错综复杂的网状结构。面向对象编程比起面向过程编程,更能应对这种复杂类型的程序开发。
- 面向对象编程相比面向过程编程,具有更加丰富的特性(封装、抽象、继承、多态)。利用这些特性编写出来的代码,更加易扩展、易复用、易维护。
- 从编程语言跟机器打交道的方式的演进规律中,我们可以总结出:面向对象编程语言比起面向过程编程语言,更加人性化、更加高级、更加智能。
用着面向对象语言,却一直干着面向过程的事
springboot - web开发,也许你一直都在面向过程开发
日常开发过程中,很多业务系统都是基于 MVC 三层架构来开发的,实际上,大部分朋友开发的 过程中,一直都是基于“贫血模型” 面向过程开发的,虽然定义了很多model、vo、po,但是不可否认,这的确是一种面向过程开发。
虽然这种开发模式已经成为标准的 Web 项目的开发模式,但它却违反了面向对象编程风格,是一种彻彻底底的面向过程的编程风格,因此而被有些人称为反模式(anti-pattern)。特别是领域驱动设计(Domain Driven Design,简称 DDD)盛行之后,这种基于贫血模型的传统的开发模式就更加被人诟病。而基于充血模型的 DDD 开发模式越来越被人提倡。
滥用getter、setter
在之前参与的项目开发中,我经常看到,有同事定义完类的属性之后,就顺手把这些属性的 getter、setter 方法都定义上。有些同事更加省事,直接用 IDE 或者 Lombok 插件(如果是 Java 项目的话)自动生成所有属性的 getter、setter 方法。
当我问起,为什么要给每个属性都定义 getter、setter 方法的时候,他们的理由一般是,为了以后可能会用到,现在事先定义好,类用起来就更加方便,而且即便用不到这些 getter、setter 方法,定义上它们也无伤大雅。
实际上,这样的做法我是非常不推荐的。它违反了面向对象编程的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格。
提供了 public 的 getter、setter 方法,这就跟将这两个属性定义为 public 公有属性,没有什么两样了。外部可以通过 setter 方法随意地修改这两个属性的值。除此之外,任何代码都可以随意调用 setter 方法。这完全违反了面向对象的封装、访问权限控制。
例如我们定义购物车,item表示购物车里面的商品,totalPrice记录总价格
public class ShoppingCart {
private int itemsCount;
private double totalPrice;
private List<ShoppingCartItem> items = new ArrayList<>();
public int getItemsCount() {
return this.itemsCount;
}
public void setItemsCount(int itemsCount) {
this.itemsCount = itemsCount;
}
public double getTotalPrice() {
return this.totalPrice;
}
public void setTotalPrice(double totalPrice) {
this.totalPrice = totalPrice;
}
public List<ShoppingCartItem> getItems() {
return this.items;
}
}
里面的所有参数都写了get、set方法,这意味着我们可以在外部任意操作每一个属性。 比如说我们添加一个商品,或者清空购物车,可能在service中是这样实现的:
ShoppingCart cart = new ShoppCart();
...
cart.getItem().add(item);
cart.setItemsCount(...);
cart.setTotalPrice(...);
...
cart.getItems().clear();
...
你可能会说,清空购物车这样的功能需求看起来合情合理啊,上面的代码没有什么不妥啊。你说得没错,需求是合理的,但是这样的代码写法,是典型的面向过程的编程,假如说清除购物车的代码,少写了一句计算总金额的逻辑,那就会直接导致购物车商品不正确;而且通过get方式获取的商品list,可以随意的往里面添加值删除值,这完全违背了面向对象的封装特性。
而正确做法是,将这部分逻辑进行封装,取消不必要的set方法:
public class ShoppingCart {
private int itemsCount;
private double totalPrice;
private List<ShoppingCartItem> items = new ArrayList<>();
public int getItemsCount() {
return this.itemsCount;
}
public double getTotalPrice() {
return this.totalPrice;
}
public List<ShoppingCartItem> getItems() {
}
public void addItem(ShoppingCartItem item) {
items.add(item);
itemsCount++;
totalPrice += item.getPrice();
}
}
基于“贫血模型”传统开发模式
MVC 三层架构中的 M 表示 Model,V 表示 View,C 表示 Controller。它将整个项目分为三层:展示层、逻辑层、数据层。
通常我们Model层只定义了接口的入口,使用@Controller接收参数;数据层一般连接数据库,写一些sql语句;service层才是真正的核心逻辑层。
就像是上面滥用getter、setter的例子,相信各位小伙伴的项目代码中不占少数。
这是一种典型的面向过程开发的模式,也就是“贫血模型”。
像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。
public class UserController {
private UserService userService;
public UserVo getUserById(Long userId) {
UserBo userBo = userService.getUserById(userId);
UserVo userVo = [...convert userBo to userVo...];
return userVo;
}
}
public class UserVo {
private Long id;
private String name;
private String cellphone;
}
public class UserService {
private UserRepository userRepository;
public UserBo getUserById(Long userId) {
UserEntity userEntity = userRepository.getUserById(userId);
UserBo userBo = [...convert userEntity to userBo...];
return userBo;
}
}
public class UserBo {
private Long id;
private String name;
private String cellphone;
}
public class UserRepository {
public UserEntity getUserById(Long userId) {
}
public class UserEntity {
private Long id;
private String name;
private String cellphone;
}
基于“充血模型”的 DDD 开发模式
领域驱动设计,即 DDD,主要是用来指导如何解耦业务系统,划分业务模块,定义业务领域模型及其交互。领域驱动设计这个概念并不新颖,早在 2004 年就被提出了,到现在已经有十几年的历史了。不过,它被大众熟知,还是基于另一个概念的兴起,那就是微服务。
在贫血模型中,数据和业务逻辑被分割到不同的类中。充血模型(Rich Domain Model)正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足面向对象的封装特性,是典型的面向对象编程风格。
在基于贫血模型的传统开发模式中,Service 层包含 Service 类和 BO 类两部分,BO 是贫血模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在 Service 类中。在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain 就相当于贫血模型中的 BO。不过,Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。而 Service 类变得非常单薄。总结一下的话就是,基于贫血模型的传统的开发模式,重 Service 轻 BO;基于充血模型的 DDD 开发模式,轻 Service 重 Domain。
为什么“贫血模型”的面向过程盛行
环境问题:
- 近朱者赤,近墨者黑
- 大多数人都是模仿别人的代码,而别人的代码基本上都是 demo,没有复杂的业务逻辑,基本是贫血模型
- 找不到好的指导与学习对象
- 思维固化,习以为常
- 接触不到复杂业务项目
- 做 web 项目的,很大一部分就是简单的 CURD,贫血模型就能解决
- 公司以任务数来衡量个人价值
个人问题:
- 不考虑项目质量属性
- 只关心当前业务,没有意识去思考后期该如何维护和响应业务变更
- 求快不求质
- 个人以任务数来自我满足
- 没有 60 分和 100 分的概念
- 需求分析、设计、编码合为一体
努力向“充血模型”靠拢
先说一下充血模型中各组件的角色:
- controller 主要服务于非业务功能,比如说数据验证
- service 服务于 use case,负责的是业务流程与对应规则
- Domain 服务于核心业务逻辑和核心业务数据
- rep 用于与外部交互数据
吃透面向对象的基本思想,充分理解面向对象的设计原则与设计模式,让你的开发事半功倍。
参考资料
王争老师《设计模式之美》
|