面向对象编程01:面向对象与面向过程

理论一:什么是面向对象?

面向对象编程和面向对象编程语言的区别?

面向对象是一种编程风格,面向对象编程语言是支持类或者对象的语法机制,使用面向对象语言能够很方便的实现面向对象编程。

核心概念: 类(class)和对象(object) 四大特性:封装、抽象、继承、多态

UML(Unified Model Language),统一建模语言。

Algorithms4/umlcheatsheet.jpg at master · gdhucoder/Algorithms4 (github.com)

img

理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?

封装(Encapsulation)

使用钱包的例子,id和createTime在初始化的时候生成,同时只提供增加和修改余额的方法。

封装的意义:可控、通过有限的方法暴露必要的属性,一方面可以控制权限和保护数据一致性,另一方面也更加易于使用。

抽象(Abstraction)

过滤非必要信息:抽象只关注功能点不关注实现过程

很多设计原则都体现了抽象的思想,比如开闭原则、基于接口而非实现编程

定义接口名称的时候要有抽象思维,不要在方法定义中暴露太多的实现细节

继承(inheritance)

符合人类的认知,但是过多的继承会导致代码可读性、可维护性变差。所以很多人觉得继承是一种反模式。比如”多用组合少用继承“这一设计思想。

多态(polymorphism)

多态的定义:子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。

多态的前提是编程语言提供的特殊语法机制——继承加方法重写

  • 父类对象可以引用子类对象,DynamicArray dynamicArray = new SortedDynamicArray();
  • 支持继承
  • 支持子类重写父类的方法

除了这种方式外,还有两种常见的方式 —— 一是利用接口类语法,二是duck-typing语法。

**多态能解决的问题:**提高代码的可扩展性和复用性。

理论三:面向对象和面向过程

发展流程:面向过程 -》 面向对象 -》 函数式编程

其中粗略的来讲,面向过程已经几乎过时,面向对象是当前主流的设计思想,而函数式编程目前还没有被广泛接受。

在实际工作中,很多人只是在用面向对象语言实现面向过程的编码罢了。

抛出问题

(1)什么是面向过程编程与面向过程编程语言?

(2)面向对象编程相比面向过程编程有哪些优势?

(3)为什么说面向对象编程语言比面向过程编程语言更高级?

(4)有哪些看似是面向对象实际是面向过程风格的代码?

(5)在面向对象编程中,为什么容易写出面向过程风格的代码?

(6)面向过程编程和面向过程编程语言就真的无用武之地了吗?

(1)定义面向过程编程也是一种编程范式,以过程(可以理解为方法、函数、操作)作为组织代码的基本单元,以数据(可以理解为成员变量、属性)与方法相分离为主要的特点。

(2)优势

  • 大规模复杂程序的开发(对于简单编程来说,面向过程反倒更有优势;对于大规模开发流程来说,面向对象编程提供了一种更加清晰的、更加模块化的代码组织方式)
  • OOP风格的代码更易复用、易扩展、易维护 —— 也就是四大特性:封装、抽象、继承、多态
  • OOP语言更加人性化、更加高级、更加智能

在文章中我讲到,面向对象编程比面向过程编程,更加容易应对大规模复杂程序的开发。但像 Unix、Linux 这些复杂的系统,也都是基于 C 语言这种面向过程的编程语言开发的,你怎么看待这个现象?这跟我之前的讲解相矛盾吗?

使用任何一个编程语言编写的程序,最终执行上都要落实到CPU一条一条指令的执行(无论通过虚拟机解释执行,还是直接编译为机器码),CPU看不到是使用何种语言编写的程序。对于所有编程语言最终目的是两种:提高硬件的运行效率和提高程序员的开发效率。然而这两种很难兼得。 C语言在效率方面几乎做到了极致,它更适合挖掘硬件的价值,如:C语言用数组char a[8],经过编译以后变成了(基地址+偏移量)的方式。对于CPU来说,没有运算比加法更快,它的执行效率的算法复杂度是O(1)的。从执行效率这个方面看,开发操作系统和贴近硬件的底层程序,C语言是极好的选择。 C语言带来的问题是内存越界、野指针、内存泄露等。它只关心程序飞的高不高,不关心程序猿飞的累不累。为了解脱程序员,提高开发效率,设计了OOP等更“智能”的编程语言,但是开发容易毕竟来源于对底层的一层一层又一层的包装。完成一个特定操作有了更多的中间环节, 占用了更大的内存空间, 占用了更多的CPU运算。从这个角度看,OOP这种高级语言的流行是因为硬件越来越便宜了。我们可以想象如果大众消费级的主控芯片仍然是单核600MHz为主流,运行Android系统点击一个界面需要2秒才能响应,那我们现在用的大部分手机程序绝对不是使用JAVA开发的,Android操作系统也不可能建立起这么大的生态。

