65.9K
CodeProject 正在变化。 阅读更多。
Home

桥接模式 - 连接接口和实现之间的鸿沟

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (23投票s)

2001年1月9日

viewsIcon

201773

downloadIcon

686

本文讨论了Bridge模式,它是什么,为什么需要它以及何时需要它。

引言

开发、市场营销和技术支持团队在软件产品的成功中扮演着至关重要的角色。开发团队当然是产品的骨干,市场营销团队在销售产品方面起着关键作用,而技术支持团队则为客户提供售后支持。我常常思考,为什么开发团队不能提供支持。直到很久以后我才找到了答案。技术支持团队应该与客户和计算机进行交互,而开发团队则花费大部分时间在黑客攻击计算机上。所以,这两个团队的成员应该有不同的思维模式,扮演完全不同的角色。最重要的是,这两个团队的操作性质是相互排斥的。例如,我们可以看到提供24x7技术支持的产品,而开发团队却很少通宵工作。一个团队的操作策略的改变不会直接影响另一个团队。简而言之,技术支持团队充当产品的接口,而开发团队实现产品。

当我阅读Erich Gamma等人(Addison-Wesley,1995)撰写的《设计模式:可重用面向对象软件的元素》一书时,我将上述场景与Bridge模式联系起来。将技术支持团队与开发团队分离,类似于将抽象与实现隔离。这正是Bridge模式的意图。在本文中,我将讨论Bridge模式,它是什么,为什么需要它以及何时需要它。还将介绍使用Bridge模式的优缺点。如果不提及Bridge模式的变体和流行库中的已知用法,讨论将不完整。让我从一个简单的编程示例开始讨论。

一个示例

互联网是一个很好的例子,可以证明“一图胜千言”。当互联网刚开始时,HTML只是一种基于文本的标记语言,主要关注文档的结构而非其呈现。然而,HTML已经经历了多次修订,以包含图形和图像。现在,互联网上有成千上万的网站包含GIF和JPEG图像。虽然GIF和JPEG是网络上流行的图像格式,但还有数百种其他图像格式,如BMP、PCX、TIFF、TARGA等,它们服务于截然不同的目的,并在不同的操作系统上很受欢迎。例如,BMP图像格式广泛用于Windows操作系统,但它也存在于OS/2、Macintosh和UNIX操作系统上。

结构和表示是图像格式的两个重要方面。结构定义了图像存储的方式,而表示则涉及图像的显示。对于给定的格式,结构在不同操作系统上保持不变,而图像的表示或显示方式可能因操作系统而异。例如,Windows BMP文件的结构在所有操作系统上保持不变,但Windows操作系统显示BMP文件所使用的机制与Macintosh或OS/2操作系统显示同一文件所使用的机制不同。另一方面,对于给定的操作系统,表示在不同图像格式之间可以保持不变。例如,Windows可以显示表示为Bitmap对象的图像,而无需了解其源格式(可能是BMP、JPEG或PCX)。总之,图像格式的表示和结构是两个不同的方面,它们应该允许根据操作系统、硬件等其他因素独立变化。

Bridge模式被“Gang of Four (GoF)”归类为结构型模式,可用于抽象和建模这些变化。根据GoF的说法,Bridge模式的意图是“将抽象与其实现分离,以便两者可以独立变化”。在本文中,我将使用GoF使用的术语来解释Bridge模式。

本文使用一个图像查看器应用程序来解释Bridge模式背后的概念。这个示例应用程序设计用于在Windows操作系统上查看BMP文件。然而,它可以轻松地扩展以查看JPEG等其他图像格式在Windows上,或查看OS/2等其他操作系统上的BMP图像。

该示例使用了两个类层次结构:CImage和CImageImp(参见图)。CImage类层次结构为客户端定义了抽象,而CImageImp类层次结构为指定的抽象提供了实现。CImage及其派生类负责处理BMP、JPEG、PCX等不同的图像格式,而CImageImp类负责在Windows、OS/2等不同操作系统上表示图像。CImage对象提供加载和显示图像的基本服务,并配置一个CImageImp对象。依赖于特定实现的(如show)服务被转发到CImageImp类(例如PaintImage)。这样,就可以在不影响CImageImp的情况下将新的图像格式添加到CImage类层次结构中,并且可以扩展CImageImp以在不影响CImage的情况下为新操作系统提供实现。简而言之,实现了Bridge模式的目标,即独立地改变抽象和实现。

