组合、委托与继承,面向对象中类之间的基本关系漫游

侯捷在《C++面向对象高级编程》课程的第11~13节讲到了面向对象中类之间的三个关系:组合、委托与继承,并举了几个例子来说明这些关系如何组合在一起构成更加强大以及实用的设计模式。现摘抄精华于此,并从其他参考书中找到一些例子补充,作为学习的总结。

类之间的基本关系:继承、组合和委托

Composition(组合)
组合表示has-a的关系,A has a B,则在A类中包含一个B的成员,A实用B成员的各种方法,并不管理也对B的成员不可见。对于A来说,B就像是一个黑箱一样,使用它的功能而无需也无法了解它的实现。这类似于黑箱复用,很好地分离了A和B,使A与B之间耦合很低,交互只使用B所提供的接口。
如下图是标准库中的代码,queue容器内部保存了一个Sequence,所有的方法都提供自这个Sequence。也就是queue has a sequence,是一种组合关系。

这里面的成员 c 是一个 deque 类型的,这一个class里有一个 c,这样的关系叫做组合。我里面有另外一种东西,我和那种东西的关系就是组合,不一定是一个,可以是多个。例如C的struct里可以放其他的struct,就是Composition。

queue里有deque,用这个图来表示,黑色菱形就是容器,它容纳了另外一个对象。queue里面的所有功能都没有自己写,完全使用了 deque c 的实现,所以写法非常简单。这是一个特例(A拥有B,A的功能由B来实现)
已经有一个功能很强大的东西,稍微改装一下,让其有一个新的功能集合,这样的模式叫做 Adapter模式。

从内存的角度看组合

既然是组合关系,那就需要完整地包含所组合的对象,所占用的空间大小算作栈上对象空间总和,不包括堆上申请的内存。

组合关系下的构造和析构

构造由内而外,Container的构造函数首先调用Component的default构造函数,然后才执行自己
Container::Container(...) : Component() { ... };
构造函数借用这样特殊的语法,外面的构造函数先调用里面的构造函数,有一个先后关系。这样的顺序是编译器帮我们加上去的

析构由外而内,Container的析构函数首先执行自己,然后再调用Component的析构函数。
Container::~Container(...) { ... ~Component() };
先把自己的东西做完,再调用Component的析构函数。
都是编译器帮我们安排的,只需要各自管各自的构造和析构,编译器会帮我们加代码形成一种合理的形式。
如果需要用参数初始化Component,则需要自己写了。【补】

Delegation(委托)。

之前的String实现是指向一个字符,但现在改成了StringRep *rep; // pimpl

和组合差不多啊?不过另外一个属于来描述更好,Composition by reference,更加准确,也就是代理。根据我的理解如果A类实实在在包含B类,B的对象生存在A的空间中,则叫做包含,UML图中用实心的黑色菱形表示,表示很“实”的概念。而如果A用一个指针来指向B,则是一种委托关系,实际上A并不包含B,B的声明周期不被A所限制,他们的寿命也不一致,是一种“虚”的概念,在UML图中用空心的白色菱形表示。等到需要用到B的时候才去创建。

pImpl (pointer to implement) Handle / Body
左手边对外不变,右边的实现可以改变,具有一种弹性,右边怎么变动都不会影响左边,客户怎么看这个字符串都不会受到影响。就像编译防火墙一样。只需要重新编译右边。

委托的内存模型 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
可以用作引用计数,以上三个字符串共享一个“Hello”字符串。

要发生共享,就不能轻易改变。以上三个对象互相不知道对方的存在,当a想要改变的时候就单独拿出来改,变成b和c共享了,这个叫做copy on write。

【实现一个常量池】

继承
有人有误解,以为上面的两个太简单,继承才是面向对象。

有两个class,表现为继承关系,在class 后面加上黄色的那一行,在标准库中取出来的。
Java只有public继承,C++还有private和protected继承
使用public继承,就是传达一种逻辑,is-a,是一种,。最容易理解的就是生物中的界门纲目科属种,每一层都 is-a 是一种上面的那钟东西,人是哺乳动物,哺乳动物是动物,就可以跟树区分开来,树是植物。


从内存的角度看继承

父类的数据被完整的继承了

子类的对象里面有个父类的成分。构造还是由内而外,析构由外而内。
只要你的类是一个父类或者将来会变成一个父类,就将析构函数设为virtual。