理论四:哪些代码看似是面向对象,实际是面向过程的?

这一章节主要回答的是上一章节的后三个问题。

三个典型的看似是面向对象风格但其实是面向编程的代码案例

1. 滥用getter、setter方法

不推荐使用Lombok这样的插件,它违反了面向对象编程的封装特性。

比如一个购物车类,有三个属性:数量、价格和购物车列表。

数量和价格定义getter和setter方法明显违反了面向对象的封装特性,items即使只暴露getter方法也是有问题的,因为上层代码拿到list之后仍然可以对购物车中的数据做修改,比如像下面这样:

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

但是如果不提供getter方法实际上是看不到购物车的,这时候怎么办呢?解决方法也很简单,以Java为例,我们可以通过 Java 提供的 Collections.unmodifiableList() 方法,让 getter 方法返回一个不可被修改的 UnmodifiableList 集合容器,而这个容器类重写了 List 容器中跟修改数据相关的方法,比如 add()、clear() 等方法。一旦我们调用这些修改数据的方法,代码就会抛出 UnsupportedOperationException 异常,这样就避免了容器中的数据被修改。具体的代码实现如下所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22

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异常

不过这样也是有风险的,因为即使无法修改List,也可以修改List中的item,这个问题可以留待日后讨论。

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

2. 滥用全局变量和全局方法

在面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。单例类对象在全局代码中只有一份,所以,它相当于一个全局变量。静态成员变量归属于类上的数据,被所有的实例化对象所共享,也相当于一定程度上的全局变量。而常量是一种非常常见的全局变量,比如一些代码中的配置参数,一般都设置为常量,放到一个 Constants 类中。静态方法一般用来操作静态变量或者外部数据。你可以联想一下我们常用的各种 Utils 类,里面的方法一般都会定义成静态方法,可以在不用创建对象的情况下,直接拿来使用。静态方法将方法与数据分离,破坏了封装特性,是典型的面向过程风格。

下面以Constants和Utils这两种常见的类为例,说说全局变量和全局方法的利与弊。

Constants

定义一个又大又全的常量类不是一个很好的设计思路,主要原因有:

  • 影响代码的可维护性,增加了代码冲突的概率
  • 增加代码的编译时间,对于比较大的工程来说,编译一次项目花费的时间可能会很长
  • 影响代码的复用性

如何解决?

  • 其一是按照功能进行拆分,比如MySQLConstants、RedisConstants等等
  • 其二是不单独设计Constants常量类,而是直接定义在使用的类中,这样也提高了类设计的内聚性和代码的复用性
Utils

Utils类我们一般会放一些很常用的操作,比如URL拼接、JSON数据的处理等等,这些方法不需要共享任何数据,不需要定义任何属性,所以是彻彻底底的面向过程的编程风格。在这种情况下,我们是可以使用Utils类的,只是要避免滥用,要避免不加思考的随意定义Utils类。

此外,与常量类一样,尽量针对不同的功能设计不同的Utils类,比如FileUtils、IOUtils、StringUtils、UrlUtils等。

3. 定义数据和方法分离的类

我们再来看最后一种面向对象编程过程中,常见的面向过程风格的代码。那就是,数据定义在一个类中,方法定义在另一个类中。你可能会觉得,这么明显的面向过程风格的代码,谁会这么写呢?实际上,如果你是基于 MVC 三层结构做 Web 方面的后端开发,这样的代码你可能天天都在写。

传统的MVC架构(前后端分离的模式)分为Controller层、Service层、Repository层(Dao层),在每一层中也会定义相应的VO、Dto、Entity等。一般情况下,VO、Dto只会定义数据不会定义方法,所有操作都在对应的controller、Service以及Dao中,而这就是典型的面向过程的编程风格。

实际上,这种开发模式叫做基于贫血模型的开发模式。

为什么在面向对象编程中容易写出面向过程风格的代码?

  • 面向过程符合流程化的思维模式,而面向对象是自底向上的,将任务进行拆解和组装
  • 面向对象设计起来比较难,而很多工程师在开发的过程中更倾向于不动脑子的方式去实现需求

面向过程及面向过程的编程语言真的再无用武之地了吗?

  • 在一些微小程序、数据处理和算法中,脚本式的面向过程的编程风格更合适
  • 面向过程是面向对象的基础,即使在面向对象语言中,类里面每个方法的实现过程就是面向过程的
  • 我们的最终目的是写出易维护、易读、易复用的高质量代码,所以这两种编程风格并不是非黑即白的。
updatedupdated2023-06-032023-06-03
加载评论