天天看点

重新认识面向对象——Java写了五年,你真的弄明白什么是面向对象了吗?不,你一直都是在面向过程编程

文章目录

  • ​​什么是面向过程编程​​
  • ​​什么是面向对象编程​​
  • ​​用着面向对象语言,却一直干着面向过程的事​​
  • ​​springboot - web开发,也许你一直都在面向过程开发​​
  • ​​滥用getter、setter​​
  • ​​基于“贫血模型”传统开发模式​​
  • ​​基于“充血模型”的 DDD 开发模式​​
  • ​​为什么“贫血模型”的面向过程盛行​​
  • ​​努力向“充血模型”靠拢​​
  • ​​参考资料​​

什么是面向过程编程

“面向过程”(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(...); // itemsCount++;
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)。

// 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;
}      

基于“充血模型”的 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 用于与外部交互数据

参考资料