OOP 学习笔记(10)——结构型模式
Contents
结构型模式
适配器(Adapter)模式
需求:栈
需要设计一个数据结构,满足后进先出。
基本操作包括:
bool empty()
:判断是否为空。void push(int)
:压入一个元素。void pop()
:弹出栈顶元素。int size()
:获取栈内元素数。int top()
:获取栈顶元素。
利用数组可以较为容易实现,但这样的工作量较大,且自行管理内存容易出问题。
考虑到 STL 中的 vector
容器,可以对其进行改造。
改造 vector
vector
的方法(接口)与栈要求的不同,但实际功能实际上均可以实现。
也就是需要转换接口。
适配器
简单来说就是将已有类的接口转换成客户希望的另一种接口,使得类获得统一环境下的兼容性。
其结构包括:
- 目标(Target):客户所期待的接口。
- 需要适配的类(Adaptee):需要适配的类。
- 适配器(Adapter):通过包装一个需要适配的类,把原接口转换成目标接口。
对象适配器模式
使用组合实现适配,称为对象适配器模式。
此处 Adapter
类与 Adaptee
类之间的关系是一种比较强关联。
适配器基类(目标类)
class Stack
{
public:
virtual ~Stack() {}
virtual bool full() = 0;
virtual bool empty() = 0;
virtual void push(int i) = 0;
virtual void pop() = 0;
virtual int size() = 0;
virtual int top() = 0;
};
适配器实现
class Vector2Stack : public Stack
{
std::vector<int> m_data;
const int m_size;
public:
Vector2Stack(int size) : m_size(size) {}
bool full()
{
return (int)m_data.size() >= m_size;
}
bool empty()
{
return (int)m_data.size() == 0;
}
void push(int i)
{
m_data.push_back(i);
}
void pop()
{
if (!empty()) m_data.pop_back();
}
int size()
{
return m_data.size();
}
int top()
{
if (!empty())
return m_data[m_data.size() - 1];
else
return INT_MIN;
}
};
类适配器模式
使用继承实现适配,称作类适配器模式。
适配器基类(目标类)
与对象适配器模式完全相同。
适配器实现
class Vector2Stack : private std::vector<int>, public Stack
{
public:
Vector2Stack(int size) : vector<int>(size) {}
bool full()
{
return false;
}
bool empty()
{
return vector<int>::empty();
}
void push(int i)
{
push_back(i);
}
void pop()
{
pop_back();
}
int size()
{
return vector<int>::size();
}
int top()
{
return back();
}
};
适配器
优点
- 通过适配器,可统一复杂的底层接口。
- 复用现有类。
- 引入适配器类,使得原有代码无需修改。
使用场景
- 需要复用已有类,然而原有接口不符合要求。
- 接口第三方组件,接口不符合要求。
- 旧类(无法修改)实现的功能需要用新接口访问。
实际上,STL 中的智能指针就是典型的适配器。
但是单纯的接口转换做不到计数控制,因此还需要别的设计模式。
代理/委托(Proxy)模式
需求
一些应用中,无法直接访问对象:
- 要访问对象在远程机器上。
- 被访问对象创建的开销大。
- 某些操作需要安全控制。
- 需要进程外的访问。
- 直接访问会带来麻烦。
- 对象需要根据访问者行为作出复杂处理。
在这种情况下,我们可在访问对象上加一个访问层,使得复杂操作在内部不对外开放,但功能接口开放。
这也就是代理/委托模式。
场景
远程代理:
- 实际应用中,常需要从其它进程或者远程地址获取资源,而这个过程耗时较长或难以直接完成,此时可利用代理/委托模式。
- 利用这种方式替换原有获取方式,可以更高效、安全。
资源安全:
- 多进程编程中,并非所有进程都对所有资源拥有访问使用修改的权限,所以进程调用中,我们可以使用这种模式检查当前进程对当前资源是由拥有权限。
void Proxy::request()
{
if (checkAuthority(nowProcess, nowResource))
{
// do something...
}
}
代理/委托
其中横线是相互关联,相互之间有使用或者定义。
RealSubject
是实质功能完成者,Proxy
是代理人,来包装 RealSubject
,对外提供接口,也负责除了 RealSubject
自身功能以外的各种功能,比如前面提到的引用计数。
实例:智能指针引用计数
#include <iostream>
using namespace std;
template <typename T>
class SmartPtr;
template <typename T>
class U_Ptr
{
int count;
T *p;
friend class SmartPtr<T>;
U_Ptr(T *ptr) : p(ptr), count(1) {}
~U_Ptr()
{
delete p;
}
};
template <typename T>
class SmartPtr
{
U_Ptr<T> *rp;
public:
SmartPtr(T *ptr) : rp(new U_Ptr<T>(ptr)) {}
SmartPtr(const SmartPtr<T> &sp) : rp(sp.rp)
{
++rp->count;
}
SmartPtr& operator=(const SmartPtr<T> &rhs)
{
++rhs.rp->count;
if (--rp->count == 0) delete rp;
rp = rhs.rp;
return *this;
}
~SmartPtr()
{
if (--rp->count == 0) delete rp;
}
// 对智能指针操作等同于对内部辅助指针操作
T& operator*()
{
return *(rp->p);
}
T* operator->()
{
return rp->p;
}
};
int main()
{
int *i = new int(2);
SmartPtr<int> ptr1(i);
SmartPtr<int> ptr2(ptr1);
SmartPtr<int> ptr3 = ptr2;
cout << *ptr1 << endl;
*ptr1 = 20;
cout << *ptr2 << endl;
return 0;
}
// 结果为:
// 2
// 20
变与不变
SmartPtr<int>
与int*
有相同的接口。- 操作符:
*
和->
。 - 赋值操作符与初始化(拷贝构造)。
- 释放(析构)。
- 操作符:
SmartPtr<int>
比int*
增加了一些控制操作。- 增加了引用计数相关部分。
- 常被称为代理模式。
- 接口不变,功能变化。
- 用于对被代理对象进行控制,这里包括引用计数、权限控制、远程代理、延迟初始化等。
- 代理类一方面提供被代理类所有接口,另一方面同时可进行额外控制操作。
代理/委托与适配器
相似:
- 均为在被访问对象之上进行封装。
- 均提供被封装对象的功能接口供外部使用。
不同:
- 代理不会改变接口,但适配器可能会。
- 代理不会改变功能,但适配器可能会。
- 适配器不会增加控制,但代理可能会。
- 适配器的核心元素时变换接口,代理的核心要素是分割访问对象与被访问对象以减少耦合,并能在中间增加各种控制功能。
装饰器(Decorator)模式
实例
有一个对象 TextView
,在窗口中显示文本。
希望接口不改变的情况下,增加滚动条、边框等。
分析:继承
使用继承,通过多态实现功能变化。
但同样会有问题:
- 随功能变多,继承类的数量急剧膨胀,其最大派生类的数目可以是所有功能的组合数。
- 如果
TextView
的基类增加新的接口,则所有派生类均需要修改。
分析:策略模式
如果用组合替代继承,也会有问题:
- 策略个数是基类中预先定义好的,比如基类中定义了边框和滑动条,则策略模式只能实现这两者。
- 如果需要增加一个新功能,则需要修改基类,增加策略个数和新方法,就会对整体框架进行改动。
装饰器
创建一个装饰类,用来包装原有类,并在保持类方法完整性的前提下,提供额外功能。
且装饰类与被包装类继承于同一个基类,这样只需要利用基类指针即可再次包装并增加更多功能。
代码实现
#include <iostream>
using namespace std;
class Component
{
public:
virtual ~Component() {}
virtual void draw() = 0;
};
class TextView : public Component
{
public:
void draw()
{
cout << "TextView." << endl;
}
};
class Decorator : public Component
{
Component *_component;
public:
Decorator(Component* component) : _component(component) {}
virtual void addon() = 0;
void draw()
{
addon();
_component -> draw();
}
};
class Border : public Decorator
{
public:
Border(Component* component) : Decorator(component) {}
void addon()
{
cout << "Bordered ";
}
};
class HScroll : public Decorator
{
public:
HScroll(Component* component): Decorator(component) {}
void addon()
{
cout << "HScrolled ";
}
};
class VScroll : public Decorator
{
public:
VScroll(Component* component): Decorator(component) {}
void addon()
{
cout << "VScrolled ";
}
};
int main()
{
TextView testView;
VScroll vs_TextView(&testView);
HScroll hs_vs_TextView(&vs_TextView);
Border b_hs_vs_TextView(&hs_vs_TextView);
b_hs_vs_TextView.draw();
return 0;
}
// 结果:
// Bordered HScrolled VScrolled TextView
分析
整个过程是一个链式的调用关系。
每个对象都不需要了解整个链的全貌。
每次都是将之前的版本完全包裹住,再增加新功能。
也就是几个新功能就包了几层。
装饰器与策略
相同:
- 通过对象组合修改对象的功能。
- 以组合替代简单继承,更加灵活,减少冗余。
不同:
- 策略模式修改对象功能的内核(行为),装饰器模式修改对象功能的外壳(结构)。
- 策略模式中组间必须了解有哪些需要选择的策略,侧重于功能选择,而装饰器模式中组件则无需了解,侧重于功能组装。
装饰器与代理
都用于改变对象的行为,可以把装饰看为一连串的代理。
装饰常用多重嵌套,而代理较少见。
总结
结构型设计模式关心对象组成结构上的抽象,包括接口、层次、对象组合等。
核心在抽象结构层次上的不变,尽可能减少类与类之间的联系与耦合,从而能够以最小代价支持新功能增加。
No Comments