装饰者(Decorator)模式

最近在学习MyBatis框架原理的时候,发现其实现二级缓存的过程中运用到了装饰者模式,所以来深入了解一下

介绍

装饰者模式又名包装(Wrapper)模式,以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案
装饰者模式动态地将责任附加到对象身上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案

结构

装饰者模式以对客户透明的方式动态地给一个对象附加上更多的责任,客户端并不会觉得对象在装饰前和装饰后有什么不同,装饰者模式可以在不使用创造更多子类的情况下,将对象的功能加以扩展

装饰者模式类图

装饰者模式中的角色:

  • 抽象构件(Component):给出一个抽象接口,以规范准备接收附加责任的对象
  • 具体构件(ConcreteComponent):定义一个将要接收附加责任的类
  • 装饰者(Decorator):持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口
  • 具体装饰者(ConcreteDecorator):负责给构件对象加上附加的责任
抽象构件
public interface Component{
    public void sampleOperation();
}
具体构件
public class ConcreteComponent implements Component{
    @Override
    public void sampleOperation(){
        //相关业务代码
    }
}
装饰者
public class Decorator implements Component{
    private Component component;

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

    @Override
    public void sampleOperation() {
        // 委派给构件
        component.sampleOperation();
    }   
}
具体装饰者
public class ConcreteDecoratorA extends Decorator {
    public ConcreteDecoratorA(Component component) {
        super(component);
    }

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

具体实现

定义被装饰者
public interface Human {  
    public void wearClothes();  
  
    public void walkToWhere();  
}  
定义装饰者
public abstract class Decorator implements Human {  
    private Human human;  
  
    public Decorator(Human human) {  
        this.human = human;  
    }  
  
    public void wearClothes() {  
        human.wearClothes();  
    }  
  
    public void walkToWhere() {  
        human.walkToWhere();  
    }  
}  
定义三种装饰,这是第一个,第二个第三个功能依次细化,即装饰者的功能越来越多
public class Decorator_zero extends Decorator {  
    public Decorator_zero(Human human) {  
        super(human);  
    }  
  
    public void goHome() {  
        System.out.println("进房子。。");  
    }  
  
    public void findMap() {  
        System.out.println("书房找找Map。。");  
    }  
  
    @Override  
    public void wearClothes() {  
        // TODO Auto-generated method stub  
        super.wearClothes();  
        goHome();  
    }  
  
    @Override  
    public void walkToWhere() {  
        // TODO Auto-generated method stub  
        super.walkToWhere();  
        findMap();  
    }  
}  
  
public class Decorator_first extends Decorator {  
    public Decorator_first(Human human) {  
        super(human);  
    }  
  
    public void goClothespress() {  
        System.out.println("去衣柜找找看。。");  
    }  
  
    public void findPlaceOnMap() {  
        System.out.println("在Map上找找。。");  
    }  
  
    @Override  
    public void wearClothes() {  
        // TODO Auto-generated method stub  
        super.wearClothes();  
        goClothespress();  
    }  
  
    @Override  
    public void walkToWhere() {  
        // TODO Auto-generated method stub  
        super.walkToWhere();  
        findPlaceOnMap();  
    }  
}  
  
public class Decorator_two extends Decorator {  
    public Decorator_two(Human human) {  
        super(human);  
    }  
  
    public void findClothes() {  
        System.out.println("找到一件D&G。。");  
    }  
  
    public void findTheTarget() {  
        System.out.println("在Map上找到神秘花园和城堡。。");  
    }  
  
    @Override  
    public void wearClothes() {  
        // TODO Auto-generated method stub  
        super.wearClothes();  
        findClothes();  
    }  
  
    @Override  
    public void walkToWhere() {  
        // TODO Auto-generated method stub  
        super.walkToWhere();  
        findTheTarget();  
    }  
}  
定义被装饰者,被装饰者初始状态有些自己的装饰
public class Person implements Human {  
    @Override  
    public void wearClothes() {  
        // TODO Auto-generated method stub  
        System.out.println("穿什么呢。。");  
    }  
  
