15 面向对象思想

1 由 “对象” 引发的误会

女友:听说,你们程序员需要面向对象编程,也没见过你写代码时,面向我呀?

程序员:不是啦,这个面向对象的对象不是你这个对象啦。

女友:什么?你还有其他对象吗?

程序员:(一股寒气袭来)

2 什么是面向对象

面向对象,英文名字叫Object Oriented,是一种软件开发方法。是 和面向过程相对应 的。我们要理解 面向对象 的好处,首先要知道什么是 面向过程

2.1 什么是面向过程

实际上,我们之前课程中写过的所有代码,使用的都是 面向过程 的思想。

我们可以使用吃饭作为例子,去体会两者的区别。

使用 “面向过程思想” ,我们会怎样去做 “吃饭” 这件事呢?

  1. 首先,我们就需要先想好吃什么;
  2. 然后去做买菜、洗菜、洗米、蒸饭、炒菜等等一系列的事情;
  3. 之后把做好的饭菜端上餐桌;
  4. 吃饭;
  5. 收拾餐具。

我们需要一步一步地,完成每一项过程,才可以最终达成 “吃饭” 这个目标。这就是 “面向过程思想” 。我们的这个思想,可以转化为下面的程序。

int main(){
    指定菜单();
    买菜();
    洗菜();
    炒菜();
    端上桌();
    吃饭();
    洗碗();
    
    return 0;
}

2.2 面向过程思想的局限性

“面向过程思想” 按部就班地完成一件事,我们很容易看出它的一个显著缺点,它实在 太麻烦 了。

除此之外,还有很多遇到很多其他问题,比如:

  1. 我不想吃米饭,我想吃馒头。
  2. 上次买的菜家里面还有,不需要去买菜。
  3. 中午吃剩下的菜家里面还有,直接热一热就可以吃了。
  4. 这次去的一家超市提供洗菜服务,不需要我们自己洗菜了。

以上这些突发事件,在编程中就叫做需求变更或者新的需求,这种事情发生是必然会发生的。

那么,有新的需求了怎么办,上面这种自己动手做饭的场景,就只能对代码进行大改了。对于程序员来说,就需要通读代码,找出可以复用的方法,然后重新调用,不能复用的就重新写一个。时间久了,方法就会越来越多,系统维护越来越复杂。

比如,我们要处理第一个问题,我们就需要添加 if 语句,判断是想吃馒头,还是想吃米饭。那之后我们有想吃蛋炒饭了呢?

2.3 什么是面向对象

我们依然想要达成 “吃饭” 的目的,这次,我们使用 “面向对象思想” 来做这件事。

这次,我们不再什么事情都自己做,我们可以认为我们是个贵族,我们只管想菜单,然后吃饭。

在我们的厨房中,有很多工作人员。比如:采购员,他负责确认库存和物品采购;大厨,负责炒菜;服务员,负责将做好的菜端上桌……

这些工作人员,就是 “对象” 。我们不再需要什么事情都自己做,而是将事情派发给工作人员们,也就是 “对象” 去做。 那,我们现在的程序,会变成什么样子呢?

int main(){
    // 究竟要吃什么还是要自己想的
    指定菜单();
    采购员.买菜();
    洗菜工.洗菜();
    大厨.炒菜();
    服务员.端上桌();
    // 总不能别人替自己吃吧?
    吃饭();
    服务员.洗碗();
    
    return 0;
}

现在,还是 “我” 在亲自指挥各个 “工作人员(对象)” 该做什么事,那么,如果我们雇佣一个 “厨房总管” 呢?

int main(){
    厨房总管.点餐(红烧肉,糖醋鱼,可乐).送达时间(一小时后).备注(可乐加冰);
    吃饭();
    
    return 0;
}

这时,我们只需要向 “厨房总管(对象)” 进行点餐和提要求,至于具体这些饭菜是怎么做出来的,由 “厨房总管(对象)” 去命令 “大厨(对象)” 等对象。“我” 只管饭菜到了桌上,开始吃。那谁来洗碗呢? “厨房总管(对象)” 会安排 “服务员(对象)” 时刻关注 “我” 有没有吃完,然后把餐具清洗干净, “我” 什么都不用安排。

甚至,在 “万物皆对象” 的思想下,“我” 又何尝不可以是个对象呢?

int main(){
    我.点餐();
    我.吃饭();
    
    return 0;
}

2.4 面向对象的优势

“面向对象思想” 最大的优势是,它可以将一项极为复杂的事情,委派给几个对象去做,而这时,我们不再需要从整件要做的事的角度思考问题,我们只需要考虑当前这个对象,需要怎么做它需要做的事。

