结构型模式(一):适配器,桥接,组合模式

1、概述

结构型模式(Structural Pattern)主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式、桥接模式、装饰器模式、适配器模式、门面模式、组合模式、享元模式。

适配器模式(Adapter Pattern):将一个类的接口转换成用户希望的另一个接口,使得原本由于接口不兼容,而不能一起工作的那些类,可以一起工作。

桥接模式(Bridge Pattern):将抽象部分与实现部分相分离,使他么都可以独立的变化。

组合模式(Composite Pattern):将对象组合成树状结构,以表示“部分-整理”的层次结构,它使得客户对单个对象和复合对象的使用具有一致性。

装饰模式(Decorator Pattern):动态的给一个对象添加一些额外的职责,就扩展功能而言,它比生成子类的方式更为灵活。

外观模式(Facade Pattern):子系统中的一组接口提供一个一致的界面,定义一个高层接口,这个接口使得这一子系统更加容易使用。

享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度的对象。

代理模式(Proxy Pattern):为其他对象提供一个代理以控制对这个对象的访问。

2、适配器模式

对于这个模式,有一个经常被拿来解释它的例子,就是 USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

2.1、实例说明

现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法,类BinarySearch的binarySearch(int[],int)方法实现了二分查找算法。现使用适配器模式设计一个类,在不修改源代码的情况下,将类QuickSort和类BinarySearch的方法适配到DataOperation接口中。

2.2、实例类图
适配器类图.jpg
2.3、实现代码
public interface DataOperation {
    int[] sort(int[] array);
    public int search(int[] array,int key);
}

public class QuickSort {
    public int[] quickSort(int[] array){
        //省略代码实现
        return null;
    }
    public void swap(int[] array,int r,int q){
        //省略代码实现
    }
    public void sort(int[] array,int i,int j){
        //省略代码实现
    }
}

public class BinarySearch {
    public int binarySearch(int[] array , int key){
        //省略代码实现
        return 0;
    }
}

public class OperationAdapter implements DataOperation {

    private QuickSort sort;
    private BinarySearch search;

    OperationAdapter(QuickSort sort,BinarySearch search){
        this.search = search;
        this.sort = sort;
    }

    @Override
    public int[] sort(int[] array) {
        return sort.quickSort(array);
    }

    @Override
    public int search(int[] array, int key) {
        return search.binerySearch(array,key);
    }
}

本例中实例使用了对象适配器,即OperationAdapter和QuickSort以及BinarySearch是关联关系而不是继承关系,下面举一个类适配器的例子


// 类适配器: 基于继承
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

public class Adaptor extends Adaptee implements ITarget {
  public void f1() {
    super.fa();
  }
  
  public void f2() {
    super.fb();
  }
  
  // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}

在实际的开发中,到底该选择类适配器还是对象适配器呢?
1、如果 Adaptee 接口并不多,那两种实现方式都可以。
2、如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那我们推荐使用类适配器,因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
3、如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。

2.4、适配器模式应用场景总结

1、封装有缺陷的接口设计,假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。

2、统一多个类的接口设计,某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。

public class ASensitiveWordsFilter { // A敏感词过滤系统提供的接口
  //text是原始文本,函数输出用***替换敏感词之后的文本
  public String filterSexyWords(String text) {
    // ...
  }
  
  public String filterPoliticalWords(String text) {
    // ...
  } 
}

public class BSensitiveWordsFilter  { // B敏感词过滤系统提供的接口
  public String filter(String text) {
    //...
  }
}

public class CSensitiveWordsFilter { // C敏感词过滤系统提供的接口
  public String filter(String text, String mask) {
    //...
  }
}

// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好
public class RiskManagement {
  private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
  private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
  private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
  
