Java——设计模式(结构型模式)

一、适配器模式(不兼容结构的协调)

在适配器模式中引入了一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。(实际上客户端拿到的对象,已经是适配器的对象了)

适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无须修改原来的适配者接口和抽象目标类接口。适配器模式定义如下:

适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起 工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。(实现不改代码,只添加类和修改配置文件完成调用其它对象方法)

  • Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
  • Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
  • Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。

根据对象适配器模式结构图,在对象适配器中,客户端需要调用 request() 方法,而适配者类 Adaptee 没有该 方法,但是它所提供的 specificRequest() 方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提 供一个包装类 Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起 来,在适配器的 request() 方法中调用适配者的 specificRequest() 方法。因为适配器类与适配者类是关联关 系(也可称之为委派关系),所以这种适配器模式称为对象适配器模式。典型的对象适配器代码如下所示:

/**
 * @author x5456
 */
public class AdaptorPattern {

    //抽象成绩操作类:目标接口
    interface ScoreOperation {
        public int[] sort(int array[]); //成绩排序

        public int search(int array[], int key); //成绩查找
    }

    //快速排序类:适配者
    class QuickSort {
        public int[] quickSort(int array[]) {
            sort(array, 0, array.length - 1);
            return array;
        }

        public void sort(int array[], int p, int r) {
            int q = 0;
            if (p < r) {
                q = partition(array, p, r);
                sort(array, p, q - 1);
                sort(array, q + 1, r);
            }
        }

        public int partition(int[] a, int p, int r) {
            int x = a[r];
            int j = p - 1;
            for (int i = p; i <= r - 1; i++) {
                if (a[i] <= x) {
                    j++;
                    swap(a, j, i);
                }
            }
            swap(a, j + 1, r);
            return j + 1;
        }

        public void swap(int[] a, int i, int j) {
            int t = a[i];
            a[i] = a[j];
            a[j] = t;
        }
    }

    //二分查找类:适配者
    class BinarySearch {
        public int binarySearch(int array[], int key) {
            int low = 0;
            int high = array.length - 1;
            while (low <= high) {
                int mid = (low + high) / 2;
                int midVal = array[mid];
                if (midVal < key) {
                    low = mid + 1;
                } else if (midVal > key) {
                    high = mid - 1;
                } else {
                    return 1; //找到元素返回1
                }
            }
            return -1; //未找到元素返回-1
        }
    }

    //操作适配器:适配器
    class OperationAdapter implements ScoreOperation {
        private QuickSort sortObj; //定义适配者QuickSort对象
        private BinarySearch searchObj; //定义适配者BinarySearch对象

        public OperationAdapter() {
            sortObj = new QuickSort();
            searchObj = new BinarySearch();
        }

        public int[] sort(int array[]) {
            return sortObj.quickSort(array); //调用适配者类QuickSort的排序方法
        }

        public int search(int array[], int key) {
            return searchObj.binarySearch(array, key); //调用适配者类BinarySearch的查找方法
        }
    }
}

// 调用者
class Client {
    public static void main(String args[]) {
        ScoreOperation operation; //针对抽象目标接口编程
        operation = (ScoreOperation) XMLUtil.getBean(); //读取配置文件,反射生成对象(只需要修改配置文件,改成OperationAdapter就行了)
        int scores[] = {84, 76, 50, 69, 90, 91, 88, 96}; //定义成绩数组
        int result[];
        int score;
        System.out.println("成绩排序结果:");
        result = operation.sort(scores);
        //遍历输出成绩 
        for (int i : scores) {
            System.out.print(i + ",");
        }
        System.out.println();
        System.out.println("查找成绩90:");
        score = operation.search(result, 90);
        if (score != -1) {
            System.out.println("找到成绩90。");
        } else {
            System.out.println("没有找到成绩90。");
        }
        System.out.println("查找成绩92:");
        score = operation.search(result, 92);

        if (score != -1) {
            System.out.println("找到成绩92。");
        } else {
            System.out.println("没有找到成绩92。");
        }
    }
}

类适配器

除了对象适配器模式之外,适配器模式还有一种形式,那就是类适配器模式,类适配器模式和对象适配器模式最大的区别在于适配器和适配者之间的关系不同,对象适配器模式中适配器和适配者之间是关联关系,而类适配器模式中适配器和适配者是继承关系

class Adapter extends Adaptee implements Target { 
    public void request() {
        specificRequest();
    }
}

由于 Java、C# 等语言不支持多重类继承,因此类适配器的使用受到很多限制,例如如果目标抽象类 Target 不 是接口,而是一个类,就无法使用类适配器;此外,如果适配者 Adapter 为最终(Final)类,也无法使用类适 配器。在 Java 等面向对象编程语言中,大部分情况下我们使用的是对象适配器,类适配器较少使用。(类适配受java中无法多继承的限制)

优点

无论是对象适配器模式还是类适配器模式都具有如下优点:

  • (1) 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  • (2) 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提 高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
  • (3) 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上 增加新的适配器类,完全符合“开闭原则”。

具体来说,类适配器模式还有如下优点:

  • 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。

对象适配器模式还有如下优点:

  • (1) 一个对象适配器可以把多个不同的适配者适配到同一个目标;
  • (2) 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也 可通过该适配器进行适配。

缺点

类适配器模式的缺点如下:

  • (1) 对于 Java、C# 等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;
  • (2) 适配者类不能为最终类,如在 Java 中不能为 final 类,C# 中不能为 sealed 类;
  • (3) 在 Java、C# 等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。

对象适配器模式的缺点如下:

  • 与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

适用场景

  • (1) 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
  • (2) 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一 起工作。

二、桥接模式(处理多维度变化)

引文

在正式介绍桥接模式之前,我先跟大家谈谈两种常见文具的区别,它们是毛笔和蜡笔。假如我们需要大中小 3 种 型号的画笔,能够绘制 12 种不同的颜色,如果使用蜡笔,需要准备 3×12 = 36 支,但如果使用毛笔的话,只需 要提供 3 种型号的毛笔,外加 12 个颜料盒即可,涉及到的对象个数仅为 3 + 12 = 15,远小于36,却能实现与 3 6 支蜡笔同样的功能。如果增加一种新型号的画笔,并且也需要具有 12 种颜色,对应的蜡笔需增加 12 支,而毛 笔只需增加一支。为什么会这样呢?通过分析我们可以得知:在蜡笔中,颜色和型号两个不同的变化维度(即两 个不同的变化原因)融合在一起,无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度;但在毛 笔中,颜色和型号实现了分离,增加新的颜色或者型号对另一方都没有任何影响。如果使用软件工程中的术 语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性(从而违反了单一原则:一个类只干一件事,否则如果另一件事变了,那么你这个类又要修改),而毛笔很好地将二者解耦,使用起来非常灵 活,扩展也更为方便。在软件开发中,我们也提供了一种设计模式来处理与画笔类似的具有多变化维度的情 况,即本章将要介绍的桥接模式。

桥接模式(JDBC的设计采用该模式)

桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。

桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使得系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。桥接定义如下:

桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。

桥接模式的结构与其名称一样,存在一条连接两个继承等级结构的桥,桥接模式结构如图所示:

  • Abstraction(抽象类):用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个 Implementor(实现类接口)类型的对象并可以维护该对象,它与 Implementor 之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。
  • RefinedAbstraction(扩充抽象类):扩充由 Abstraction 定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在 Abstraction 中声明的抽象业务方法,在 RefinedAbstraction 中可以调用在 Implementor 中定义的业务方法。
  • Implementor(实现类接口):定义实现类的接口,这个接口不一定要与 Abstraction 的接口完全一致,事 实上这两个接口可以完全不同,一般而言,Implementor 接口仅提供基本操作,而 Abstraction 定义的接口 可能会做更多更复杂的操作。Implementor 接口对这些基本操作进行了声明,而具体实现交给其子类。通过 关联关系,在 Abstraction 中不仅拥有自己的方法,还可以调用到 Implementor 中定义的方法,使用关联 关系来替代继承关系。
  • ConcreteImplementor(具体实现类):具体实现 Implementor 接口,在不同的 ConcreteImplementor 中提供基本操作的不同实现,在程序运行时,ConcreteImplementor 对象将替换其父类对象,提供给抽象 类具体的业务操作方法。

桥接模式是一个非常有用的模式,在桥接模式中体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等。熟悉桥接模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。

在使用桥接模式时,我们首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。例如:对于毛笔而言,由于型号是其固有的维度,因此可以设计一个抽象的毛笔类,在该类中声明并部分实现毛笔的业务方法,而将各种型号的毛笔作为其子类;颜色是毛笔的另一个维度,由于它与毛笔之间存在一种“设置”的关系,因此我们可以提供一个抽象的颜色接口,而将具体的颜色作为实现该接口的子类。在此,型号可认为是毛笔的抽象部分,而颜色是毛笔的实现部分,结构示意图如图所示:

public class BridgingPattern {

    //像素矩阵类:辅助类,各种格式的文件最终都被转化为像素矩阵,不同的操作系统提供不同的方式显示像素矩阵
    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); //显示像素矩阵
    }

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

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

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

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

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


// 客户端调用
class Client {
    public static void main(String args[]) {
        Image image = (Image) XMLUtil.getBean("image");
        ImageImp imp = (ImageImp) XMLUtil.getBean("os");
        image.setImageImp(imp);
        image.parseFile("小龙女");
    }
}

适配器模式与桥接模式的联用

在某系统的报表处理模块中,需要将报表显示和数据采集分开,系统可以有多种报表显示方式也可以有多种数据采集方式,如可以从文本文件中读取数据,也可以从数据库中读取数据,还可以从 Excel 文件中获取数据。如果需要从 Excel 文件中获取数据,则需要调用与Excel 相关的 API,而这个 API 是现有系统所不具备的,该 API 由厂商提供。使用适配器模式和桥接模式设计该模块。

在设计过程中,由于存在报表显示和数据采集两个独立变化的维度,因此可以使用桥接模式进行初步设计;为了 使用 Excel 相关的 API 来进行数据采集则需要使用适配器模式。系统的完整设计中需要将两个模式联用,如图所示:

优点

(1)分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使 得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同 一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维 度组合对象。

(2)在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类 的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。

(3)桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。

缺点

(1)桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。

(2)桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。

适用场景

(1)如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过 桥接模式可以使它们在抽象层建立一个关联关系。

(2)“抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子 类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。

(3)一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。

(4)对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

三、组合模式(树形结构的处理)

猜你喜欢

转载自www.cnblogs.com/x54256/p/10266352.html