8.面向对象程序设计
8.1 类和对象的概念
类:类是用来创建同一类型的对象的“模板”,在一个类中定义了该类对象所应具有的成员变量以及方法。
对象:对象是类的实例。
8.2 类之间的关系
系统中的类有那些关系:依赖、关联(聚合、合成)、泛化、实现。
1.依赖:对于外部类或对象的引用;
5.关联:关联暗示两个类在概念上位于相同的级别;
6.聚合:表示一种“拥有”关系,是两个类之间一种整体 / 局部的关系;
7.合成:表示一种更强“拥有”关系,就像人和腿的关系一样。组合而成的新对象对组成部分的内容分配和释放有绝对责任;
8.泛化:表现为继承 extends;
9.实现:表现为实现 implements。
8.3 面向对象程序设计(OOP)
在面向对象出现以前,结构化程序设计是程序设计的主流,结构化程序设计又称为面向过程的程序设计。这种设计方法开发的软件稳定性、可修改性和可重用性都比较差。
与过程相比对象是稳定的。面向对象的软件系统是由对象组成的,复杂的对象是由比较简单的对象组合而成的。也就是说,面向对象方法学使用对象分解取代了传统的功能分解。
面向对象的精髓在于考虑问题的思路是从现实世界人类思维习惯出发的,只要领会了这一点,就领会了面向对象的思维方法。万事万物皆为对象,大至日月星辰,小至沙粒微尘,都是对象。对象包容了一切事物,不仅仅是那些看得见摸得着的是实体,如:地球、汽车、树叶,还包括那些客观存在的事物,如:社会、互联网、朋友圈子等等,包罗万象。
以开车为例,用面向过程的思想去考虑,那么你先得知道怎么启动,怎么踩油门,怎么挂档。这些应该是司机的活,你要把这些步骤都实现出来。如果用面向对象的思想,把自己看成领导,只需要下达命令,告诉它你要去哪里就行了(例如,调用 drive() 方法),具体怎么开,怎么踩油门,怎么挂档,不需要我们去管。
那么 dirve() 这个方法放到车里是否合适呢,是不是应该放到“司机”类更合理呢?封装是很灵活的,没有对与错之分,只有好与更好,需要具体问题具体分析。因为 dirve() 方法要用到油门和车档,而这些东西都在车里面,因此如果将它封装到车这个类里面可能更好些。
下面我们通过对比面向过程和面向对象的设计方式体会什么才是面向对象的思维。
8.4 出圈游戏 —— 面向过程 VS 面向对象
8.4.1 游戏规则
假设有 5 个小孩儿手拉手围成一圈。从第一个小孩儿开始以顺时针方向依次报数 —— “1,2,3”,报 3 的人出列,第四个人从 1 开始重新报数,报到 3 时再出列。如此下去,直到所有人全部出列为止,要求按照出列的顺序输出他们的序号。
下面来看图理解,首先有 5 个小孩围成一个圈:
 
 

图一、 5 个小孩儿围成一圈
 
 

图二、 数到 3 的小孩儿退出去
 
 

图三、再从 2 号开始数三个人, 5 号退出,然后是 2 号,最后是 4 号。
最终输出的顺序应该是 3、1、5、2、4。
8.4.2 出圈游戏 —— 面向过程(cirgame/ CircleGame1.as)
下面用面向过程的思想写这个程序,通过读注释先来看一下这个程序:
// 有 5 个小孩儿围成的圈
var array:Array = new Array(5);
for (var i = 0; i < array.length; i++) {
// 如果元素值为 true 表示他在圈内,如果是 false 表示不在圈内
array[i] = true;
}