    @Override  
    public void walkToWhere() {  
        // TODO Auto-generated method stub  
        System.out.println("去哪里呢。。");  
    }  
}  
测试类
public class Test {  
    public static void main(String[] args) {  
        Human person = new Person();  
        Decorator decorator = new Decorator_two(new Decorator_first(  
                new Decorator_zero(person)));  
        decorator.wearClothes();  
        decorator.walkToWhere();  
    }  
}  
运行结果
穿什么呢。。
进房子。。
去衣柜找找看。。
找到一件D&G。。
去哪里呢。。
书房找找Map。。
在Map上找找。。
在Map上找到神秘花园和城堡。。

其实就是进房子找衣服,然后找地图这样一个过程,通过装饰者的三层装饰,把细节变得丰富,关键点:

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

简化

大多数情况下,装饰者模式的实现都比上面给出的例子要简单
如果只有一个ConcreteComponent类,那么可以考虑去掉抽象的Component类(接口),把Decorator作为ConcreteComponent的子类

如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,可以把Decorator和ConcreteDecorator的责任合并成一个类,甚至在只有两个ConcreteDecorator的情况下,也可以这样做

透明性要求

装饰者模式对客户端的透明性要求程序不要声明一个ConcreteComponent类型的变量,而应当声明一个Component类型的变量。
然而纯碎的装饰者模式很难找到,装饰者模式的用意是在不改变接口的前提下,增强所考虑的类的性能,在增强性能的时候,往往需要建立新的公开的方法。这就导致了大多数的装饰者模式的实现都是“半透明”的,而不是完全透明的,换言之,允许装饰者模式改变接口,增加新的方法,这意味着客户端可以声明ConcreteDecorator类型的变量,从而可以调用ConcreteDecorator类中才有的方法。
半透明的装饰者模式是介于装饰者模式和适配器模式之间的,适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数装饰者模式实际上是半透明的装饰者模式,这样的装饰者模式也称作半装饰、半适配器模式
装饰者模式和适配器模式都是包装模式(Wrapper Pattern),它们都是通过封装其他对象达到设计的目的,但是它们的形态有很大区别
理想的装饰者模式在对被装饰对象进行功能增强的同时,要求具体构件、装饰者的接口与抽象构件的接口完全一致,而适配器模式则不然,一般而言,适配器模式并不要求对源对象的功能进行增强,但是会改变源对象的接口,以便和目标接口相符合
装饰者模式有透明和半透明两种,这两种的区别就在于装饰者的接口与抽象构件的接口是否完全一致。透明的装饰者模式也就是理想的装饰者模式,要求具体构件、装饰者的接口与抽象构件的接口完全一致。相反,如果装饰者的接口与抽象构件的接口不一致,也就是说装饰者的接口比抽象构件的接口宽的话,装饰者实际上已经成了一个适配器,这种装饰者模式也是可以接受的,称为“半透明”的装饰模式

在适配器模式里面,适配器类的接口通常会与目标类的接口重叠,但往往并不完全相同,换言之,适配器类的接口会比被装饰的目标类接口宽
显然,半透明的装饰者模式实际上就是处于适配器模式与装饰者模式之间的灰色地带。如果将装饰者模式与适配器模式合并成为一个“包装模式”的话,那么半透明的装饰者模式倒可以成为这种合并后的“包装模式”的代表

优缺点

优点
  • 装饰者模式与继承关系的目的都是要扩展对象的功能,但是装饰者模式可以提供比继承更多的灵活性。装饰者模式允许系统动态决定加上一个需要的装饰,或者除掉一个不需要的装饰。继承关系则不同,继承关系是静态的,它在系统运行前就决定了
  • 通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合
缺点

由于使用装饰者模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是,另一方面,使用装饰者模式会产生比使用继承关系更多的对象。更多的对象会使得查错变得困难,特别是这些对象看上去都很相像

Java IO流中的应用

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

Java I/O库部分对象结构图

根据上图可以看出:

  • 抽象构件(Component):由InputStream扮演,这是一个抽象类,为各种子类型提供统一的接口
  • 具体构件(ConcreteComponent):由ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream等类扮演,它们实现了抽象构建所规定的接口
  • 抽象装饰者:由FilterInputStream扮演,它实现了InputStream所规定的接口
  • 具体装饰者:由BufferedInputStream、DataInputStream以及两个不常用到的类LineNumberInputStream、PushbackInputStream扮演
    InputStream类型中的装饰者模式是半透明的,看一下作为装饰者模式中抽象构件的InputStream的源代码
public abstract class InputStream implements Closeable {
    public abstract int read() throws IOException;

    public int read(byte b[]) throws IOException {}

    public int read(byte b[], int off, int len) throws IOException {}

    public long skip(long n) throws IOException {}

    public int available() throws IOException {}

