C++

设计模式(七)桥(Bridge)模式

每天一个设计模式

Posted by VK on February 10, 2020

设计模式(七)桥(Bridge)模式

Bridge

在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化。那么为了应对这种“多维度的变化”(即两个以上变化的原因)的系统,可采用Bridge模式来进行设计,使系统中类的个数更少,且系统扩展更为方便。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。

思想 :将一个类得抽象定义和具体实现解耦

场景

1.当一个对象有多个变化因素的时候,考虑依赖于抽象的实现,而不是具体的实现。如上面例子中手机品牌有2种变化因素,一个是品牌,一个是功能。

2.当多个变化因素在多个对象间共享时,考虑将这部分变化的部分抽象出来再聚合/合成进来,如上面例子中的通讯录和游戏,其实是可以共享的。

3.当我们考虑一个对象的多个变化因素可以动态变化的时候,考虑使用桥接模式,如上面例子中的手机品牌是变化的,手机的功能也是变化的,所以将他们分离出来,独立的变化。

优点 1.将实现抽离出来,再实现抽象,使得对象的具体实现依赖于抽象,满足了依赖倒转原则。

2.将可以共享的变化部分,抽离出来,减少了代码的重复信息。

3.对象的具体实现可以更加灵活,可以满足多个因素变化的要求。

缺点 1.客户必须知道选择哪一种类型的实现。

设计中有超过一维的变化我们就可以用桥模式。如果只有一维在变化,那么我们用继承就可以圆满的解决问题。一个类的抽象定义已经确定,但是,其实现代码甚至原理可能会不同。比如:我们最熟悉的图形界面中的window的实现,无论在什么操作系统,什么平台的机器上,一个window应具有的抽象定义基本上是一致的,但是,其实现代码肯定会因为平台不同,机器的代码指令不同而不同。此时,如果希望您写的window类能跨平台,应用Bridge模式就是一个好主意。

实现:该模式的实现方法很简单,就是除了定义类的抽象定义之外,将一个类的所有实现代码独立出一个实现类。这样一来,无论是抽象定义还是实现类都能分别修改和重用,但只要两部分的交互接口不变,还是可以方便的互相组装。当然,实际上也没有必要隔离出“所有实现代码”,只需要隔离需要的部分就行了。因此,也可以说,从代码结构来看,Builder模式是一种变种的Bridge模式的。

也经常有人将Bridge模式和接口相比较,如果隔离出所有的实现,那么的确接口的方式也能做到抽象定义和实现分离。

  • 优点:
    • 将可能变化的部分单独封装起来,使得变化产生的影响最小,不用编译不必要的第代码。
    • 抽象部分和实现部分可以单独的变动,并且每一部分的扩充都不会破坏桥梁模式搭起来架子。
    • 实现细节对客户透明

      • 缺点:
        • 结构比较复杂。
        • 抽象类的修改影响到子类。

实例:将抽象部份与它的实现部份分离,使它们都可以独立地变化。

将抽象(Abstraction)与实现(Implementation)分离,使得二者可以独立地变化。

桥接模式号称设计模式中最难理解的模式之一,关键就是这个抽象和实现的分离非常让人奇怪,大部分人刚看到这个定义的时候都会认为实现就是继承自抽象,那怎么可能将他们分离呢。

《大话设计模式》中就Bridge模式的解释:手机品牌和软件是两个概念,不同的软件可以在不同的手机上,不同的手机可以有相同的软件,两者都具有很大的变动性。如果我们单独以手机品牌或手机软件为基类来进行继承扩展的话,无疑会使类的数目剧增并且耦合性很高,(如果更改品牌或增加软件都会增加很多的变动)两种方式的结构如下:

Bridge

所以将两者抽象出来两个基类分别是PhoneBrand和PhoneSoft,那么在品牌类中聚合一个软件对象的基类将解决软件和手机扩展混乱的问题,这样两者的扩展就相对灵活,剪短了两者的必要联系,结构图如下:

Bridge2

UML结构图如下:

BridgeUML

抽象基类及接口

1、Abstraction::Operation():定义要实现的操作接口

2、AbstractionImplement::Operation():实现抽象类Abstaction所定义操作的接口,由其具体派生类ConcreteImplemenA、ConcreteImplemenA或者其他派生类实现。

3、在Abstraction::Operation()中根据不同的指针多态调用AbstractionImplement::Operation()函数。

理解: Bridge用于将表示和实现解耦,两者可以独立的变化.在Abstraction类中维护一个AbstractionImplement类指针,需要采用不同的实现方式的时候只需要传入不同的AbstractionImplement派生类就可以了.

Bridge的实现方式其实和Builde十分的相近,可以这么说:本质上是一样的,只是封装的东西不一样罢了.两者的实现都有如下的共同点:

抽象出来一个基类,这个基类里面定义了共有的一些行为,形成接口函数(对接口编程而不是对实现编程),这个接口函数在Buildier中是BuildePart函数在Bridge中是Operation函数;

其次,聚合一个基类的指针,如Builder模式中Director类聚合了一个Builder基类的指针,而Brige模式中Abstraction类聚合了一个AbstractionImplement基类的指针(优先采用聚合而不是继承);