  public String filterSensitiveWords(String text) {
    String maskedText = aFilter.filterSexyWords(text);
    maskedText = aFilter.filterPoliticalWords(maskedText);
    maskedText = bFilter.filter(maskedText);
    maskedText = cFilter.filter(maskedText, "***");
    return maskedText;
  }
}

// 使用适配器模式进行改造
public interface ISensitiveWordsFilter { // 统一接口定义
  String filter(String text);
}

public class ASensitiveWordsFilterAdaptor implements ISensitiveWordsFilter {
  private ASensitiveWordsFilter aFilter;
  public String filter(String text) {
    String maskedText = aFilter.filterSexyWords(text);
    maskedText = aFilter.filterPoliticalWords(maskedText);
    return maskedText;
  }
}
//...省略BSensitiveWordsFilterAdaptor、CSensitiveWordsFilterAdaptor...

// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统,
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。
public class RiskManagement { 
  private List<ISensitiveWordsFilter> filters = new ArrayList<>();
 
  public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
    filters.add(filter);
  }
  
  public String filterSensitiveWords(String text) {
    String maskedText = text;
    for (ISensitiveWordsFilter filter : filters) {
      maskedText = filter.filter(maskedText);
    }
    return maskedText;
  }
}

3、替换依赖的外部系统,当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。具体的代码示例如下所示:


// 外部系统A
public interface IA {
  //...
  void fa();
}
public class A implements IA {
  //...
  public void fa() { //... }
}
// 在我们的项目中,外部系统A的使用示例
public class Demo {
  private IA a;
  public Demo(IA a) {
    this.a = a;
  }
  //...
}
Demo d = new Demo(new A());

// 将外部系统A替换成外部系统B
public class BAdaptor implemnts IA {
  private B b;
  public BAdaptor(B b) {
    this.b= b;
  }
  public void fa() {
    //...
    b.fb();
  }
}
// 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,
// 只需要将BAdaptor如下注入到Demo即可。
Demo d = new Demo(new BAdaptor(new B()));

4、兼容老版本接口,在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated,并将内部实现逻辑委托为新的接口实现。
JDK1.0 中包含一个遍历集合容器的类 Enumeration。JDK2.0 对这个类进行了重构,将它改名为 Iterator 类,并且对它的代码实现做了优化。但是考虑到如果将 Enumeration 直接从 JDK2.0 中删除,那使用 JDK1.0 的项目如果切换到 JDK2.0,代码就会编译不通过。为了避免这种情况的发生,我们必须把项目中所有使用到 Enumeration 的地方,都修改为使用 Iterator 才行。

public class Collections {
  public static Emueration emumeration(final Collection c) {
    return new Enumeration() {
      Iterator i = c.iterator();
      
      public boolean hasMoreElments() {
        return i.hashNext();
      }
      
      public Object nextElement() {
        return i.next():
      }
    }
  }
}

5、适配不同格式的数据,适配器模式主要用于接口的适配,实际上,它还可以用在不同格式的数据之间的适配。比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。再比如,Java 中的 Arrays.asList() 也可以看作一种数据适配器,将数组类型的数据转化为集合容器类型。

List<String> stooges = Arrays.asList("Larry", "Moe", "Curly");

3.桥接模式

关于桥接模式,很多书籍、资料中,还有另外一种理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”

3.1、实例说明

如果要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows,Linux,Unix)上播放多种格式的视频文件,如MPEG,AVI,WMV等常见视频格式。

