设计模式 - 桥接模式

来源:互联网 发布:软件测试专业知识 编辑:程序博客网 时间:2024/05/04 02:51

桥接模式是结构型模式里面相对复杂的一种模式,但是同时也是很有用的。


我们先来考虑这么一种情况(这个例子是设计模式书上的一个例子),Window是一个类,这个类用来画一个窗口。那么我们现在如果要支持XP风格和WIN7风格,应该怎么做呢?通常首先能想到的就是子类化Window,比如增加2个类:WindowsXP和Windows7。然后这2个子类可以各自实现各自的风格,这个没有问题。而且这也是常用办法之一:继承。

那么我们现在考虑这么一个情况,假如我们要丰富Window类库,比如支持IconWindow,IconWindow用来支持在窗口上画一个图标。然后这个又要同时支持XP风格和WIN7风格,怎么办呢?

那么假如我们现在又要支持TextWindow(比如画一些奇怪形状文字),那么我们又得从Window派生一个子类TextWindow,然后TextWindow又得派生2个子类来支持XP和WIN7风格。这个会使得类库变的非常臃肿而且复杂。那么有解决这个问题的办法吗?有,我们可以使用桥接模式来改造这个类库。接下来我们就介绍一下桥接模式。

意图
将抽象部分和它的实现部分分离,使它们都可以独立地变化。

结构图
这个模式基本分成2部分:左边的抽象部分和右边的实现部分。然后抽象部分聚合了右边的实现类,也就是说左边的抽象类的对象里面保存了一个实现类的对象。通过这个实现类的对象来实现具体的功能。其实我们也可以这么理解,桥接模式用对象组合的方式替代了类继承。下面我们就来改造一下上面的Window类库。


我们先来讲一下每个类的职责:
CWindow抽象了一个方法,叫做DrawContents()。也就是桥接模式中的抽象部分。

CWindowImpl则是实现类,比如实现XP,WIN7风格的窗口。

我们先来看看CWindow这个抽象类,这个类很简单,现在就包括2部分:DrawContents方法和_impl成员。_impl是指向实现类CWindowImpl的一个指针,这个也就是桥接模式的一个关键点。这个指针起了桥梁的作用,把抽象类和实现类关联起来。感觉桥接模式的命名也是这么来的。ok,先来看看CWindow的代码实现:

class CWindow  
{  
public:  
    virtual void DrawContents() = 0;  
  
protected:  
    CWindowImpl* _impl;  
};  
相当简单。

然后我们来看看CWindow的子类CIconWindow和CTextWindow:


class CIconWindow: public CWindow  
{  
public:  
    CIconWindow(const char* pszIconName, CWindowImpl* impl): _pszIconName(pszIconName){_impl = impl;}  
    virtual void DrawContents()  
    {  
        _impl->DrawIcon(_pszIconName);  
    }  
  
  
    ~CIconWindow()  
    {  
        if (_impl)  
        {  
            delete _impl;  
            _impl = NULL;  
        }  
    }  
private:  
    const char* _pszIconName;  
};  
  
class CTextWindow: public CWindow  
{  
public:  
    CTextWindow(const char* pszText, CWindowImpl* impl):_pszText(pszText){_impl = impl;}  
  
    virtual void DrawContents()  
    {  
        _impl->DrawText(_pszText);  
    }  
  
    ~CTextWindow()  
    {  
        if (_impl)  
        {  
            delete _impl;  
            _impl = NULL;  
        }  
    }  
  
private:  
    const char* _pszText;  
};  
也是相当的简单,关键代码就是在DrawContents的实现里面,通过_impl来调用实现类的函数。


接下来就看看实现类,首先是基类CWindowImpl:

class CWindowImpl  
{  
public:  
    virtual void DrawText(const char* pszText) = 0;  
    virtual void DrawIcon(const char* pszIconName) = 0;  
  
protected:  
    CWindowImpl(){}  
};  
这个基类提供了2个方法,也就是说实现类可以支持画icon图标和文字。看它的2个子类:


