OOP 学习笔记(10)——结构型模式

OOP 学习笔记(10)——结构型模式

Contents

结构型模式

适配器(Adapter)模式

需求:栈

需要设计一个数据结构,满足后进先出

13

基本操作包括:

  • bool empty():判断是否为空。
  • void push(int):压入一个元素。
  • void pop():弹出栈顶元素。
  • int size():获取栈内元素数。
  • int top():获取栈顶元素。

利用数组可以较为容易实现,但这样的工作量较大,且自行管理内存容易出问题。

考虑到 STL 中的 vector 容器,可以对其进行改造

改造 vector

vector 的方法(接口)与栈要求的不同,但实际功能实际上均可以实现。

也就是需要转换接口。

适配器

简单来说就是将已有类的接口转换成客户希望的另一种接口,使得类获得统一环境下的兼容性。

其结构包括:

  • 目标(Target):客户所期待的接口。
  • 需要适配的类(Adaptee):需要适配的类。
  • 适配器(Adapter):通过包装一个需要适配的类,把原接口转换成目标接口。

对象适配器模式

14

使用组合实现适配,称为对象适配器模式。

此处 Adapter 类与 Adaptee 类之间的关系是一种比较强关联。

适配器基类(目标类)

15

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;
    }
};

类适配器模式

16

使用继承实现适配,称作类适配器模式。

适配器基类(目标类)

与对象适配器模式完全相同。

适配器实现

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...
    }
}

代理/委托

17

其中横线是相互关联,相互之间有使用或者定义。

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,在窗口中显示文本。

希望接口不改变的情况下,增加滚动条、边框等。

分析:继承

18

使用继承,通过多态实现功能变化。

但同样会有问题:

  • 随功能变多,继承类的数量急剧膨胀,其最大派生类的数目可以是所有功能的组合数。
  • 如果 TextView 的基类增加新的接口,则所有派生类均需要修改。

分析:策略模式

19

如果用组合替代继承,也会有问题:

  • 策略个数是基类中预先定义好的,比如基类中定义了边框和滑动条,则策略模式只能实现这两者。
  • 如果需要增加一个新功能,则需要修改基类,增加策略个数和新方法,就会对整体框架进行改动。

装饰器

20

创建一个装饰类,用来包装原有类,并在保持类方法完整性的前提下,提供额外功能。

且装饰类与被包装类继承于同一个基类,这样只需要利用基类指针即可再次包装并增加更多功能。

代码实现

#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

分析

整个过程是一个链式的调用关系。

每个对象都不需要了解整个链的全貌。

每次都是将之前的版本完全包裹住,再增加新功能。

也就是几个新功能就包了几层。

装饰器与策略

相同

  • 通过对象组合修改对象的功能。
  • 以组合替代简单继承,更加灵活,减少冗余。

不同

  • 策略模式修改对象功能的内核(行为),装饰器模式修改对象功能的外壳(结构)。
  • 策略模式中组间必须了解有哪些需要选择的策略,侧重于功能选择,而装饰器模式中组件则无需了解,侧重于功能组装。

装饰器与代理

都用于改变对象的行为,可以把装饰看为一连串的代理。

装饰常用多重嵌套,而代理较少见。

总结

结构型设计模式关心对象组成结构上的抽象,包括接口、层次、对象组合等。

核心在抽象结构层次上的不变,尽可能减少类与类之间的联系与耦合,从而能够以最小代价支持新功能增加

 

点赞 0

No Comments

Add your comment