您的位置:首页 > 编程语言 > C语言/C++

Thinking in C++ (1-3)

2006-07-02 22:41 218 查看


复用现有的具体实现

当一个类被建立并且通过检测证明其可用,便可以将它看作一组可用的代码(在理想条件下)。要使可复用性(reusabilility)达到许多人所期望的那样并不是件容易的事;一个好的设计需要经验和好的洞察力。但是一旦你完成了这一优异的设计,它便可以被复用。代码复用是面向对象编程的最为伟大的优势之一。

复用一个类的最简单的方法,就是直接创建这个类的一个对象,同时你也可以把这个类放在一个新的类中。我们把这个机制叫做“创建一个成员对象”。为了使你的新类包含你所需要的功能,它可以由任意数量,种类的其他类以任意的方式所组成。由于你正在使用已有的类来组合一个新的类,这一概念叫做“组合”(或者叫做“聚合”,显得更广义些)。组合经常表现为一种“has-a”(a包含b)关系,好比说“一辆车有一个引擎”(Passat W12有两个引擎)



(上面这个UML图通过菱形的箭头来表示组合关系,这表示这只有一辆车。一个更简单的形式:一条没有菱形箭头的线,表示一个联系)

组合十分的灵活。你的新类中的成员对象往往是私有的,使使用这一类的客户端程序员无法访问到这些成员。这一机制使得你可以在不被客户端代码的干扰下修改这些成员。你也可以在运行时修改这些成员,以达到动态修改你的程序的行为的目的。将在后边介绍的“继承”机制并不具备这一灵活性,这是因为采用继承机制创建类时,编译器必须设定编译时限制。

由于继承在面向对象编程中的举足轻重的地位,人们常常喜欢过分强调它的作用,于是导致了新手有了这样一种错觉:在任何场合下都要使用继承。这将导致设计变得笨拙和繁杂。其实你在创建新类时,应该首先考虑采用组合,因为它简单而灵活。如果你做到这一点,你的设计将会很整洁。当你拥有了一定的经验,你就会恰当地发现什么时候需要使用继承。

继承:接口复用

就它自己而言,对象这一机制使一个非常方便的工具。他在概念层面就支持为数据和功能打包,于是你便可以恰当的描述一个位于解空间的思路,而不是受迫性的使用底层的机器语言的特性。这些概念用class关键字表示,它们是编程语言中的基本单元。

但是遗憾的是,当你创建好了一个类,即使一个新的类于它的功能非常相似,你也要为这个新类编写全新的代码。如果能使用现有的类的话,复制它,然后对其进行添加和修改,这样看上去好多了。这便是“继承”为你带来的效果,一旦原先的类(称为基类或超类或父类)被修改了,那么对于复制的类(称为派生类或继承类或子类)也会反映出这些修改。



(上面的UML图中,箭头从派生类出发指向基类。就像待会儿你所看到的,派生类可以不止一个)

一个类型所能做的不仅仅是描述一个对象的集合中的约束;他也会与其他的类型存在着某种关系。两个类型可以拥有同样的特征和行为,但是其中的一个可能会比另一个拥有更多的特征,可以发送更多的消息(也许是以不同的方式处理消息)。继承描述了使用基类型和派生类型的概念的那些类型之间的相似之处。基类型拥有所有被派生类型所共享的一致的特征和行为。你可以创建一个基类型来描述你的系统中某些类的思路的核心。同时,可以从这一基类派生出其他类型来描述认识这一核心不同的途径。

举例说,一个垃圾回收机可以收集不同种类的垃圾。基类型就是“垃圾”,每件垃圾都有自己的重量,价值,等等,以及如何处理它(粉碎,溶解还是丢弃)。由此,派生出更多特定种类的垃圾,它们拥有附加的特征(比如说瓶子有自己的颜色)或行为(铝罐可以被压扁,铁罐有磁性)。同时,行为可能是各不相同的(纸张的价值有它的种类和新旧程度所决定)。使用继承机制,你可以构建一个层次化的类型来描述你正在试图解决的某一类型的问题。

然后是经典的“形状”的例子,它也许会应用在CAD系统或游戏模拟领域中。基类型是“形状”,每个形状都有自己的大小,颜色,位置,等等。每个形状都可以被画出,擦除,移动,着色,等等。在此基础上可以派生(继承)出若干特定的类型:圆,正方形,三角形,等等,每个继承的类型都可能拥有一些附加的特征和行为。比如某些特定形状可以被翻转。一些行为可能是不同的,好比说如何计算这个形状的面积。层次化的类型使得各种形状之间相同点和不同点体现得很清晰。