而在使用的时候,都把对这个类的使用封装在一个函数中,在Bridge中是封装在Director::Construct函数中,因为装配不同部分的过程是一致的,而在Bridge模式中则是封装在Abstraction::Operation函数中,在这个函数中调用对应的AbstractionImplement::Operation函数.就两个模式而言,Builder封装了不同的生成组成部分的方式,而Bridge封装了不同的实现方式.

桥接模式就将实现与抽象分离开来,使得RefinedAbstraction依赖于抽象的实现,这样实现了依赖倒转原则,而不管左边的抽象如何变化,只要实现方法不变,右边的具体实现就不需要修改,而右边的具体实现方法发生变化,只要接口不变,左边的抽象也不需要修改。

代码如下:

#include<iostream>
using namespace std;

class Abstraction{
public:
	Abstraction(){}
	virtual ~Abstraction(){}; 
	virtual void Operation() = 0;
};


class RefinedAbstractionA:public Abstraction{
public:
	RefinedAbstractionA(AbstractionImpl* imp){ _imp = imp;};
	virtual ~RefinedAbstractionA(){
		delete this->_imp;
		_imp = NULL;
	};
	virtual void Operation() override{
		cout << "RefinedAbstractionB::Operation" << endl;
		this->_imp->Operation();
	};

private:
	AbstractionImpl* _imp;
};



class RefinedAbstractionB:public Abstraction{
public:
	RefinedAbstractionB(AbstractionImpl* imp){ _imp = imp;};
	virtual ~RefinedAbstractionB(){
		delete this->_imp;
		_imp = NULL;
	};
	virtual void Operation() override{ 
		cout << "RefinedAbstractionB::Operation" << endl;
		this->_imp->Operation();
	};

private:
	AbstractionImpl* _imp;
};


class AbstractionImpl{
public:
	AbstractionImpl(){};
	virtual ~AbstractionImpl(){};
	virtual void Operation() = 0;
};


class ConcreteAbstractionImplA:public AbstractionImpl{
public:
	ConcreteAbstractionImplA(){};
	~ConcreteAbstractionImplA(){};
	virtual void Operation(){
 		cout << "ConcreteAbstractionImplementA Operation" << endl;
	};	
};


class ConcreteAbstractionImplB:public AbstractionImpl{
public:
	ConcreteAbstractionImplB(){};
	~ConcreteAbstractionImplB(){};
	virtual void Operation(){
 		cout << "ConcreteAbstractionImplementB Operation" << endl;
	};	
};



int main()
{
	Abstraction* pA = new RefinedAbstractionA(new ConcreteAbstractionImplA());
	pA->Operation();

	Abstraction* pB = new RefinedAbstractionB(new ConcreteAbstractionImplB());
	pB->Operation();

    /* 将抽象部分与它的实现部分分离,使得它们可以独立地变化
     1、抽象Abstraction与实现AbstractionImplement分离;
     2、抽象部分Abstraction可以变化,如new RefinedAbstractionA(imp)、new RefinedAbstractionB(imp2); 
     3、实现部分AbstractionImplement也可以变化,如new ConcreteAbstractionImplementA()、new ConcreteAbstractionImplementB();
    */
 
	AbstractionImpl* imp = new ConcreteAbstractionImplA();        //实现部分ConcreteAbstractionImplementA
	Abstraction* abs = new RefinedAbstractionA(imp);                        //抽象部分RefinedAbstractionA
	abs->Operation();
	cout << "-----------------------------------------" << endl;
	
	return 0;
}

代码说明: Bridge模式将抽象和实现分别独立实现,在代码中就是Abstraction类和AbstractionImplement类。

使用组合(委托)的方式将抽象和实现彻底地解耦,这样的好处是抽象和实现可以分别独立地变化,系统的耦合性也得到了很好的降低。 GoF的那句话中的“实现”该怎么去理解:“实现”特别是和“抽象”放在一起的时候我们“默认”的理解是“实现”就是“抽象”的具体子类的实现,但是这里GoF所谓的“实现”的含义不是指抽象基类的具体子类对抽象基类中虚函数(接口)的实现,是和继承结合在一起的。而这里的“实现”的含义指的是怎么去实现用户的需求,并且指的是通过组合(委托)的方式实现的,因此这里的实现不是指的继承基类、实现基类接口,而是指的是通过对象组合实现用户的需求。

实际上上面使用Bridge模式和使用带来问题方式的解决方案的根本区别在于是通过继承还是通过组合的方式去实现一个功能需求。

备注:由于实现的方式有多种,桥接模式的核心就是把这些实现独立出来,让他们各自变化。

将抽象部分与它的实现部分分离:实现系统可能有多角度(维度)分类,每一种分类都可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合。

在发现需要多角度去分类实现对象,而只用继承会造成大量的类增加,不能满足开放-封闭原则时,就要考虑用Bridge桥接模式了。

合成/聚合复用原则:尽量使用合成/聚合,尽量不要使用类继承。 优先使用对象的合成/聚合将有助于保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。

重构成本:中。将所有的(或很大部分)实现代码分离开来总还是一件不大,但是,也不小的事。所以标个“中”在这里。