• 设计模式之桥接模式


    2018-09-20 10:53:13

    前言

      滥用继承会带来麻烦(实际上何止是麻烦,还会带来性能上的额外开销,关于继承的问题,可以查看继承相关的文章)。比如,对象的继承关系是在编译时就定义好了,所以无法在运行时改变从父类继承的实现。子类实现与它的父类有非常紧密的依赖关系。以至于父类中的任何变化必然会导致子类发生变化。当你需要复用子类时,如果继承下来的实现不适合解决新的问题,则父类必须重写或被其它更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。导致这种问题的原因是继承是一种强耦合的关系,是is -a 的关系。

    合成聚合原则

      优先使用对象合成、聚合,而不是类继承。(想一下两种模式的适配器,更推荐使用对象适配器就是这个原则的体现了)

      合成(Composition,有时也叫组合)和聚合都是关联的特殊种类。聚合表示一种弱的拥有关系,体现的是A对象可以包含B对象,但是B对象不是A对象的一部分;合成则是一种强的拥有关系,体现了严格的整体和部分的关系,部分和整体的生命周期是一样的。优先使用对象的聚合、合成将有助于你保持每个类被封装,并被集中在单个任务上,这样类和类的继承就会保持较小的规模,并且不太可能增长为不可控制的庞然大物。

      聚合和组合关系,这其实是需要从宏观上来理解的,而不能从微观角度理解。组合必然是几个组件,大家每个都不能少,少了就不是一个整体,这是一个强拥有的关系。而聚合,多你一个不多,少你一个不少,就像《大话设计模式》里讲的大雁和雁群的关系,大雁A、大雁B、大雁C、大雁D等无数多只大雁聚合在一起就是一个雁群,可是你能说大雁A离开了雁群这个雁群就不是雁群了吗?显然不能。但是在具体实现上,其实大都是以类的数据成员来实现的。最多就是在组合的情况下class内嵌套class,但是这种做法并不好,因为过多的嵌套,可能会造成这个类的臃肿。

    桥接模式

      桥接模式(Bridge),将抽象部分与它的实现部分分离,使它们都可以独立地变化。这里的抽象与它的实现分离,并不是指让抽象类和派生类分类,这没有任何意义。实现指的是抽象类和它的派生类用来实现自己的对象。其实就是说,一个系统的实现,可能有多个角度的分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独自变化。举例来说,手机即可以按照品牌分类(手机是抽象,功能是实现),也可以按照功能分类(功能是实现,而手机变成了抽象)。

    按照品牌分类实现类图:

      按照功能分类实现结构图:

      可以看到上面两种接口图,模块与模块之间有着极强的耦合度,对按照手机品牌分类的结构中,当有新手机品牌加入的时候,你需要为这一款手机重新开发功实现TaoBao、Game(我知道你会说软件会兼容,但是这里暂时不考虑这个,就假设没有兼容性)。对按照功能来进行分类的时候,每增加一个新功能就要为对应的品牌独立开发相应的功能。

      实现方式是多种多样的,而桥接模式的核心意图就是把实现独立出来,让它们各自独立的变化,并且它们自己的变化不会影响到其它的实现,从而达到应对变化的目的。按照桥接的思路进行设计结构图如下:

    桥接模式UML类图

    • Abstraction(抽象类):用于定义抽象类的接口,并且维护一个指向 Implementor 实现类的指针。它与 Implementor 之间具有聚合关系。
    • RefinedAbstraction(扩充抽象类):扩充由 Abstraction 定义的接口,在 RefinedAbstraction 中可以调用在 Implementor 中定义的业务方法。
    • Implementor(实现类接口):定义实现类的接口,这个接口不一定要与 Abstraction 的接口完全一致,事实上这两个接口可以完全不同。
    • ConcreteImplementor(具体实现类):实现了 Implementor 定义的接口,在不同的 ConcreteImplementor 中提供基本操作的不同实现。在程序运行时,ConcreteImplementor 对象将替换其父类对象,提供给 Abstraction 具体的业务操作方法。

    桥接模式的优缺点

    优点:

      1.桥接分离了抽象和实现的具体联系,使得实现和抽象都可以去按照各自的维度去变化,而且相互之间不会产生影响。所谓隔离了抽象和实现,其实就是通过聚合的方式来实现了组件的动态添加和离开,子类不同形式的组合能够构建出不同的对象。

      2.在设计上它可以用来代替多层继承方案。当继承体系达到了一定深度之后,会导致大量的子类,以及冗余的功能,并且子类本身的管理也是一个问题。

      3.桥接模式提高了系统的可扩展性,因为抽象和实现,可以按照各自不同的维度进行变化,进而组合出不同的类型

    缺点:

      1.增加了设计和理解难度,因为关联关系从一开始就是建立在抽象层面的,意味着编码工作要针对抽象层进行。

      2.桥接模式要求系统正确的识别各个独立变化的维度,但是识别这些维度也需要一定的考量。

    桥接模式的适用场景

    • 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
    • “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
    • 一个系统存在多个(≥ 2)独立变化的维度,且这多个维度都需要独立进行扩展。
    • 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

    代码示例

      桥接模式使用的关键是分清楚谁是实现,谁是抽象。假设我们有一间非常大的图书馆,馆藏丰富,内含n个区域,每个区域中有不同种类的书籍,每个图书管理员负责其中一各区域。那图书管理员则是一个抽象。因为图书是客观存在的,它不会因为图书管理员的离开而消失。。也就是说我们可以通过实现的子类和抽象的子类进行组合,可得出任意形状的系统。使用桥接模式。图书区内上架什么书或者下架什么书,对图书管理员不会产生影响(只要保持方法稳定)。

    1.实现的基类(UML类图中的Implementor)

    #ifndef IMPLEMENTOR_H_
    #define IMPLEMENTOR_H_
    
    class Implementor
    {
    public:
        virtual void display()=0;
        Implementor() = default;
        virtual ~Implementor() = default;
    };
    #endif
    Abstract Implementor

    2.实现的具体类(UML类图中的ConcreteImplementor)

    #ifndef IMPLEMENTOR1_H_
    #define IMPLEMENTOR1_H_
    
    #include "Implementor.h"
    #include <iostream>
    class Implementor1:public Implementor
    {
    public:
        void display() override;
        Implementor1() = default;
        ~Implementor1() = default;
    };
    #endif
    
    #include "Implementor1.h"
    
    void Implementor1::display()
    {
        std::cout << "Book1 " << std::endl;
    }
    Concrete Implementor

    3.抽象的基类(UML类图中的Abstraction)

    #ifndef ABSTRACT_H_
    #define ABSTRACT_H_
    
    #include "Implementor.h"
    class Abstract
    {
    public:
        virtual void sell() = 0;
        Abstract() = default;
        virtual ~Abstract() = default;
    protected:
        Implementor* m_pobjImplementor{nullptr};
    };
    
    #endif
    Abstract

    4.抽象的具体类(UML类图中的RefinedAbstraction)

    #ifndef REFINEDABSTRACTION1_H_
    #define REFINEDABSTRACTION1_H_
    
    #include "Abstract.h"
    #include <string>
    #include <iostream>
    
    class RefinedAbstraction1:public Abstract
    {
    public:
        void sell() override;
        RefinedAbstraction1(std::string strName,Implementor* objImplementor): m_strName(strName)
        {
        m_pobjImplementor = objImplementor;
        }
    private:
        std::string m_strName;
    };
    #endif
    
    #include "RefinedAbstraction1.h"
    
    void RefinedAbstraction1::sell()
    {
        if(nullptr != m_pobjImplementor)
        {
        std::cout << "My name is:" << m_strName<<".I am responsible for " << std::endl;
            m_pobjImplementor->display();
        }
    }

    5.客户端代码(main)

    #include "RefinedAbstraction1.h"
    #include "Implementor1.h"
    #include "Abstract.h"
    
    int main(int argc,char *aargv[])
    {
        Implementor1 objConcreteImplement;
        RefinedAbstraction1 concreteAbstraction("Yang",&objConcreteImplement);
        Abstract *ab = &concreteAbstraction;
        ab->sell();
        return (1);
    }
    Client
  • 相关阅读:
    句柄定义ODBC操作数据
    应用程序浏览器供初学者使用的 wxHTML
    类语言结构化程序设计 & 面向对象程序设计
    数字操作符九度OJ 1019 简单计算器
    类字符串java学习笔记06正则表达式
    启动命令mac安装mysql后,启动mysql ERROR 2002 (HY000)错误解决办法
    链接全局变量再说BSS段的清理
    能力知识程序员学习能力提升三要素
    修改中断内存越界的一种定位方法
    特征方向说说主成分分析(PCA)的源头
  • 原文地址:https://www.cnblogs.com/ToBeExpert/p/9681501.html
Copyright © 2020-2023  润新知