一、概述
桥接模式主要是将抽象和实现解耦,使得两者都可以独立变化
在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。不同颜色和字体的文字、不同品牌和功率的汽车
桥接将继承转为关联,降低类之间的耦合度,减少代码量
试想一下,以手机为例,手机有翻盖式的,有推拉式的还有直板式的,而手机的品牌又多种多样,如果我们将每一种手机样式和品牌组合,那么就需要创建非常多的类很容易发生类爆炸问题,比如小米翻盖手机、小米推拉式手机、小米直板式手机……华为翻盖手机、华为推拉式手机等等等等,如果我们为每一种都创建一个类,这样做就会造成我们的项目中需要维护的对象大量冗余,维护成本也随之提高,而且实际上这些种类往往是无比繁多的。
桥接模式的好处就在于,我们将其中一个部分结构分离出去,成为一个独立的扩展,然后通过组合的方式将独立出去的部分再添加到我们的本类中,而本类可以通过抽象类继承的方式来进行扩展,独立的部分也可以通过实现接口或继承的方式进行扩展。
二、角色与UML图
2.1 角色与职责
Abstraction(抽象化角色)
- 定义抽象类的接口
- 维护一个指向Implementor类型对象的指针
RefinedAbstraction(扩展抽象化角色)
- 扩充一由Abstraction定义的接口
Implementor(抽象实现化角色)
- 定义实现类的接口,该接口不一定要和Abstraction的接口完全一致,事实上这两个接口可以完全不同。一般来讲,Implementor接口仅提供基本操作,而Abstraction则定义了基于这些基本操作的较高层次的操作
ConcreteImplementor(具体实现化角色)
- 实现Implementor接口并定义它的具体实现
2.2 UML图
三、示例
以手机为例,我们来构建不同品牌的不同样式的手机,可以将品牌与手机的样式拆分为两个独立的模块,使其可扩展性更强
具体类图如下所示:
首先我们创建抽象类BasePhone
/**
* @author ZhongJing </p>
* @Description 手机抽象类 </p>
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
public abstract class BasePhone {
/**
* 组合品牌
*/
private Brand brand;
protected void open() {
this.brand.open();
}
protected void close() {
this.brand.close();
}
protected void call() {
this.brand.call();
}
}
然后我们编写一个折叠手机类和直板式手机类,让这两个类继承我们的BasePhone
抽象类
/**
* @author ZhongJing </p>
* @Description 折叠手机实现类 </p>
*/
public class FoldedBasePhone extends BasePhone {
public FoldedBasePhone(Brand brand) {
super(brand);
}
public void open() {
System.out.print("折叠手机");
super.open();
}
public void close() {
System.out.print("折叠手机");
super.close();
}
public void call() {
System.out.print("折叠手机");
super.call();
}
}
/**
* @author ZhongJing </p>
* @Description 直板手机实现类 </p>
*/
public class UpRightBasePhone extends BasePhone {
public UpRightBasePhone(Brand brand) {
super(brand);
}
public void open() {
System.out.print("直板手机");
super.open();
}
public void call() {
System.out.print("直板手机");
super.call();
}
public void close() {
System.out.print("直板手机");
super.close();
}
}
创建品牌的抽象接口,也可以是抽象类,按照个人喜好
/**
* @author ZhongJing </p>
* @Description 品牌抽象接口 </p>
*/
public interface Brand {
void open();
void close();
void call();
}
创建华为和小米两个品牌
/**
* @author ZhongJing </p>
* @Description 华为手机品牌 </p>
*/
public class HuaWei implements Brand {
@Override
public void open() {
System.out.println("华为手机开机");
}
@Override
public void close() {
System.out.println("华为手机关机");
}
@Override
public void call() {
System.out.println("华为手机打电话");
}
}
/**
* @author ZhongJing </p>
* @Description 小米手机品牌 </p>
*/
public class Mi implements Brand {
@Override
public void open() {
System.out.println("小米手机开机了");
}
@Override
public void close() {
System.out.println("小米手机关机");
}
@Override
public void call() {
System.out.println("小米手机打电话");
}
}
按照我们的类图创建好之后我们就可以开始测试了
/**
* @author ZhongJing </p>
* @Description 测试类 </p>
*/
public class Client {
public static void main(String[] args) {
// 获取折叠手机
FoldedBasePhone phone1 = new FoldedBasePhone(new Mi());
phone1.open();
phone1.call();
phone1.close();
System.out.println("-------------");
FoldedBasePhone phone2 = new FoldedBasePhone(new HuaWei());
phone2.open();
phone2.call();
phone2.close();
System.out.println("-------------");
// 获取直板手机
UpRightBasePhone phone3 = new UpRightBasePhone(new Mi());
phone3.open();
phone3.call();
phone3.close();
System.out.println("-------------");
UpRightBasePhone phone4 = new UpRightBasePhone(new HuaWei());
phone4.open();
phone4.call();
phone4.close();
}
}
运行结果如下:
折叠手机小米手机开机了
折叠手机小米手机打电话
折叠手机小米手机关机
-------------
折叠手机华为手机开机
折叠手机华为手机打电话
折叠手机华为手机关机
-------------
直板手机小米手机开机了
直板手机小米手机打电话
直板手机小米手机关机
-------------
直板手机华为手机开机
直板手机华为手机打电话
直板手机华为手机关机
四、使用场景
桥接模式的使用场景如下
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时
在Java中的InputStreamReader
就使用到了桥接模式,不过不是纯粹的桥接模式,而是桥接模式+适配器模式
上图可以看到,在大体上来讲,这是一个桥接模式,通过分离其中一部分实现再组合对象的方式来进行操作
可以看到所有的操作基本上实际上都是由组合进来的sd来操作的
而我们的sd是由一个叫StreamDecoder.forInputStreamReader()
方法获得的,这个StreamDecoder
就是一个适配器,点进去源码我们可以看到是他对Reader和InputStream进行了适配
而这种方式就是我们在适配器模式一问文提到的对象适配器