面向对象与面向过程

目录

1.概述

1.1 什么是面向对象编程和面向对象编程语言?

1.2 什么是面向过程编程与面向过程编程语言?

2.哪些代码设计看似是面向对象,实际是面向过程的?

2.1 滥用getter和setter方法

2.2 滥用全局变量和全局方法

2.3 定义数据和方法分离的类

2.3.1 贫血模型

2.3.2 充血模型


1.概述

1.1 什么是面向对象编程和面向对象编程语言?

面向对象编程的英文缩写为OOP,面向对象编程语言的英文缩写为OOPL。这两个概念可以用下面两句话来概括:

  • 面向对象编程是一种编程范式或编程风格。它以类或对象为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石。
  • 面向对象编程语言是支持类或对象的语法机制,能方便地实现面向对象编程四大特性(抽象、封装、继承、多态)的编程语言。

一般来讲,面向对象编程都是通过使用面向对象编程语言来实现的,但是,不使用面向对象编程语言,我们照样可以进行面向对象编程。反过来讲,即使使用面向对象编程语言,写出来的代码也不一定是面向对象编程风格的,也可能是面向过程编程风格的。一般来讲,面向对象软件开发要经历三个阶段:面向对象分析(OOA)、面向对象设计(OOD)、面向对象编程(OOP)。

1.2 什么是面向过程编程与面向过程编程语言?

类比面向对象编程与面向对象编程语言的定义,对于面向过程编程和面向过程编程语言这两个概念,给出如下定义:

  • 面向过程编程也是一种编程范式或编程风格,它也过程(可以理解为方法、函数、操作)作为组织代码的基本单位,以数据(可以理解为成员变量、属性)与方法相分离为主要特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
  • 面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性(比如多态、继承、封装),仅支持面向过程编程。

总结来看,面向过程和面向对象最基本的区别是代码的组织方式不同。面向过程风格的代码被组织成了一组方法集合及其数据结构,方法和数据结构的定义是分开的。面向对象风格的代码被组织成一组类,方法和数据结构被绑定在一起,定义在类中。

面向对象编程相比面向过程编程有如下优势:

  • OOP更加能够应对大规模复杂程序的开发。
  • OOP风格的代码更易复用,易扩展,易维护。
  • OOP语言更加人性化,更高级,更智能。

2.哪些代码设计看似是面向对象,实际是面向过程的?

在用面向对象编程语言进行软件开发的时候,我们有时候会写出面向过程风格的代码。有些是有意为之,并无不妥,有些是无意为之,会影响到代码的质量。下面通过三个例子来看一下,什么样的代码看似是面向对象风格,实际是面向过程风格的。

2.1 滥用getter和setter方法

在我们的项目开发中,经常看到有同事定义完类的属性之后,就顺手把这些属性的getter和setter方法都定义上。他们的理由一般是,为了以后可能会用到,现在事先定义好,而且即便用不到这些getter、setter方法,定义上它们也无伤大雅。实际上,这种做法违反了面向对象编程的封装性,相当于将面向对象编程风格退化成了面向过程编程风格。比如下面的代码:


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;
  }
  
  public void addItem(ShoppingCartItem item) {
    items.add(item);
    itemsCount++;
    totalPrice += item.getPrice();
  }
  // ...省略其他方法...
}

在这段代码中,ShoppingCart是一个简化后的购物车类。itemsCount和totalPrice,虽然将它们定义成private私有属性,但是提供了public的getter、setter方法,这就跟将这两个属性定义成public公有属性,没有什么两样了。外部可以通过setter方法随意地修改这两个属性的值,这就会导致其跟items属性的值不一致。而面向对象封装的定义是:通过访问权限控制,隐藏内部数据,外部仅能通过类提供的有限的接口访问、修改内部数据。所以,暴露不应该暴露的setter方法,明显违反了面向对象的封装特性,代码就退化成了面向过程编程风格了。

接着,我们再来看一下items这个属性,它的getter方法,返回的是一个List集合容器。外部调用者在拿到这个容器之后,是可以操作容器内部数据的,也就是说,是可以修改items中的数据。例如:


ShoppingCart cart = new ShoppCart();
...
cart.getItems().clear(); // 清空购物车

在我们看来,清空购物车这样的功能需求看起来是合情合理的,上面的代码没有什么不妥。但是,这样的代码写法,会导致itemsCount和totalPrice、items三者数据不一致。我们不应该将清空购物车的业务逻辑暴露给上层代码。正确的写法应该是,ShoppingCart类中定义一个clear()方法,将清空购物车的业务逻辑封装在里面,透明地给调用者使用。clear()方法的具体实现代码如下:


public class ShoppingCart {
  // ...省略其他代码...
  public void clear() {
    items.clear();
    itemsCount = 0;
    totalPrice = 0.0;
  }
}

你可能还会说,我有一个需求是要查看购物车中都买了啥,此时ShoppingCart类不得不提供items属性的getter方法了,那该怎么办呢?我们可以通过Java提供的Collections.unmodifiableList()方法,让getter方法返回一个不可被修改的UnmodifiableList集合容器,而这个容器重写了List容器中跟修改数据相关的方法,比如add(),clear()等方法。一旦我们调用了这些修改数据的方法,代码就会抛出UnsupportedOperationException异常,这样就避免了容器中的数据被修改。代码如下:


public class ShoppingCart {
  // ...省略其他代码...
  public List<ShoppingCartItem> getItems() {
    return Collections.unmodifiableList(this.items);
  }
}

public class UnmodifiableList<E> extends UnmodifiableCollection<E>
                          implements List<E> {
  public boolean add(E e) {
    throw new UnsupportedOperationException();
  }
  public void clear() {
    throw new UnsupportedOperationException();
  }
  // ...省略其他代码...
}

