罗庄建设局网站,福建:网站建设,深圳优化服务,wordpress更换域名打不开【学习难度#xff1a;★★★☆☆#xff0c;使用频率#xff1a;★★★☆☆】
2.1. 模式动机 在正式介绍桥接模式之前#xff0c;我先跟大家谈谈两种常见文具的区别#xff0c;它们是毛笔和蜡笔。假如我们需要大中小3种型号的画笔#xff0c;能够绘制12种不同的颜色★★★☆☆使用频率★★★☆☆】
2.1. 模式动机 在正式介绍桥接模式之前我先跟大家谈谈两种常见文具的区别它们是毛笔和蜡笔。假如我们需要大中小3种型号的画笔能够绘制12种不同的颜色如果使用蜡笔需要准备3×12 36支但如果使用毛笔的话只需要提供3种型号的毛笔外加12个颜料盒即可涉及到的对象个数仅为 3 12 15远小于36却能实现与36支蜡笔同样的功能。如果增加一种新型号的画笔并且也需要具有12种颜色对应的蜡笔需增加12支而毛笔只需增加一支。为什么会这样呢通过分析我们可以得知在蜡笔中颜色和型号两个不同的变化维度即两个不同的变化原因融合在一起无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度但在毛笔中颜色和型号实现了分离增加新的颜色或者型号对另一方都没有任何影响。如果使用软件工程中的术语我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性而毛笔很好地将二者解耦使用起来非常灵活扩展也更为方便。在软件开发中我们也提供了一种设计模式来处理与画笔类似的具有多变化维度的情况即本章将要介绍的桥接模式。 设想如果要绘制矩形、圆形、椭圆、正方形我们至少需要4个形状类但是如果绘制的图形需要具有不同的颜色如红色、绿色、蓝色等此时至少有如下两种设计方案
第一种设计方案是为每一种形状都提供一套各种颜色的版本。第二种设计方案是根据实际需要对形状和颜色进行组合 对于有两个变化维度即两个变化的原因的系统采用方案二来进行设计系统中类的个数更少且系统扩展更为方便。设计方案二即是桥接模式的应用。桥接模式将继承关系转换为关联关系从而降低了类与类之间的耦合减少了代码编写量。
2.2. 模式定义 桥接模式(Bridge Pattern)将抽象部分与它的实现部分分离使它们都可以独立地变化。它是一种对象结构型模式又称为柄体(Handle and Body)模式或接口(Interface)模式。
2.3. 模式结构 桥接模式包含如下角色
Abstraction抽象类用于定义抽象类的接口它一般是抽象类而不是接口其中定义了一个Implementor实现类接口类型的对象并可以维护该对象它与Implementor之间具有关联关系它既可以包含抽象业务方法也可以包含具体业务方法。RefinedAbstraction扩充抽象类扩充由Abstraction定义的接口通常情况下它不再是抽象类而是具体类它实现了在Abstraction中声明的抽象业务方法在RefinedAbstraction中可以调用在Implementor中定义的业务方法。Implementor实现类接口定义实现类的接口这个接口不一定要与Abstraction的接口完全一致事实上这两个接口可以完全不同一般而言Implementor接口仅提供基本操作而Abstraction定义的接口可能会做更多更复杂的操作。Implementor接口对这些基本操作进行了声明而具体实现交给其子类。通过关联关系在Abstraction中不仅拥有自己的方法还可以调用到Implementor中定义的方法使用关联关系来替代继承关系。ConcreteImplementor具体实现类具体实现Implementor接口在不同的ConcreteImplementor中提供基本操作的不同实现在程序运行时ConcreteImplementor对象将替换其父类对象提供给抽象类具体的业务操作方法。
2.4. 时序图 2.5. 代码分析 在使用桥接模式时我们首先应该识别出一个类所具有的两个独立变化的维度将它们设计为两个独立的继承等级结构为两个维度都提供抽象层并建立抽象耦合。通常情况下我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构抽象部分而将另一个维度设计为“实现类”层次结构实现部分。例如对于毛笔而言由于型号是其固有的维度因此可以设计一个抽象的毛笔类在该类中声明并部分实现毛笔的业务方法而将各种型号的毛笔作为其子类颜色是毛笔的另一个维度由于它与毛笔之间存在一种“设置”的关系因此我们可以提供一个抽象的颜色接口而将具体的颜色作为实现该接口的子类。在此型号可认为是毛笔的抽象部分而颜色是毛笔的实现部分结构示意图如图10-4所示 在图10-4中如果需要增加一种新型号的毛笔只需扩展左侧的“抽象部分”增加一个新的扩充抽象类如果需要增加一种新的颜色只需扩展右侧的“实现部分”增加一个新的具体实现类。扩展非常方便无须修改已有代码且不会导致类的数目增长过快。 在具体编码实现时由于在桥接模式中存在两个独立变化的维度为了使两者之间耦合度降低首先需要针对两个不同的维度提取抽象类和实现类接口并建立一个抽象关联关系。对于“实现部分”维度典型的实现类接口代码如下所示
interface Implementor {public void operationImpl();
}在实现Implementor接口的子类中实现了在该接口中声明的方法用于定义与该维度相对应的一些具体方法。 对于另一“抽象部分”维度而言其典型的抽象类代码如下所示
abstract class Abstraction {protected Implementor impl; //定义实现类接口对象public void setImpl(Implementor impl) {this.implimpl;}public abstract void operation(); //声明抽象业务方法
}在抽象类Abstraction中定义了一个实现类接口类型的成员对象impl再通过注入的方式给该对象赋值一般将该对象的可见性定义为protected以便在其子类中访问Implementor的方法其子类一般称为扩充抽象类或细化抽象类(RefinedAbstraction)典型的RefinedAbstraction类代码如下所示
class RefinedAbstraction extends Abstraction {public void operation() {//业务代码impl.operationImpl(); //调用实现类的方法//业务代码}
}对于客户端而言可以针对两个维度的抽象层编程在程序运行时再动态确定两个维度的子类动态组合对象将两个独立变化的维度完全解耦以便能够灵活地扩充任一维度而对另一维度不造成任何影响。
2.6. 模式分析 理解桥接模式重点需要理解如何将抽象化(Abstraction)与实现化(Implementation)脱耦使得二者可以独立地变化。
抽象化抽象化就是忽略一些信息把不同的实体当作同样的实体对待。在面向对象中将对象的共同性质抽取出来形成类的过程即为抽象化的过程。实现化针对抽象化给出的具体实现就是实现化抽象化与实现化是一对互逆的概念实现化产生的对象比抽象化更具体是对抽象化事物的进一步具体化的产物。脱耦脱耦就是将抽象化和实现化之间的耦合解脱开或者说是将它们之间的强关联改换成弱关联将两个角色之间的继承关系改为关联关系。桥接模式中的所谓脱耦就是指在一个软件系统的抽象化和实现化之间使用关联关系组合或者聚合关系而不是继承关系从而使两者可以相对独立地变化这就是桥接模式的用意。
2.7. 实例 如果需要开发一个跨平台视频播放器可以在不同操作系统平台如Windows、Linux、Unix等上播放多种格式的视频文件常见的视频格式包括MPEG、RMVB、AVI、WMV等。现使用桥接模式设计该播放器。
2.7.1 老方法类图 Sunny软件公司欲开发一个跨平台图像浏览系统要求该系统能够显示BMP、JPG、GIF、PNG等多种格式的文件并且能够在Windows、Linux、Unix等多个操作系统上运行。系统首先将各种格式的文件解析为像素矩阵(Matrix)然后将像素矩阵显示在屏幕上在不同的操作系统中可以调用不同的绘制函数来绘制像素矩阵。系统需具有较好的扩展性以支持新的文件格式和操作系统。 Sunny软件公司的开发人员针对上述要求提出了一个初始设计方案其基本结构如图10-1所示 在图10-1的初始设计方案中使用了一种多层继承结构Image是抽象父类而每一种类型的图像类如BMPImage、JPGImage等作为其直接子类不同的图像文件格式具有不同的解析方法可以得到不同的像素矩阵由于每一种图像又需要在不同的操作系统中显示不同的操作系统在屏幕上显示像素矩阵有所差异因此需要为不同的图像类再提供一组在不同操作系统显示的子类如为BMPImage提供三个子类BMPWindowsImp、BMPLinuxImp和BMPUnixImp分别用于在Windows、Linux和Unix三个不同的操作系统下显示图像。 我们现在对该设计方案进行分析发现存在如下两个主要问题
(1)、由于采用了多层继承结构导致系统中类的个数急剧增加图10-1中在各种图像的操作系统实现层提供了12个具体类加上各级抽象层的类系统中类的总个数达到了17个在该设计方案中具体层的类的个数 所支持的图像文件格式数×所支持的操作系统数。(2)、系统扩展麻烦由于每一个具体类既包含图像文件格式信息又包含操作系统信息因此无论是增加新的图像文件格式还是增加新的操作系统都需要增加大量的具体类例如在图10-1中增加一种新的图像文件格式TIF则需要增加3个具体类来实现该格式图像在3种不同操作系统的显示如果增加一个新的操作系统Mac OS为了在该操作系统下能够显示各种类型的图像需要增加4个具体类。这将导致系统变得非常庞大增加运行和维护开销。 如何解决这两个问题我们通过分析可得知该系统存在两个独立变化的维度图像文件格式和操作系统如图10-2所示 在图10-2中如何将各种不同类型的图像文件解析为像素矩阵与图像文件格式本身相关而如何在屏幕上显示像素矩阵则仅与操作系统相关。正因为图10-1所示结构将这两种职责集中在一个类中导致系统扩展麻烦从类的设计角度分析具体类BMPWindowsImp、BMPLinuxImp和BMPUnixImp等违反了“单一职责原则”因为不止一个引起它们变化的原因它们将图像文件解析和像素矩阵显示这两种完全不同的职责融合在一起任意一个职责发生改变都需要修改它们系统扩展困难。 如何改进我们的方案是将图像文件格式对应图像格式的解析与操作系统对应像素矩阵的显示两个维度分离使得它们可以独立变化增加新的图像文件格式或者操作系统时都对另一个维度不造成任何影响。
2.7.2 新方法完整代码 为了减少所需生成的子类数目实现将操作系统和图像文件格式两个维度分离使它们可以独立改变Sunny公司开发人员使用桥接模式来重构跨平台图像浏览系统的设计其基本结构如图10-5所示 在图10-5中Image充当抽象类其子类JPGImage、PNGImage、BMPImage和GIFImage充当扩充抽象类ImageImp充当实现类接口其子类WindowsImp、LinuxImp和UnixImp充当具体实现类。完整代码如下所示
package com.zyz.demo;/*** author zyz* version 1.0* data 2023/5/13 20:41* Description:*//*** 像素矩阵类辅助类各种格式的文件最终都被转化为像素矩阵不同的操作系统提供不同的方式显示像素矩阵*/
class Matrix {//此处代码省略
}/*** 抽象图像类抽象类*/
abstract class Image {protected ImageImp imp;public void setImageImp(ImageImp imp) {this.imp imp;}public abstract void parseFile(String fileName);}/*** 抽象操作系统实现类实现类接口*/
interface ImageImp {public void doPaint(Matrix m); //显示像素矩阵m
}/*** Windows操作系统实现类具体实现类*/
class WindowsImp implements ImageImp {Overridepublic void doPaint(Matrix m) {//调用Windows系统的绘制函数绘制像素矩阵System.out.print(在Windows操作系统中显示图像);}
}/*** Linux操作系统实现类具体实现类*/
class LinuxImp implements ImageImp {Overridepublic void doPaint(Matrix m) {//调用Linux系统的绘制函数绘制像素矩阵System.out.print(在Linux操作系统中显示图像);}
}/*** Unix操作系统实现类具体实现类*/
class UnixImp implements ImageImp {Overridepublic void doPaint(Matrix m) {//调用Unix系统的绘制函数绘制像素矩阵System.out.print(在Unix操作系统中显示图像);}
}/*** JPG格式图像扩充抽象类*/
class JPGImage extends Image {Overridepublic void parseFile(String fileName) {//模拟解析JPG文件并获得一个像素矩阵对象m;Matrix m new Matrix();imp.doPaint(m);System.out.println(fileName 格式为JPG。);}
}/*** PNG格式图像扩充抽象类*/
class PNGImage extends Image {Overridepublic void parseFile(String fileName) {//模拟解析PNG文件并获得一个像素矩阵对象m;Matrix m new Matrix();imp.doPaint(m);System.out.println(fileName 格式为PNG。);}
}/*** BMP格式图像扩充抽象类*/
class BMPImage extends Image {Overridepublic void parseFile(String fileName) {//模拟解析BMP文件并获得一个像素矩阵对象m;Matrix m new Matrix();imp.doPaint(m);System.out.println(fileName 格式为BMP。);}
}/*** GIF格式图像扩充抽象类*/
class GIFImage extends Image {Overridepublic void parseFile(String fileName) {//模拟解析GIF文件并获得一个像素矩阵对象m;Matrix m new Matrix();imp.doPaint(m);System.out.println(fileName 格式为GIF。);}
}编写如下客户端测试代码
package com.zyz.demo;/*** author zyz* version 1.0* data 2023/5/13 20:56* Description: 客户端*/
public class Client {public static void main(String[] args) {Image image;ImageImp imp;image new JPGImage();imp new WindowsImp();image.setImageImp(imp);image.parseFile(小龙女);}
}测试结果 如果需要更换图像文件格式或者更换操作系统只需修改配置文件即可在实际使用时可以通过分析图像文件格式后缀名来确定具体的文件格式在程序运行时获取操作系统信息来确定操作系统类型无须使用配置文件。当增加新的图像文件格式或者操作系统时原有系统无须做任何修改只需增加一个对应的扩充抽象类或具体实现类即可系统具有较好的可扩展性完全符合“开闭原则”。
2.8. 优点
桥接模式的优点:
分离抽象接口及其实现部分。桥接模式有时类似于多继承方案但是多继承方案违背了类的单一职责原则即一个类只有一个变化的原因复用性比较差而且多继承结构中类的个数非常庞大桥接模式是比多继承方案更好的解决方法。桥接模式提高了系统的可扩充性在两个变化维度中任意扩展一个维度都不需要修改原有系统。实现细节对客户透明可以对用户隐藏实现细节。
2.9. 缺点
桥接模式的缺点:
桥接模式的引入会增加系统的理解与设计难度由于聚合关联关系建立在抽象层要求开发者针对抽象进行设计与编程。桥接模式要求正确识别出系统中两个独立变化的维度因此其使用范围具有一定的局限性。
2.10. 适用环境
在以下情况下可以使用桥接模式
如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性避免在两个层次之间建立静态的继承联系通过桥接模式可以使它们在抽象层建立一个关联关系。抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合即系统需要对抽象化角色和实现化角色进行动态耦合。一个类存在两个独立变化的维度且这两个维度都需要进行扩展。虽然在系统中使用继承是没有问题的但是由于抽象化角色和具体化角色需要独立变化设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统桥接模式尤为适用。
2.11. 模式应用 一个Java桌面软件总是带有所在操作系统的视感(LookAndFeel)如果一个Java软件是在Unix系统上开发的那么开发人员看到的是Motif用户界面的视感在Windows上面使用这个系统的用户看到的是Windows用户界面的视感而一个在Macintosh上面使用的用户看到的则是Macintosh用户界面的视感Java语言是通过所谓的Peer架构做到这一点的。Java为AWT中的每一个GUI构件都提供了一个Peer构件在AWT中的Peer架构就使用了桥接模式。
2.12. 模式扩展
适配器模式与桥接模式的联用:
桥接模式和适配器模式用于设计的不同阶段桥接模式用于系统的初步设计对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色使它们可以分别进行变化而在初步设计完成之后当发现系统与已有类无法协同工作时可以采用适配器模式。但有时候在设计初期也需要考虑适配器模式特别是那些涉及到大量第三方应用接口的情况。
2.13. 总结
桥接模式将抽象部分与它的实现部分分离使它们都可以独立地变化。它是一种对象结构型模式又称为柄体(Handle and Body)模式或接口(Interface)模式。桥接模式包含如下四个角色抽象类中定义了一个实现类接口类型的对象并可以维护该对象扩充抽象类扩充由抽象类定义的接口它实现了在抽象类中定义的抽象业务方法在扩充抽象类中可以调用在实现类接口中定义的业务方法实现类接口定义了实现类的接口实现类接口仅提供基本操作而抽象类定义的接口可能会做更多更复杂的操作具体实现类实现了实现类接口并且具体实现它在不同的具体实现类中提供基本操作的不同实现在程序运行时具体实现类对象将替换其父类对象提供给客户端具体的业务操作方法。在桥接模式中抽象化(Abstraction)与实现化(Implementation)脱耦它们可以沿着各自的维度独立变化。桥接模式的主要优点是分离抽象接口及其实现部分是比多继承方案更好的解决方法桥接模式还提高了系统的可扩充性在两个变化维度中任意扩展一个维度都不需要修改原有系统实现细节对客户透明可以对用户隐藏实现细节其主要缺点是增加系统的理解与设计难度且识别出系统中两个独立变化的维度并不是一件容易的事情。桥接模式适用情况包括需要在构件的抽象化角色和具体化角色之间增加更多的灵活性避免在两个层次之间建立静态的继承联系抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响一个类存在两个独立变化的维度且这两个维度都需要进行扩展设计要求需要独立管理抽象化角色和具体化角色不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统。
2.14 扩展读取xml文件
为了让系统具有更好的灵活性和可扩展性我们引入了配置文件将具体扩充抽象类和具体实现类类名都存储在配置文件中再通过反射生成对象将生成的具体实现类对象注入到扩充抽象类对象中其中配置文件config.xml的代码如下所示
?xml version1.0?
config!--RefinedAbstraction--classNameMyPNGImage/className!--ConcreteImplementor--classNameMyUnixImp/className
/config用于读取配置文件config.xml并反射生成对象的XMLUtil类的代码如下所示
package com.zyz.demo.config;import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
/*** author zyz* version 1.0* data 2023/5/13 21:43* Description:*/
public class XMLUtil {/*** 该方法用于从XML配置文件中提取具体类类名并返回一个实例对象* param args* return*/public static Object getBean(String args) {try {String path F:\\java学习资料(后端)\\github管理后端学习资料\\后端学习\\设计模式\\代码\\DesignPatterns-Java-Examples\\7. 桥接模式\\src\\main\\resources\\config.xml;//创建文档对象DocumentBuilderFactory dFactory DocumentBuilderFactory.newInstance();DocumentBuilder builder dFactory.newDocumentBuilder();Document doc;doc builder.parse(new File(path));NodeList nlnull;Node classNodenull;String cNamenull;nl doc.getElementsByTagName(className);if(args.equals(image)) {//获取第一个包含类名的节点即扩充抽象类classNodenl.item(0).getFirstChild();}else if(args.equals(os)) {//获取第二个包含类名的节点即具体实现类classNodenl.item(1).getFirstChild();}cNameclassNode.getNodeValue();//通过类名生成实例对象并将其返回Class cClass.forName(com.zyz.demo. cName);Object objc.newInstance();return obj;}catch(Exception e) {e.printStackTrace();return null;}}
}编写如下客户端测试代码
package com.zyz.demo;import com.zyz.demo.config.XMLUtil;/*** author zyz* version 1.0* data 2023/5/13 21:47* Description: 客户端*/
public class Client1 {public static void main(String[] args) {Image image;ImageImp imp;image (Image)XMLUtil.getBean(image);imp (ImageImp) XMLUtil.getBean(os);image.setImageImp(imp);image.parseFile(杨过);}
}测试结果