设计模式——结构型模式

编程入门 行业动态 更新时间:2024-10-24 16:21:14

结构型模式

结构型模式主要是用于处理类或者对象的组合,它描述了如何来类或者对象更好的组合起来,是从程序的结构上来解决模块之间的耦合问题。它主要包括适配器模式、桥接模式、组合模式、装饰模式、外观模式、享元模式、代理模式这个七个模式。

五、适配器模式

将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以在一起工作。

5.1 前言

接口的改变,是一个需要程序员们必须(虽然很不情愿)接受和处理的普遍问题。程序提供者们修改他们的代码;系统库被修正;各种程序语言以及相关库的发展和进化,都需要程序员去重新修改代码。

  • iPhone,你可以使用USB接口连接电脑来充电,假如只有iPhone没有电脑,怎么办呢?苹果提供了iPhone电源适配器。可以使用这个电源适配器充电。这个iPhone的电源适配器就是类似我们说的适配器模式。(电源适配器就是把电源变成需要的电压,也就是适配器的作用是使得一个东西适合另外一个东西。)

  • 最典型的例子就是很多功能手机,每一种机型都自带有充电器,有一天自带充电器坏了,而且市场没有这类型充电器可买了。怎么办?万能充电器就可以解决。这个万能充电器就是适配器。

  • 当一个第三方库的API改变将会发生什么。过去你只能是咬紧牙关修改所有的客户代码,而情况往往还不那么简单。你可能正从事一项新的项目,它要用到新版本的库所带来的特性,但你已经拥有许多旧的应用程序,并且它们与以前旧版本的库交互运行地很好。你将无法证明这些新特性的利用价值,如果这次升级意味着将要涉及到其它应用程序的客户代码。

适配器(Adapter)模式为对象提供了一种完全不同的接口。可以运用适配器(Adapter)来实现一个不同的类的常见接口,同时避免了因升级和拆解客户代码所引起的纠纷。

5.2 概览

适配器模式(Adapter Pattern),把一个类的接口变换成客户端所期待的另一种接口, Adapter模式使原本因接口不匹配或者不兼容而无法在一起工作的两个类能够在一起工作。又称为转换器模式、变压器模式、包装器模式(把已有的一些类包装起来,使之能有满足需要的接口)。

5.2.1 类图

  • 目标角色(Target):定义Client使用的与特定领域相关的接口。用户所期待的新接口。
  • 客户角色(Client):与符合Target接口的对象协同。
  • 被适配角色(Adaptee):定义一个已经存在并已经使用的接口,这个接口需要适配。需要适配的旧代码。
  • 适配器角色(Adapter) :适配器模式的核心。它将对被适配Adaptee角色已有的接口转换为目标角色Target匹配的接口。对Adaptee的接口与Target接口进行适配。将原接口转换为目标接口。

5.2.2 适用场景

  • 系统需要使用现有的类,而这些类的接口不符合系统的接口。
  • 想要建立一个可以重用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
  • 两个类所做的事情相同或相似,但是具有不同接口的时候。
  • 旧的系统开发的类已经实现了一些功能,但是客户端却只能以另外接口的形式访问,但我们不希望手动更改原有类的时候。
  • 使用第三方组件,组件接口定义和自己定义的不同,不希望修改自己的接口,但是要使用第三方组件接口的功能。

5.3 实现

该模式的核心是:Adapter适配器首先要实现目标角色的接口,并且在Adapter适配器中包含一个Adaptee被适配角色的对象,然后在目标接口角色中的旧方法中调用新接口的新方法。

// 目标角色:用户想使用的接口
public interface Target {
    /**
     * 这个方法将来有可能继续改进
     */
    public  void  hello();

    /**
     * 该方法不变
     */
    public void world();
}
// Adapter:适配器类
public class Adapter implements Target {
    // 关联一个被适配的新接口
    private Adaptee adaptee;
    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    // 实现新老接口的适配转换
    @Override
    public void hello() {
        adaptee.greet();
    }
    @Override
    public void world() {
        adaptee.world();
    }
}
// 被适配角色:添加了新方法的角色,要将其转换为旧接口
public class Adaptee {
    /**
     * 加入新的方法
     */
    public  void greet(){
        System.out.println("'Greet '");;
    }
    /**
     * 源类含有的方法
     */
    public void world(){
        System.out.println("'world'");;
    }
}
// 客户端
public class Client {
    public static void main(String[] args) throws IOException {
//        TargetInterface target = new Adapter();
//        target.hello();
//        target.world();

        Adaptee adaptee = new Adaptee();
        Adapter adapter = new Adapter(adaptee);
        adapter.hello();
        adapter.world();
    }
}

5.4 效果

实际上,共有两种适配器模式:类适配器和对象适配器。

类适配器和对象适配器有不同的权衡(此部分内容是《GOF设计模式》)

  • 类适配器

    • 用一个具体的Adapter类对Adaptee和Target进行匹配。结果是当我们想要匹配一个类以及所有它的子类时,类Adapter将不能胜任工作。
    • 使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子类。
    • 仅仅引入了一个对象,并不需要额外的指针以间接得到 Adaptee。
  • 对象适配器则

    • 允许一个Adapter与多个Adaptee—即Adaptee本身以及它的所有子类(如果有子类的话)—同时工作。Adapter也可以一次给所有的Adaptee添加功能。
    • 使得重定义Adaptee的行为比较困难。这就需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身。

使用Adapter模式时需要考虑的其他一些因素有

(1)Adapter的匹配程度

对Adaptee的接口与Target的接口进行匹配的工作量各个Adapter可能不一样。工作范围可能是,从简单的接口转换(例如改变操作名 )到支持完全不同的操作集合。Adapter的工作量取决于Target接口与Adaptee接口的相似程度
(2)可插入的Adapter

当其他的类使用一个类时,如果所需的假定条件越少,这个类就更具可复用性。如果将接口匹配构建为一个类,就不需要假定对其他的类可见的是一个相同的接口。也就是说,接口匹配使得我们可以将自己的类加入到一些现有的系统中去,而这些系统对这个类的接口可能会有所不同。
(3)使用双向适配器提供透明操作

使用适配器的一个潜在问题是,它们不对所有的客户都透明。被适配的对象不再兼容 Adaptee的接口,因此并不是所有 Adaptee对象可以被使用的地方它都可以被使用。双向适配器提供了这样的透明性。在两个不同的客户需要用不同的方式查看同一个对象时,双向适配器尤其有用。

5.5 优点

  • 通过适配器,客户端可以调用同一接口,因而对客户端来说是透明的。这样做更简单、更直接、更紧凑。
  • 复用了现存的类,解决了现存类和复用环境要求不一致的问题。
  • 将目标类和适配者类解耦,通过引入一个适配器类重用现有的适配者类,而无需修改原有代码。
  • 一个对象适配器可以把多个不同的适配者类适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。

六、代理模式

代理模式: 为其他真实的对象提供一种代理,以便更好地控制对这个对象的访问。它可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或添加客户需要的额外服务。

6.1 前言

