一、桥接模式
1.1概述
桥接模式是一种结构型设计模式,它的作用是将抽象部分和实现部分分离开来,使它们能够独立地变化。这样,抽象部分和实现部分可以分别进行扩展,而不会相互影响。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
在桥接模式中,抽象部分和实现部分之间通过一个称为“桥”的接口进行连接。这个桥接口定义抽象部分所需要的所有方法,而实现部分则实现这些方法。这种设计方式可以使得实现部分的变化不会对抽象部分造成影响,因为抽象部分只依赖于桥接口,而不依赖于具体的实现部分。
桥接模式通常使用在需要跨越多个平台或多个产品版本的场景中。它可以提高代码的可复用性和可维护性,同时也可以使得系统更加灵活和可扩展。
现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。我们可以利用继承的方式来设计类的关系
我们可以发现有很多的类,假如我们再增加一个形状或再增加一种颜色,就需要创建更多的类。
试想,在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸,扩展起来不灵活。每次在一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式。
1.2结构
桥接(Bridge)模式包含以下主要角色:
- 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
1.3实现
【例】视频播放器
需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。
类图如下:
实现化(Implementor)角色
package com.yanyu.Bridge;
//视频文件
public interface VideoFile {
void decode(String fileName);
}
具体实现化(Concrete Implementor)角色
package com.yanyu.Bridge;
//avi文件
public class AVIFile implements VideoFile {
public void decode(String fileName) {
System.out.println("avi视频文件:"+ fileName);
}
}
package com.yanyu.Bridge;
//rmvb文件
public class REVBBFile implements VideoFile {
public void decode(String fileName) {
System.out.println("rmvb文件:" + fileName);
}
}
抽象化(Abstraction)角色
package com.yanyu.Bridge;
//操作系统版本
public abstract class OperatingSystemVersion {
protected VideoFile videoFile;
public OperatingSystemVersion(VideoFile videoFile) {
this.videoFile = videoFile;
}
public abstract void play(String fileName);
}
扩展抽象化(Refined Abstraction)角色
package com.yanyu.Bridge;
//Windows版本
public class Windows extends OperatingSystemVersion {
public Windows(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}
package com.yanyu.Bridge;
//mac版本
public class Mac extends OperatingSystemVersion {
public Mac(VideoFile videoFile) {
super(videoFile);
}
public void play(String fileName) {
videoFile.decode(fileName);
}
}
客户端类
package com.yanyu.Bridge;
//测试类
public class Client {
public static void main(String[] args) {
OperatingSystemVersion os = new Windows(new AVIFile());
os.play("战狼3");
}
}
好处:
桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可,其他类不需要发生变化。
实现细节对客户透明
1.4应用场景
适合应用场景:
- 如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类), 可以使用桥接模式。类的代码行数越多, 弄清其运作方式就越困难, 对其进行修改所花费的时间就越长。 一个功能上的变化可能需要在整个类范围内进行修改, 而且常常会产生错误, 甚至还会有一些严重的副作用。
- 桥接模式可以将庞杂类拆分为几个类层次结构。 此后, 你可以修改任意一个类层次结构而不会影响到其他类层次结构。 这种方法可以简化代码的维护工作, 并将修改已有代码的风险降到最低。
1.5JDK源码解析
桥接模式(Bridge Pattern)是一种结构型设计模式,它将抽象与实现分离,使它们可以独立变化。在桥接模式中,抽象和实现可以分别在两个不同的类层次结构中定义,而通过组合的方式将它们联系起来。这样做的好处是可以减少继承带来的耦合问题,提高系统的灵活性和可扩展性。
在 JDK 中,桥接模式的应用非常广泛,下面我们分别来看几个典型的例子:
1. JDBC
在 JDBC 中,数据访问 API 和数据库驱动实现是分离的。数据访问 API 是一组接口和类,定义了数据库操作的抽象方法和常量。而具体的数据库驱动实现则是一组不同的 jar 包,每个包对应一种数据库的不同驱动实现。这样设计的好处是可以实现代码的复用,将代码的改动范围限定在实现层次结构中,不影响客户端的使用。
2. AWT
在 AWT 中,抽象窗口工具包(Abstract Window Toolkit,简称 AWT)提供了一系列抽象类和接口,用于实现和显示图形界面。而具体的 GUI 组件实现则是由不同的操作系统提供的。比如,Windows 提供的 AWT 组件实现和 Linux 提供的 AWT 组件实现是不同的,但它们都可以通过抽象类和接口来实现统一的调用方式,从而保证了跨平台的兼容性。
3. java.util.logging
在 java.util.logging 中,抽象日志记录器(Logger)定义了一组抽象方法和常量,用于记录日志信息。而具体的日志记录实现则是通过不同的日志 Handler 实现的。比如,FileHandler、ConsoleHandler、StreamHandler 等,它们都继承自抽象类 AbstractHandler,实现了具体的日志记录方式。这样设计的好处是可以将日志记录器与具体记录方式分离,提高代码的可扩展性和灵活性。
总之,桥接模式是一种非常实用的设计模式,它可以将抽象和实现分离,使它们可以独立变化。在 JDK 中,有很多典型的桥接模式应用,这些应用不仅为我们提供了很好的学习案例,还可以帮助我们更好地理解桥接模式的思想和作用。
二、外观模式
2.1概述
外观模式(Facade Pattern)是一种结构型设计模式,它为一组复杂的子系统接口提供了一个统一的接口,以方便客户端使用。外观模式通过将复杂的系统封装在一个简单的外观对象中,简化了客户端的调用过程,同时隐藏了系统的复杂性。
外观模式通常会定义一个简单的高层接口,这个接口封装了系统的所有复杂流程和方法调用,并将这些流程和方法调用转化为若干个简单的方法,供客户端直接调用。这样客户端就不需要了解系统的内部实现细节,也不需要知道哪些子系统需要协同工作才能完成一个请求。
总的来说,外观模式提供了一种简单的方式来使用复杂系统,并使得系统更加易于使用和维护。外观(Facade)模式是“迪米特法则”的典型应用
2.2 结构
外观(Facade)模式包含以下主要角色:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
2.3实现
【例】智能家电控制
小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:
外观(Facade)角色
package com.yanyu.Facade;
//智能音箱
public class SmartAppliancesFacade {
//定义三个私有属性,分别代表三种电器设备
private Light light;
private TV tv;
private AirCondition airCondition;
//构造方法,初始化三种电器设备的对象
public SmartAppliancesFacade() {
light = new Light();
tv = new TV();
airCondition = new AirCondition();
}
//公共方法,根据语音指令控制电器的开关
public void say(String message) {
//如果语音指令包含“打开”,则调用on()方法
if(message.contains("打开")) {
on();
//如果语音指令包含“关闭”,则调用off()方法
} else if(message.contains("关闭")) {
off();
//否则,打印提示信息
} else {
System.out.println("我还听不懂你说的!!!");
}
}
//私有方法,起床后一键开电器
private void on() {
//打印提示信息
System.out.println("起床了");
//调用三种电器设备的on()方法,分别开启灯、电视和空调
light.on();
tv.on();
airCondition.on();
}
//私有方法,睡觉一键关电器
private void off() {
//打印提示信息
System.out.println("睡觉了");
//调用三种电器设备的off()方法,分别关闭灯、电视和空调
light.off();
tv.off();
airCondition.off();
}
}
定义了一个智能音箱类,作为三种电器设备的门面,提供了一个统一的接口,使得客户端可以通过语音指令来控制电器的开关,而不需要了解电器的具体实现细节。外观模式可以简化客户端与子系统之间的交互,降低系统的复杂度和耦合度
子系统(Sub System)角色
package com.yanyu.Facade;
//灯类
public class Light {
public void on() {
System.out.println("打开了灯....");
}
public void off() {
System.out.println("关闭了灯....");
}
}
package com.yanyu.Facade;
//电视类
public class TV {
public void on() {
System.out.println("打开了电视....");
}
public void off() {
System.out.println("关闭了电视....");
}
}
package com.yanyu.Facade;
//空调类
public class AirCondition {
public void on() {
System.out.println("打开了空调....");
}
public void off() {
System.out.println("关闭了空调....");
}
}
客户端类
package com.yanyu.Facade;
//测试类
public class Client {
public static void main(String[] args) {
//创建外观对象
SmartAppliancesFacade facade = new SmartAppliancesFacade();
//客户端直接与外观对象进行交互
facade.say("打开家电");
facade.say("关闭家电");
}
}
- 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
2.4应用场景
- 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
三、 桥接模式实验
任务描述
某软件公司欲开发一个数据转换工具,可以将数据库中的数据转换成多种文件格式,例如 TXT、XML、PDF 等格式,同时该工具需要支持多种不同的数据库。
本关任务:用桥接模式对模拟程序进行框架搭建,如图。
实现方式
明确类中独立的维度。 独立的概念可能是: 抽象/平台, 域/基础设施, 前端/后端或接口/实现。
了解客户端的业务需求, 并在抽象基类中定义它们。
确定在所有平台上都可执行的业务。 并在通用实现接口中声明抽象部分所需的业务。
为你域内的所有平台创建实现类, 但需确保它们遵循实现部分的接口。
在抽象类中添加指向实现类型的引用成员变量。 抽象部分会将大部分工作委派给该成员变量所指向的实现对象。
如果你的高层逻辑有多个变体, 则可通过扩展抽象基类为每个变体创建一个精确抽象。
客户端代码必须将实现对象传递给抽象部分的构造函数才能使其能够相互关联。 此后, 客户端只需与抽象对象进行交互, 无需和实现对象打交道。
编程要求
根据提示,在右侧编辑器 Begin-End 内补充 "DataHandler.java"、"FileConvertor.java"、"PDFConvertor.java"、"XMLConvertor.java" 和 "TXTConvertor.java" 文件的代码。
测试说明
平台会自动从 xml 文件中读取内容,然后对你编写的代码进行测试:
预期输出:
从Oracle数据库中读取数据
转换成PDF格式的数据
预期输出:
从SQLServer数据库中读取数据
转换成TXT格式的数据
客户端类
package step1;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
public class XMLUtils {
//定义一个静态方法,根据传入的标签名,返回对应的类的实例对象
public static Object getBean(String name) {
try {
//创建DOM文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
//解析XML文件,获取Document对象
doc = builder.parse(new File("/data/workspace/myshixun/src/config.xml"));
//获取包含类名的文本结点
NodeList nl = doc.getElementsByTagName(name);
Node classNode = nl.item(0).getFirstChild();
//获取结点的值,即类名
String cName = classNode.getNodeValue();
//通过类名生成实例对象并将其返回
Class c = Class.forName(cName);
Object obj = c.getDeclaredConstructor().newInstance();
return obj;
}
catch(Exception e) {
//捕获并打印异常
e.printStackTrace();
return null;
}
}
}
这段代码是一个XML工具类,它使用了DOM方式来解析XML文件,获取其中的类名,并通过反射机制来创建类的实例对象。这样可以实现配置文件和代码的分离,提高代码的可维护性和可扩展性
package step1;
public class Client {
public static void main(String[] args) {
//创建一个抽象化角色的对象,通过XML配置文件获取其具体实现类的名称
FileConvertor fileConvertor = (FileConvertor) XMLUtils.getBean("className1");
//将一个实现化角色的对象注入到抽象化角色中,通过XML配置文件获取其具体实现类的名称
fileConvertor.setDataHandler((DataHandler) XMLUtils.getBean("className2"));
//调用抽象化角色的方法,实现对文件的转换
fileConvertor.translate();
}
}
实现化角色
package step1;
/********** Begin *********/
//定义一个抽象的数据处理类,作为实现化角色
public abstract class DataHandler{
//定义一个抽象的数据读取方法,由子类实现
public abstract void readData();
}
/********** End *********/
具体实现化角色
package step1;
//定义一个从Oracle数据库中读取数据的类,继承自DataHandler类,作为具体实现化角色
public class OracleHandler extends DataHandler{
//重写父类的抽象方法,实现具体的数据读取操作
@Override
public void readData() {
System.out.println("从Oracle数据库中读取数据");
}
}
package step1;
public class SQLServerHandle extends DataHandler{
@Override
public void readData() {
System.out.println("从SQLServer数据库中读取数据");
}
}
抽象化角色
package step1;
/********** Begin *********/
//定义一个抽象的文件转换类,作为抽象化角色
public abstract class FileConvertor{
//定义一个受保护的数据处理类的引用,作为实现化角色的接口
protected DataHandler handler;
//定义一个公共的方法,用于设置数据处理类的对象,实现桥接的过程
public void setDataHandler(DataHandler handler) {
this.handler = handler;
}
//定义一个抽象的方法,用于转换文件,由子类实现,调用数据处理类的方法
public abstract void translate();
}
/********** End *********/
扩展抽象类
package step1;
//定义一个PDF文件转换类,继承自FileConvertor类,作为扩展抽象化角色
public class PDFConvertor extends FileConvertor{
//重写父类的抽象方法,实现具体的文件转换操作
@Override
public void translate() {
/********** Begin *********/
//调用实现化角色的方法,读取数据
handler.readData();
/********** End *********/
//打印提示信息,表示转换成PDF格式的数据
System.out.println("转换成PDF格式的数据");
}
}
package step1;
public class TXTConvertor extends FileConvertor{
@Override
public void translate() {
/********** Begin *********/
handler.readData();
/********** End *********/
System.out.println("转换成TXT格式的数据");
}
}
package step1;
public class XMLConvertor extends FileConvertor{
@Override
public void translate() {
/********** Begin *********/
handler.readData();
/********** End *********/
System.out.println("转换成XML格式的数据");
}
}