【设计模式】结构型模式其二: 桥接模式

166 阅读8分钟

结构型模式其二:《桥接模式》

什么是桥接模式

桥接模式(Bridge Pattern)是一种常见的设计模式,它可以将抽象部分和实现部分分离开来,使它们可以独立地变化和演化。桥接模式的核心思想是将一个类的抽象部分与它的实现部分分离开来,使它们可以独立地变化,从而实现系统的松耦合。

桥接模式中包含以下几个角色:

  • 抽象化角色(Abstraction):定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化角色(Refined Abstraction):扩展抽象化角色,实现抽象类中的业务方法。
  • 实现化角色(Implementor):定义实现化角色的接口,但是不提供具体的实现。
  • 具体实现化角色(Concrete Implementor):实现实现化角色的接口,提供具体的实现。

学习它肯定是要知道什么时候使用

使用场景

  1. 当需要将一个类或者系统分成多个维度,而这些维度又需要独立变化时,可以使用桥接模式。例如,在操作系统中,可以将不同的文件系统和不同的存储设备分成两个维度,将它们分别进行抽象和实现,使得它们可以独立变化和演化。
  2. 当需要避免类的爆炸性增长时,可以使用桥接模式。在系统中,如果存在多个类或者子系统之间的关系,每个类或者子系统都需要与其他类或者子系统进行交互,这时候如果直接使用继承或者组合的方式,可能会导致类的数量爆炸式增长,难以维护。使用桥接模式可以将类或者子系统之间的关系进行抽象和分离,使得系统更加简洁和易于维护。
  3. 当需要提高系统的可扩展性和可维护性时,可以使用桥接模式。桥接模式可以将系统分成多个维度,使得系统更加灵活和可扩展,同时也可以提高系统的可维护性和可测试性,降低系统的耦合度。
  4. 需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系
  5. 一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立地进行扩展
  6. 不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统

一句话, 使抽象和实现分离并使用组合的方式来组合功能

桥接模式的定义

又被称为柄体(Handle and Body)模式或接口(Interface)模式 用抽象关联取代了传统的多层继承 将类之间的静态继承关系转换为动态的对象组合关系

从名字来看,它是将抽象部分与实现部分按桥(组合)连接起来

image.png

案例讲解

案例:某软件公司要开发一个跨平台图像浏览系统,要求该系统能够显示BMP、JPG、GIF、PNG等多种格式的文件,并且能够在Windows、Linux、UNIX等多个操作系统上运行。系统首先将各种格式的文件解析为像素矩阵(Matrix),然后将像素矩阵显示在屏幕上,在不同的操作系统中可以调用不同的绘制函数来绘制像素矩阵。另外,系统需具有较好的扩展性,以便在将来支持新的文件格式和操作系统。 试使用桥接模式设计该跨平台图像浏览系统。

辅助类(该类只是为了实现将文件转化为像素矩阵)

作用就是将图片转化为像素矩阵,辅助类,不需要过多关注

//像素矩阵类,辅助类
public class Matrix {
    //代码省略
}

抽象操作系统实现类

为什么将其作为抽象类接口?

因为它的实现是变化的,可以是windows,也可以是Linux。

//抽象操作系统实现类,充当实现类接口
public interface ImageImp {
   public void doPaint(Matrix m);  //显示像素矩阵m
}

上面抽象类的具体实现

下面是不同操作系统的实现

//Windows操作系统实现类,充当具体实现类
public class WindowsImp implements ImageImp {
  public void doPaint(Matrix m) {
   //调用Windows系统的绘制函数绘制像素矩阵
   System.out.print("在Windows操作系统中显示图像:");
  }
}
//Unix操作系统实现类,充当具体实现类
public class UnixImp implements ImageImp {
  public void doPaint(Matrix m) {
   //调用Unix系统的绘制函数绘制像素矩阵
   System.out.print("在Unix操作系统中显示图像:");
  }
}
//Linux操作系统实现类,充当具体实现类
public class LinuxImp implements ImageImp {
  public void doPaint(Matrix m) {
   //调用Linux系统的绘制函数绘制像素矩阵
   System.out.print("在Linux操作系统中显示图像:");
  }
}

抽象图像类

这里记得注入实现方法,使用set注入实现方法。

将一个实现类接口对象注入到类中,实现了抽象部分和实现部分的分离。(依赖注入)

//抽象图像类,充当抽象类
public abstract class Image {
   protected ImageImp imp;