因为某个对象消耗太多资源,而且你的代码并不是每个逻辑路径都需要此对象, 你曾有过延迟创建对象的想法吗 ( if和else就是不同的两条逻辑路径) ? 你有想过限制访问某个对象,也就是说,提供一组方法给普通用户,特别方法给管理员用户?以上两种需求都非常类似,并且都需要解决一个更大的问题:你如何提供一致的接口给某个对象让它可以改变其内部功能,或者是从来不存在的功能? 可以通过引入一个新的对象,来实现对真实对象的操作或者将新的对象作为真实对象的一个替身。即代理对象。

例子1:经典例子就是网络代理,你想访问Facebook或者twitter ,如何绕过GFW,找个代理网站。

例子2:假如说我现在想买一辆二手车,虽然我可以自己去找车源,做质量检测等一系列的车辆过户流程,但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢?于是我就通过中介公司来买车,他们来给我找车源,帮我办理车辆过户流程,我只是负责选择自己喜欢的车,然后付钱就可以了。

6.2 概览

6.2.1 类图

  • 抽象主题角色(Subject):

    定义真实主题角色RealSubject和抽象主题角色Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxy。是客户端使用的现有接口。

  • 真实对象(RealSubject):

    定义了代理角色(proxy)所代表的具体对象。是真实对象的类。

  • 代理对象(Proxy):

    代理对象通过持有真实主题RealSubject的引用,不但可以控制真实主题RealSubject的创建或删除,可以在真实主题RealSubject被调用前进行拦截,或在调用后进行某些操作。

6.2.2 适用场景

  • 在不直接操作对象的情况下,实现对对象的访问
  • 中介隔离:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
  • 增加功能:代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。

6.3 实现

6.3.1 静态代理

/**
 * 抽象主题角色
 * 定义真实对象与代理对象共同的接口
 */
public interface Subject {

    void buyHouse();

    void buyBed();

}
/**
 * 真实对象
 * 定义了真实对象的主操作
 */
public class RealSubject implements Subject {

    @Override
    public void buyHouse() {
        System.out.println("买房.......");
    }

    @Override
    public void buyBed() {
        System.out.println("买床.......");
    }
}
/**
 * 代理对象
 * 此为静态代理模式
 * 关联了一个真实对象,在实现抽象角色中的方法时除了会调用真实对象外,还可以添加额外的操作
 */
public class ProxySubject implements Subject {
	// 关联一个真实对象
    private RealSubject realSubject;
    public ProxySubject(RealSubject realSubject) {
        this.realSubject = realSubject;
    }
	// 实现Subject中的方法
    @Override
    public void buyHouse() {
        // 预处理
        preAddress("房子");
        // 调用真实对象的方法
        realSubject.buyHouse();
        // 后处理
        afterAddress("房子");
    }
    @Override
    public void buyBed() {
        // 预处理
        preAddress("床");
        // 调用真实对象的方法
        realSubject.buyBed();
        // 后处理
        afterAddress("床");
    }

    // 额外的方法
    //1 预处理方法
    private void preAddress(String thing) {
        System.out.println("请付"+thing+"钱.....");
    }
    //2 后处理方法
    private void afterAddress(String thing) {
        System.out.println("确认签收:"+thing+".....");
    }
}
/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        Subject proxySubject = new ProxySubject(realSubject);
        
        // 在不访问真实对象的前提下,通过代理对象间接地实现对其的访问
        proxySubject.buyHouse();
        proxySubject.buyBed();
    }
}

6.3.2 动态代理

参见JAVA高级—反射与动态代理

/**
 * 动态代理公有接口/抽象接口
 */
public interface Human {
    String getName();
    void eat(String food);
}
/**
 * 被代理类
 */
public class Superman implements Human {
    @Override
    public String getName() {

        System.out.println("超人...");
        return "超人";
    }

    @Override
    public void eat(String food) {
        System.out.println("超人吃:" + food);
    }
}
/**
 * 代理类:动态代理的目的是动态地创建一个代理类对象【使用java.lang.reflect.Proxy】,而不需要提前写死
 * 要想实现动态代理,需要解决:
 * 1、如何根据加载到内存中的被代理类,动态地创建一个代理类对象,通过该代理类对象实现对被代理类的额外操作
 * 2、当通过代理类的对象调用方法时,如何动态地调用被代理类中的同名方法
 */