Bridge模式有四个参与者,包括Abstraction、Refined Abstraction、Implementor 和 Concrete Implementor。在本例中,抽象图像类CImage被称为Abstraction,具体图像类CBmpImage(用于处理Windows位图)被称为Refined Abstraction,抽象图像实现类CImageImp被称为Implementor,而实现Implementor接口的具体类CWinImp被称为Concrete Implementor。使用CImage抽象的应用程序是客户端。根据操作系统,客户端可以将CImage子类(Refined Abstraction)配置为具体的CImageImp类对象(Concrete Implementor)。

CImage维护对CImageImp对象的引用。当客户端在CImage中调用Load或Show方法时,它会执行一些预处理,并将请求转发给CImageImp对象,调用InitImageInfo或PaintImage方法来提供实际实现。将图像和图像实现分离到单独的类层次结构中,使得它们能够独立变化。下面给出了表示Bridge模式参与者之间关系的UML图。Listing 1包含类声明,Listing 2包含示例方法实现。

使用Bridge模式的优点

  1. 将抽象与实现解耦 - 继承在编译时将抽象与实现紧密耦合。Bridge模式可用于避免抽象与实现之间的绑定,并在运行时选择实现。

  2. 减少子类数量 - 有时,纯粹的继承会增加子类的数量。假设我们的图像查看器的完整版本支持3个不同操作系统上的6种图像格式。纯继承将产生18个子类,而应用Bridge模式则将子类需求减少到9个。

  3. 代码更简洁,可执行文件大小减小 - 在上述示例中,特定于操作系统的代码封装在CImageImp子类中。这使得代码更简洁,没有大量的预处理器语句,如#ifdefs、#ifndefs。此外,可以轻松地为特定操作系统条件编译CImageImp子类,以减小可执行文件的大小。

  4. 接口和实现可以独立变化 - 为接口和实现维护两个不同的类层次结构,使得一个可以独立于另一个而变化。

  5. 改进的可扩展性 - 抽象和实现可以独立扩展。如前所述,上述示例可以轻松地扩展为在Windows上查看其他图像格式,或在其他操作系统上查看BMP图像。

  6. 客户端代码松耦合 - 抽象将客户端代码与实现分离。因此,可以在不影响客户端代码的情况下更改实现,并且在实现更改时无需重新编译客户端代码。(注意:在上述示例中,为了简单起见,应用程序使用正确的CImageImp对象配置CImage对象。然而,可以使用替代方法,如抽象工厂来选择CImageImp对象。)

使用Bridge模式的缺点

1.双重间接 - 在上述示例中,特定于操作系统的功能由CImageImp类的子类实现。CImage类必须将消息委托给实现适当方法的CImageImp子类。这会对性能产生轻微影响。

变体

Handle/Body

引用计数是一种允许具有相同值的多个对象共享该值的一种表示的技术。共享表示的优点在于在处理大对象时减少内存开销。一个简单的例子是String类,其中多个对象可以共享相同的String表示。String类被称为Handle类,String表示被称为Body类。Handle类指定接口,Body类维护引用计数并实现实际表示。客户端通过Handle类指定的接口与Body类交互。Handle/Body分离将客户端与实现更改隔离开来。这种结构与Bridge模式非常相似,但意图不同。

退化Bridge(Degenerate Bridge)

有时,对于给定的Abstraction,可能只有一个Implementor类。因此,不需要抽象Implementor类。这导致Abstraction和Implementor类之间存在一对一的关系。这种分离仍然有助于在不影响任何客户端的情况下更改Implementor。GoF将这种情况称为退化Bridge模式,其中Abstraction和Implementor之间存在一对一的关系。GoF用libg++的一个例子来解释退化Bridge。Libg++定义了实现常见数据结构的类,例如Set、LinkedSet、HashSet、LinkedList和HashTable。Set是一个抽象类,它定义了一个集合的抽象,而LinkedList和HashTable分别是链表和哈希表的具体实现者。LinkedSet和HashSet是Set的实现者,它们在Set与其具体对应物LinkedList和HashTable之间建立了桥梁。

已知用法

本节介绍Bridge模式的已知用法。本节介绍的一些已知用法摘自GoF的设计模式书籍。

MFC和Bridge模式