    //注入实现类接口对象
   public void setImageImp(ImageImp imp) {
      this.imp = imp;
   } 
    // 转化文件的方法
   public abstract void parseFile(String fileName);
}

对图片格式的具体实现类

// 然后下面四段代码为图像实现类,可以将其转化为不同格式的文件

//GIF格式图像类,充当扩充抽象类
public class GIFImage extends Image {
   public void parseFile(String fileName) {
      //模拟解析GIF文件并获得一个像素矩阵对象m;
      Matrix m = new Matrix(); 
      imp.doPaint(m);
      System.out.println(fileName + ",格式为GIF。");
  }
}
//BMP格式图像类,充当扩充抽象类
public class BMPImage extends Image {
   public void parseFile(String fileName) {
      //模拟解析BMP文件并获得一个像素矩阵对象m;
      Matrix m = new Matrix(); 
      imp.doPaint(m);
      System.out.println(fileName + ",格式为BMP。");
  }
}
//JPG格式图像类,充当扩充抽象类
public class JPGImage extends Image {
   public void parseFile(String fileName) {
      //模拟解析JPG文件并获得一个像素矩阵对象m;
      Matrix m = new Matrix(); 
      imp.doPaint(m);
      System.out.println(fileName + ",格式为JPG。");
  }
}
//PNG格式图像类,充当扩充抽象类
public class PNGImage extends Image {
   public void parseFile(String fileName) {
      //模拟解析PNG文件并获得一个像素矩阵对象m;
      Matrix m = new Matrix(); 
      imp.doPaint(m);
      System.out.println(fileName + ",格式为PNG。");
  }
}

XML文件

为了切换操作系统和切换图片格式不修改源代码,我打算使用配置文件

<?xml version="1.0"?>
<config>
   <!--RefinedAbstraction-->
   <className>designpatterns.bridge.JPGImage</className> 
   <!--ConcreteImplementor-->
   <className>designpatterns.bridge.WindowsImp</className>
</config>

XML文件读取类

读取客户端传来的字符串,比如

public class XMLUtil {
   //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
   public static Object getBean(String args) {
      try {
         //创建文档对象
         DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
         DocumentBuilder builder = dFactory.newDocumentBuilder();
         Document doc;                    
         doc = builder.parse(new File("./config.xml")); 
         NodeList nl=null;
         Node classNode=null;
         String cName=null;
         nl = doc.getElementsByTagName("className");
         
         //获取第一个包含类名的结点,即扩充抽象类
         if(args.equals("image")) {
               classNode=nl.item(0).getFirstChild();
               
         }
         //获取第二个包含类名的结点,即具体实现类
         else if(args.equals("os")) {
               classNode=nl.item(1).getFirstChild();
         }
         
           cName=classNode.getNodeValue();
           //通过类名生成实例对象并将其返回
           Class c=Class.forName(cName);
         Object obj=c.getConstructor().newInstance();
           return obj;       
        }   
        catch(Exception e) {
            e.printStackTrace();
            return null;
        }
   }
}

客户端测试

//客户端测试类
public class Client {
   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("小龙女");
   }
}

输出:

在Windows操作系统中显示图像:小龙女,格式为JPG。

类的解释

实例代码 (1) Matrix:像素矩阵类,辅助类
(2) ImageImp:抽象操作系统实现类,充当实现类接口
(3) WindowsImp:Windows操作系统实现类,充当具体实现类
(4) LinuxImp:Linux操作系统实现类,充当具体实现类
(5) UnixImp:UNIX操作系统实现类,充当具体实现类
(6) Image:抽象图像类,充当抽象类
(7) JPGImage:JPG格式图像类,充当扩充抽象类
(8) PNGImage:PNG格式图像类,充当扩充抽象类
(9) BMPImage:BMP格式图像类,充当扩充抽象类
(10) GIFImage:GIF格式图像类,充当扩充抽象类
(11) Client:客户端测试类

模式比较

桥接模式:用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化

适配器模式:当发现系统与已有类无法协同工作时(比如我的接口名叫hello,但实现功能的接口名叫world)

模式优缺点

模式优点

  • 分离抽象接口及其实现部分
  • 可以取代多层继承方案,极大地减少了子类的个数
  • 提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,不需要修改原有系统,符合开闭原则

模式缺点

  • 会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程
  • 正确识别出系统中两个独立变化的维度并不是一件容易的事情