比如:“我” 这个对象,只需要做两件事,点菜、吃饭。至于菜怎么做的,“我” 不在乎。所以我们在写 “我” 的代码时,完全不需要考虑怎么做饭。

再比如:“大厨” 这个对象,他只管炒菜,菜是怎么买来的,怎么洗的,抄完怎么被端上餐桌,他完全不在乎。所以,我们在写 “大厨” 的代码时,只需要考虑怎么炒菜。

“面向对象思想” ,实际上就是大事化小、化整为零、人员管理等思想,在编程领域的体现。

在需求变更时,我们需要做的工作也会简单很多。

比如:“家里有菜,就不需要买” 这个需求,只需要让 “采购员(对象)” 确认一下即可。“家里有剩菜,把菜热一热就可以” 这个需求,只需要让 “厨房总管 (对象)” 检查一下冰箱。

有了对象,我们在处理需求变更时,只需要修改涉及到的对象的代码,因此,可以很大程度降低我们改代码的工作量。

3 面向对象中的常见概念

3.1 对象

对象是现实世界中一个客观存在的事物。它可以是具体事物,如一辆车、一个人;也可以是一个抽象的事物,如一项功能、一次活动。

3.2 对象的属性和行为

属性 是用来描述对象的特征的数据,也就是对象中的成员变量。如“三角形”对象的三条边长、“学生”对象的身高体重等。

行为 是用来描述对象动态特征的操作序列,也就是对象中的方法(函数)。如“三角形”对象的求面积方法、“厨师”对象的炒菜方法等。

3.3 类

类即 “类型” ,是对象抽象的结果,也就是找到一组具有相似属性和功能的对象,忽略掉这些对象的个别的、非本质的特征,找出这些对象的共性,从而得出一个抽象的概念。比如一个班上有几十名学生,每名学生都是一个 “学生” 对象,每名同学的姓名、学号等等诸多方面的具体值都是不同的,但这些学生都具有姓名、学号等属性,因此他们具有共性,他们都属于“学生类”。

类是对象的抽象,对象是类的特例,或者说是类的具体表现形式。

3.4 封装

封装具有两重含义。

一是:把对象的全部属性和行为结合在一起,成为一个 “对象” 。

二是:尽可能隐藏对象的实现细节,从而对外形成一个边界,仅保留有限的接口,使之可以与外部产生联系。这有助于让对象更加易用,也可以更好地保证数据的安全性。

我们可以通过 “电视机” 来理解 “封装” 概念。一台电视机,它是由显示屏、扬声器以及其他各种线路和零件组成,而这些东西,都 “封装” 在电视机的外壳中,仅仅保留了显示屏用于输出,几个按钮或者旋钮用于输入。

这样有什么好处呢?

我们作为用户,不需要理解电视机内部的实现原理,就可以对电视机进行使用。这是 封装的第一个好处:使对象更加易用。

我们作为用户,即使对电视机原理一窍不通,仅仅通过电视机外部的按钮,对电视机进行胡乱的操作,也很难把电视机弄坏。这是 封装的第二个好处:更好地保证数据的安全性。

3.5 继承与派生

对相似的对象,提取其共性,就可以抽象出类,那么我们可不可以对相似的类,继续提取共性,从而得到更加抽象的类呢?

举个例子,我们想象这样一个游戏,玩家的敌人有两种,分别是使用近战武器的剑客、使用远程武器的弓箭手。我们分别对剑客、弓箭手进行抽象,就可以得到 “剑客类” 、 “弓箭手类” 。“剑客类” 和 “弓箭手类” 中,都包含生命值、攻击力属性,也都包含 “攻击” 这一行为(方法),因此我们可以对 “剑客类” 和 “弓箭手类” 进行抽象,得到 “敌人类” 。

更加抽象的 “敌人类” 中的属性和行为,在 “剑客类” 和 “弓箭手类” 中都有,这种现象,就像是父辈的财产和意志由子代 “继承” 一样。因此,我们称 “剑客类” 和 “弓箭手类” 继承 了 “敌人类” ,其中 “剑客类” 和 “弓箭手类” 被称为 “子类” , “敌人类” 被称为 “父类”

反过来,我们可以说 “敌人类” 派生 出了 “剑客类” 和 “弓箭手类” ,其中 “剑客类” 和 “弓箭手类” 被称为 “派生类” , “敌人类” 被称为 “基类”

3.6 多态

多态是指不同的子类在继承父类后分别都重写覆盖了父类的方法,即父类同一个方法,在继承的子类中表现出不同的形式。

比如上一个例子中的 “剑客类” 和 “弓箭手类” 都继承了 “敌人类” 的 “攻击” 方法,但剑客执行 “攻击” 时,会挥砍,而弓箭手在执行 “攻击” 时,会射箭。