public class ProxyHuman {
    /**
     * 调用此方法动态地返回一个代理类的对象。解决问题1
     *
     * @param obj 被代理类对象
     * @return 代理类对象实例
     */
    public static Object getProxyInstance(Object obj) {
        // 核心接口:内部只有一个方法invoke(),调用被代理类中的同名方法,同时可添加附加操作。
        // 解决问题2
        InvocationHandler handler = new MyInvocationHandler(obj);
        // 运用反射,通过参数类的加载器、接口【被代理类所实现的接口,即公有接口】、处理器【当调用返回的代理类中的方法时,将会自动执行该处理器内的invoke()方法】,返回一个代理类对象
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
// 处理器
public class MyInvocationHandler implements InvocationHandler {

    // 被代理类对象【类型不要写死】
    private Object object;

    public MyInvocationHandler(Object obj) {
        this.object = obj;
    }

    /**
     * 动态代理实现的核心
     * 当我们调用代理类对象中的方法a时,该invoke就会被自动调用
     * @param proxy     代理类对象
     * @param method    要执行的同名方法
     * @param args      方法中所需的参数
     * @return          执行方法的返回值
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        HumanUtils humanUtils = new HumanUtils();
        humanUtils.method1();
        // 通过反射【调用类中方法】,执行被代理类中的方法
        Object returnValue = method.invoke(object, args);   // 反射:将method与object被代理类对象绑定
        humanUtils.method2();
        return returnValue;
    }
}
/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        System.out.println("===========动态代理===========");
        // 被代理类对象
        Superman superman = new Superman();
        // 代理类对象【动态生成】
        Human proxyInstance = (Human) ProxyHuman.getProxyInstance(superman);
        proxyInstance.getName();

        proxyInstance.eat("麻辣烫");
    }
}

6.4 效果

Proxy模式可以对用户隐藏另一种称之为写时复制(copy-on-write)的优化方式,该优化与根据需要创建对象有关。拷贝一个庞大而复杂的对象是一种开销很大的操作,如果这个拷贝根本没有被修改,那么这些开销就没有必要。用代理延迟这一拷贝过程,我们可以保证只有当这个对象被修改的时候才对它进行拷贝。在实现copy-on-write时必须对实体进行引用计数。拷贝代理仅会增加引用计数。只有当用户请求一个修改该实体的操作时,代理才会真正的拷贝它。在这种情况下,代理还必须减
少实体的引用计数。当引用的数目为零时,这个实体将被删除。copy-on-write可以大幅度的降低拷贝庞大实体时的开销。

代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。

代理模式的缺点
由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
实现代理模式需要额外的工作,有些代理模式的实现非常复杂。

6.5 相关模式

1)适配器模式Adapter :适配器Adapter 为它所适配的对象提供了一个不同的接口。相反,代理提供了与它的实体相同的接口,代理对象除了实现接口中规定的方法外,还可以定义额外的操作。如在AOP中的通知。

  1. 装饰器模式Decorator:尽管Decorator的实现部分与代理相似,但 Decorator的目的不一样。Decorator为对象添加一个或多个功能,而代理则控制对对象的访问。

七、装饰器模式

装饰器模式:动态地给一个对象添加一些额外的职责或者行为,而不需要更改原有代码。它能够适配原始接口,并且使用组合而不是子类来扩展功能。

7.1 前言

  • 若你从事过面向对象开发,实现给一个类或对象增加行为,可以使用继承机制,这是所有面向对象语言的一个基本特性。如果已经存在的一个类缺少某些方法,或者须要给方法添加更多的功能,你也许会仅仅继承这个类来产生一个新类—这建立在额外的代码上。通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。如果你希望改变一个已经初始化的对象的行为,你怎么办?或者,你希望继承许多类的行为,该怎么办?前一个,只能在于运行时完成,后者显然时可能的,但是可能会导致产生大量的不同的类这一可怕的事情。

  • 有时需要在现有代码中添加或删除一些功能,同时对现有的代码结构不会造成影响,并且这些删除或增加的功能有不足以做成一个子类。这种情况下可以使用装饰者模式,因为它可以在不改变现有代码的情况下满足我们的需求。装饰者模式聚合了它将要装饰的原有对象,实现了与原有对象相同的接口,代理委托原有对象的所有公共接口调用,并且在装饰者的子类中实现新增的功能。

7.2 概览

通常可以使用继承来实现功能的拓展,如果这些需要拓展的功能的种类很繁多,那么势必生成很多子类,增加系统的复杂性,同时,使用继承实现功能拓展,我们必须可预见这些拓展功能,这些功能是编译时就确定了,是静态的。

装饰器模式提供了改变子类的灵活方案。装饰器模式在不必改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。当拥有一组子类时,装饰器模式更加有用。如果你拥有一族子类(从一个父类派生而来),你需要在与子类独立使用情况下添加额外的特性,你可以使用装饰器模式,以避免代码重复和具体子类数量的增加。

7.2.1 类图

装饰者与被装饰者拥有共同的超类,继承的目的是继承类型,而不是行为

  • **抽象组件角色(Component):**定义一个对象接口,以规范准备接受附加责任的对象,即可以给这些对象动态地添加职责
  • **具体组件角色(ConcreteComponent) :**被装饰者,定义一个将要被装饰增加功能的类。可以给这个类的对象添加一些职责
  • **抽象装饰器(Decorator):**维持一个指向构件Component对象的实例,并定义一个与抽象组件角色接口一致的接口
  • **具体装饰器角色(ConcreteDecorator):**向组件添加职责

7.2.2 适用场景

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  • 处理那些可以撤消的职责。
  • 当不能采用生成子类的方法进行扩充时。
    • 一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。
    • 另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

7.3 实现

/**
 * 抽象组件:定义统一的基础行为
 * 也可以是一个抽象类
 */
public interface Component {

    // 穿衣服
    public void wearClothes();

    // 去哪找衣服
    public void walkToWhere();

}
/**
 * 被装饰者
 * 被装饰者初始状态有些自己的装饰
 */
public class ConcreteComponent implements Component {

    @Override
    public void wearClothes() {
        System.out.println("穿什么呢。。");
    }

    @Override
    public void walkToWhere() {
        System.out.println("去哪里呢。。");
    }
}
/**
 * 装饰者
 */
public class Decorator implements Component {

	// 聚合一个原有的“装饰对象”,这个对象只是一个与真正被装饰对象拥有相同方法的接口
    private Component component;

    public Decorator(Component component) {
        this.component = component;
    }

	// 实现原对象的方法
    @Override
    public void wearClothes() {
        component.wearClothes();
    }

    @Override
    public void walkToWhere() {
        component.walkToWhere();
    }
}
/**
 * 具体装饰类
 * 即抽象装饰类的子类:可以定义多种不同的功能
 */
public class ConcreteDecorator01 extends Decorator{

    public ConcreteDecorator01(Component component) {
        super(component);
    }

    // 额外的方法
    public void goHome() {
        System.out.println("进01。。");
    }
    public void findMap() {
        System.out.println("01找找Map。。");
    }

    // 重写,将额外的方法添加至方法中
    @Override
    public void wearClothes() {
        super.wearClothes();
        goHome();
    }

    @Override
    public void walkToWhere() {
        super.walkToWhere();
        findMap();
    }
}
public class ConcreteDecorator02 extends Decorator {

    public ConcreteDecorator02(Component component) {
        super(component);
    }

    public void goClothesPress() {
        System.out.println("去02找找看。。");
    }

    public void findPlaceOnMap() {
        System.out.println("在02上找找。。");
    }

    @Override
    public void wearClothes() {
        super.wearClothes();
        goClothesPress();
    }

    @Override
    public void walkToWhere() {
        super.walkToWhere();
        findPlaceOnMap();
    }


}
public class ConcreteDecorator03 extends Decorator{

    public ConcreteDecorator03(Component component) {
        super(component);
    }

    public void findClothes() {
        System.out.println("找到一件03。。");
    }

    public void findTheTarget() {
        System.out.println("在03上找到神秘花园和城堡。。");
    }

    @Override
    public void wearClothes() {
        super.wearClothes();
        findClothes();
    }

    @Override
    public void walkToWhere() {
        super.walkToWhere();
        findTheTarget();
    }

}
/**
* 客户端
*/
public class Client {

    public static void main(String[] args) {

        Component concreteComponent = new ConcreteComponent();

//        Decorator concreteDecorator03 = new ConcreteDecorator03(concreteComponent);
//        concreteDecorator03.wearClothes();
//        concreteDecorator03.walkToWhere();

        // 装饰者模式可以按照链式方式进行执行
        // 初始化时:01->02->03
        Decorator decorator = new ConcreteDecorator03(
                                                    new ConcreteDecorator02(
                                                        new ConcreteDecorator01(
                                                            concreteComponent
                                                                                )
                                                                            )
                                                    );

        // 运行时:03->
        //         	03的super:即02->
        //         	02的super:即01->
        //         	01的super:即component->
        //         	执行被装饰者的基本方法
        // 然后按照01->02->03的顺序调用额外新增的装饰方法
        decorator.wearClothes();
        decorator.walkToWhere();
    }
}

7.3.1 关键点

  • Decorator抽象类中,持有Component接口,方法全部委托给该接口调用,目的是交给该接口的实现类即子类进行调用。
  • Decorator抽象类的子类(具体装饰者),里面都有一个构造方法调用super(human),这一句就体现了抽象类依赖于子类实现即抽象依赖于实现的原则。因为构造里面参数都是Component接口,只要是该Component的实现类都可以传递进去,即表现出Decorator decorator = new ConcreteDecorator03(new ConcreteDecorator02(new ConcreteDecorator01(concreteComponent)));这种结构的样子。所以当调用decorator. wearClothes();decorator. walkToWhere()的时候,又因为每个具体装饰者类中,都先调用super.wearClothes和super.walkToWhere()方法,而该super已经由构造传递并指向了具体的某一个装饰者类(这个可以根据需要调换顺序),那么调用的即为装饰类的方法,然后才调用自身的装饰方法,即表现出一种装饰、链式的类似于过滤的行为。
  • 具体被装饰者类,可以定义初始的状态或者初始的自己的装饰,后面的装饰行为都在此基础上一步一步进行点缀、装饰。
  • 装饰者模式的设计原则为:对扩展开放、对修改关闭,这句话体现在我如果想扩展被装饰者类的行为,无须修改装饰者抽象类,只需继承装饰者抽象类,实现额外的一些装饰或者叫行为即可对被装饰者进行包装。所以:扩展体现在继承、修改体现在子类中,而不是具体的抽象类,这充分体现了依赖倒置原则,这是自己理解的装饰者模式。

7.4 效果

7.4.1 装饰模式的特点

  • 装饰对象和真实对象有相同的接口。这样客户端对象就可以以和真实对象相同的方式和装饰对象交互

  • 装饰对象包含一个真实对象的索引(reference)

  • 装饰对象接受所有的来自客户端的请求。它把这些请求转发给真实的对象

  • 装饰对象可以在转发这些请求以前或以后增加一些附加功能。这样就确保了在运行时,不用修改给定对象的结构就可以在外部增加附加的功能。在面向对象的设计中,通常是通过继承来实现对给定类的功能扩展

7.4.2 优缺点

  • 比静态继承更灵活: 与对象的静态继承(多重继承)相比, Decorator模式提供了更加灵活的向对象添加职责的方式。可以用添加和分离的方法,用装饰在运行时刻增加和删除职责。相比之下,继承机制要求为每个添加的职责创建一个新的子类。这会产生许多新的类,并且会增加系统的复杂度。此外,为一个特定的Component类提供多个不同的 Decorator类,这就使得你可以对一些职责进行混合和匹配。使用Decorator模式可以很容易地重复添加一个特性。
  • 避免在层次结构高层的类有太多的特征 Decorator模式提供了一种“即用即付”的方法来添加职责。它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,你可以定义一个简单的类,并且用 Decorator类给它逐渐地添加功能。可以从简单的部件组合出复杂的功能。这样,应用程序不必为不需要的特征付出代价。同时更易于不依赖于 Decorator所扩展(甚至是不可预知的扩展)的类而独立地定义新类型的 Decorator。扩展一个复杂类的时候,很可能会暴露与添加的职责无关的细节。
  • Decorator与它的Component不一样 Decorator是一个透明的包装。如果我们从对象标识的观点出发,一个被装饰了的组件与这个组件是有差别的,因此,使用装饰不应该依赖对象标识。
  • 有许多小对象 采用Decorator模式进行系统设计往往会产生许多看上去类似的小对象,这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,排错也很困难。

7.5 相关模式

7.5.1 装饰模式和适配器模式

这是两个没有什么关联的模式,放到一起来说,是因为它们有一个共同的别名:Wrapper。 这两个模式功能上是不一样的,适配器模式是用来改变接口的,而装饰模式是用来改变对象功能的。

7.5.2 装饰模式和策略模式

这两个模式可以组合使用。 策略模式也可以实现动态的改变对象的功能,但是策略模式只是一层选择,也就是根据策略选择一下具体的实现类而已。而装饰模式不是一层,而是递归调用,无数层都可以,只要组合好装饰器的对象组合,那就可以依次调用下去,所以装饰模式会更灵活。 而且策略模式改变的是原始对象的功能,不像装饰模式,后面一个装饰器,改变的是经过前一个装饰器装饰过后的对象,也就是策略模式改变的是对象的内核,而装饰模式改变的是对象的外壳。 这两个模式可以组合使用,可以在一个具体的装饰器里面使用策略模式,来选择更具体的实现方式。

7.5.3 装饰模式和组合模式

这两个模式有相似之处,都涉及到对象的递归调用,从某个角度来说,可以把装饰看成是只有一个组件的组合。 但是它们的目的完全不一样,装饰模式是要动态的给对象增加功能;而组合模式是想要管理组合对象和叶子对象,为它们提供一个一致的操作接口给客户端,方便客户端的使用。

7.6 总结

  • 使用装饰器设计模式设计类的目标是: 不必重写任何已有的功能性代码,而是对某个基于对象应用增量变化。

  • 装饰器设计模式采用这样的构建方式: 在主代码流中应该能够直接插入一个或多个更改或“装饰”目标对象的装饰器,同时不影响其他代码流。

  • Decorator模式采用对象组合而非继承的手法,实现了在运行时动态的扩展对象功能的能力,而且可以根据需要扩展多个功能,避免了单独使用继承带来的“灵活性差”和“多子类衍生问题”。同时它很好地符合面向对象设计原则中“优先使用对象组合而非继承”和“开放-封闭”原则。

也许装饰器模式最重要的一个方面是它的超过继承的能力。“问题”部分展现了一个使用继承的子类爆炸。

基于装饰器模式的解决方案,UML类图展现了这个简洁灵活的解决方案。

八、组合模式

组合模式(Composite Pattern)又叫做部分-整体模式,模糊了简单元素和复杂元素的概念 ,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。组合模式让你可以优化处理递归或分级数据结构。

8.1 前言

在数据结构里面,树结构是很重要,我们可以把树的结构应用到设计模式里面。

  • 多级树形菜单

  • 文件和文件夹目录。

    我们对于这个图片肯定会非常熟悉,这两幅图片我们都可以看做是一个文件结构,对于这样的结构我们称之为树形结构。在数据结构中我们了解到可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。我们可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是**由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。**这就是组合模式的设计动机:组合模式定义了如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理。

  • 剪发卡:首先,一张卡可以在总部,分店,加盟店使用,那么总部可以刷卡,分店也可以刷卡,加盟店也可以刷卡,这个属性结构的店面层级关系就明确啦。 那么,总店刷卡消费与分店刷卡消费是一样的道理,那么总店与分店对会员卡的使用也具有一致性。

我们可以使用简单的对象组合成复杂的对象,而这个复杂对象又可以组合成更大的对象。我们可以把简单这些对象定义成类,然后定义一些容器类来存储这些简单对象。客户端代码必须区别对象简单对象和容器对象,而实际上大多数情况下用户认为它们是一样的。对这些类区别使用,就使得程序更加复杂。递归使用的时候更麻烦,而我们如何使用递归组合,使得用户不必对这些类进行区别呢?

8.2 概览

组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。

有时候又叫做部分-整体模式,它使我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。

上面的图展示了计算机的文件系统,文件系统由文件和目录组成,目录下面也可以包含文件或者目录,计算机的文件系统是用递归结构来进行组织的,对于这样的数据结构是非常适用使用组合模式的。

在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。

8.2.1 类图

  • **抽象构件角色(Component):**是组合中的对象声明接口,在适当的情况下,声明所有类共有接口的默认行为。声明一个接口用于访问和管理Component子部件。这个接口可以用来管理所有的子对象。在递归结构中定义一个接口,用于访问一个父部件,并在合适的情况下实现它。
  • **树叶构件角色(Leaf):**在组合树中表示叶节点对象,叶节点没有子节点。并在组合中定义图元对象的行为。
  • **树枝构件角色(Composite):**定义有子部件的那些部件的行为以及存储子部件。在Component接口中实现与子部件有关的操作。
  • **客户角色(Client):**通过Component接口操纵组合部件的对象。

从模式结构中我们看出了叶子节点和容器对象都实现Component接口,这也是能够将叶子对象和容器对象一致对待的关键所在。

8.2.2 分类

  • 将管理子元素的方法定义在Composite类中
  • 将管理子元素的方法定义在Component接口中,此时这样Leaf类就需要对这些方法空实现。

8.2.3 适用场景

  • 当发现需求中是体现部分与整体层次结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑组合模式了。

8.3 实现

在文件系统中,可能存在很多种格式的文件,如果图片,文本文件、视频文件等等,这些不同的格式文件的浏览方式都不同,同时对文件夹的浏览就是对文件夹中文件的浏览,但是对于客户而言都是浏览文件,两者之间不存在什么差别,现在只用组合模式来模拟浏览文件。

// 抽象角色:文件类,统一的接口,其实现有文件夹类与文件
public abstract class File {
    // 文件名称
    String name;
    
    public File(String name){
        this.name = name;
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public abstract void display();
}
// 组合类:文件夹类,内部含有文件类对象(文件夹和文件)
public class Folder extends File{
    // 文件夹中的内容列表(文件夹或文件)
    // 该部分在调用了Folder的构造方法后执行,创建一个新的子文件列表
    private List<File> files = new ArrayList<File>();
    
    public Folder(String name){
        super(name);
    }
    
    /**
     * 浏览文件夹中的文件
     */
    public void display() {
        for(File file : files){
            file.display();
        }
    }
    
    /**
     * 向文件夹中添加文件
     */
    public void add(File file){
        files.add(file);
    }
    
    /**
     * 从文件夹中删除文件
     */
    public void remove(File file){
        files.remove(file);
    }
}
// 叶子节点:具体的文件类型
public class TextFile extends File{

    public TextFile(String name) {
        super(name);
    }

    public void display() {
        System.out.println("这是文本文件,文件名:" + super.getName());
    }
    
}
// 叶子节点:具体的文件类型
public class ImagerFile extends File{

    public ImagerFile(String name) {
        super(name);
    }

    public void display() {
        System.out.println("这是图像文件,文件名:" + super.getName());
    }

}
// 叶子节点:具体的文件类型
public class VideoFile extends File{

    public VideoFile(String name) {
        super(name);
    }

    public void display() {
        System.out.println("这是影像文件,文件名:" + super.getName());
    }

}
// 客户端
public class Client {
    public static void main(String[] args) {
        /**
         * 我们先建立一个这样的文件系统
         *                  总文件
         *                  
         *   a.txt    b.jpg                   c文件夹              
         *                      c_1.text  c_1.rmvb    c_1.jpg   
         *                                                       
         */ 
        // 总文件夹
        Folder zwjj = new Folder("总文件夹");
        // 向总文件夹中放入三个文件:1.txt、2.jpg、1文件夹
        TextFile aText= new TextFile("a.txt");
        ImagerFile bImager = new ImagerFile("b.jpg");
        Folder cFolder = new Folder("C文件夹");
        
        // 向总文件夹中添加文件
        zwjj.add(aText);
        zwjj.add(bImager);
        zwjj.add(cFolder);
        
        // 向C文件夹中添加文件:c_1.txt、c_1.rmvb、c_1.jpg 
        TextFile cText = new TextFile("c_1.txt");
        ImagerFile cImage = new ImagerFile("c_1.jpg");
        VideoFile cVideo = new VideoFile("c_1.rmvb");
        
        cFolder.add(cText);
        cFolder.add(cImage);
        cFolder.add(cVideo);
         
        // 遍历C文件夹
        cFolder.display();
        // 将c_1.txt删除
        cFolder.remove(cText);
        System.out.println("-----------------------");
        cFolder.display();
    }
}

8.4 效果

8.4.1 特点

  • 定义了包含基本对象和组合对象的类层次结构

    基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断的递归下去。客户代码中,任何用到基本对象的地方都可以使用组合对象。

  • 简化客户代码

    客户可以一致地使用组合结构和单个对象。通常用户不知道 (也不关心)处理的是一个叶节点还是一个组合组件。这就简化了客户代码 , 因为在定义组合的那些类中不需要写一些充斥着选择语句的函数。

  • 使得更容易增加新类型的组件

    新定义的Composite或Leaf子类自动地与已有的结构和客户代码一起工作,客户程序不需因新的Component类而改变。

  • 使你的设计变得更加一般化

    容易增加新组件也会产生一些问题,那就是很难限制组合中的组件。有时你希望一个组合只能有某些特定的组件。使用Composite时,你不能依赖类型系统施加这些约束,而必须在运行时刻进行检查。

8.4.2 优缺点

(1)优点
  • 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
  • 客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
  • 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
  • 更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。
(2)缺点

使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联

8.5 相关模式

  • 装饰模式Decorator经常与Composite模式一起使用。当装饰和组合一起使用时,它们通常有一个公共的父类。因此装饰必须支持具有 Add、Remove和GetChild 操作的Component接口。

  • 享元模式Flyweight让你共享组件,但不再能引用他们的父部件。

  • 迭代器模式Iterator可用来遍历Composite。

  • 观察者模式Visitor将本来应该分布在Composite和L e a f类中的操作和行为局部化。

8.6 总结

  1. 组合模式用于将多个对象组合成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(叶子对象)和组合对象(容器对象)的使用具有一致性。

  2. 组合对象的关键在于它定义了一个抽象构建类,它既可表示叶子对象,也可表示容器对象,客户仅仅需要针对这个抽象构建进行编程,无须知道他是叶子对象还是容器对象,都是一致对待。

  3. 组合模式虽然能够非常好地处理层次结构,也使得客户端程序变得简单,但是它也使得设计变得更加抽象,而且也很难对容器中的构件类型进行限制,这会导致在增加新的构件时会产生一些问题。

九、桥接模式

桥接模式即将抽象部分与它的实现部分分离开来,使他们都可以独立变化。

9.1 前言

在软件系统中,某些类型由于自身的逻辑,它具有两个或多个维度的变化,那么如何应对这种“多维度的变化”?如何利用面向对象的技术来使得该类型能够轻松的沿着多个方向进行变化,而又不引入额外的复杂度?

设想如果要绘制矩形、圆形、椭圆、正方形,我们至少需要4个形状类,但是如果绘制的图形需要具有不同的颜色,如红色、绿色、蓝色等,此时至少有如下两种设计方案:

  • 第一种设计方案是为每一种形状都提供一套各种颜色的版本。

  • 第二种设计方案是根据实际需要对形状和颜色进行组合。

对于有两个变化维度(即两个变化的原因)的系统,采用方案二来进行设计系统中类的个数更少,且系统扩展更为方便。设计方案二即是桥接模式的应用。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。

如何应对这种“多维度的变化”?如何利用面向对象的技术来使得该类型能够轻松的沿着多个方向进行变化,而又不引入额外的复杂度?

9.2 概览

桥接模式即将抽象部分与它的实现部分分离开来,使他们都可以独立变化。桥接模式将继承关系转化成关联关系,它降低了类与类之间的耦合度,减少了系统中类的数量,也减少了代码量。

将抽象部分与他的实现部分分离并不是将抽象类与他的派生类分离,而是抽象类和它的派生类用来实现自己的对象。

  • 抽象化:抽象化就是忽略一些信息,把不同的实体当作同样的实体对待。在面向对象中,将对象的共同性质抽取出来形成类的过程即为抽象化的过程。

  • 实现化:针对抽象化给出的具体实现。它和抽象化是一个互逆的过程,实现化产生的对象比抽象化更具体,是对抽象化事物的进一步具体化的产物。

  • 脱耦:脱耦就是将抽象化和实现化之间的耦合解脱开,或者说是将它们之间的强关联改换成弱关联,将两个角色之间的继承关系改为关联关系。

将抽象部分与他的实现部分分离就是实现系统可能有多个角度分类,每一种角度都可能变化,那么把这种多角度分类给分离出来让他们独立变化,减少他们之间耦合。桥接模式中的所谓脱耦,就是指在一个软件系统的抽象化和实现化之间使用关联关系(组合或者聚合关系)而不是继承关系,从而使两者可以相对独立地变化,这就是桥接模式的用意。

9.2.1 类图

  • **抽象类(Abstraction):**定义抽象类的接口,维护一个指向Implementor类型对象的指针
  • **扩充抽象类(RefinedAbstraction):**扩充由Abstraction定义的接口
  • **实现类接口(Implementor):**定义实现类的接口,该接口不一定要与 Abstraction的接口完全一致;事实上这两个接口可以完全不同。一般来讲, Implementor接口仅提供基本操作,而 Abstraction则定义了基于这些基本操作的较高层次的操作,如形状与上色(形状就为高级操作)
  • **具体实现类(ConcreteImplementor):**实现Implementor接口并定义它的具体实现

9.2.2 适用场景

  • 你不希望在抽象和他的实现部分之间有一个固定的绑定关系,如在程序的运行时刻实现部分应该可以被选择或者切换。

  • 类的抽象以及他的实现都可以通过生成子类的方法加以扩充。这时bridge模式使你可以对不同的抽象接口和实现部分进行组合,并对他们进行扩充。

  • 对一个抽象的实现部分的修改应该对客户不产生影响,即客户的代码不需要重新编译。

  • 你想对客户完全隐藏抽象的实现部分。

  • 你想在多个实现间共享实现,但同时要求客户并不知道这一点。

9.3 实现

/**
 * 抽象类
 * 形状类,内聚合有一个实现类接口对象Color
 * 此为将抽象部分与他的实现部分分离,以实现更多层次的组合
 */
public abstract class Shape {

    // 采用聚合的方式,而不是让实现类去继承本抽象类
    // 聚合一个实现类接口对象,以实现上色的高层次操作
    private Color color;

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }

    public abstract void draw();
}
/**
 * 扩充抽象类
 * 具体的图形类
 * 抽象角色的扩充
 */
public class Circle extends Shape {

    @Override
    public void draw() {
        super.getColor().paint("这是一个圆....");
    }
}

public class Rectangle extends Shape {

    @Override
    public void draw() {
        super.getColor().paint("这是一个长方形....");
    }
}

public class Square extends Shape {
    @Override
    public void draw() {
        super.getColor().paint("这是一个正方形....");
    }
}
/**
 * 实现类接口,该接口并不继承于抽象类,而是将该实现类接口聚合至抽象类中
 * 该接口与抽象类的方法不一样
 * 是一种对于抽象类的基本操作,抽象类则含有该接口对象,以实现更高层次的操作
 */
public interface Color {
    public void paint(String shape);
}
/**
 * 具体实现类
 * 实现接口中的方法
 */
public class Black implements Color {
    @Override
    public void paint(String shape) {
        System.out.println(shape+"黑色的");
    }
}

public class Gray implements Color {
    @Override
    public void paint(String shape) {
        System.out.println(shape+"灰色的");
    }
}

public class Red implements Color {
    @Override
    public void paint(String shape) {
        System.out.println(shape+"红色的");
    }
}
public class Client {

    public static void main(String[] args) {
        // 从两个角度定义
        //1 上色
        Color black = new Black();

        //2 图形选择
        Shape circle = new Circle();

        circle.setColor(black);
        circle.draw();
    }
}

9.4 特点

9.4.1 优点

Bridge模式有以下一些优点:

  • 分离接口及其实现部分

    一个实现未必不变地绑定在一个接口上。抽象类的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现。将Abstraction与Implementor分离有助于降低对实现部分编译时刻的依赖性,当改变一个实现类时,并不需要重新编译 Abstraction类和它的客户程序。为了保证一个类库的不同版本之间的二进制兼容性,一定要有这个性质。另外,接口与实现分离有助于分层,从而产生更好的结构化系统,系统的高层部分仅需知道Abstraction和Implementor即可

  • 提高可扩充性

    由于模式中的抽象类与实现类接口均可被扩展,所以要添加或删除功能将变得更方便。你可以独立地对Abstraction和Implementor层次结构进行扩充,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。同时,方便两部分的任意组合,以实现不同的效果。

  • 实现细节对客户透明

    你可以对客户隐藏实现细节,例如共享 Implementor对象以及相应的引用计数机制(如果有的话)

9.4.2 缺点

桥接模式的缺点

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

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

9.5 相关模式

  • 抽象工厂Abstract Factory 模式可以用来创建和配置一个特定的Bridge模式。

  • 适配器模式用来帮助无关的类协同工作,它通常在系统设计完成后才会被使用。然而,Bridge模式则是在系统开始时就被使用,它使得抽象接口和实现部分可以独立进行改变。

  • 桥接模式与装饰的区别:

    • 装饰模式:

      这两个模式在一定程度上都是为了减少子类的数目,避免出现复杂的继承关系。但是它们解决的方法却各有不同,装饰模式把子类中比基类中多出来的部分放到单独的类里面,以适应新功能增加的需要,当我们把描述新功能的类封装到基类的对象里面时,就得到了所需要的子类对象,这些描述新功能的类通过组合可以实现很多的功能组合

    • 桥接模式:

      桥接模式则把原来的基类的实现化细节抽象出来,在构造到一个实现化的结构中,然后再把原来的基类改造成一个抽象化的等级结构,这样就可以实现系统在多个维度上的独立变化 。

9.6 总结

  1. 桥接模式实现了抽象化与实现化的脱耦。他们两个互相独立,不会影响到对方
  2. 对于两个独立变化的维度,使用桥接模式再适合不过了
  3. 对于“具体的抽象类”所做的改变,是不会影响到客户

十、外观模式

为复杂的子系统提供单一的统一接口。通过为最重要的用力提供接口,能够简化大型和复杂系统的使用。

10.1 前言

现代的软件系统都是比较复杂的,设计师处理复杂系统的一个常见方法便是将其“分而治之”,把一个系统划分为几个较小的子系统。如果把医院作为一个子系统,按照部门职能,这个系统可以划分为挂号、门诊、划价、化验、收费、取药等。看病的病人要与这些部门打交道,就如同一个子系统的客户端与一个子系统的各个类打交道一样,不是一件容易的事情。

首先病人必须先挂号,然后门诊。如果医生要求化验,病人必须首先划价,然后缴费,才可以到化验部门做化验。化验后再回到门诊室。

上图描述的是病人在医院里的体验,图中的方框代表医院。

解决这种不便的方法便是引进门面模式,医院可以设置一个接待员的位置,由接待员负责代为挂号、划价、缴费、取药等。这个接待员就是门面模式的体现,病人只接触接待员,由接待员与各个部门打交道。

10.2 概述

许多复杂的系统可以简化为几个子系统暴露的用例接口,这样可以让客户端代码不需要知道子系统的内部结构与联系。即客户端代码和复杂的子系统解耦,并且能让开发人员更简单地使用子系统,这被称为外观模式,其中外观对象负责暴露所有子系统的功能。与隐藏了对象的内部结构和逻辑的封装类似,外观模式隐藏了子系统的复杂内部结构,只向外提供可访问的通用接口,这样做的结果是用户只能访问由外观模式向外提供的功能,无法随意使用或者重用子系统内部的某些具体功能函数。

外观模式需要适配多个内部子系统接口到一个客户端代码接口上,它通过创建一个新的接口来实现这一点,该接口由适配器模式来适配现有的接口(有时需要多个旧类来为新代码提供所需功能)。

10.2.1 类图

**外观模式:**为子系统中的一组接口提供一个一致的界面, Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。引入外观角色之后,用户只需要直接与外观角色交互,用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度。

门面模式没有一个一般化的类图描述,最好的描述方法实际上就是以一个例子说明。

由于门面模式的结构图过于抽象,因此把它稍稍具体点。假设子系统内有三个模块,分别是ModuleA、ModuleB和ModuleC,它们分别有一个示例方法,那么此时示例的整体结构图如下:

  • **外观角色(Facade):**是模式的核心,他被客户client角色调用,知道各个子系统的功能。同时根据客户角色已有的需求预订了几种功能组合。

  • **子系统角色(Subsystem classes):**实现子系统的功能,并处理由Facade对象指派的任务。对子系统而言,Facade和Client角色是未知的,没有Facade的任何相关信息;即没有指向Facade的实例。

  • **客户角色(Client):**调用Facade角色获得完成相应的功能。

10.2.2 适用场景

  • 如果你希望为一个复杂的子系统提供一个简单接口的时候,可以考虑使用外观模式,使用外观对象来实现大部分客户需要的功能,从而简化客户的使用
  • 如果想要让客户程序和抽象类的实现部分松散耦合,可以考虑使用外观模式,使用外观对象来将这个子系统与它的客户分离开来,从而提高子系统的独立性和可移植性
  • 如果构建多层结构的系统,可以考虑使用外观模式,使用外观对象作为每层的入口,这样可以简化层间调用,也可以松散层次之间的依赖关系

10.3 实现

/**
 * 复杂的子系统类,这些子系统类是独立的,其内部的操作是很复杂的
 * 电视类
 */
public class Television {

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

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

}

/**
 * 电灯类
 */
public class Light {

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

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

}

/**
 * 空调类
 */
public class AirCondition {

    public void on(){
        System.out.println("打开了空调....");
    }

    public void off(){
        System.out.println("关闭了空调....");
    }

}

/**
 * 屏幕类
 */
public class Screen {

    public void up(){
        System.out.println("升起银幕....");
    }

    public void down(){
        System.out.println("下降银幕....");
    }

}
/**
 * 外观类
 * 关联所有的子系统类对象
 */
public class Facade {

    // 关联子系统类对象
    Television television;
    Light light;
    AirCondition airCondition;
    Screen screen;

    public Facade(Television television, 
                  Light light, 
                  AirCondition airCondition, 
                  Screen screen) 
    {
        this.television = television;
        this.light = light;
        this.airCondition = airCondition;
        this.screen = screen;
    }

    // 定义任意的方法可以任意组合任意的操作
    public void on(){
        //1 打开全部的设备
        television.on();
        light.on();
        airCondition.on();
        screen.up();
    }

    public void on01(){
        //2 打开部分设备
        television.on();
        light.on();
    }

    public void off(){
        television.off();
        light.off();
        airCondition.off();
        screen.down();
    }

}
/**
 * 客户端类
 */
public class Client {

    public static void main(String[] args) {

        //实例化组件
        Light light = new Light();
        Television tv = new Television();
        AirCondition ac = new AirCondition();
        Screen screen = new Screen();

        // 创建外观类
        Facade facade = new Facade(tv,light,ac,screen);

        facade.on();
        facade.off();

    }

}

在门面模式中,通常只需要一个门面类,并且此门面类只有一个实例,换言之它是一个单例类。当然这并不意味着在整个系统里只有一个门面类,而仅仅是说对每一个子系统只有一个门面类。或者说,如果一个系统有好几个子系统的话,每一个子系统都有一个门面类,整个系统可以有数个门面类。

10.4 特点

10.4.1 优点

  • 对客户屏蔽子系统组件,减少了客户处理的对象数目并使得子系统使用起来更加容易。通过引入外观模式,客户代码将变得很简单,与之关联的对象也很少。

  • 实现了子系统与客户之间的松耦合关系,这使得子系统的组件变化不会影响到调用它的客户类,只需要调整外观类即可。

  • 降低了大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。

  • 只是提供了一个访问子系统的统一入口,并不影响用户直接使用子系统类。

10.4.2 缺点

  • 不能很好地限制客户使用子系统类,如果对客户访问子系统类做太多的限制则减少了可变性和灵活性。

  • 在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。

10.5 相关模式

  • **抽象工厂模式:**Abstract Factory式可以与Facade模式一起使用以提供一个接口,这一接口可用来以一种子系统独立的方式创建子系统对象。 Abstract Factory也可以代替Facade模式隐藏那些与平台相关的类。

  • **中介模式:**Mediator模式与Facade模式的相似之处是,它抽象了一些已有的类的功能。然而,Mediator的目的是对同事之间的任意通讯进行抽象,通常集中不属于任何单个对象的功能。Mediator的同事对象知道中介者并与它通信,而不是直接与其他同类对象通信。相对而言,Facade模式仅对子系统对象的接口进行抽象,从而使它们更容易使用;它并不定义新功能,子系统也不知道Facade的存在。通常来讲,仅需要一个Facade对象,因此Facade对象通常属于Singleton模式。

  • Adapter模式:

    适配器模式是将一个接口通过适配来间接转换为另一个接口。外观模式的话,其主要是提供一个整洁的一致的接口给客户端。

10.6 总结

  • 根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。

  • 外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,外观类充当了客户类与子系统类之间的“第三者”,同时降低客户类与子系统类的耦合度。外观模式就是实现代码重构以便达到“迪米特法则”要求的一个强有力的武器。

  • 外观模式最大的缺点在于违背了“开闭原则”。当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。

  • 外观模式要求一个子系统的外部与其内部的通信通过一个统一的外观对象进行,外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。

  • 外观模式从很大程度上提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节,通过外观角色即可调用相关功能。

  • 不要试图通过外观类为子系统增加新行为 ,不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的。外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,新的行为的增加应该通过修改原有子系统类或增加新的子系统类来实现,不能通过外观类来实现。

十一、享元模式

运行共享技术有效地支持大量细粒度对象的复用。系统使用少量对象,而且这些都比较相似,状态变化小,可以实现对象的多次复用。

11.1 前言

在一个系统中如果有多个相同的对象,那么只共享一份就可以了,不必每个都去实例化一个对象。

  • 一个文本系统,每个字母定一个对象,那么大小写字母一共就是52个,那么就要定义52个对象。如果有一个1M的文本,那么字母是何其的多,如果每个字母都定义一个对象那么内存早就爆了。那么如果要是每个字母都共享一个对象,那么就大大节约了资源。
  • 在JAVA语言中,String类型就是使用了享元模式。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,JAVA会确保一个字符串常量在常量池中只有一个拷贝。String a=“abc”,其中"abc"就是一个字符串常量。
  • 五子棋游戏,五子棋同围棋一样,包含多个“黑”或“白”颜色的棋子,所以用享元模式比较好。

11.2 概述

面向对象可以非常方便的解决一些扩展性的问题,但是在这个过程中系统务必会产生一些类或者对象,如果系统中存在对象的个数过多时,将会导致系统的性能下降。对于这样的问题解决最简单直接的办法就是减少系统中对象的个数。

享元模式提供了一种解决方案,使用共享技术实现相同或者相似对象的重用。也就是说实现相同或者相似对象的代码共享。共享模式是支持大量细粒度对象的复用,所以享元模式要求能够共享的对象必须是细粒度对象。

  • 内部状态:在享元对象内部不随外界环境改变而改变的共享部分
  • 外部状态:随着环境的改变而改变,不能够共享的状态就是外部状态

在实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种对象也成为细粒度对象。

由于享元模式区分了内部状态和外部状态,所以我们可以通过设置不同的外部状态使得相同的对象可以具备一些不同的特性,而内部状态设置为相同部分。在我们的程序设计过程中,我们可能会需要大量的细粒度对象来表示对象,如果这些对象除了几个参数不同外其他部分都相同,这个时候我们就可以利用享元模式来大大减少应用程序当中的对象。

如何利用享元模式呢?这里我们只需要将他们少部分的不同的部分当做参数移动到类实例的外部去,然后再方法调用的时候将他们传递过来就可以了。这里也就说明了一点:内部状态存储于享元对象内部,而外部状态则应该由客户端来考虑,当调用Flyweight对象的操作时,将该状态传递给它。

11.2.1 类图

  • **Flyweight:**抽象享元类。所有具体享元类的超类或者接口,通过这个接口,Flyweight可以接受并作用于外部状态
  • **ConcreteFlyweight:**具体享元类。指定内部状态,为内部状态增加存储空间
  • **UnsharedConcreteFlyweight:**非共享具体享元类。指出那些不需要共享的Flyweight子类
  • FlyweightFactory: 享元工厂类。用来创建并管理Flyweight对象,它主要用来确保合理地共享Flyweight,当用户请求一个Flyweight时,FlyweightFactory就会提供一个已经创建的Flyweight对象或者新建一个(如果不存在)

享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,用唯一标识码(key)判断。用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。Flyweight模式是一个提高程序效率和性能的模式,会大大加快程序的运行速度。

11.2.2 适用场景

  • 如果一个系统中存在大量的相同或者相似的对象,由于这类对象的大量使用,会造成系统内存的耗费,可以使用享元模式来减少系统中对象的数量。

  • 大部分的对象可以按照内部状态进行分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。

  • 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。

11.3 实现

假如我们有一个绘图的应用程序,通过它我们可以绘制出各种各样的形状、颜色的图形,那么这里形状和颜色就是内部状态了,通过享元模式我们就可以实现该属性的共享了。

/**
 * 抽象的享元类
 */
public abstract class Shape {

    public abstract void draw(User user);

}
/**
 * 具体享元类
 * 内部对象包含有内部状态信息:这些信息都是细粒度对象信息,如形状与颜色
 * 内部对象>=内部状态
 */
public class Circle extends Shape {

    private String color;
    public Circle(String color){
        this.color = color;
    }

    public void draw(User user) {
        System.out.println(user.getName()+"画了一个" + color +"的圆形");
    }
}
/**
 * 非共享具体享元类
 * 外部对象:包含有外部状态的信息
 */
public class User {

    private String name;
    //创建User构造函数,获取用户名
    public User (String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return name;
    }

}
/**
 * 享元工厂类
 * 使用Map结构来提供一个用于存储享元对象的享元池
 */
public class FlyweightFactory {

    static Map<String, Shape> shapes = new HashMap<String, Shape>();

    public static Shape getShape(String key){
        Shape shape = shapes.get(key);
        //如果shape==null,表示不存在,则新建,并且保持到共享池中
        if(shape == null){
            shape = new Circle(key);
            shapes.put(key, shape);
        }
        return shape;
    }

    public static int getSum(){
        return shapes.size();
    }

}
/**
 * 客户端
 */
public class Client {

    public static void main(String[] args) {

        /**
         * 客户端定义外部状态类
         * 根据需要由客户端进行配置
         */
        User zdp = new User("zdp");
        User zjx = new User("zjx");

        // 由于享元工厂中定义的是静态方法,所以可以使用类名+方法名的方式进行调用
        Shape shape1 = FlyweightFactory.getShape("红色");
        shape1.draw(zdp);

        Shape shape2 = FlyweightFactory.getShape("灰色");
        shape2.draw(zdp);

        Shape shape3 = FlyweightFactory.getShape("绿色");
        shape3.draw(zdp);

        Shape shape4 = FlyweightFactory.getShape("红色");
        shape4.draw(zdp);

        Shape shape5 = FlyweightFactory.getShape("灰色");
        shape5.draw(zjx);

        Shape shape6 = FlyweightFactory.getShape("灰色");
        shape6.draw(zjx);

        System.out.println("一共绘制了"+FlyweightFactory.getSum()+"种颜色的圆形");
    }

}

11.4 特点

11.4.1 优点

  • 享元模式的优点在于它能够极大的减少系统中对象的个数。

  • 享元模式由于使用了外部状态,外部状态相对独立,不会影响到内部状态,所以享元模式使得享元对象能够在不同的环境被共享。

11.4.2 缺点

  • 由于享元模式需要区分外部状态和内部状态,使得应用程序在某种程度上来说更加复杂化了。

  • 为了使对象可以共享,享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。

11.5 相关模式

  • 创建型模式:

    创建对象都需要花费时间和资源。最好的例子就是创建Java常量字符串,因为从不创建实例,他们返回的是不可变的缓存实例。应用程序使用对象池来达到加速创建的目的,并维持一个一个较低的内存占有率。建造者模式与享元模式的区别是,创建型模式是保存可变域对象的容器,而享元模式是保存不可变域对象。由于是不可变的,因此他们的内部状态是在创建时设置的,并且在每个方法调用时从外部给出外部状态。

  • 外观模式:

    享元模式知道如何制作很多的小对象,而外观模式时单个对象简化并隐藏由许多对象组成的子系统的复杂性。

11.6 总结

  • 享元模式可以极大地减少系统中对象的数量。但是它可能会引起系统的逻辑更加复杂化。

  • 享元模式的核心在于享元工厂,它主要用来确保合理地共享享元对象。

  • 内部状态为不变共享部分,存储于享元享元对象内部,而外部状态是可变部分,它应当油客户端来负责。

更多推荐

设计模式——结构型模式

本文发布于:2023-06-14 08:43:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1457824.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:模式   结构

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!