人们使用面向对象设计有两个很重要的原因,一个是从现实的角度去考虑问题而不是机器的角度,这使开发变得更快更敏捷。第二个原因便是为了可扩展编程与代码复用,使得在软件开发过程的重复劳动更少。本次讲座要讲的就是如何利用“封装变化的事物”这一思想来进行面向对象的设计,达到代码复用和可扩展编程的目的。
注:此讲座需要大家提前掌握面向对象的一些概念:类,对象,继承,封装(之前YGui的面向对象讲座中讲述)
在这个世界,万事万物都是不一样的,我们在编写程序的时候面对的问题也都各不一样,一般情况下面对不同的问题我们会有不同的解决方法,这样的设计方法可以使我们的程序设计高度针对我们所要解决的问题,使得代码执行起来很高效。但是,当我们有大量的问题需要解决的时候,这样的设计方法可能会导致大量的工作甚至是大量的重复劳动,此时我们希望能将以前的解决方案与代码能最大程度的用到新的问题上来减少我们的劳动。其次,即使我们已经解决得很好的问题也有可能随着时间的推移,要求做出一些改变或者增加一些功能来适应新的情况,此时,我们希望最大程度不改变原有代码的结构,同时能应付新的变化。在这种情况,我们的一个解决方法就是把那些变化的事物都封装起来,使得从外部看他们都是一样的,这样我们就可以从容面对各种变化的事物以及问题了。而这种思想,在程序设计中是通过面向对象的技术来实现的。
我们会通过几个例子来逐步了解如何来封装变化的事物。
首先假设我们在写一个游戏,在这个游戏中我们会想玩家提供数种不同的交通工具,例如:轿车、卡车、大巴甚至是坦克。他们都是不同的交通工具,也就是变化的物,而我们希望在程序代码中不需要对不同交通工具写不同的代码来控制他们(起码对于基本的一些操作,如前进、后退是这样),取而代之的是统一的方法,例如:
vehicle.moveFoward();
这时我们使用接口来达到这个目的,把不同交通工具动作的具体实现代码封装在接口之内:
interface vehicle {
void moveFoward();
void moveBack();
}
class moto implements vehicle {
void moveFoward() { this.x += 1; }
void moveBack() { this.x -= 1; }
}
class tank implements vehicle {
void moveFoward() { this.x += 0.2; }
void moveBack() { this.x -= 0.2; }
}
通过这样的方式,我们可以对任意多不同的交通工具编写不同的行动代码,而在使用这些交通工具的地方,代码可以统一为vehicle.moveFoward();这样就使得这个游戏的扩展性得到了提升。
当然,我们可能需要用到一些特殊的交通工具的独有的能力,比如说坦克的开炮,面向对象的运行时类型识别可以帮助我们很容易的实现这一点,在这个地方我们先不讲。
我们再来看一个复杂点的例子。
比如我们在编写一个文本处理软件(就像WORD那样),那么给用户提供后悔药是一件很重要的事。通过“撤销”功能,用户可以将失误的操作取消,使文本恢复原来的样子。
很显然我们需要记录用户的操作并将它们按先后次序排好,使得用户可以一个一个的撤销它们。问题是,当用户点击“撤销”按钮时,我们读出用户最后的操作,然后呢?
当然,我们可以判断出用户做了什么类型的操作,改动了哪些地方,然后把改动的地方都恢复。但是,当我们给用户提供越多的可选操作的时候我们也就需要在“撤销”的method里面去判断越多种不同的情况,并且写大量的代码来针对不同的操作做不同的恢复。
这并不是真正的大问题,问题是,当我们发布这个软件后,我们希望对她进行升级,给用户提供更多的功能,我们不得不重新修改“撤销”的method,编写新的代码来支持新的恢复。
我们选择封装这些变化的事情来解决这个问题。
无论用户做了什么样的操作,从删除一个字符到插入了一张图片,我们到把这些变化的事封装起来使得从外部看起来用户就是执行了一个操作,那么当用户点击“撤销”时,无论是执行了恢复被删除的字符还是取消了插入图片,我们也把他们都封装起来使得外面看来用户就是撤销了一个命令而已。由于不同的命令有着具体不同的撤销方法,我们命令的执行和撤销都交给该具体的对象自己处理,而用户执行某个操作时,只是通知那个对象去执行,撤销是通知那个对象去撤销。(这其实就是命令设计模式,关于设计模式以后我们会详细讲到)
interface Command {
void execute();
void undo();
}
class DeleteChar implements Command {
void execute() {/*save the information about the selected char then delete it*/}
void undo() {/*use the saved information to get-back the deleted char*/}
}
class InsertPic implements Command {
void execute() {/*insert the picture save the information about it*/}
void undo(/*delete the inserted picture*/)
}
这样,无论以后我们为用户提供什么样的操作,在撤销的method里面,我们只需要读出用户最后执行的命令,然后执行其undo方法。
…
Command lastCommand = getLastCommand();
lastCommand.undo();
…
即可。
到目前为止,我们使用面向对象的思想来设计进行得很顺利,可是有时候我们会遇到有些复杂的问题我们暂时无法解决,或者解决问题的代码很简单但是很繁琐。这时我们希望使用别人编写好的代码,不幸的很可能对方的设计并没有考虑到我们的软件系统,我们无法直接使用他们已经编写好的类,此时修改我们的整个软件系统去适应他们编写好的类显然也是不现实的,这时候封装变化的事物这一思想再一次发挥了他的作用。
别人的代码能解决我们的问题我们却不能直接使用往往是因为一些小的原因,例如参数要求不用(比如我们需要一个画圆的方法以圆心坐标和半径长度为参数画出一个圆,而别人写好的画圆的方法却是以圆的外接正方形的左上角坐标和直径长度为参数来画圆的,你或许怀疑这样的画圆方法的存在,然而windows GDI正是使用这样的参数画圆),或者是他的某一个方法不能直接完成你所要求的功能,需要将他写的若干method组合起来使用,或者仅仅是因为他的类不是由你所要求的某个子类继承的,此时我们将其他所有人写的类封装起来,使他们看起来和我们自己写的类是一样的。
例如,我们现在拿到了一个可以以圆的外接正方形的左上角坐标和直径长度为参数来画圆的类:
class SomeonesCircle {
draw(int x, int y, int length) {/*draw the circle*/}
}
现在我们来封装他
class Mycircle {
public:
draw(int x, int y, int radius) { circle.draw(x-radius, y-radius, radius*2); }
private:
SomeonesCircle circle;
}
这样别人写好的类就可以为我们的系统工作了,即使我们的circle是由我们的某个根类,例如:MyShape继承而来,也完全没有问题。
至此我们所接触的例子只是面向对象的设计中的冰山一角,同时我要再次声明:面向对象并不只是为了复用代码和可扩展性编程而出现的。希望大家在以后的学习中多从别人的代码和设计中去学习如何使用面向对象的思想去设计,同时去开发自己的面向对象的设计。