3.2、实例类图
桥接模式.jpg
3.3、实例代码
public interface VideoFile {
    void decode(String osType,String fileName);
}
public class MPEGFile implements VideoFile{
    @Override
    public void decode(String osType, String fileName) {
        System.out.println("MPEG格式:"+fileName+"在"+osType+"操作系统播放");
    }
}
public class AVIFile implements VideoFile {
    @Override
    public void decode(String osType, String fileName) {
        System.out.println("AVI格式:"+fileName+"在"+osType+"操作系统播放");
    }
}
public abstract class OperationSystemVersion {
    protected VideoFile videoFile;
    public void setVideoFile(VideoFile videoFile){
        this.videoFile = videoFile;
    }
    public abstract void play(String fileName);
}
public class WindowsVersion extends OperationSystemVersion {
    @Override
    public void play(String fileName) {
        videoFile.decode("Windows",fileName);
    }
}
public class LinuxVersion extends OperationSystemVersion {
    @Override
    public void play(String fileName) {
        videoFile.decode("Linux",fileName);
    }
}
public class UinuxVersion extends OperationSystemVersion {
    @Override
    public void play(String fileName) {
        videoFile.decode("Uinux",fileName);
    }
}
public class Client {
    public static void main(String[] args) {
        VideoFile videoFile = new AVIFile();
        OperationSystemVersion version = new WindowsVersion();
        version.setVideoFile(videoFile);
        version.play("一球成名");
    }
}

在客户端代码中,只需要修改具体视频格式文件类(AVIFile)和具体播放器类(WindowsVersion),即可在不同的操作系统中播放不同格式的文件,可将具体类的类名存储在配置文件中,从而实现在不修改源代码的基础上更换具体类,满足开闭原则。

4、组合模式

组合模式,将一组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,你的业务场景必须能够表示成树形结构。所以,组合模式的应用场景也比较局限,它并不是一种很常用的设计模式。

4.1、实例说明

使用组合模式设计一个杀毒软件的框架,该软件即可以对某个文件夹杀毒,也可以对某个指定的文件进行杀毒。

4.2、实例类图
组合模式.jpg
4.3、实例代码
public abstract class AbstractFile {
    protected abstract void add(AbstractFile element);
    protected abstract void remove(AbstractFile element);
    protected abstract void play();
}

public class Folder extends AbstractFile{

    String fileName;
    List<AbstractFile> files;

    Folder(String fileName){
        this.fileName = fileName;
        files = new ArrayList<>();
    }

    @Override
    protected void add(AbstractFile element) {
        files.add(element);
        System.out.println("add file:"+element);
    }

    @Override
    protected void remove(AbstractFile element) {
        files.remove(element);
        System.out.println("remove file:"+element);
    }

    @Override
    protected void play() {
        System.out.println("遍历文件夹:");
    }
}
public class ImageFile extends AbstractFile {

    String fileName;

    ImageFile(String fileName){
        this.fileName = fileName;
    }
    @Override
    protected void add(AbstractFile element) {
        System.out.println("不支持该方法");
    }

    @Override
    protected void remove(AbstractFile element) {
        System.out.println("不支持该方法");
    }

    @Override
    protected void play() {
        System.out.println("显示图片:"+fileName);
    }
}
public class TxtFile extends AbstractFile {

    String fileName;

    TxtFile(String fileName){
        this.fileName = fileName;
    }
    @Override
    protected void add(AbstractFile element) {
        System.out.println("不支持该方法");
    }

    @Override
    protected void remove(AbstractFile element) {
        System.out.println("不支持该方法");
    }

    @Override
    protected void play() {
        System.out.println("显示图片:"+fileName);
    }
}
public class VideoFile extends AbstractFile {

    String fileName;

    VideoFile(String fileName){
        this.fileName = fileName;
    }
    @Override
    protected void add(AbstractFile element) {
        System.out.println("不支持该方法");
    }

    @Override
    protected void remove(AbstractFile element) {
        System.out.println("不支持该方法");
    }

    @Override
    protected void play() {
        System.out.println("显示图片:"+fileName);
    }
}
public class Client {
    public static void main(String[] args) {
        AbstractFile folder = new Folder("files");
        ImageFile file1 = new ImageFile("messie.jpg");
        TxtFile txtFile = new TxtFile("设计模式.txt");
        VideoFile videoFile = new VideoFile("组合模式.mp4");
        folder.add(file1);
        folder.add(txtFile);
        folder.add(videoFile);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容