// 圈内还剩多少人,最开始人都在,等于 array.length
var leftCount:int = array.length;
// 当前所报的数,初始为 0
var countNum:int = 0;
// 圈子的数组下标,表示当前指向的是谁
var index:int = 0;
while(leftCount > 0) {
if (array[index] == true) {
// 如果当前这个人在圈内则报数
countNum++;
if (countNum == 3) {
// 如果所报的数是 3 则出列,剩余人数减1,并且下一次从新开始报数
trace("out " + (index + 1));
array[index] = false;
leftCount--;
countNum = 0;
}
}

// 数组下标增加
index++;
if (index == array.length) {
// 如果下标是最后一个位则归 0,因为这个圈是圆的
index = 0;
}
}
用 array 数组代表这个围成的圈,开始让圈数组中的每个元素都为 true,表示它们都在圈内,如果设为 false 则表示不在圈内,后面报数的时候就不予考虑了。
接下来定义三个变量分别表示圈内还剩多少人,所报的数是多少和数组下标。
下面 while 循环开始,只有圈内还有人(leftCount > 0)就进行循环,首先判断当前 index 所指的元素是否为真,如果是则报数加 1,再判断是不是加到 3 了,如果是则打印出当前的数组下标,再将该元素设为 false,剩余人数减1,下一次从新开始报数。
最后让数组下标加 1,当指到最后时,将数组下标置为 0,因为这是一个圈,要用循环的数组来表示。
8.4.3 出圈游戏 —— 面向对象(cirgame/ CircleGame2.as)
回顾上一个例子,在面向过程的程序中,明明是围成的一个圈儿,却要看成是一个数组;明明是一个个小孩儿却要看成是数组的一个个元素。这不就是为了让计算机看懂吗?但是,面向对象是更加接近人类的思维模式,我们在现实中看到的就是一个个小孩儿,怎么能说是数组?那么这一个个小孩儿就是一个个对象,他们都是 Kid。围成的这个圈,就是一个 KidCircle。很自然吧,比大自然还自然!下面来体会面向对象的设计思想:
package cirgame {
public class CircleGame2 {
public function CircleGame2() {
var kc:KidCircle = new KidCircle(5);
var countNum:int = 0;
var k:Kid = kc.head;

while (kc.count > 0) {
countNum++;
if (countNum == 3) {
trace(k.id + 1);
kc.remove(k);
countNum = 0;
}
k = k.right;
}

}
}
}
// 每个 Kid 都有自己的 id,并且左右手还拉着其它的 Kid
class Kid {
var id:uint;
var left:Kid;
var right:Kid;
}
// 这个圈子里可以加入或移除一些 Kid
class KidCircle {
var count:uint = 0;
var head:Kid;
var rear:Kid;

function KidCircle(n:uint) {
for (var i = 0; i < n; i++) {
var kid:Kid = new Kid();
add(kid);
}
}

function add(kid:Kid):void {
kid.id = count;
if (count == 0) {
head = kid;
rear = kid;
kid.left = kid;
kid.right = kid;
} else {
rear.right = kid;
kid.left = rear;
kid.right = head;
head.left = kid;
rear = kid;
}
count++;
}

function remove(kid:Kid):void {
if (count <= 0) {
return;
} else if (count == 1) {
head = rear = null;
} else {
kid.left.right = kid.right;
kid.right.left = kid.left;
if (kid == head) {
head = kid.right;
} else if (kid == rear) {
rear = kid.left;
}
}
count--;
}
}
这段程序中设计了两个类,代表两类客观事物 —— 小孩(Kid)和圈子(KidCircle)。从 8.4.1 的图中可以确切地看到。Kid 有三个属性:id 号、左手和右手,左手拉着一个 Kid,右手拉着一个 Kid,因此 left 和 right 存放两个 Kid 的引用。
下面是 KidCircle 类,代表围成的圈子,这个圈子可以加入或移除一些 Kid,因此有 add 和 remove 两个方法。head 和 rear 两个成员变量用于指向队首和队尾的两个 Kid,因为些添加的 Kid 要放在队尾(rear)的后面,因为这是一个圈子,所以还需要让队尾的小孩儿拉住队首(head)的小孩儿,因此需要保存这两个成员变量。
最后是测试类,主要的逻辑和前一个例子比较像,这里就不多解释了。
通过这个例子大家可以看出,我们无形之间就完成了一个数据结构 —— 双向循环链表。而前面面向过程的例子,实际上就是一个顺序的存储结构 —— 线性表。
后面,我会带大家写一个贪吃蛇的游戏,目的是学习面向对象编程的思想(并非该游戏本身),在贪吃蛇中我们就会运用到类似于单向链表的结构,如果双向链表掌握了,那么单向一定没问题。
通过本节请大家认真体会面向对象的设计思想。一次学会,终身受用。