转自 面向对象的三大基石面向对象的三大基石(封装,继承和复合,多态)
三大基石之一 封装
1.什么是封装?
封装(encapsulation)又叫隐藏实现(Hiding the implementation)。就是只公开代码单元的对外接口,而隐藏其具体实现。
比如你的手机,手机的键盘,屏幕,听筒等,就是其对外接口。你只需要知道如何按键就可以使用手机,而不需要了解手机内部的电路是如何工作的。封装机制就像手机一样只将对外接口暴露,而不需要用户去了解其内部实现。细心观察,现实中很多东西都具有这样的特点。
2.如何实现封装?
在程序设计里,封装往往是通过访问控制实现的。C++,Java,AS3中都有 Public, Protected, Private 等访问控制符。通过用Public将信息暴露,Private,Protected将信息隐藏,来实现封装。
一个优秀的OOP程序员会尽量不对外公开代码,即最喜欢用Private关键字。因为在OOP中,对代码访问控制得越严格,日后你对代码修改的自由就越大。
--摘自《AS3殿堂之路》。这一点会在下面给出更详细的解释。
3.为什么要封装?封装带来的好处。
a.封装使得对代码的修改更加安全和容易。将代码分成了一个个相对独立的单元。
只要电话的外部接口(键盘,屏幕,使用方法等)不发生改变,那么不管电话内部电路,技术如何改进,你都不需要重新学习就可以使用新一代的电话。同样,只要汽车的方向盘,刹车等外部接口不变,那么,不论如何改造它的发动机,你也一样会驾驶这类汽车。
封装所带来的好处是:明确的指出了那些属性和方法是外部可以访问的。这样当你需要调整这个类的代码时,只要保证公有(Public:)属性不变,公有方法的参数和返回值类型不变,那么你就可以尽情的修改这个类,而不会影响到程序的其他部分,或者是使用到这个类的其他程序。这就是为什么刚刚说的“在OOP中,对代码访问控制得越严格,日后你对代码修改的自由就越大”。
b.封装使整个软件开发复杂度大大降低。
能很好的使用别人的类(class),而不必关心其内部逻辑是如何实现的。你能很容易学会使用别人写好的代码,这就让软件协同开发的难度大大降低。
c.封装还避免了命名冲突的问题。
封装有隔离作用。电话上的按键和电视遥控器上的按键肯定用处不同。但它们都可以叫做按键,为什么你没有弄混呢?很显然一个属于电话类一个属于遥控器类。不同的类中可以有相同名称的方法和属性,但不会混淆。
三大基石之二 继承与复合
核心思想不仅仅是重用现有的代码,用一些已有的类去创建新的类。
1.继承
继承,Inheritance,是一种看起来很神奇的代码重用方式。
除了这一点,继承(这里只限于公有继承,私有继承和保护继承方式会破坏向上转换这一特性)还产生了另一种奇妙的东西叫做向上转换或向上映射(Upcasting)。就是说在代码中可以将子类对象作为父类对象来使用。
其实,这里所提到的继承思想在人类的生活中并不常见(程序设计中继承也较难掌握),多出现在人类对自然界的分类。我们观察周围的世界发现,人们更喜欢用的是复合和一种特殊形式的继承(对抽象类的继承,会在多态中讲到。如汽车继承于交通工具这个抽象类)。
2.复合
复合,Composition,即将各个部分组合在一起。程序设计中就是用已有类的对象来产生新的类。
桌子由木板和钉子组合而成,台灯使用灯座,灯管,电线,接头等拼起来的。我们发现自己周围的很多东西都是由更小的其它东西拼凑构成的,就像积木一样。相信你小的时候也曾拆开过许多你觉得好奇的东西,去一看究竟。去看看这个新的类(class)到底是由那些其他的类构成的。其实在你很小的时候你已经理解了复合。
程序设计中,复合体现在生成的新类里用到了现有类的实例,具体的代码例子在这里也不给出了,相信你一看就能明白。
复合使生成新类更加简便和直观,实现也非常容易,相比继承这种通过已有类构造新类的方法,大多数人(包括在现实生活中)更喜欢复合。
3.何时用继承,何时用复合?
在实际编程中,使用复合的频率要远远超过继承,对于OOP新手而言,要慎用继承,勤用复合。有些极端的OOP人士推崇复合憎恨继承,声称继承是“邪恶”的,一切都是复合。虽然是极端之言,博大家一笑,但实际应用中复合确实比继承灵活,而且更加直白。
虽然继承号称面向对象的三大基石之一,但不正确的使用继承不仅仅是代码维护的灾难,也是真是逻辑的扭曲。因此,在看完了下面的一段后,你还是暂时不明白何时用复合何时用继承的话,请优先考虑复合。
a.需要用到向上转换时请考虑继承。
b.用“has a”和“is a”来区分复合和继承。即考虑新类是有一个(has a)现有类的对象(如新类为汽车,现有类为轮胎,请考虑复合),或是新类是一个(is a)现有类的特殊情况(如蝴蝶和昆虫,考虑继承)。
三大基石之三 多态
多态,Polymorphism,意思是多种形态。
广义上的多态包括以下几个例子:
a.家里的保险丝坏了,你用铁丝代替,这就实行了强制类型转换,强制把铁丝当做保险丝使用。这叫做强制多态。在代码中的体现是强制类型转换,就像用铁丝去代替保险丝可能产生危险,强制类型转换也可能造成精度丢失等后果。
b.另一种多态叫重载多态,运算符重载和函数(方法)重载就是重载多态。
c. 还有参数多态,代码中的表现是类模板和函数模板。
d.下面,重点介绍下包含多态。下面解释一下抽象类和接口。
抽象类(Abstract Class)
人们将一类东西所共同具有的某种功能整合形成了一个抽象类。交通工具表明,这个抽象类的子类汽车,火车等都具有运送人或货物的功能。药物都具有治疗作用。能够用来喝茶的叫做茶具...
接口(Interface)
再说接口,Interface,接口仅仅包含一组方法声明没有具体的代码实现。只要是实现同一个接口的类都具有这个接口的特征。接口如同协议描述了实现接口的对象向外部的承诺。这样其他的对象就可以根据这个协议来和实现接口的对象交流。
其实,抽象类就实现了接口的功能,如果你暂时不理解接口的话,可以把抽象类理解为接口。
为什么要用抽象类或是接口?
抽象类作为一个父类中和了子类的一些共同的行为。重要的是在使用时可以作为子类的共同类型而存在,同时给与了子类最大的灵活性。
OOP编程中有一条重要的法则,即依赖倒转原则(Dependence Inversion Principle), 其含义是:要做任何具体代码实现,首先要依赖于抽象类的实现。这个原则有多种表述,其中的一种广为人知的解释为:要根据接口编程,而不是根据实现编程。在具体的代码中要尽量使用抽象类,而不使用具体类(这当然只是理想化的境界)。
下面举一个代码例子,实际上就是《C++面向对象编程基础》里的例子。有一个抽象类Shape,具有draw和erase方法,当然,draw和erase都是空方法,在C++中用纯虚函数实现。Round类继承于Shape类重写了Shape的draw和erase方法用来画圆和清除图形。Line类继承于Shape类重写了Shape的draw和erase方法用来画直线和清除图形。Triangle类继承于Shape类重写了Shape的draw和erase方法用来画三角形和清除图形。它们的UML表述为:
这样的话我们可以写这样一个drawshape的函数:
void drawShape( Shape& s ){ s.draw(); }
注意这个函数的参数类型是Shape ,就意味着这里可以放 Round, Line, 或 Triangle 类的实例,当:
Round r; drawShape(r);
即可以画出一个圆, 当:
Line l; drawShape(l);
画出的就是一条线。现在你大概能体会到抽象类(接口)给代码带来的灵活性了吧。而且我们还可以随时向Shape父类中添加子类。