class CWindowsXPImpl: public CWindowImpl  
{  
public:  
    virtual void DrawText(const char* pszText)  
    {  
        std::cout << "XP style, DrawText: " << pszText <<"\n";  
    }  
  
    virtual void DrawIcon(const char* pszIconName)  
    {  
        std::cout << "XP style, DrawIcon: " << pszIconName << "\n";  
    }  
};  
  
class CWindows7Impl: public CWindowImpl  
{  
public:  
    virtual void DrawText(const char* pszText)  
    {  
        std::cout << "Win7 style, DrawText: " << pszText <<"\n";  
    }  
  
    virtual void DrawIcon(const char* pszIconName)  
    {  
        std::cout << "Win7 style, DrawIcon: " << pszIconName << "\n";  
    }  
};  
上面2个实现类的职责就是实现各自风格的画图和画文字函数。


ok,再看看如果调用:

CWindowImpl* impl = new CWindowsXPImpl();  
    CWindow* win = new CIconWindow("test.ico", impl);  
    win->DrawContents();  
    delete win;  
  
    impl = new CWindows7Impl();//如果改成XP实现类,那么就是会创建一个XP风格的文字窗口  
    CWindow* win2 = new CTextWindow("hello world.", impl);  
    win2->DrawContents();  
  
    delete win2;  
调用过程也是相当的简单。这个调用例子里面创建了2个窗口:XP风格的图片窗口和win7风格的文字窗口。那么假如我们要创建XP风格的文字窗口该怎么办呢?太简单了,只要生成一个XP风格的实现类,然后再把这个类传给CTextWindow就可以了。
和文章最开始的那个方案向比较,我们会发现类库结构清晰了很多,而且抽象类和实现类解耦了(当然还是有关联)。假如我们现在要增加一个新的窗口,比如图文窗口,那么只需要增加一个CWindow子类CIconTextWindow,CIconTextWindow里面的DrawContents分别调用实现类的DrawIcon和DrawText就行了。也就是说只增加了一个类。假如用前面的那个办法,就得增加3个类。

总结一下桥接模式有哪些优点呢:
1. 分离接口及其表现部分, 一个实现未必不变地绑定在一个接口上。抽象类的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现。就像前面的例子里面,实现类CWindow7Impl既可以给CIconWindow用,也可以给CTextWindow用。
2. 提高可扩充性, 可以独立地对抽象类和实现类层次结构进行扩充。就像刚才讲的CIconTextWindow,我们只扩充了抽象类,而没有更改实现类。 

现在我们再来关注一个细节,看看前面的调用代码,我们是怎么生成实现类的对象的呢?好像是hard code,这个很不爽。其实在抽象类里面如何创建实现类对象还是有学问的。这里有很多方法,具体应该看情况而定。我的例子里是客户端用hard code来生成的实现类对象,当然有时这样也够了。这里再介绍其他2种方法:

1. 假如抽象类知道所有的实现类,那么抽象类自己就可以决定使用哪个实现类。比如collection是个抽象类,然后支持list和vector。那么当客户端申请一个固定大小的空间的时候,collection可以使用vector,而当客户端申请一个0size的容器,那么就使用list。这只是个例子,只是说有时候抽象类本身是可以决定使用哪个实现类的;

2. 使用抽象工厂模式,这个工厂类唯一的职责就是返回一个实现类对象。比如上面的Window例子中,这个factory可以检测当前的系统,是XP,就返回XP风格的实现类对象,是win7就返回win7风格的。当然还是扩展到win8。其实在桥接模式里面使用抽象工厂模式来得到实现类对象是个挺好的办法。通过这个工厂类,抽象类不需要直接和任何一个实现类耦合。

 相关模式
1. 抽象工厂模式,可以使用抽象工厂模式在抽象类中创建实现类对象。
2. 适配器模式,通常适配器模式是在系统设计完成后才被使用的,换句话说是个补救办法。而桥接模式应该在开始设计系统的时候就被使用。 

好了,讲完了。再重复一次,桥接模式使得抽象接口和实现部分分离,然后抽象部分和实现部分可以独立地进行改变。


转载:http://blog.csdn.net/zj510/article/details/8129142