【Design Pattern】Structural — Decorator

Posted by 西维蜀黍 on 2019-04-09, Last Modified on 2022-12-10

定义

装饰器模式以对客户端透明的方式,给一个对象附加上更多的责任(以实现对对象功能的扩展)。

换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰器模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰器模式的模式动机。

UML

  • Component:接口,定义一个抽象接口,真实对象和装饰对象具有相同的接口,以便动态的添加职责
  • ConcreteComponent:具体的组件,e.g., Component1
  • Decorator:装饰器类,继承了Component接口,目的是扩展Component类的功能,并且持有一个构建引用,进行请求转发
  • ConcreteDecorator:具体装饰器类,用于给实际对象添加职责,e.g., Decorator1,Decorator2。

the abstract Decorator class maintains a reference (component) to the decorated object (Component) and forwards all requests to it (component.operation()). This makes Decorator transparent (invisible) to clients of Component.

Subclasses (Decorator1,Decorator2) implement additional behavior (addBehavior()) that should be added to the Component (before/after forwarding a request to it). The sequence diagram shows the run-time interactions: The Client object works through Decorator1 and Decorator2 objects to extend the functionality of a Component1 object. The Client calls operation() on Decorator1, which forwards the request to Decorator2. Decorator2 performs addBehavior() after forwarding the request to Component1 and returns to Decorator1, which performs addBehavior() and returns to the Client.

组件接口(Component)角色

public interface Component {
       public void sampleOperation();
}

具体组件(Concrete Component)角色

public class ConcreteComponent implements Component {
    @Override
    public void sampleOperation() {
        // 写相关的业务代码
    }
}

装饰器(Decorator)角色

public abstract class Decorator implements Component{
    private Component component;
    
    public Decorator(Component component){
        this.component = component;
    }

    @Override
    public void sampleOperation() {
        // 委派给组件
        component.sampleOperation();
    }
    
}

具体装饰器(Concrete DecoratorA)角色

public class ConcreteDecoratorA extends Decorator {

    public ConcreteDecoratorA(Component component) {
        super(component);
    }
    
    @Override
    public void sampleOperation() {
     super.sampleOperation();
        // 写相关的业务代码
    }
}

具体装饰器(Concrete DecoratorB)角色

public class ConcreteDecoratorB extends Decorator {

    public ConcreteDecoratorB(Component component) {
        super(component);
    }
    
    @Override
    public void sampleOperation() {
      super.sampleOperation();
        // 写相关的业务代码
    }
}

适用环境

在以下情况下,可以使用装饰器模式:

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

例子

现在考虑这样一个场景,现在有一个煎饼摊,人们去买煎饼(Pancake),有些人要加火腿(Ham)的,有些人要加鸡蛋(Egg)的,有些人要加生菜(Lettuce)的,当然土豪可能有啥全给加上^_^。用上述的装饰器模式来进行编码。

1 定义煎饼接口IPancake

/**
 * 定义一个煎饼接口
 */
public interface IPancake {
    /**
     * 定义烹饪的操作
     */
    void cook();
}

2 定义具体的煎饼Pancake

/**
 * 具体的煎饼对象,可用其他装饰类进行动态扩展。
 */
public class Pancake implements IPancake{
    public void cook() {
        System.out.println("的煎饼");
    }
}

3 定义抽象装饰类PancakeDecorator

/**
 * 实现接口的抽象装饰类,建议设置成abstract.
 */
public abstract class PancakeDecorator implements IPancake {

    /***/
    private IPancake pancake;

    public PancakeDecorator(IPancake pancake) {
        this.pancake = pancake;
    }

    public void cook() {
        if (this.pancake != null) {
            pancake.cook();
        }
    }
}

4 具体装饰类EggDecorator

/**
 * 对煎饼加鸡蛋的装饰类,继承PancakeDecorator,覆盖cook操作
 */
public class EggDecorator extends PancakeDecorator {
    public EggDecorator(IPancake pancake) {
        super(pancake);
    }

    /**
     * 覆盖cook方法,加入自身的实现,并且调用父类的cook方法,也就是构造函数中
     * EggDecorator(IPancake pancake),这里传入的pancake的cook操作
     */
    @Override
    public void cook() {
        System.out.println("加了一个鸡蛋,");
        super.cook();
    }
}

5 具体装饰类HamDecorator

/**
 * 对煎饼加火腿的装饰类,继承PancakeDecorator,覆盖cook操作
 */
public class HamDecorator extends PancakeDecorator {
    public HamDecorator(IPancake pancake) {
        super(pancake);
    }

    /**
     * 覆盖cook方法,加入自身的实现,并且调用父类的cook方法,也就是构造函数中
     * EggDecorator(IPancake pancake),这里传入的pancake的cook操作
     */
    @Override
    public void cook() {
        System.out.println("加了一根火腿,");
        super.cook();
    }
}

6 具体装饰类LettuceDecorator

/**
 * 对煎饼加生菜的装饰类,继承PancakeDecorator,覆盖cook操作
 */
public class LettuceDecorator extends PancakeDecorator {
    public LettuceDecorator(IPancake pancake) {
        super(pancake);
    }