ShoppingCart cart = new ShoppingCart();
List<ShoppingCartItem> items = cart.getItems();
items.clear();//抛出UnsupportedOperationException异常

总结来看,在设计实现类的时候,除非真的需要,否则,尽量不要给属性定义setter()方法。除此之外,尽管getter方法相对setter方法要安全一些,但是如果返回的是集合容器,也要防止集合内部数据被修改的危险。

2.2 滥用全局变量和全局方法

在面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。Constants类和Utils类是我们最常用的全局变量和全局方法。首先,让我们看一段Constants类的代码:


public class Constants {
  public static final String MYSQL_ADDR_KEY = "mysql_addr";
  public static final String MYSQL_DB_NAME_KEY = "db_name";
  public static final String MYSQL_USERNAME_KEY = "mysql_username";
  public static final String MYSQL_PASSWORD_KEY = "mysql_password";
  
  public static final String REDIS_DEFAULT_ADDR = "192.168.7.2:7234";
  public static final int REDIS_DEFAULT_MAX_TOTAL = 50;
  public static final int REDIS_DEFAULT_MAX_IDLE = 50;
  public static final int REDIS_DEFAULT_MIN_IDLE = 20;
  public static final String REDIS_DEFAULT_KEY_PREFIX = "rt:";
  
  // ...省略更多的常量定义...
}

这段代码中,我们把程序中所有用到的常量都集中到Constants类中。实际上,定义一个如此大而全的Constants类,并不是一个很好的设计思路,原因如下:

  • 影响代码的可维护性:如果参与项目的开发人员很多,在开发过程中,可能都要涉及修改这个类,这个类就会变得越来越大,查找修改某个常量就变得比较费时,还会增加提交代码冲突的概率。
  • 增加代码的编译时间:当Constants类中包含很多常量定义时,依赖这个类的代码就会很多。那每次修改Constants类,都会导致依赖它的类文件重新编译,因此会浪费很多不必要的编译时间。在我们的开发中,每次运行单元测试,都会触发一次编译的过程,这个编译时间就有可能会影响到我们的开发效率。

那应该如何改进Constants类的设计呢?有两种思路可以借鉴:

  • 将Constants类拆解为功能单一的多个类,比如跟MySQL配置相关的,就放置到MySqlConstants类中。
  • 尽量减少使用Constants,如果某个类用到了常量,我们就把这个常量定义到这个类中。比如,RedisConfig类用到了Redis配置相关的常量,那我们就直接将这些常量定义到RedisConfig中,这样也会提高类设计的内聚性和代码的复用性。

讲完了Constants类,我们再讨论一下Utils类。实际上,只包含静态方法不包含任何属性的Utils类,是彻彻底底的面向过程的编程风格。但是,Utils类存在的目的是解决代码复用的问题,它在软件开发中还是很有价值的。所以,这里并不是说完全不能用Utils类,而是说要避免滥用,不要不假思索地随意去定义Utils类。

2.3 定义数据和方法分离的类

在后端开发中,代码结构一般分为Controller层、Service层、Respository层。相应地,我们又会定义VO、BO、Entity数据模型。一般来说,VO、BO、Entity中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的Controller类、Service类、Respository类中。这就是典型的面向过程的编程风格。

实际上,这种开发模式叫做基于贫血模型的MVC三层架构开发模式,也是我们常见的一种Web项目的开发模式。虽然这种开发模式已经成为标准的Web项目开发模式,但它却违反了面向对象编程风格,是一种彻彻底底的面向过程的编程风格,因此有些人称为反模式。特别是在领域驱动设计(DDD)盛行之后,这种基于贫血模型的传统开发模式就更加被人诟病了。而基于充血模型的DDD开发模式越来越被人提倡。

2.3.1 贫血模型

目前几乎所有的业务后端系统,都是基于贫血模型的。例如下面的代码:


// 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是一个纯粹的数据结构,只包含数据,不包含任何业务逻辑。像UserBO这种只包含数据,不包含业务逻辑的类,就叫做贫血模型。

2.3.2 充血模型

在贫血模型中,数据和业务逻辑被分割到不同的类中。充血模型正好相反,数据和对应的业务逻辑被封装到同一个类中。因此,这种充血模型满足对象的封装特性,是典型的面向对象编程风格。

在基于充血模型的DDD开发模式中,Service层包含Service类和Domain类两部分,Domain就相当于贫血模型中的BO。不过,Domain与BO的区别在于它是基于充血模型开发的,即包含数据,也包含业务逻辑,相应的Service类会变的非常单薄。此时的Service类主要是有下面这几个职责:

  • Service类负责与Repository交流,获取数据库中的数据并转化成领域模型,然后由领域模型来完成业务逻辑,最后将数据存回数据库。
  • Service类负责一些非功能性及与第三方系统交互的工作,比如幂等,事务,记录日志,调用其他接口等。

尽管Service层被改造成了充血模型,但是Controller层和Repository层还是贫血模型,是否有必要进行充血模型建模呢?答案是没必要的,Controller层主要负责接口的暴露,Repository层主要负责与数据库打交道,这两层的业务逻辑并不多,比较简单,没必要做充血模型。

那我们在什么时候应该考虑基于充血模型的DDD开发模式,什么时候考虑基于贫血模型的传统开发模式?

如果我们开发的系统业务比较简单,简单到就是基于SQL的CRUD操作,根本不需要精心设计充血模型,此时我们应该考虑使用贫血模型。如果我们的业务非常复杂,比如包含各种利息计算模型,还款模型等复杂业务的金融系统,此时我们应该考虑充血模型。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值