适配器模式和外观模式有些相似,甚至其实现方式也相似,不同的只是他们的侧重点。

1. 适配器模式

适配器模式,我想用处应该非常广泛,特别是对于环境多变的场合,其定义如下:

将一个类的接口,转换成客户希望的另一个接口。适配器让原本不兼容的类可以合作无间。

这解决了本章开篇的那个需求:“将一个方块放入一个圆洞中!”。

也就是说,适配器通过接口转换解决了兼容性问题。那么他是通过什么方式实现的呢?

在HeadFirst中,作者介绍了两种实现方式:类适配器、对象适配器。

类适配器使用多继承的方式实现,及同时继承客户期望的类和需要被适配的类,并重载客户类中的接口,使其应用被适配类的接口。其UML图如下(来自wikipedia):

类适配器方案

而对象适配器则使用组合的方式应用被适配者的接口,其UML图如下(同样来自wikipedia):

对象适配器方案

请注意,这种方案中的Adaptor必须与客户类接口一致,我们可以通过继承客户类并重载其方法的方案来实现这个效果。

下面我来举两个例子分别说明这两种实现方案:

这两个例子的场景是这样的:原本我们有一个矩形类:ORect,使用左上角 (topLeft) 和右下角 (bottomRight) 两个点 (OPoint) 来记录,ORect 包含两个方法:setRect(OPoint, OPoint) 和 area(),分别用于设置矩形和获取其面积。

后来,我们需要将这个类移植到一个新的软件中,这个软件的接口使用的是IRect,使用左上角的两个坐标 (left, top) 和宽 (width)、高 (height)。同样有两个方法 setRect(int left, int top, int with, int height) 和 getArea()。那么我们如何处理?看下面的代码:

原始类和接口类:

// 原始类,需要被适配
class ORect {
private:
    int left;
    int top;
    int right;
    int bottom;
public:
    // 使用左上角、右下角的坐标构造矩形
    void setRect(int l, int t, int r, int b) {
        left = l;
        top = t;
        right = r;
        bottom = b;
    }
    // 获取面积
    int area() {
        return (right - left) * (bottom - top);
    }
};
// 接口类,是新的客户代码需要的接口
class IRect {
public:
    // 使用左上角坐标以及宽和高构造矩形
    virtual void setRect(int l, int t, int w, int h) = 0;
    // 获取面积
    virtual int getArea() = 0;
};

类适配器:

// 适配器类,共有继承接口的方法,私有继承原始类的方法
class ClassAdapterRect : public IRect, private ORect {
private:
    int left;
    int top;
    int width;
    int height;
public:
    virtual void setRect(int l, int t, int w, int h) {
        // 为避免歧义,显式调用ORect类的setRect方法。
        // 若不这样,程序在运行时会崩溃(已测试)
        ORect::setRect(l, t, l + w, t + h);
    }
    virtual int getArea() {
        return area();
    }
};

对象适配器:

// 对象适配器,共有继承接口的方法,组合入原始类的方法
class ObjectAdapterRect : public IRect, private ORect {
private:
    int left;
    int top;
    int width;
    int height;
    
    // 原始类
    ORect originalRect;
public:
    virtual void setRect(int l, int t, int w, int h) {
        // 为避免歧义,显式调用ORect类的setRect方法。
        // 若不这样,程序在运行时会崩溃(已测试)
        originalRect.setRect(l, t, l + w, t + h);
    }
    virtual int getArea() {
        return originalRect.area();
    }
};

测试代码:

int main() {
    const int l= 1, t= 2, r= 4, b= 4, w= r - l, h= b - t;
    
    // 调用原始类的方法
    std::cout << "--------------Original Rectangle-------------" << std::endl;
    ORect orect;
    std::cout << "left= " << l << ", top= " << t
        << ", right= " << r << ", bottom= " << b << std::endl;
    orect.setRect(l, t, r, b);
    std::cout << "area= " << orect.area() << std::endl;
    
    // 调用类适配器的方法
    std::cout << "-----------Class Adapter Rectangle-----------" << std::endl;
    ClassAdapterRect ca;
    std::cout << "left= " << l << ", top= " << t
        << ", width= " << w << ", height= " << h << std::endl;
    ca.setRect(l, t, w, h);
    std::cout << "area= " << ca.getArea() << std::endl;
    
    // 调用对象适配器的方法
    std::cout << "----------Object Adapter Rectangle-----------" << std::endl;
    ObjectAdapterRect oa;
    std::cout << "left= " << l << ", top= " << t
        << ", width= " << w << ", height= " << h << std::endl;
    oa.setRect(l, t, w, h);
    std::cout << "area= " << oa.getArea() << std::endl;
    return 0;
}

运行结果:

--------------Original Rectangle-------------

left= 1, top= 2, right= 4, bottom= 4

area= 6

-----------Class Adapter Rectangle-----------

left= 1, top= 2, width= 3, height= 2

area= 6

----------Object Adapter Rectangle-----------

left= 1, top= 2, width= 3, height= 2

area= 6

效果完全一致。适配器很成功。

在这里我想吐个槽,《Head First》是一本针对Java语言的书,在说到类适配器的时候只是一代而过,也没有示例代码,只说用多继承,又没说清楚怎么用,搞得我非常纠结。查阅网上的多继承资料,也没有突破,最后终于想到了四人帮的《Design Patterns》,他们是用c++为例做的介绍。于是我通过研究他们的示例代码才终于弄明白类适配器要如何使用。其关键在于:

共有继承客户接口,私有继承原始类!

在《Design Patterns》上的原话是这样: "The key to class adapters is to use one inheritance branch to inherit the interface and another branch to inherit the implementation. The usual way to make this distinction in C++ is to inherit the interface publicly and inherit the implementation privately."

这么一弄,豁然开朗。

共有继承客户接口保证了接口是客户所期望的。而私有继承原始类使得原始类的方法隐藏起来,不会被外界代码继续调用。真是奇妙的c++!

2. 外观模式

外观模式实际上实现方法完全与适配器相同。只是侧重点不一样。外观模式的定义如下:

提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

什么意思呢?实际上很简单,就是将复杂的系统操作傻瓜化,提供一个傻瓜接口。

从本质上来说,外观模式难道不是接口转换吗?所以其实现方式是与适配器模式完全相同的。这里我就不再举例。

3. 总结

让我们回想一下,命令模式是不是也能具有外观模式这样的功能?将复杂的命令封装成统一接口,然后使用一个串将这一系列复杂的接口连接起来。一键操作。

乍看起来,似乎的确是这样,但是请注意命令模式的定义,命令模式只是需要保证一个统一的命令接口,以方便组织,而并未强调简化操作或接口。

所以,学习的设计模式多起来以后,会发现他们其实都大同小异,实现方式就那么几种,稍微有些变化就成了一个新的设计模式。

那为什么还要单独写出这么多设计模式呢?我想,针对不同的应用场合,你能根据这些设计模式的侧重点,很快的对应到一个合适的模式,并能很快的实现这个系统,简化开发流程,这就是设计模式的作用了。

打个不太恰当的比方,设计模式就像提供了一个表,你在使用复杂函数的时候,使用查找表的方法肯定比一步步计算要快得多。

posted on 2011-02-17 12:18  Tankery  阅读(280)  评论(0编辑  收藏  举报