C++面向对象高级开发-继承、复合、委托与组合设计模式
Notes
- Composition 复合
- Delegation 委托
- Inheritance 继承
- Inheritance+Composition继承复合结构
- Delegation+Inheritance委托继承结构
- 设计模式总结
Composition 复合
template <class T, class Sequence = deque<T>>
class queue {
...
protected:
Sequence c; //底层容器
public:
bool empty() const {return c.empty();}
size_type size() const {return c.size();}
reference front() {return c.front();}
reference back() {return c.back();}
// deque是两端可进出,queue是末端进前端出(先进先出)
void push(const value_type& x) {c.push_back(x);}
void pop() {c.pop_front();}
};
当一个类中包含另外一个类对象时,可称为复合,表现为has-a关系。在上例中,queue与Sequence即deque的关系为:
由于queue的所有功能都由deque实现,即queue为deque的Adapter(适配器模式)。从内存占用上看,如图所示:
queue中含有deque,deque中含有2个ITR <T>
及2个指针,共40字节,1个ITR<T>
含有4个指针,共16字节。
类名泛化后,Container复合Component。构造由内而外,当创建Container Object时,会执行其构造函数,在初值列中即调用Component的构造函数,而后在执行只属于自己部分的构造函数。红色部分的Component构造函数由C++编译器实现,无需手动写出。编译器自动使用默认构造函数,若需使用带参构造函数,则需在初值列覆盖原默认构造函数即可。析构由外而内,先完成Container独有的析构函数部分,最后执行Component的析构函数。
Delegation 委托
图中示例代码与String字符串类设计中的代码高度相似,取自标准库。String字符串累设计中使用char* m_data;
为字符串数据,图中String类使用String Presentation(StringRep*)
对象存储字符串数据。在指向关系图中,菱形变为空心,代表菱形侧为指针。Delegation又可称为Composition by reference
。
Delegation与Composition的不同: Composition在Container对象创建时,Component对象同时创建,生命期同时开始。Delegation对象创建时,数据为指针引用,故在访问对象数据时指针所指向对象的生命周期才开始,故Delegation的对象与指针所指对象的生命周期不同步。
对于字符串类本身的设计在另外一个类中实现,本身只有接口设计时,此种设计方法为Point to Implementation
,即pImpl
,也可记为Handle/Body
。设计为pImpl类设计时,由于接口类和实现类的分离,实现类可随时更换即为此设计的优点。此种设计又被称为编译防火墙。图中a、b、c为三个StringRep*
指针,指向同一个StringRep对象,此时的引用计数值为3。
a、b、c的内容均为Hello,若a要更改Hello的值,单独拿一份给a修改数据,b和c仍然共享原数据。此时的行为被称作
copy-on-write
。写时写副本。
Inheritance 继承
struct _List_node_base
{
_List_node_base* _M_next;
_List_node_base* _M_prev;
};
template<typename _Tp>
struct _List_node
: public _List_node_base
{
_Tp _M_data;
};
代码取自标准库,_List_node
继承自_List_node_base
。继承关系如图所示:
在上述代码中,注意继承部分前含public关键字,所以C++中支持public继承(表现为is-a关系)、private继承和protected继承。
类名泛化后,Derived子类继承Base父类。子类对象包含父类成分,构造由内而外,析构由外而内。与Composition部分原理相同。红色部分的构造函数由C++编译器实现,无需手动写出。
设计规范10:
父类的析构函数必须为虚函数,否则会出现无定义行为。
虚函数:
在成员函数之前增加virtual关键字,即为虚函数。函数的继承应从继承权角度考虑。不允许子类重新定义(Override)父类为非虚函数。希望派生类重新定义且含有默认定义时为虚函数。而派生类必须重新定义时为纯虚函数,无默认定义(纯虚函数可以有定义,此后续补充)。
如一个文档程序在打开文件时,打开文档、检查文件名、查找文件、读取文件等操作在众多应用中都存在。由于每个程序读取文件的方式都不同,实例中OnFileOpen中的Serialize()即为虚函数。不为纯虚函数时,虚函数可为空函数。在使用时,创建子类对象,子类对象调用父类函数。实例中子类对象调用父类方法的全名为CDocument::OnFileOpen(&myDoc);
,按序执行OnFileOpen时,到Serialize函数处通过其全名执行子类Override函数。将有固定的动作,且关键部分延缓至子类决定的函数的设计方法称为模板方法(Template Method)。此处的Template与C++的Template意义不同。在应用程序框架中会大量使用到模板方法。详细代码如下图所示:
Inheritance+Composition继承复合结构
图中上半部分一个子类继承自父类,且子类有一个Component,Base与Component存在于子类的对象中。可根据如下代码得出结论:
#include <iostream>
#include <cstdlib>
#define BOOL_OK 1
class Base;
class Derived;
class Component;
class Base {
public:
Base();
virtual ~Base();
};
inline Base::Base() {
std::cout << "Class Base Constructor Executed." << std::endl;
}
inline Base::~Base() {
std::cout << "Class Base De-constructor Executed." << std::endl;
}
class Component {
public:
Component();
~Component();
};
inline Component::Component() {
std::cout << "Class Component Constructor Executed." << std::endl;
}
inline Component::~Component() {
std::cout << "Class Component De-Constructor Executed." << std::endl;
}
class Derived : public Base {
public:
Derived();
~Derived();
Component* comp1 = new Component;
};
inline Derived::Derived() {
std::cout << "Class Derived Constructor Executed." << std::endl;
}
inline Derived::~Derived() {
std::cout << "Class Derived De-constructor Executed." << std::endl;
comp1->~Component();
}
int main(int argc, const char * argv[]) {
Derived d1;
return EXIT_SUCCESS;
}
执行结果为:
Class Base Constructor Executed.
Class Component Constructor Executed.
Class Derived Constructor Executed.
Class Derived De-constructor Executed.
Class Component De-Constructor Executed.
Class Base De-constructor Executed.
Program ended with exit code: 0
所以可得出结论,构造函数仍是由内而外,Derived的构造函数首先调用Base的default构造函数,然后调用Component的default构造函数后才执行Derived自身的构造函数。
Derived::Derived(...): Base(), Component() {...};
析构函数仍是由外而内,Derived的析构函数首先执行Derived自身的析构函数,然后调用Component的析构函数,最后执行Base的析构函数。
Derived::Derived(...) {... ~Component(), ~Base()};
图中下半部分为子类中有父类的部分,父类中含有Component部分,所以构造函数的调用次序依次为Component、Base、Derived。而析构函数的调用次序依次为Derived、Base、Component。
Delegation+Inheritance委托继承结构
Subject为存储数据的类,Observer为访问数据的类(窗口访问数据)。Subject可拥有多个Observer,Observer可派生子类,故可有多个Observer访问Subject数据,为Inheritance继承的关系,Subject与Observer为Delegation的关系。attach函数注册Observer对象,放入m_views容器中。notify函数为当数据产生变更时,遍历Observer对象调用update函数以更新数据。代码细节如下图:
当考虑设计一个文件系统时,目录中可以存放文件,目录中还可以与其他目录或文件一起放在另一个目录中。如图中泛化所示,文件可用Primitive类表示,Composite类是一个容器,可容纳Primitive类,也可容纳自身类型,可创建Primitive类与Composite类的父类Component类,Primitive与Composite均继承自Component类,如图中的紫色框内部分所示。此种设计方法为Composite设计模式。
类图规范说明:类图格式为对象名:类型名
- 对象名前加下划线_表示该变量为静态变量
- 方法名前加-号为private私有类型
- 方法名前加+号为public默认类型
- 方法名前加#号为protected类型
当考虑设计框架类时,其派生类可能在未来被创建时类名才会确定。所以在设计此类时,可考虑派生类创建自身副本作为原型,框架类可访问后通过复制即可创建其对应的派生类。在派生类中增加静态变量即创建了自身副本。创建自身副本时调用构造函数,因静态变量在类定义内部,故自身私有构造函数可以被调用。构造函数调用addPrototype(this);
,此函数为继承自框架类函数,将传入的指向派生类的指针放入prototypes容器,此例为数组内部。clone函数直接返回派生类对象副本。框架类可以通过静态变量对象调用派生类的clone函数获得派生类对象副本。若将clone函数设为静态函数则不需静态变量的做法是错误的,因为静态函数的调用必须通过类名调用。代码如下:
在框架类Image中,clone函数被设计为纯虚函数,框架类自己无法实现clone函数,须由派生类实现,故设计为纯虚函数强制派生类实现clone函数。在private中有原型容器,此例为数组。findAndClone函数在原型容器中可能使用class名称或数组位元访问获取原型并调用clone函数。
注:class本体的private数据必须在类体外做一次定义。
在派生类中,clone函数直接返回派生类副本。在private部分声明静态变量_landSatImage,调用私有构造函数,调用addPrototype送至框架类(父类)。因在调用私有addPrototype父类会寻找并调用派生类的clone函数时会使用默认构造函数调用,所以在clone函数内部不可以再使用默认构造函数,故需要增加一个新的构造函数,不可以放在public区域的原因是需要通过原型构建新对象,所以新的构造函数为protected或private均可以。新构造函数可增加dummy参数区别于默认构造函数。
设计模式总结
- Adapter 设计模式
- Handle/Body 设计模式
- Template Method 设计模式
- Observer 设计模式
- Composite 设计模式
- Prototype 设计模式