以同样的术语将解决方案转变成问题,这种做法有着巨大的好处。因为你不需要问题和解决方案之间的描述媒介的模型。通过对象,层次化的结构就是最主要的模型,于是你就可以直接从对真实世界中的系统的描述过渡到代码形式的描述。从开始到结束这一过程太简单,竟成为了人们使用面向对象设计的最大的障碍之一。一个久经沙场的老手,他常常考虑的是那些复杂的解决方案,往往会倒在这一简单性面前。

当你继承了一个现有的类型,你便得到一个新的类型。这个新的类型不仅包括了那个已经存在的类型的所有成员(尽管private成员对新类来说是不可见不可访问的),而且更重要的是它复制了基类的接口。也就是说,所有你可以发送给这个基类的对象的消息都可以发送给这一派生类的对象。于是通过发送给这个类的消息,我们便知道这个类的类型。这意味着派生类与基类是同一类型的。就像在前面的例子中一样,“一个圆是一个形状”。继承的类型等价性是理解面向对象编程的重要门槛之一。

由于基类和派生类拥有同样的接口,所以这儿一定会有伴随着这一接口的具体实现。这就是说,当一个对象接收一个特定的消息时,一定会有某些代码执行了。如果你简单的继承了一个类而没做其他任何事,基类接口的方法将会原封不动地复制到派生类中。这就意味着派生类的对象不仅拥有相同的类型,而且拥有相同的行为,这么做没什么意义。

为派生类与原先的类产生差异有两种途径。第一种非常直接:直接在派生类中添加新的函数。这些新的函数不是基类的接口的组成部分。这意味着基类没有达到你的需求,所以你添加了更多的函数。某些场合下这种简单标准的继承的用法是解决你的问题的完美的方案。然而,你也应该仔细观察,你的基类也许也会需要这些新的函数。这一发现和迭代的过程在你的面向对象编程生涯中将会经常发生。



FilpVertical() //垂直翻转
FlipHorizontal() //水平翻转
尽管继承在某些时候意味着你正在向接口中添加新的函数,但事实并非总需要这样。第二种也是更为重要的一种使你的新创建的类与基类之间产生差异的途径是,改变一个现有的基类函数的行为。这种机制称为“覆盖”这一函数。



通过在你的派生类中直接对一个方法进行重新定义,便可对其进行覆盖。也许你会说,“这里我正在使用一个相同的接口的函数,但是我希望他在我的新类型里做一些其他的事情。”

“a是一个b”关系与“a像一个b”关系
对于继承的某些问题一定会引起一些争论:继承是否应该只覆盖基类的函数(也就是说不允许在派生类中添加基类中没有的函数)?这意味着派生类将会与基类拥有完全相同的类型,因为它们的接口是相同的。于是乎,你可以使用一个派生类的对象来完整的代替一个基类的对象。这可以被看作“纯替代”,而且经常被称作“替代原则”。在某种意义上,这是继承机制的合理的设计方法。我们通常把这种基类和派生类之间的关系称为“a 是一个 b”关系(is-a关系),你可以说“一个圆是一个形状”。一个类是否可以被继承,可以通过测试你是否可以合理的对其使用 “is-a”。

有些时候你可能必须在一个派生的类型中添加新的接口元素,像这样对接口进行扩充,并创建一个新的类型。这一新的类型仍然可以替代基类型,但是这一替代已经不是完美的了,因为你的新函数对于继类型来说是不可访问的。我们可以把这种关系叫做“a像一个b”(is-like-a)关系;新的类型拥有旧类型的接口,但是它同时包含其他的函数,所以你不能够说它们两者是完全一样的。比如说空调。设想你的房间已经布好线可以控制冷气的开馆;也就是,它拥有了一个接口是你可以控制制冷功能。设想一下如果空调坏了你更换了一个热力泵,热力泵可以制冷也可以取暖。那么这个热力泵就is-like-an空调,他可以做比空调更多的事。由于你的房间的控制系统只能控制冷气,所以它被限制只能与热力泵的制冷系统进行通信。新的物件的接口扩展了,但是现有的系统并不知道除了现有接口以外的任何事情。



当然,在看过这个设计之后你会发现“制冷系统”这一基类并不够一般化,为了包含取暖功能,我们将起改名为“温度控制系统”——这样替代原则就再一次成立了。上边的框图是软件设计和现实世界中都可能发生的一个实例。

当你看到了替代原则很容易会认为纯替代应该作为唯一的替代方式,而且事实上使用这种方式会达到很好的效果。然而你也会发现在某些时候在派生类中为接口添加新的函数也不失为一种好的方法。这需要大家的审时度势。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: