设计模式---结构性模式

设计模式---结构性模式,第1张

适配模式 概述:

适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

术语

  • source:待适配的类/对象/接口

  • Adapter:适配器

  • destination:适配后可用的类/对象/接口

模式结构

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


适配器分类
​​​​
  • 类适配器模式(class adapter pattern) Adapter 类,通过继承 source 类,实现 Destination 类接口,完成 source->Destination 的适配。

  • 对象适配器模式(object adapter pattern) 将 Adapter 类作修改,不是继承 source 类,而是持有 source 类的实例,以解决兼容性的问题。 即:持有 source 类,实现 Destination 类接口,完成source->Destination 的适配,Adapter 模式最大的作用还是将原本不兼容的接口融合在一起工作

  • 接口适配器模式(default adapter pattern) 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求 适用于一个接口不想使用其所有的方法的情况

类适配器和对象适配器的权衡
  • 类适配器使用对象继承的方式,是静态的定义方式;而对象适配器使用对象组合的方式,是动态组合的方式。
  • 对于类适配器,由于适配器直接继承了Adaptee,使得适配器不能和Adaptee的子类一起工作,因为继承是静态的关系,当适配器继承了Adaptee后,就不可能再去处理  Adaptee的子类了。
  • 对于对象适配器,一个适配器可以把多种不同的源适配到同一个目标。换言之,同一个适配器可以把源类和它的子类都适配到目标接口。因为对象适配器采用的是对象组合的关系,只要对象类型正确,是不是子类都无所谓。
  • 对于类适配器,适配器可以重定义Adaptee的部分行为,相当于子类覆盖父类的部分实现方法。
  •  对于对象适配器,要重定义Adaptee的行为比较困难,这种情况下,需要定义Adaptee的子类来实现重定义,然后让适配器组合子类。虽然重定义Adaptee的行为比较困难,但是想要增加一些新的行为则方便的很,而且新增加的行为可同时适用于所有的源。
  • 对于类适配器,仅仅引入了一个对象,并不需要额外的引用来间接得到Adaptee。
  • 对于对象适配器,需要额外的引用来间接得到Adaptee。
  • 建议尽量使用对象适配器的实现方式,多用合成/聚合、少用继承。当然,具体问题具体分析,根据需要来选用实现方式,最适合的才是最好的
 案例:

我们的需求就是将电源输入220V(适配者)转换为5V输出(目标)

使用前:

 /**
 * 220V的电压
 */
public class Voltage220V {
    private double voltage;

    public Voltage220V() {
        this.voltage = 220;
    }

    public double getVoltage() {
        return voltage;
    }

    public void setVoltage(double voltage) {
        this.voltage = voltage;
    }
}


public class Phone {
//    充电
    public void charge(Voltage220V voltage220V){
        double voltage = voltage220V.getVoltage() / 40;
        System.out.println("最终手机充电所用电压:" + voltage + "V");
    }
}


public class Client {
    public static void main(String[] args) {
        Phone phone = new Phone();
//        已知有一个220V的电源,要用它给手机进行充电,我们只能将220V的电源进行处理后才能给手机充上电
//        还一种方案:新增5V的一个Voltage5V,Voltage的电压可以被手机使用
//        但是这违背现实生活现象,现实生活中只有220V的电源,其他的电源都是通过适配得来的
        phone.charge(new Voltage220V());
    }
}
使用后

目标角色

/**
 * 目标接口
 */
interface Voltage5V{
    double getVoltage();
}

适配器者角色

public class Voltage220V {
    private double voltage;

    public Voltage220V() {
        this.voltage = 220;
    }

    public double getVoltage() {
        return voltage;
    }

    public void setVoltage(double voltage) {
        this.voltage = voltage;
    }
}

适配器角色

/**
 * 适配器:里面封装了source源到destination目标的过程
 */
class VoltageAdapter implements  Voltage5V{
    private Voltage220V voltage220V;

    public VoltageAdapter(Voltage220V voltage220V) {
        this.voltage220V = voltage220V;
    }

    @Override
    public double getVoltage() {
        return voltage220V.getVoltage() / 40;
    }
}
public class Phone {
//    充电
    public void charge(Voltage5V voltage5V){
        double voltage = voltage5V.getVoltage();
        System.out.println("最终手机充电所用电压:" + voltage + "V");
    }
}

电源适配器类实现了电源目标,继承了适配者。这样就适配了。

测试:

public class Client {
    public static void main(String[] args) {
        Phone phone = new Phone();
//        已知有一个220V的电源,要用它给手机进行充电,我们只能将220V的电源进行处理后才能给手机充上电
//        VoltageAdapter适配器对Voltage220V这个不能直接使用的电源适配后就可以使用了
        phone.charge(new VoltageAdapter(new Voltage220V()));
    }
}

总结:

 适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得以广泛应用,在Spring等开源框架、驱动程序设计(如JDBC中的数据库驱动程序)中也使用了适配器模式。

主要优点

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

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

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

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

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

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

主要缺点

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

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

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

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

适用场景

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

桥接模式 概述:

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


术语:

  • Bridge:桥接

  • Abstraction:抽象类

  • Implementor:实现

  • concrete:具体的

角色:

  • Client 类:桥接模式的调用者

  • Abstraction:维护了 Implementor/ 即它的实现类 ConcreteImplementorA.., 二者是聚合关系,Abstraction充当桥接类

  • xxxAbstraction :抽象的具体子类

  • Implementor :行为实现类的接口

  • ConcreteImplementorA /B:具体行为的实现类A、B

模式结构

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

案例

需求:手机(型号 + 品牌) *** 作问题;

完成手机各品牌各型号的项目设计;

列如:折叠式的华为、小米、Vivo,直立式的华为、小米、Vivo,旋转式的、滑盖的...

要求该项目设计易于扩

使用前

上图是常见的需求设计方案,非常不便于维护,手机的型号与品牌耦合度太高,当要新增滑盖(Slide)式的手机时,对应的品牌手机也要新增;

 使用后

              

 /** 手机型号
 */
public abstract class Abstraction {
    protected Implementor implementor;

    public abstract void call();
}

class Folded extends Abstraction {
    private String name = "折叠式";

    Folded(Implementor implementor) {
        this.implementor = implementor;
    }

    @Override
    public void call() {
        System.out.println(this.implementor.getName() + this.name + "正在通话中");
    }
}

class Upright extends Abstraction {
    private String name = "直立式";

    Upright(Implementor implementor) {
        this.implementor = implementor;
    }

    @Override
    public void call() {
        System.out.println(this.implementor.getName() + this.name + "正在通话中");
    }
}

class Slide extends Abstraction {
    private String name = "滑盖式";

    Slide(Implementor implementor) {
        this.implementor = implementor;
    }

    @Override
    public void call() {
        System.out.println(this.implementor.getName() + this.name + "正在通话中");
    }
}

 /** 手机品牌
 */
public interface Implementor {
    String getName();
}

class HW implements Implementor{
    private String name = "华为";

    @Override
    public String getName() {
        return name;
    }
}

class Mi implements Implementor{
    private String name = "小米";

    @Override
    public String getName() {
        return name;
    }
}

class Vivo implements Implementor{
    private String name = "Vivo";

    @Override
    public String getName() {
        return name;
    }
}

测试: 

public class Client {
    public static void main(String[] args) {
        Folded folded = new Folded(new HW());
        folded.call();

        Upright upright = new Upright(new Mi());
        upright.call();

        Slide slide = new Slide(new Vivo());
        slide.call();
    }
}

从结果可以看出来:

使用桥接模式,对该项目进行设计,型号或品牌的扩展,都不会影响另一方;

即手机型号扩展,手机品牌不受影响;手机品牌的上市退市,不会影响到手机型号;

注意事项及细节

实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性 对于系统的高层部分,只需要知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本

注意:

  • 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程
  •  桥接模式要求正确识别出系统中两个独立变化的维度(抽象、和实现),因此其使用范围 有一定的局限性,即需要有这样的应用场景。
总结:

桥接模式是设计Java虚拟机和实现JDBC等驱动程序的核心模式之一,应用较为广泛。在软件开发中如果一个类或一个系统有多个变化维度时,都可以尝试使用桥接模式对其进行设计。桥接模式为多维度变化的系统提供了一套完整的解决方案,并且降低了系统的复杂度。

主要优点

  • 分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维度组合对象。
  • 在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。
  • 桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。

主要缺点

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

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

适用场景

  • 如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  • “抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
  •  一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
  • 对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。

装饰者模式 概述:

装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。
 

术语:
  • Component:组件(主体)

  • concreteComponent:被装饰者

  • Decorator:装饰者

注意:concreteComponent、Decorator都会实现或继承Component

角色:

  • Client 类:装饰模式的调用者

  • Component:主体

  • concreteComponent:被装饰者

  • xxxConcreteComponent:具体的被装饰者

  • Decorator:装饰器

  • xxxDecorator:子装饰器

模式结构

  • Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明 *** 作。
  • ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
  • Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
  • ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。

设计模式在JAVA I/O库中的应用

装饰模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了。

  由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是Java I/O库的基本模式。

  Java I/O库的对象结构图如下,由于Java I/O的对象众多,因此只画出InputStream的部分。

 抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型提供统一的接口。

具体构件(ConcreteComponent)角色:由ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream等类扮演。它们实现了抽象构件角色所规定的接口。

抽象装饰(Decorator)角色:由FilterInputStream扮演。它实现了InputStream所规定的接口。

具体装饰(ConcreteDecorator)角色:由几个类扮演,分别是BufferedInputStream、DataInputStream以及两个不常用到的类LineNumberInputStream、PushbackInputStream。

案例

单体咖啡与调味组合的饮料计价项目

使用前

这个项目最容易想到的设计方案就是采用继承的设计方案,设计思路如下:

Drink===》饮品

Juice===》果汁

.......

Coffee===》咖啡

ChinaCoffee===》中式咖啡

ASeasoningChinaCoffee===》被A调料修饰的中式咖啡

BSeasoningChinaCoffee===》被B调料修饰的中式咖啡

......

XxxCoffee===》其它咖啡

ASeasoningXxxCoffee===》被A调料修饰的其它咖啡

BSeasoningXxxCoffee===》被B调料修饰的其它咖啡

......

上面每个类中都有getPrice计价的功能,从结果上来看,确实可以完成项目需求,但是整个设计体系过于臃肿,不便于后期的扩展与维护;

 /**
 * 饮料包括单体咖啡+调料
 */
public abstract class Drink {
    protected double price;
    protected int n;
    protected DrinkSeasoning seasoning;

    public abstract double getPrice();
}

/**
 * 单体咖啡
 */
abstract class Coffee extends Drink {
}

/**
 * 单体果汁
 */
abstract class Juice extends Drink {
}

class ChinaCoffee extends Coffee{
    ChinaCoffee(double price,int n){
        this.price = price;
        this.n = n;
    }

    @Override
    public double getPrice() {
        return this.price*this.n+this.seasoning.getPrice();
    }
}


/**
 * @author 小李飞刀
 * @site www.javaxl.com
 * @company
 * @create  2020-02-22 18:32
 *
 * 调料的抽象接口
 */
public interface DrinkSeasoning {
    public abstract double getPrice();
}

/**
 * A类调料
 */
class ADrinkSeasoning implements  DrinkSeasoning{
    protected double price;
    protected int n;
    ADrinkSeasoning(double price,int n){
        this.price = price;
        this.n = n;
    }
    @Override
    public double getPrice() {
        return this.price*this.n;
    }
}


/**
 * B类调料
 */
class BDrinkSeasoning implements  DrinkSeasoning{
    private double price;
    protected int n;
    BDrinkSeasoning(double price,int n){
        this.price = price;
        this.n = n;
    }
    @Override
    public double getPrice() {
        return this.price*this.n;
    }
}


public class Client {
    public static void main(String[] args) {
        ChinaCoffee chinaCoffee = new ChinaCoffee(6,1);
        ADrinkSeasoning aDrinkSeasoning = new ADrinkSeasoning(2,2);
        chinaCoffee.seasoning = aDrinkSeasoning;
        System.out.println("中式咖啡1份+A调料2份,最终价格为:"+chinaCoffee.getPrice());

//        思考1:如果我要下单中式咖啡1份+A调料3份+B调料2份,计算出最终的价格,那代码该怎么改动呢?
//        思考2:在原有的咖啡订单下,追加B调料2份,计算出最终的价格,那代码该怎么改动呢?
    }
}
使用后

 * 饮料包括单体咖啡+调料
 */
public abstract class Drink {
    protected double price;
    protected int n;

    public abstract double getPrice();
}

/**
 * 单体咖啡
 */
abstract class Coffee extends Drink {
}

/**
 * 单体果汁
 */
abstract class Juice extends Drink {
}

class ChinaCoffee extends Coffee {
    ChinaCoffee(double price, int n) {
        this.price = price;
        this.n = n;
    }

    @Override
    public double getPrice() {
        return this.price * this.n;
    }
}


package com.javaxl.design.decorator.after;

/**
 * @author 小李飞刀
 * @site www.javaxl.com
 * @company
 * @create  2020-02-22 22:26
 */
public class DecoratorDrink extends Drink {
    private Drink drink;

    public DecoratorDrink(Drink drink, double price, int n) {
        this.drink = drink;
        this.price = price;
        this.n = n;
    }

    @Override
    public double getPrice() {
        return this.price * this.n + drink.getPrice();
    }
}

class ADecoratorDrink extends DecoratorDrink {
    public ADecoratorDrink(Drink drink, double price, int n) {
        super(drink, price, n);
    }
}

class BDecoratorDrink extends DecoratorDrink {
    public BDecoratorDrink(Drink drink, double price, int n) {
        super(drink, price, n);
    }
}


package com.javaxl.design.decorator.after;


/**
 * @author 小李飞刀
 * @site www.javaxl.com
 * @company
 * @create  2020-02-22 18:50
 */
public class Client {
    public static void main(String[] args) {
        ChinaCoffee chinaCoffee = new ChinaCoffee(6,1);
//        假定A类调料2元一份,B类调料3元一份
        Drink order = new ADecoratorDrink(chinaCoffee, 2, 2);
        System.out.println("中式咖啡1份+A调料2份,最终价格为:"+order.getPrice());

//        思考1:如果我要下单中式咖啡1份+A调料3份+B调料2份,计算出最终的价格,那代码该怎么改动呢?
        order = new ADecoratorDrink(order,2,1);
        System.out.println("式咖啡1份+A调料3份,最终价格为:"+order.getPrice());
        order = new BDecoratorDrink(order,3,2);
        System.out.println("式咖啡1份+A调料3份+B调料2份,最终价格为:"+order.getPrice());

//        思考2:在原有的咖啡订单下,追加B调料2份,计算出最终的价格,那代码该怎么改动呢?
        order = new BDecoratorDrink(order,3,2);
        System.out.println("式咖啡1份+A调料3份+B调料4份,最终价格为:"+order.getPrice());
    }
}

总结:

        装饰模式降低了系统的耦合度,可以动态增加或删除对象的职责,并使得需要装饰的具体构件类和具体装饰类可以独立变化,以便增加新的具体构件类和具体装饰类。在软件开发中,装饰模式应用较为广泛,例如在JavaIO中的输入流和输出流的设计、javax.swing包中一些图形界面构件功能的增强等地方都运用了装饰模式。

主要优点

  • 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
  •  可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
  • 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
  • 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。

主要缺点

  • 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
  • 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。

适用场景

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  • 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)。

外观模式 概述:

        外观模式:为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用


术语:Facade:外观

角色:

  • Client 类 外观模式的调用者
  • ComponentA 子系统A 电脑
  • ComponentB 子系统B 投影仪
  • Facade HomeTheaterFacade 家庭影院

模式结构

  • Facade(外观角色):在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。
  • SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。

案例

需求:组装一个家庭影院;

电脑打开、投影仪打开、音箱打开、灯光调暗、零食拿出来,电影开始;

零食收起来、灯光调亮、音箱关闭、投影仪关闭、电脑关闭,电影结束

使用前
 /** 电脑(故意写两个用不上的功能,依次体现外观模式的优点)
 */
public class ComponentA {
    public void m1(){
        System.out.println("电脑功能一...");
    }
    public void m2(){
        System.out.println("电脑功能二...");
    }

    public void on(){
        System.out.println("电脑打开...");
    }

    public void off(){
        System.out.println("电脑关闭...");
    }
}

//投影仪
class ComponentB {
    public void on(){
        System.out.println("投影仪打开...");
    }

    public void off(){
        System.out.println("投影仪关闭...");
    }
}

//音箱
class ComponentC {
    public void on(){
        System.out.println("音箱打开...");
    }

    public void off(){
        System.out.println("音箱关闭...");
    }
}

//、灯光
class ComponentD {
    public void on(){
        System.out.println("灯光调亮...");
    }

    public void off(){
        System.out.println("灯光调暗...");
    }
}

//零食
class ComponenE {
    public void on(){
        System.out.println("零食拿出来...");
    }

    public void off(){
        System.out.println("零食收起来...");
    }
}


public class Client {
    public static void main(String[] args) {
        new ComponentA().on();
        new ComponentB().on();
        new ComponentC().on();
        new ComponentD().off();
        new ComponenE().on();
        System.out.println("电影开始了...");

        System.out.println();
        new ComponenE().off();
        new ComponentD().on();
        new ComponentC().off();
        new ComponentB().off();
        new ComponentA().off();
        System.out.println("电影结束了...");
    }
}

从上面代码可以看出:

客户端调用依赖了所有的子系统(ABCDE),如果该需求反复出现,对于客户端调用而言,就不是很方便了;

另一方面,此需求完成只需要依赖各个子系统的其中一部分功能,其它功能客户端用不上,依照迪米特法则我们也应该解耦客户端与各个子系统的关系;

使用后

使用后类图: 

 /** 电脑(故意写两个用不上的功能,依次体现外观模式的优点)
 */
public class ComponentA {
    public void m1(){
        System.out.println("电脑功能一...");
    }
    public void m2(){
        System.out.println("电脑功能二...");
    }

    public void on(){
        System.out.println("电脑打开...");
    }

    public void off(){
        System.out.println("电脑关闭...");
    }
}

//投影仪
class ComponentB {
    public void on(){
        System.out.println("投影仪打开...");
    }

    public void off(){
        System.out.println("投影仪关闭...");
    }
}

//音箱
class ComponentC {
    public void on(){
        System.out.println("音箱打开...");
    }

    public void off(){
        System.out.println("音箱关闭...");
    }
}

//、灯光
class ComponentD {
    public void on(){
        System.out.println("灯光调亮...");
    }

    public void off(){
        System.out.println("灯光调暗...");
    }
}

//零食
class ComponentE {
    public void on(){
        System.out.println("零食拿出来...");
    }

    public void off(){
        System.out.println("零食收起来...");
    }
}


public class ComponentFacade {
    ComponentA componentA =new ComponentA();
    ComponentB componentB = new ComponentB();
    ComponentC componentC = new ComponentC();
    ComponentD componentD = new ComponentD();
    ComponentE componenE = new ComponentE();
    public void on(){
        componentA.on();
        componentB.on();
        componentC.on();
        componentD.off();
        componenE.on();
        System.out.println("电影开始了...");
    }

    public void off(){
        componenE.off();
        componentD.on();
        componentC.off();
        componentB.off();
        componentA.off();
        System.out.println("电影结束了...");
    }
}


public class Client {
    public static void main(String[] args) {
        ComponentFacade componentFacade = new ComponentFacade();
        componentFacade.on();
        System.out.println();
        componentFacade.off();
    }
}

从上面可以看出,客户端只依赖了外观类,彻底解耦了与各个子系统之间的关系;

注意事项及细节

  • 屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性

  • 对客户端与子系统的耦合关系 - 解耦,让子系统内部的模块更易维护和扩展

  • 当系统需要进行分层设计时,可以考虑使用 Facade 模式

总结:

        外观模式是一种使用频率非常高的设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,使子系统与客户端的耦合度降低,且客户端调用非常方便。

主要优点
       外观模式的主要优点如下:

  • 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
  • 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。
  • 一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。

主要缺点

  • 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活 性。
  • 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。

适用场景

  • 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
  • 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
  • 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。

代理模式 概述:

代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问

术语

  • Proxy:代理

模式结构

 代理模式包含如下三个角色:

  •  Subject(抽象主题角色):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。

  • Proxy(代理主题角色):它包含了对真实主题的引用,从而可以在任何时候 *** 作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题 *** 作之前或之后还需要执行其他 *** 作,而不仅仅是单纯调用真实主题对象中的 *** 作。

  • RealSubject(真实主题角色):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务 *** 作,客户端可以通过代理主题角色间接调用真实主题角色中定义的 *** 作。

分类:
  • 静态代理
  • 动态代理(jdk代理 )
  • Cglib代理

静态代理

角色

  • 接口 ITeacherDao
  • 目标对象 TeacherDAO
  • 代理类 TeacherDAOProxy

细节

代理对象与目标对象要实现相同的接口 调用的时候通过调用代理对象的方法来调用目标对象

 /** 目标类
 */
public class TeacherDAO implements ITeacherDao {
    public void teach() {
        System.out.println("老师传授知识");
    }
}

//目标接口
interface ITeacherDao {
    void teach();
}

//代理类
class TeacherDAOProxy implements ITeacherDao {
    private ITeacherDao teacherDAO;

    public TeacherDAOProxy(ITeacherDao teacherDAO) {
        this.teacherDAO = teacherDAO;
    }

    @Override
    public void teach() {
        System.out.println("老师正式授课前的准备工作,如学生全部签到...");
        teacherDAO.teach();
        System.out.println("老师结束授课,如下课铃声响起...");
    }
}


public class Client {
    public static void main(String[] args) {
        TeacherDAOProxy proxy = new TeacherDAOProxy(new TeacherDAO());
        proxy.teach();
    }
}

动态代理jdk代理 ​

角色 ​

  • 接口 ​ ITeacherDao ​
  • 目标对象 ​ TeacherDAO ​
  • 代理类 ​ TeacherDAOProxy ​

细节 ​

不需要实现接口,但是目标对象要实现接口,否则不能用动态代理 ​ 代理对象的生成,是利用 JDK 的 API,动态的在内存中构建代理对象 ​ 代理类所在包:java.lang.reflect.Proxy

/**
 * 目标接口
 */
interface ITeacherDao {
    String teach();

    ITeacherDao sleep(int minutes);
}

class TeacherDao implements ITeacherDao{
    @Override
    public String teach() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(new Date())+":老师传授知识";
    }

    @Override
    public ITeacherDao sleep(int minutes) {
        System.out.println("老师睡了" + minutes + "分钟");
        return this;
    }

}

//真实代理类的外衣
class TeacherDaoProxy{
    private ITeacherDao target;

    public TeacherDaoProxy(ITeacherDao target) {
        this.target = target;
    }

    public Object xxx(){
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object obj = null;
                        String methodName = method.getName();
                        System.out.println("目标方法" + methodName + ":jdk代理开始...");
                        System.out.println("真实代理对象:"+proxy.getClass());
                        System.out.println("目标对象:"+target.getClass());
                        if("sleep".equals(methodName)){
//                            method.invoke(target, args);
//                            obj = proxy;
                            obj = method.invoke(target, args);
                        }else {
//                        proxy是真实代理类,method是目标方法,args是目标方法携带的参数
                            obj = method.invoke(target, args);
                        }
                        System.out.println("目标方法" + methodName + ":jdk代理结束...");
                        return obj;
                    }
                });
    }
}


public class Client {
    public static void main(String[] args) {
        TeacherDaoProxy proxy = new TeacherDaoProxy(new TeacherDao());
        ITeacherDao ins = (ITeacherDao) proxy.xxx();
        System.out.println("===========代理类实例被使用   begin=============");
        System.out.println(ins);
        System.out.println("===========代理类实例被使用   end=============");
        System.out.println(ins.teach());
//        System.out.println(proxy.execute());
        System.out.println("===========代理类实例被使用   begin=============");
        ins.sleep(10);
        System.out.println("===========代理类实例被使用   end=============");
        ins.sleep(20).sleep(60);
    }
}

 

 

注意:java.lang.reflect.InvocationHandler.invoke第一个参数proxy的使用场景,链式编程中会用到;

Cglib代理 ​

角色 ​

  • 接口 ​ ITeacherDao ​
  • 目标对象 ​ TeacherDAO ​
  • 代理类 ​ TeacherDAOProxy

细节 ​

  • 目标对象与代理对象都不需要实现接口 ​
  • Cglib 代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展 ​ Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口.它广泛的被许多 AOP 的框架使用 ​
  • Cglib 包的底层是通过使用字节码处理框架 ASM 来转换字节码并生成新的类 ​

注意:

  • 需要引入 cglib 的 jar 文件
  • 在内存中动态构建子类,注意代理的类不能为 final,否则报错java.lang.IllegalArgumentException:
  • 目标对象的方法如果为 final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法.

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.text.SimpleDateFormat;
import java.util.Date;

class TeacherDao {
    public String teach() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        return sdf.format(new Date()) + ":老师传授知识";
    }

    public TeacherDao sleep(int minutes) {
        System.out.println("老师睡了" + minutes + "分钟");
        return this;
    }

}

//真实代理类的外衣
class TeacherDaoProxy implements MethodInterceptor {
    private Object target;

    public TeacherDaoProxy(Object target) {
        this.target = target;
    }

    //返回一个代理对象:	是 target  对象的代理对象
    public Object getProxyInstance() {
        //1. 创建一个工具类
        Enhancer enhancer = new Enhancer();
        //2. 设置父类
        enhancer.setSuperclass(target.getClass());
        //3. 设置回调函数
        enhancer.setCallback(this);
        //4. 创建子类对象,即代理对象
        return enhancer.create();
    }

    /**
     * @param proxyIns  由CGLib动态生成的代理类实例
     * @param method    上文中实体类所调用的被代理的方法引用
     * @param args      参数值列表
     * @param methodProxy   生成的代理类对方法的代理引用
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object proxyIns, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        String methodName = method.getName();
        Object res;
        System.out.println("目标方法" + methodName + ":cglib代理开始...");
        System.out.println("真实代理对象:" + proxyIns.getClass());
        System.out.println("目标对象:" + target.getClass());
        if ("sleep".equals(methodName)) {
//                            method.invoke(target, args);
//                            obj = proxy;
            res = method.invoke(target, args);
//            res = methodProxy.invokeSuper(proxyIns,args);
            res = proxyIns;
        } else {
//                        proxy是真实代理类,method是目标方法,args是目标方法携带的参数
            res = method.invoke(target, args);
        }
        System.out.println("目标方法" + methodName + ":cglib代理结束...");
        return res;
    }
}

public class Client {
    public static void main(String[] args) {
        TeacherDao proxy = (TeacherDao) new TeacherDaoProxy(new TeacherDao()).getProxyInstance();
        proxy.sleep(111).sleep(222);
    }
}

总结

        代理模式是常用的结构型设计模式之一,它为对象的间接访问提供了一个解决方案,可以对对象的访问进行控制。代理模式类型较多,其中远程代理、虚拟代理、保护代理等在软件开发中应用非常广泛。

 

模式优点

       代理模式的共同优点如下:

  • 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  • 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。


 

模式缺点
       代理模式的主要缺点如下:

  • 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。
  • 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂。

至此,结构性模式介绍完毕,由于作者水平有限难免有疏漏,欢迎留言纠错。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/langs/719761.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存