    /**
     * 覆盖cook方法,加入自身的实现,并且调用父类的cook方法,也就是构造函数中
     * EggDecorator(IPancake pancake),这里传入的pancake的cook操作
     */
    @Override
    public void cook() {
        System.out.println("加了一颗生菜,");
        super.cook();
    }
}

7 客户端调用以及结果

/**
 * 调用客户端
 */
public class Client {
    public static void main(String[] args) {
        System.out.println("=========我是土豪都给我加上===========");
        IPancake p1 = new Pancake();
        IPancake p2 = new EggDecorator(p1);
        IPancake p3 = new HamDecorator(p222);
        IPancake p4 = new LettuceDecorator(p3);
        panckeWithEggAndHamAndLettuce.cook();

        System.out.println("==========我是程序猿,加两个鸡蛋补补==============");
        IPancake p11 = new Pancake();
        IPancake p12 = new EggDecorator(p11);
        IPancake p13 = new EggDecorator(p12);
        pancakeWithTwoEgg.cook();
    }
}

输出结果

=========我是土豪都给我加上===========
加了一颗生菜,
加了一根火腿,
加了一个鸡蛋,
的煎饼
==========我是程序猿,加两个鸡蛋补补==============
加了一个鸡蛋,
加了一个鸡蛋,
的煎饼
复制代码

总结

关于装饰器模式的使用,在我看来主要有一下几点需要注意:

  • 抽象装饰器和具体被装饰的对象实现同一个接口;
  • 抽象装饰器里面要持有接口对象,以便请求传递;
  • 具体装饰器覆盖抽象装饰器方法并用super进行调用,传递请求。

装饰器模式优缺点

装饰模式的优点

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

装饰模式的缺点

  • 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
  • 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。

进一步讨论

装饰器模式与继承关系

装饰器模式与继承关系的目的都是要扩展对象的功能,但是装饰器模式可以提供比继承更多的灵活性。装饰器模式允许系统动态决定“贴上”一个需要的“装饰”,或者除掉一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了。

通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。

decorator 模式与 adapter/proxy 模式差异

decorator 模式与 adapter/proxy 模式有相同点,相同点在于它们都会增加新的 functionality 给 Component。但区别在于在decorator 模式中,要求Decorator类需要继承了Component接口,而在adapter/proxy 模式中,并没有要求proxy类一定要实现与Adaptee实现了的接口。

这意味着,在没有Class的programming lanuage中(比如Golang),没有decorator 模式。

proxy 模式增加的 functionality 相对来说和 Component本身的 functionality没有关系,比如如果要增加权限管理,则通过proxy 模式更合适,因为权限管理和狭义的业务逻辑没有关系。

模式的简化

大多数情况下,装饰模式的实现都比在上面定义中给出的示意实现要简单。对模式进行简化时,需要注意以下的情况:

1 一个装饰器类的接口必须与被装饰的类的接口相容。

ConcreteDecorator类和ConcreteComponent必须继承自一个共同的父类,这个在装饰模式结构图中就有说明,但是在实际使用时,如果在模式的实现上有所简化,就必须特别注意这一点。

2 尽量保持Component作为一个“轻”类。

抽象构件的职责是接口而不是存储数据吗,注意不要把太多的逻辑和状态放在Component类里。Component可以是接口、抽象类或者具体类。

3 如果只有一个ComcreteComponent类而没有抽象的Component类(接口),那么Decorator类经常可以是ConcreteComponent的一个子类,如下图:

由上图可知,没有抽象的接口Component也是可以的,但ConcreteComponent就要扮演双重的角色。

4 如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。

甚至只有两个ConcreteDecorator类的情况下,也可以这样做;但是具体装饰类大于三个的话,使用一个单独的抽象装饰类就有必要了,如下图:

由上图可知,没有抽象的Decorator也是可以的,只是ConcreteDecorator需要扮演双重的角色。

半透明的装饰器模式

装饰器模式和适配器模式都是“包装模式(Wrapper Pattern)”,它们都是通过封装其他对象达到设计的目的的,但是它们的形态有很大区别。

理想的装饰器模式在对被装饰对象进行功能增强的同时,要求具体组件角色、装饰器角色的接口与抽象组件角色的接口完全一致。而适配器模式则不然,一般而言,适配器模式并不要求对源对象(或者说被适配类)的功能进行增强,但是会改变源对象的接口,以便和目标接口相符合。

装饰器模式有透明和半透明两种,这两种的区别就在于装饰器角色的接口与抽象组件角色的接口是否完全一致。

  • 透明的装饰器模式也就是理想的装饰器模式,要求具体组件角色、装饰器角色的接口与抽象组件角色的接口完全一致
  • 相反,如果装饰器角色的接口与抽象组件角色接口不一致,也就是说装饰器角色的接口比抽象组件角色的接口宽的话,装饰器角色实际上已经成了一个适配器角色,这种装饰器模式也是可以接受的,称为**“半透明”的装饰器模式**,如下图所示。

装饰器模式的应用

装饰器模式在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。

Reference