在MFC中,将对象存储/检索到/从持久化机制(如文件)的过程称为序列化。MFC使用Bridge模式来实现序列化。CArchive和CFile类实现了对象序列化。CArchive类提供了将对象写入/读取到/从持久化机制的接口,而CFile及其子类则实现了不同持久化机制的实现,如内存、磁盘文件、套接字等。

在构造CArchive对象时,它会与CFile类(或其派生类)的对象进行配置,从中获取序列化所需的必要信息,包括文件名和请求的操作类型(读取或写入)。执行序列化操作的客户端可以使用CArchive对象,而无需考虑CFile类实现的持久化机制。

Java和Bridge模式

Java使用Bridge模式来分离组件(Components)和组件对等体(Component Peers)。Java应用程序可以在不同平台上运行,因此客户端代码能够创建组件而不必绑定到具体的实现。组件和组件对等体表示为两个不同的类/接口层次结构。每个AWT Component子类都有一个对应的Component Peer子接口,它可以与之通信。平台特定的类实现了这些Component Peer接口。

其他已知用法(来自GoF设计模式书籍)

ET++ Window/WindowPort设计通过WindowPort还保持对Window的反向引用来扩展Bridge模式。WindowPort实现类使用此引用来通知Window有关WindowPort特定的事件:输入事件的到达、窗口大小调整等。

NeXT的AppKit在图形图像的实现和显示中使用Bridge模式。

Bridge与Strategy

Strategy模式经常与Bridge模式混淆。尽管这两种模式在结构上相似,但它们试图解决两个不同的设计问题。Strategy主要关注封装算法,而Bridge则将抽象与其实现分离,为相同的抽象提供不同的实现。

文章在C++应用程序中应用Strategy模式详细讨论了Strategy模式。

Bridge与Adapter

Adapter模式(对象适配器)的结构可能看起来类似于Bridge模式。但是,Adapter旨在改变现有对象的接口,并且主要目的是使不相关的类协同工作。

摘要

本文不仅介绍了Bridge模式是什么,还深入探讨了它为什么以及何时真正需要。总而言之,纯粹的继承将抽象和实现硬编码在一起。当一个抽象可以有不同的实现,并且两者可以独立变化时,就可以使用Bridge模式。

致谢

特别感谢我的朋友 Sree Meenakshi,感谢她为改进本文的清晰度和呈现方式提出的宝贵建议。

Listing 1 - 包含重要类成员的CImage和CImageImp类声明

class CImage
{
    // Method declarations

    public :
        virtual INT Load( LPCSTR, CRuntimeClass * )    = 0;
        virtual INT Show( CWnd *, WPARAM );

    // Data members

    protected :
        CImageImp    * m_pImageImp;
};

class CBmpImage : public CImage
{
    // Method declarations

    public :
        virtual INT Load( LPCSTR, CRuntimeClass * );
};

class CImageImp : public CObject
{
    // Method declarations

    public :
        virtual INT    InitImageInfo( LPSTR )           = 0;
        virtual BOOL    PaintImage( CWnd *, CRect * )   = 0;

    // Attributes

    public :
        LPBYTE      m_pImage;
        LONG        m_lNormalWidth;
        LONG        m_lNormalHeight;
};

class CWinImp : public CImageImp
{
    // Method declarations

    public :
        INT        InitImageInfo( LPSTR );
        BOOL       PaintImage( CWnd *, CRect * );

    // Attributes

    protected :
        BYTE        * m_pBmi;
        CPalette    * m_pPalette;
};

Listing 2 - CImage和CBmpImage类的Show和Load方法的实现

INT CImage::Show( CWnd * pWnd, WPARAM wParam )
{
    // Step 1 - Check and delegate this method to m_pImageImp

    ASSERT( m_pImageImp != NULL );
    return m_pImageImp->PaintImage( pWnd, ( CRect * ) wParam );
}

INT CBmpImage::Load( LPCSTR lpszFileName, CRuntimeClass * pRuntimeClass )
{
    // Some initialization code before creating image implementation object

    …
    …
    //  Initialize image information, after creating image implementation object

    m_pImageImp = ( CImageImp * ) pRuntimeClass->CreateObject();
    if( m_pImageImp == NULL )
    {
        …
        …
        return FAILURE;
    }
    m_pImageImp->InitImageInfo(..);
    …
    …
    return SUCCESS;
}
© . All rights reserved.