桥接模式
桥接模式(Bridge Pattern)是一种用于将抽象部分和实现部分分离的设计模式。它通过将实现部分抽象化,使得抽象部分和实现部分可以独立地变化,从而实现系统的松耦合和可扩展性。
在桥接模式中,抽象部分和实现部分分别由两个抽象类或接口来定义,并通过一个桥接接口来将它们连接起来。这样,在系统需要增加新的抽象部分或实现部分时,只需要扩展抽象类或实现类,而不需要修改原有的代码,从而实现系统的可扩展性和灵活性。
上面两段话是不是非常令人困惑?没关系,GoF原本的描述就是这样的,这也让我很迷惑。不用担心,接下来我会努力用人话以及程序实例说明桥接模式的意义所在。我个人的理解是,如UML类图所示,Abstraction类和Implmentor类是组合关系(Abstraction has a Implmentor),表现为Abstraction类可能拥有一个Implmentor类的成员变量。Implmentor类作为Abstraction类的一种属性存在,而桥接模式则通过一种扩展性更好的方式,形成了一种类之间组合关系的结构。
举个例子,假设有一个图形库,其中包含多种不同的图形(如圆形、矩形等)和多种不同的颜色(如红色、蓝色等)。如果使用桥接模式,可以将图形和颜色分别抽象化,分别由两个抽象类定义,并通过一个桥接接口将它们连接起来。这样,在需要增加新的图形或颜色时,只需要扩展对应的抽象类或实现类,而不需要修改原有的代码,从而实现系统的可扩展性和灵活性。
#include <iostream>
// 颜色接口,可以对应UML中的Implmentor类
class Color {
public:
virtual void draw() = 0;
};
// 红色实现类,可以对应UML中的ConcreteImplmentorA类
class RedColor : public Color {
public:
void draw() {
std::cout << "draw in red color." << std::endl;
}
};
// 蓝色实现类,可以对应UML中的ConcreteImplmentorB类
class BlueColor : public Color {
public:
void draw() {
std::cout << "draw in blue color." << std::endl;
}
};
// 图形接口,可以对应UML中的Abstraction类
class Shape {
protected:
Color* color_;
public:
Shape(Color* color) : color_(color) {}
virtual void draw() = 0;
};
// 圆形类,可以对应UML中的RefineAbstraction类
class Circle : public Shape {
public:
Circle(Color* color) : Shape(color) {}
void draw() {
std::cout << "draw a circle, ";
color_->draw();
}
};
// 矩形类,可以对应UML中的RefineAbstraction类
class Rectangle : public Shape {
public:
Rectangle(Color* color) : Shape(color) {}
void draw() {
std::cout << "draw a rectangle, ";
color_->draw();
}
};
int main() {
// 创建红色实现类和蓝色实现类
Color* red = new RedColor();
Color* blue = new BlueColor();
// 创建圆形和矩形,并设置颜色
Shape* circle = new Circle(red);
Shape* rectangle = new Rectangle(blue);
// 绘制图形
circle->draw();
rectangle->draw();
// 释放内存
delete red;
delete blue;
delete circle;
delete rectangle;
return 0;
}
在上面的示例中,我们定义了一个颜色接口和两个颜色实现类(红色实现类和蓝色实现类),以及一个图形接口和两个图形类(圆形类和矩形类)。在图形类中,我们使用了一个颜色对象来表示图形的颜色,从而将抽象部分和实现部分分离。在main函数中,我们创建了红色和蓝色实现类,以及圆形和矩形对象,并设置它们的颜色。最后,我们调用它们的绘制方法来绘制图形。在程序结束时,我们释放了内存(最好养成习惯)。
绘图系统中的桥接模式
在Qt的绘图系统中,QPaintDevice
、QWidget
、QPaintEngine
和QRasterPaintEngine
这些类的角色和关系,可以用桥接模式来描述。在这个模型中:
QPaintDevice
可以看作是"抽象部分"(Abstraction)。它定义了一些公共接口,比如width()
,height()
,logicalDpiX()
等,这些接口用于描述一个绘图设备的基本属性。值得注意的是,QPaintDevice
有一个paintEngine
方法,返回一个QPaintEngine
指针。QWidget
是QPaintDevice
的一个具体子类,它代表了一个可视的窗口。这里,QWidget
就是"具体的抽象部分"(RefineAbstraction)。QWidget
重写了QPaintDevice
的paintEngine
方法,并返回QRasterPaintEngine
指针QPaintEngine
可以看作是"实现部分"(Implmentor)。它定义了一些低级别的绘图接口,比如drawPath()
,drawImage()
等,这些接口用于在具体的绘图设备上进行绘制。QRasterPaintEngine
是QPaintEngine
的一个具体子类,它实现了在基于光栅的设备上进行绘制。这里,QRasterPaintEngine
就是"具体的实现部分"(ConcreteImplmentor)。
这样一来,QPaintDevice
和QPaintEngine
就形成了一个桥接,使得绘图设备的抽象(比如QWidget
)和具体的绘制操作(比如在光栅设备上绘制)能够独立地变化。
在这个桥接模式中,QPainter
就扮演了一个"客户端"(Client)的角色(没有在UML类图中画出来,但会在后面的代码示例中体现),它使用QPaintDevice
提供的抽象接口,并通过QPaintEngine
来进行具体的绘制操作。
可以使用C++程序来描述这几个Qt类的关系。请注意,这只是笔者根据自己对Qt源代码的理解,进行的简单抽象,实际代码与程序实例有一定差异,省略了很多的细节。但用于描述Qt绘图系统如何使用桥接模式,应该是勉强足够的。
class QPaintDevice {
public:
virtual QPaintEngine* paintEngine() const = 0; // 纯虚函数
// 其他方法...
};
class QWidget : public QPaintDevice {
public:
QPaintEngine* paintEngine() const override {
// 返回一个QRasterPaintEngine实例
static QRasterPaintEngine instance;
return &instance;
}
// 其他方法...
};
class QPaintEngine {
public:
virtual void drawPath(const QPainterPath &path) = 0; // 纯虚函数
// 其他方法...
};
class QRasterPaintEngine : public QPaintEngine {
public:
void drawPath(const QPainterPath &path) override {
// 在光栅设备上绘制路径
// 具体实现...
}
// 其他方法...
};
class QPainter {
public:
QPainter(QPaintDevice *device) : device(device), engine(device->paintEngine()) {}
void drawPath(const QPainterPath &path) {
engine->drawPath(path);
}
private:
QPaintDevice *device;
QPaintEngine *engine;
};
上面的代码示例中,QPainter
是客户端代码,它接受一个QPaintDevice
对象,并通过QPaintDevice
的paintEngine
方法获取对应的QPaintEngine
对象。然后在需要绘图时,QPainter
会通过QPaintEngine
的drawPath
方法进行绘制。这样,QPainter
就能够在不同的设备上进行绘制,而不需要关心具体的绘制过程是如何实现的。这桥接模式的设计思想:将抽象(QPaintDevice)和实现(QPaintEngine)分离,使得二者可以独立地变化。
总结
可以看到,桥接模式很好地体现了依赖倒转原则(Dependency Inversion Principle,DIP)。依赖倒转原则是面向对象设计中的一个重要原则,它要求高层模块不应该依赖低层模块,而是应该依赖于抽象。而桥接模式正是通过将实现部分抽象化,使得抽象部分和实现部分可以独立地变化,从而实现了高层模块对低层模块的解耦,符合了依赖倒转原则。
具体来说,在桥接模式中,抽象部分和实现部分分别由两个抽象类或接口来定义,并通过一个桥接接口来将它们连接起来。高层模块只需要依赖于抽象类或接口,而不需要依赖于具体的实现类,从而实现了依赖倒转。
最后,需要强调,尽管文章大致阐述了Qt绘图系统如何使用桥接模式,但仍强烈建议读者通过对Qt源代码进行调试。实际调试才能完全理解Qt源代码的设计思想,以及Qt设计者如何活用设计模式,学到精髓。