    public void close() throws IOException {}

    public synchronized void mark(int readlimit) {}

    public synchronized void reset() throws IOException {}

    public boolean markSupported() {}
}

下面是作为装饰者模式中抽象装饰者的FilterInputStream的源代码

public class FilterInputStream extends InputStream {
    protected FilterInputStream(InputStream in) {}

    public int read() throws IOException {}

    public int read(byte b[]) throws IOException {}

    public int read(byte b[], int off, int len) throws IOException {}

    public long skip(long n) throws IOException {}

    public int available() throws IOException {}

    public void close() throws IOException {}

    public synchronized void mark(int readlimit) {}

    public synchronized void reset() throws IOException {}

    public boolean markSupported() {}
}

FilterInputStream的接口与InputStream的接口是完全一致的
下面是具体装饰者PushbackInputStream的源代码

public class PushbackInputStream extends FilterInputStream {
    private void ensureOpen() throws IOException {}

    public PushbackInputStream(InputStream in, int size) {}

    public PushbackInputStream(InputStream in) {}

    public int read() throws IOException {}

    public int read(byte[] b, int off, int len) throws IOException {}

    public void unread(int b) throws IOException {}

    public void unread(byte[] b, int off, int len) throws IOException {}

    public void unread(byte[] b) throws IOException {}

    public int available() throws IOException {}

    public long skip(long n) throws IOException {}

    public boolean markSupported() {}

    public synchronized void mark(int readlimit) {}

    public synchronized void reset() throws IOException {}

    public synchronized void close() throws IOException {}
}

这个装饰类提供了额外的方法unread(),这就意味着PushbackInputStream是一个半透明的装饰类,它破坏了理想的装饰者模式的要求。如果客户端持有一个类型为InputStream对象的引用in的话,如果in的真实类型是PushbackInputStream,只要客户端不需要使用unread()方法,那么客户端一般没有问题。但是如果客户端必须使用这个方法,就必须进行向下类型转换,将in的类型转换为PushbackInputStream之后才能调用这个方法。但是这个类型转换意味着客户端必须知道它拿到的引用是指向一个类型为PushbackInputStream的对象,这就破坏了使用装饰者模式的原始用意
下面是使用I/O流读取文件内容的简单操作示例

public class IOTest {
    public static void main(String[] args) throws IOException {
        // 流式读取文件
        DataInputStream dis = null;
        try{
            dis = new DataInputStream(
                    new BufferedInputStream(
                            new FileInputStream("test.txt")
                    )
            );
            //读取文件内容
            byte[] bs = new byte[dis.available()];
            dis.read(bs);
            String content = new String(bs);
            System.out.println(content);
        }finally{
            dis.close();
        }
    }
}

最里层是一个FileInputStream对象,然后把它传递给一个BufferedInputStream对象,经过BufferedInputStream处理,再把处理后的对象传递给了DataInputStream对象进行处理,这个过程其实就是装饰器的组装过程,FileInputStream对象相当于原始的被装饰的对象,而BufferedInputStream对象和DataInputStream对象则相当于装饰器

MyBatis中的装饰者模式

MyBatis在二级缓存实现中运用到了装饰者模式


当开一个会话时,一个SqlSession对象会使用一个Executor对象来完成会话操作,MyBatis的二级缓存机制的关键就是对这个Executor对象做文章。如果用户在mybatis-config.xml里配置了"cacheEnabled=true",那么MyBatis在为SqlSession对象创建Executor对象时,会对Executor对象加上一个装饰者:CachingExecutor,这时SqlSession使用CachingExecutor对象来完成操作请求。CachingExecutor对于查询请求,会先判断该查询请求在Application级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果,如果缓存中没有,再交给真正的Executor对象来完成查询操作,之后CachingExecutor会将真正Executor返回的查询结果放置到缓存中,然后再返回给用户
CachingExecutor是Executor的装饰者,以增强Executor的功能,使其具有缓存查询的功能

MyBatis自身提供了丰富的,并且功能强大的二级缓存的实现,它拥有一系列的Cache接口装饰者,可以满足各种对缓存操作和更新的策略。MyBatis定义了大量的Cache的装饰器来增强Cache缓存的功能

MyBatis自身提供的二级缓存的实现

参考1:终结篇:MyBatis原理深入解析(三)
参考2:JAVA设计模式初探之装饰者模式
参考3:设计模式详解——装饰者模式

推荐阅读更多精彩内容