继承要搭配虚函数才能发挥最强而有力的效果。在语法上,任何成员你函数前加virtual。数据可以被继承下来,占用了内存的一部分。函数的继承是继承的调用权,子类可以调用父类的函数,是调用权。子类是否要重新定义呢?
non-virtual函数:你不希望derived class重新定义(override,覆写)它,这个术语一定要用在虚函数被重新定义。
virtual函数:希望derived重新定义,而且已经有了一个默认的实现了
pure virtual函数:希望子类一定要重新定义,你对它目前完全没有定义。virtual void draw() const = 0;

希望日后矩形、椭圆形都可以继承Shape。
error函数:如果以后有更加精细的错误想法的话,我作为父类的设计者允许子类去override,在此将子类设计为虚函数。
draw纯虚函数:这个函数一定要被所有的子类重新定义,因为Shape作为父类,完全不知道怎么去画形状,是一个非常抽象的概念。

一个非常经典的例子:PPT,当我要开启一个文件时候,打开菜单,会出现一个对话框,里头可以看到需要选择的文件,按下OK后程序收到一个filename,检查filename是否正确,然后到硬盘里找这个文件在不在,然后open之,之后读出来。这些步骤任何人来写都差不多,有人把它先写好,让后面的人用,只有开启文件后面读的过程无法事先写好,除此都可以事先写好。

父类有一些函数没办法事先写出来,要让子类去写,就要设计为virtual function。Serialize()还没有写出来,但我们可以先使用它,留着给子类去事先。
我们把CDocument继承下来,写CMyDoc,把Serialize()写完。在main里创建一个子类对象,通过子类对象调用父类的函数。

Template Method,把需要实现的细节留给使用者,自己编写框架,并在这个框架中事先使用它,子类中延后实现。

大家写程序的竞争地方不在于一般性的地方,在于具体的领域,应用程序的框架可以先写出来。把固定的步骤先写好,留下无法决定的地方,让你的子类去定义它,这种框架当然造福人类啦。

MFC就是这个框架,它所做的事情就是这样。OnFileOpen就是MFC这套产品里的命名。

为什么可以延后实现子类的方法而先在父类中调用呢?【补充】this指针

继承和组合关系都存在的情况下的构造和析构
但是谁先呢?先后影响不太大,不过可以深刻理解内存的layout。
情况一:

情况二:

功能最强大的是委托+继承

还是PPT的例子,开出了四个窗口如下

左边是一个subject,右边的是observer,左边只有一个,右边可以有好多观察者。
下边的子类都是一个Observer,创建出来后都可以放在左边你的 m_views,有多个对象“看”这么一个数据。不同的Observer有四个(PPT例子)。

为了解决Observer这种事情,左边放一个delegation,右边的是一个父类,必须可以派生出一堆子类出来。作为内容物应该放入一个注册和注销的方法,attach,附着observer这种东西。把它放到容器中。还应该有一个方法,notify,群访一遍,通知右边的Observer,右边的一个函数应该定义一个update,通知更新数据。实际上这个是两个类之间的消息传递。

【第十三节课】

如果现在要写一个File System 或者 Window System,大窗口里有小窗口,目录里有文件,目录还可以和其他文件组合起来放在其他的目录里。该使用哪些武器把它们关联起来。

Primitive个体,不是组合物。Composite,组合物、合金。作为Composite应该作为一个容器,可以容纳很多个左边这种东西,一个容器,既可以放左边的这种右边也可以。为左边跟右边写一个父类,左边is-a上面的东西,右边也是is-a上面这个,右边的容器不写死为左边或者右边,声明为上边的这个类型,而且是指针。在容器里一定要放一样大的,所以是指针。
右边这个Composite应该具备一个函数,add。这样左边和右边都可以存了。继承跟委托画成紫色框中的这样了。

Component的add方法,Primitive不需要去override,,所以在基类中写空函数就够了。

现在需要一个继承体系,想要创建未来才会出现的子类怎么办。就是说现在在写框架,但是要等到使用框架的人派生,名称才会出现。不知道未来的Class是什么,怎么创建它呢。name还没有出现,怎么办呢。下面派生的子类,都创建一个自己,当做一个圆形,让框架有办法看到你们创建的原型。现在要创建未来的Class对象。后面发生的子类自己创建一个出来,反正你们创建出来的东西可以被框架看到,就可以当做一个蓝本。

子类自己创造 自己。而创建出的要注册在父类中,在上面的地方准备一个空间,下面的把自己的原型放到上面的空间中。井号表示protected下划线表示静态变量。addPrototype是父类写的,子类把自己挂上去。

posted @ 2017-12-26 12:08  narutow  阅读(1140)  评论(0编辑  收藏  举报