1、面向对象 为什么要写这篇文档? 我写这篇文档只是想把我所认识的面向对象理论给表达出来,并希望可以给一些开发者以启示作用。 而我目前并不打算开放它们,因为还没有写完呢。所以现在请勿评论。 什么是面向对象? 面向对象是一种程序设计的范式。面向对象使用对象,类,封装,继承,多态,消息等概念来进行程序设计。 为什么面向对象? 面向对象符合人类对世界的认知规律。 简单说,人类个体认识世界是从对周围事物的观察开始的,而且是从具体到抽象的,并且认识到事物的属性,状态,行为等等,以及事物之间的相互关系。而面向对象就是对这种思维方式的抽象表达。 面向对象与面向过程并不矛盾。
2、 面向对象着重刻画系统中对象的关系和相互作用,已经对象本身的属性,状态,和行为。面向过程则是侧重于描述过程。而面向对象的程序设计,是无法离开面向过程的范式。 什么是对象? 万事万物皆对象。 我以为这句话很精彩,因为它带有佛偈一样的色彩。 于是,不同的人理解这句话,有着不同的看法。 我们并不想在这句话本身做更多的讨论,那太宗教哲学了。 你们看那天上的飞鸟,不稼不穑,我尚且养活它们,你们不比那飞鸟贵重得多吗? ----《马太福音》 我们在这里至少看到了飞鸟就是一种对象,而在我们的世界中,充满了对象,人,飞鸟,百合花。 齿轮,轴承,阀。桌椅,鼠标,液晶显示器。
3、 我们认知世界是从具体到抽象的。 如果只是把看得见,摸得着的东西定义为对象,那么这未免太简单了。 一个过程,一个办法,一个概念也可以是对象。 至于,一个对象是具体的,还是抽象的,我想这是一个哲学问题,也许取决于每一个人对一个事物的认识程度罢。 对象有其类型 我们不得不诠释类型的概念。 int a = 1; 这里,变量a的类型是int,那么它的类型就是int。当我们要表示年龄,数量,这样的信息的时候,int类型的量就可以完成描述了。 而一个矩形呢?它的类型是什么呢? 我们知道在C语言里面采用struct关键字来描述这样的类型。 struct Point { in
4、t x; int y; }; struct Point p1; struct Point p2; p1.x = 0; p1.y = 0; p2.x = 0; p2.y = 1; Point就是我们自定义的类型了,那么这个类型的变量,我们就可以称为是一个Point类型的对象了。 在上面的代码中,我们说Point是类型,而p1和p2是Point类型的两个实例。 对象和类 初学者常常对语言中的static成员感到疑惑,于是我们讲解一下static关键字的面向对象的意义。这个static已经不是C语言中的static了。看如下代码: class Point {
5、 static int count; int x; int y; } 这里,我们有三个成员,其中,count被冠以static关键字。那么它与成员x,y就有了非常大的差别了,我们称count是类成员,而x和y是对象成员。count成员不属于某一个对象,不是某个对象的属性,而是属于整个类。在一个二维平面上,有若干个点,那么每一个点就是一个Point类的实例对象,它们有自己的坐标,一个点的x值(横坐标),和另外一个点的x值是两个不同的量(尽管它们的值可能相等),而这个平面上点的数量只有一个,是Point实例的个数,并且,这个数量不属于某一个Point实例的属性。
6、[对于C++的程序员来说,类成员往往被直接翻译为静态成员了,但是这似乎不能直观地反映它的意义。] 在这个系列的专题中,请大家注意对象这个词,有时候,它可能强调对象的类型的概念,有时候则表达对象实例的含义,而有时候则含有这两重意思,读者要仔细辨析和体会。但是初学者,还是要注意区分类型和实例之间的关系。 在这个系列的专题中,我尽量采用C++和Java代码作为例子,而且会忽略其中的一些语法要素,比如是private还是public,是int,还是Integer。并且还有可能用C#,JavaScript,Ruby的代码作为例子。但是大家能体会到代码所表达的意义即可,不必深究其中的语法细节。
7、 面向对象初步 我们开始讲述和程序设计相关的面向对象理论,主要涉及到消息,封装,继承,多态等内容。 消息 其实我并不喜欢消息这个概念,因为当时我学习面向对象导论的时候,已经有过Windows编程的经验了,而面向对象的消息和Windows的消息机制并无关系。 因为面向对象强调对象的行为和对象之间的相互作用,而这种行为是以消息体现出来的。当某一个对象收到消息的时候,则对象对此做出反应。而实际体现到具体的语言上,则往往是以方法的调用来体现这一概念的。 obj.function(); 我们给对象obj发送某消息,就是用调用对象obj的方法来表明这一概念的。如果有Objec
8、tive-C的基础,那么理解这个概念将更加容易。 [一般有C语言基础的人, 更习惯于称方法为函数, 这里我统一称其为方法, 因为我以为方法更多地带有行为的意味, 而函数更多地强调了数学上多输入, 单输出的数学函数的概念. ] 面向对象的几个重要概念 封装(Encapsulation),继承(Inheritance),多态(Polymorphism)是面向对象三个重要的特性。除此以外,还有许多重要的概念,比如说数据的封装和隐藏,方法的重写,等等。在后面我们将一一进行讲解。 封装(encapsulation) 封装,就是将数据和数据的操作进行封装,这涵盖了数据的封装,数据的隐藏,对象的
9、抽象和描述,封装方法对数据进行操作,等等。 数据的封装 我们依然以Point为例。如果说一个Point的实例就表示了在笛卡尔二维坐标系上的一个点。那么我们说,我们用Point对象刻画了坐标系上的点。而一个Point对象封装了两个int数据,很显然,二维平面上点,用一个横坐标和一个纵坐标就能唯一确定了。所以我们的代码是这样的。 class Point ...{ int x; int y; } 类Point封装了两个整型的数据,x和y。 数据的封装,是刻画对象的最基本的一种方式,它反映了has-a的关系。 我们说,对象Point有一个x(横坐标),也有一个y(
10、纵坐标)。Point对象,和x,y对象就构成了has-a的关系。 据此,我们也可以刻画Rect对象了。 class Rect ...{ Point p1; //Top-left point; Point p2; //Bottom-right point } 于是,我们有了一个Rect类,它用两个顶点描述了这个矩形的性质。 数据的隐藏(Data Hiding) [通常,Data Hiding被翻译为信息隐藏,而不是数据隐藏。但是我习惯于使用数据隐藏这个词汇。] 在讲述数据隐藏之前,我们要先讲述为什么要实现数据隐藏。如果,我们把Point限制为表达屏
11、幕上一点的坐标,那么有如下的规则: ( x >= 0, y >= 0, x <= ScreenWidth, y <= ScreenHeight) 但是,如果数据没有被隐藏,那么下面的代码可能就是一种尴尬。 Point p; p.x = -15; p.y = -30; 显然,这是一个无效的点坐标。我们无法在这个点上进行操作。比如说取得该点的象素值。所以,我们决定不给程序员这样的自由,而把数据封装起来。 class Point ...{ private int x; private int y; } Point p; p.x = 5; //Err
12、or, Compile error. OK了,程序员不可以胡作非为了,但是程序员也访问不到它了。 对数据的操作封装 class Point ...{ private int x = 0; private int y = 0; public void setX(int x) ...{ if (x >= 0 && x <= ScreenWidth) ...{ this.x = x; } } } 这里,我们给出了方法setX(),它实现了对隐藏的数据的操作,并判断了输入的
13、合法性。那么这就完整地实现了对数据的封装和隐藏。 我们真正要隐藏什么? 例子显示,数据的封装和隐藏,在对数据的操作上,具有防火墙功效,然而这只是最简单的一个方面,究竟我们在隐藏什么呢?实际上数据的封装和隐藏实现了对对象操作复杂性的封装和隐藏。 就像驾驶汽车一样,在汽车内有着数不清的零部件和子系统,它们都是对象(我是说,它们都可以被看作是对象),他们都很复杂,而驾驶员只要看仪表,操纵方向盘等设置就可以兜风了。而根本不用去直接操作发动机本身,而且,甚至不知道发动机都做了哪些事情,这种复杂性被操纵方向盘等方法给隐藏掉了。 继承 继承是面向对象中扩展对象的一种方法。子类通过继承父
14、类,以获得父类的属性和方法。 我们通常称子类也为派生类,而称父类为基类。下面的代码中,Button类就是继承关系中的基类,ImageButton类派生自Button类,于是,Button就是ImageButton类的父类(基类)。ImageButton就是Button类的一个派生类。 class Button ...{ string text = ""; Rect rect = null; //... } //C++ class ImageButton : public Button //C# class ImageButton : Button
15、 //Java class ImageButton extends Button class ImageButton : Button ...{ Image image = new Image(); //... } 有旧,有新,有借,有蓝;在一只鞋里放一枚六便士的银币。 ----维多利亚时代的谚语 我们强调有旧,有新,也就说,为什么要继承呢?在上面的代码中我使用了C#的语法,而忽略了一些语法细节,但是Java的语法更好的说明了继承的作用,就是其目的在于扩展。 这里,ImageButton不但有了父类中的属性,比如text,也有了Button的区域,并且
16、有了新的成员,Image对象。这是Button对象所没有的。 当然了,这并不是全部,继承也会继承父类的方法(我们暂且不去理会基类中的private方法)。 class Button ...{ private String text = ""; private Rect rect = null; public void setText(String text) ...{ this.text = text; } } class ImageButton extends Button ...{
17、 private Image image = null; public void setImage(Image image) ...{} } //... ... ImageButton button = new ImageButton(); button.setText("Cancel"); button.setImage(new Image()); button实例,不但可以访问父类的方法setText(...),设置按钮的文本,还可以调用ImageButton类自己的方法setImage()去设置该按钮的图片。 我们还有许多派生的例子,比如说: c
18、lass CDialog ...{ } class MyDialog : public CDialog ...{ } 这样,我就创建了一个自己的对话框类。 is-a 讲述一个最重要的对象关系,is-a,用自然语言表达就是说“是一个”,“是一种”。它表达了一种正确的派生关系。 Button button = new ImageButton(); 或者,把代码写成如下的形式: ImageButton imageButton = new ImageButton(); Button button = (Button)imageButton; 这份代码,表明了这样一点,
19、一个ImageButton对象,也是一个Button对象。这种关系就像是说猫是一种动物,飞机是一种交通工具一样。这就是我们所说的is-a关系。 An ImageButton is-a Button, also. 这个关系非常重要,它将直接影响到设计,和代码的可理解性。因为继承给了我们如此简单的扩展方式,于是,滥用继承也成为了一个问题。 class B extends A ...{ } 我们要思考继承的合理性,语法上成立的代码,未必是合理的代码,代码上显示B对象也是A对象。但是我们要用自然语言去解读一下,一个B对象is-a A对象嘛?这很自然嘛?很合理嘛?很通顺嘛? 当然了,也
20、不必那么完全符合我们对自然界的认识。有很多成功的继承,虽然看起来很怪异,但是它确实是行之有效的扩展。 多继承 比如说现在的许多手机,它既是一个移动电话,也是一个MP3。那么很显然,它确实具有两个合理的is-a关系。 而,C++支持多继承,正是因为事物可能具有多种性质。 class iPhone : public MobilePhone, public iPod ...{ } 我很高兴C++支持多继承,也很高兴Apple给了这样一个合理的例子,让我觉得我没有错误地使用多继承。 继承还是聚合? C++支持多继承,而绝大多数的面向对象语言不支持这种特性,于是通常采用
21、聚合的方式来实现对象的扩展,这是一个非常好的习惯。 然而,聚合不能取代继承。 聚合引发的对象关系是has-a,而继承引发的对象关系是is-a。从设计的角度来讲,这种差别很大,尽管我们可以用聚合实现继承,但是在我们需要is-a关系的时候,聚合就不那么方便了。 class iPhone extends MobilePhone ...{ private iPod o = new iPod(); } 但是,我们这样写,是被鼓励的。而且对于Java和C#来说,这样似乎是唯一的做法。 多继承还是单继承? 尽管C++支持多继承,我们也不能滥用多继承,因为软件工程证明了,大
22、多数的多继承是有害的,尽管许多C++的类库使用了多继承的方式,但是如果你没有想清楚,就不要设计出这样的继承关系。尽量用聚合来代替继承关系。 多态 多态是面向对象理论中至关重要的概念。它关注的是对象行为方面的问题,在派生关系中的对象的行为发生变化,这就是所谓的多态性。 我们本节的代码基本用C++代码作为例子(因为我们有机会深入到虚表去论证一些重要的问题,尽管也许没有这个必要),而对于Java开发者或是C#的开发者来说,也都是可以理解和接受的。我们依然以Button作为例子: class Button { public: virtual void OnDraw(
23、) { /* code for drawing a Button */ } }; class ImageButton : public Button { public: virtual void OnDraw() { /* code for drawing an ImageButton */ } }; 这里,依然是ImageButton类继承了Button类,并且重写了基类中的虚函数OnDraw();。这是什么意思呢?从设计的角度来说,当我们设计Button类的时候,当然不能确定将来会有什么样
24、的派生类出现,但是,一个未知的派生类Button显然有自己的风格,样式,或者说外观,也就是说有其自己的显示方式,于是,我们在设计Button类的时候,就让OnDraw()方法为virtual方法。而对于Java来说,一个非private和非final的方法,就等同于virtual方法了。这意味着,尽管我们在编写Button类的时候不知道将来会出现什么样式的Button,但是已经知道(预计)它(可能)会有自己的绘制行为了。 Button* pButton = new ImageButton(); pButton->OnDraw(); //Code will go into Image
25、Button::OnDraw(), not Button::OnDraw(). 那么上面的代码完整地展示了多态的工作情况,我们用基类的指针变量调用了派生类的方法,那么这种行为就叫做多态。这是多态在具体编程语言上的表述和体现。 通常,比较完整的说法是:“用基类的指针或者引用变量去调用派生类对象的实例方法。” 在这里,我们需要强调的是,“用基类的指针或者引用”,因为这是多态所以关键的因素。比如下面的代码,于运行之前,我们无从知道基类的指针到底是指向了哪一个派生类的对象,也许是一个ImageButton类的对象,也许是一个CheckButton类的对象实例。 void drawBut
26、ton(Button* pButton) { Button->OnDraw(); //Who knows, what's the type of the instance which pButton points to. } 但是,尽管我们不知道pButton指针指向的对象的实际类型(我指的是在编译期),但是这段代码却是可以通过编译,并且顺利运行,而具体会呈现出什么行为,完全是由运行时实际指向对象的类型决定的,这就是动态绑定,或者称为延迟绑定。 理解多态至关重要,它的运用非常广泛,也非常灵活,它是面向对象中最核心和最基础的概念。我们在这节中只讲最基本的概念
27、和语法。但是在后续的章节中,我们会更加深入的讨论多态。多态将贯通整个专题。 我们所要讲的是多态的理论,而不是实现,我们暂时不会涉及到C++的虚表,或者JVM的invokevirtual指令,这些细节似乎并不能帮助我们理解多态理论的运用和对象设计。但是需要的时候,我会以ASM的角度来阐述面向对象多态的实现细节。 面向接口编程(上) 前面的章节对于本篇来说,只是基础和铺垫,而且讲的很简单,因为那些很容易理解。我们从这个章节开始,用大量的代码的配合,来阐述面向接口编程。 接口的演化形式 现在我们回顾一下继承相关的知识。我们现在给出一组新的继承体系。它们是和图形相关的,我们可以
28、假设这样的一种需求,就是我们要实现一个Windows的画图板,至少要能在上面绘制几个圆形和矩形。于是,我们很自然底定义了如下的classes。 class Shape { void draw(); } class Circle : Shape { void draw() { /* draw a circle on some device */ } } class Rect : Shape { void draw() { /* draw a Rect on some dev
29、ice */
}
}
首先要说的是,我们确实是无法给出Shape类的方法draw()的实现。因为它是一个抽象的类型(不要和语言中的抽象类或abstract关键字混淆,但是它们确实有莫大的联系,但是我所谓的抽象是一种真实的抽象),我们不知道一个所谓的“图形”应该如何被画出来。这就像你让我画一个图形出来一样,我感到很为难。我画个圆圈或者是多边形,五角星,似乎都不是一个具有抽象意义的图形。
我们再回顾一下多态,下面的代码更好的诠释了多态的作用。
Array
30、ew Circle(), new Rect() }; void drawShapes() { foreach(Shape shape in shapes) { shape.draw(); } } 这是一段伪码,但是很自然地表现除了多态的从容,foreach从容器中循环枚举出Shape类的各种不同的派生类对象,它们都多态性地调用了它们各自类型的draw()方法。而这个过程又绝没有很明显的显露出某一个具体的派生类的类型的参与。于是,我们还可以很自然地再派生出圆角矩形,或者是五角星,都让它们继承自Shape类即可,它们也可以很自然地被放进s
31、hapes容器中,而且又不会修改循环处的分毫代码,这就是我们所追求的可扩展性和“新增代码不会影响已有的代码”。 嗯,这一切都很完美,不是嘛? 然而,这真的很完美嘛? 表格(Table)是一个Shape(对象)嘛?文本(Text)是一个Shape嘛?如果我们要有若干个图层(Layer),每一个图层是一个Shape嘛?如果是,这些出现在画图板上的元素,它们很好的诠释了is-a的信念嘛? 没有,因为一个Table不是一个Shape,Text也不是Shape,但是它们也都可以被画在上面,于是我们需要进行一次重要的演化。 我们在C++的语法教材中不强调接口的概念,而是用纯抽象类来
32、表达这一个含义,但是我更愿意用Java的interface关键字作为表达。但是我们要清楚,无论是接口,还是基类,抽象基类,多态性都是存在于这样的语法关系中的。 interface IDraw...{ void draw(); } class Circle implements Shape ...{ void draw() ...{ /**//* draw a circle on some device */ } } class Rect implements IDraw...{ void draw() ...{
33、 /**//* draw a Rect on some device */ } } 这次演化,似乎是没有本质改变的,特别是对于C++的编译器来说,编译出来的代码都可能没有丝毫的不同。也许很多人开始叹气了,但是我想说的是,对于设计来说,这种变化是本质的变化。因为对象的关系变化了,之前,我们说一个Circle对象也是一个Shape,它满足is-a的经典关系,但是现在,这种关系被打破了。 can-do & Constraint 我们在前面的章节中,已经提到了has-a和is-a的对象关系了,现在,我们的重点是can-do的关系,这种关系表示约束(Constraint)。
34、
于是,在上面的代码中,我们说Circle类实现了IDraw接口,或者说Circle对象能够完成IDraw接口所要求的行为。
void draw(IDraw d)
{
d.draw();
}
而这个函数就更直观地表达出约束的概念了,“画可画之对象”,准确地说,这个函数更直接地完成 IDraw接口的约束语义表达:只有实现了约束的对象才可以被传入,并调用其draw()方法(注意,而并不是有draw()方法的对象都可以被传入和调用,相关问题可以对比C++09的concept)。
Array
35、), new Circle(), new Table(), new Rect(), new TextArea() }; void draw() { foreach(IDraw d in drawList ) { d.draw(); } } 我用这段伪码,明确地表示了这样的一个新情况,Circle被画到画图板中去了,Table和TextArea也被画到上面去了。 依然是存有怀疑,难道Table派生自Shape就有问题嘛?难道一定要弄出一个IDraw,让它看起来有道理才是合理的? 不是的。首先,如果IDraw接口没有取代Sh
36、ape类,我们也要承认这样几个事情,Shape类还是带着IDraw接口的含义,尽管我们不把它抽象出来。 而且,设计没有对错之分,是不是适应需求才是最实际的评判标准。我们当前的例子还很简单,我们只涉及到了元素的绘制(draw),但是也许还有其他的问题(新需求总是很多的),比如ALPHA混合,图层的遮挡,元素的选择,蚂蚁线的绘制,等等。例如,当我们选择了一个Rect的时候,他的四周有蚂蚁线,而我们选择了一个TextAread的时候,它的四周是带有8个调节点的边框。可是,我们不打算让蚂蚁线和边框参与其他元素的遮挡计算和ALPHA混合计算,而且它们也不参与序列化,于是,它们既不是Shape,也不应该
37、实现IDraw接口。经验告诉我们,Shape类是不足以成为所有元素的基类的,IDraw接口也不是万能的。具体的解决方案要看需求,如何应对这些需求,我们会在后面的内容中有所涉及。 面向接口编程(下) 约束的理解是一个很辩证的问题,我觉得我们需要一个好的例子才能把这个事情论述清楚。我很喜欢鸟儿在天上飞这个例子,因为它和大自然紧紧的联系了起来。 class Bird { fly() { /* some code make birds fly on the sky */ } } class Dove : Bird { fly() { /* some c
38、ode make a dove fly on the sky */ } } class Penguin : Bird { fly() { /* fly, fly, Oh, could I fly? */ } } 在这段代码中,有一个基类,class Bird,鸟类,然后派生类是Dove(鸽子),和Penguin(企鹅)。简而言之,这三个类描述了这样一个世界,“但凡是个鸟就能飞”。哦,世界还是我们所认识的世界嘛?企鹅都能飞了。 问题就在于:是鸟就一定能飞嘛? 显然不是,这个世界除了企鹅外,还有许多鸟类没有飞行的能力,也就是说鸟类(Bird)与飞行(fly())
39、并没有一种必然的关系的。或许,我们不该把fly()方法设计到Bird类中去。于是,我们采用一个叫做IFly的接口,根据can-do的对象关系。我们可以称这个接口的实现类为能飞的事物。 interface IFly { fly() { /* some code make something fly on the sky */ } } class Dove : Bird, IFly { fly() { /* some code make a dove on the sky */ } } class Penguin : Bird /* And wound n
40、ot implements IFly */ { } 然后我们发现,这样的设计很好,我们可以很简单地让一些不是鸟类的动物飞了起来。 class Butterfly : Insect, IFly { fly() { /* a butterfly flies */ } } 嗯,这次演化,和Shape类演化到IDraw接口,具有相似的原因。 一些Java,或者C#的教科书,往往会这样写:“Java不支持多继承,但是支持实现多个接口... ...”, 我以为这样的话本身没有问题,但是许多初学者会误以为实现多个接口也能完成多继承的功能。而从设计的角度来说,用聚合来代替继承
41、才是一种选择,这个是我们前面说过的,而实现多接口并不是完成这种对象关系表达的合理途径。因为接口的语义往往是can-do,而且需要强调的是,这是一种约束的语义。而继承表达的是is-a的关系。继承的对象关系使得派生类传承了一种(行为)能力。而,接口没有。 我们想想,是IFly使得一只鸟儿飞上了天嘛?不,它没有赋予鸟儿这种行为能力。而真正完成飞行能力的是实现代码本身,而并不是“实现接口”这一举动。 想飞?这很容易。 许多动态的教本语言,比如Ruby,Python,JavaScript,它们只要拥有了fly这样的方法,就可以被调用了,于是,无论有没有实现IFly接口,对象都起飞了。所以说
42、实现接口并没有赋予对象以能力,而是约束对象,或者说要求对象具有某种能力。 一些语言,如果只支持单继承,比如Ruby,如果想聚合某种功能,可以通过Mixins来完成这种功能的扩展。 class Bird end module Action def fly end end class Dove < Bird include Action end dove = Dove.new dove.fly 接口与应用 前面我们所讲的东西足够理论了,但是实际的软件中,大量用到面向接口编程,而没有我们这么多推演和辩证的过程。因为接口有许多重要的性质。
43、例如,COM的出现,可以解决Dll Hell的问题。结果,COM的开发者总是要定义出许多的COM接口。而它们的实现反而并不重要了。 接 口的使用,会有许多种原因,有时候仅仅就是为了做到“接口和实现分离”。而这给软件工程带来了许多新话题。而相当多的设计模式都是以接口编程为基础的,经典的设计模式的实现,往往就是一个精彩的接口展示。而又不仅仅是模式本身,还有些原则,比如DIP,往往是采用接口编程的方式。当然了DIP还有很多争 论,我们就不讨论它本身的问题了。 对象的粒度 粒度的变化 对象关系的演化 我们依然用Button作为例子来说明对象关系中粒度的变化。我们在ImageButt
44、on的OnDraw方法中加入了许多的代码,这些代码完成了一个按钮的绘制工作,可能要绘制按钮的3D边框,或者表明的渐变色,以及上面的文本,比如OK或者Cancel,此外还有一个Icon,这里我们就认定它为一个Image。 class ImageButton : public Button { public: void OnDraw() { /**//* code for draw button frame*/ //... /**//* code for draw button text */ //.
45、 /**//* code for draw button image */ //... } }; 这段代码展示了一个ImageButton是如何显示自己的。但是事情还没有结束呢,现在我们要设计一个新的控件,一个ImageLabel,也就是不仅仅有文本的标签,它也想要一张图片来装点自己。 class ImageLabel : public Label ...{ public: void OnDraw() ...{ /**//**//**//* code for draw Labelframe*/
46、 //... /**//**//**//* code for draw Label text */ //... /**//**//**//* code for draw Label image */ //... } }; OK,我们观察每一处变化,并假定Draw一个Image是一个复杂的实现。但是无论是在Button上画一个Image,还是在Lable上画一个Image,它们都(应该)是相同的逻辑(比如以Win32的程序为例,在Button的DC画,与在Label的DC上画,它们的逻辑几乎
47、是完全是一回事情)。那么,为什么不把这样的绘制行为进行一下封装呢? class ImageDrawing { public: void DrawImage() { /* draw an image */ } } class ImageButton : public Button, protected ImageDrawing { public: void OnDraw() { /* code for painting button and frame*/ //...
48、 DrawImage(); //call ImageDrawing::DrawImage() } }; class ImageLabel : public Label, protected ImageDrawing { public: void OnDraw() { /* code for painting lebel text and frame*/ //... DrawImage(); //call ImageDrawing::DrawImage() } }
49、 于是,我们把绘制Image的逻辑封装到了ImageDrawing类中,而用继承的方式,把这种功能集成了进来。需要有两点说明:第一是这段代码限于在C++内,Java和C#很难用类似的代码形式表达这种关系,但是可以求助于聚合。第二,所以封装为类,而不是函数(方法),是因为类对象更容易维护状态。 保护继承和Implement-with 在上面的代码中,我们重点描述了对象关系的演化,而演化的结果就是这种关系,implement-with,以...实现(用...实现)。那么这种关系更易于用C++的代码进行描述,或者采用Ruby的Mixins等语法特性。那么这是在讲述了has-a,is-a,
50、can-do对象关系后的又一种关系。 为什么要用保护继承呢?那么我们来看下面的代码。下面的代码用最直接的方式说明了is-a的关系问题。 To be, or not to be: that is the question. ----Shakespeare Button* button = new ImageButton(); //OK ImageDrawing* d = new ImageButton(); //Compile Error. OK,这就是我们想要的结果,我们不想接受那样的事实,一个ImageButton也是一个“Image绘制”?我觉得用自然的语言表达得如此尴尬。






