装饰者模式在JDK和Mybatis中是怎么应用的?

介绍

有一个卖煎饼的店铺找上了你,希望你能给她们的店铺开发一个收银系统,已知一个煎饼的价格是8元,一个鸡蛋的价格是1元,一根香肠的价格是2元。

你二话不说写出了如下代码

// 煎饼类
public class Battercake {

    protected String getDesc() {
        return "煎饼";
    }

    protected int cost() {
        return 8;
    }
}

// 加鸡蛋的煎饼
public class BattercakeWithEgg extends Battercake{

    @Override
    public String getDesc() {
        return super.getDesc() + " 加一个鸡蛋";
    }

    @Override
    public int cost() {
        return super.cost() + 1;
    }
}

// 加鸡蛋和香肠的煎饼
public class BattercakeWithEggSausage extends BattercakeWithEgg {

    @Override
    public String getDesc() {
        return super.getDesc() + " 加一根香肠";
    }

    @Override
    public int cost() {
        return super.cost() + 2;
    }
}

测试一下,正常工作

public class Test {

    public static void main(String[] args) {

        Battercake battercake = new Battercake();
        // 煎饼 销售价格:8
        System.out.println(battercake.getDesc() + " 销售价格:" + battercake.cost());

        Battercake battercakeWithEgg = new BattercakeWithEgg();
        // 煎饼 加一个鸡蛋 销售价格:9
        System.out.println(battercakeWithEgg.getDesc() + " 销售价格:" + battercakeWithEgg.cost());

        Battercake battercakeWithEggSausage = new BattercakeWithEggSausage();
        // 煎饼 加一个鸡蛋 加一根香肠 销售价格:11
        System.out.println(battercakeWithEggSausage.getDesc() + " 销售价格:" + battercakeWithEggSausage.cost());

    }
}

但是这样会造成一个问题,煎饼的搭配种类很多。比如,加1根香肠的煎饼,加2个鸡蛋的煎饼,加2个鸡蛋和1根香肠的煎饼,如果对每一种可能都写一个实现,会造成类爆炸。

这个时候你就应该想到用装饰者模式了。来看看如何改造上面的代码

// 组件类
public abstract class ABattercake {

    protected abstract String getDesc();
    protected abstract int cost();
}

// 具体组件实现类
public class Battercake extends ABattercake {

    protected String getDesc() {
        return "煎饼";
    }

    protected int cost() {
        return 8;
    }
}

// 抽象装饰器类
public class AbstractDecorator extends ABattercake {

    private ABattercake aBattercake;

    public AbstractDecorator(ABattercake aBattercake) {
        this.aBattercake = aBattercake;
    }

    protected String getDesc() {
        return this.aBattercake.getDesc();
    }

    protected int cost() {
        return this.aBattercake.cost();
    }
}

// 具体的装饰器实现类
public class EggDecorator extends AbstractDecorator {

    public EggDecorator(ABattercake aBattercake) {
        super(aBattercake);
    }

    @Override
    protected String getDesc() {
        return super.getDesc() + " 加一个鸡蛋";
    }

    @Override
    protected int cost() {
        return super.cost() + 1;
    }
}

// 具体的装饰器实现类
public class SausageDecorator extends AbstractDecorator {

    public SausageDecorator(ABattercake aBattercake) {
        super(aBattercake);
    }

    @Override
    protected String getDesc() {
        return super.getDesc() + " 加一根香肠";
    }

    @Override
    protected int cost() {
        return super.cost() + 2;
    }
}

如果有人想买加2个鸡蛋和1根香肠的煎饼,实现方式如下

public class Test {

    public static void main(String[] args) {
        ABattercake aBattercake = new Battercake();
        aBattercake = new EggDecorator(aBattercake);
        aBattercake = new EggDecorator(aBattercake);
        aBattercake = new SausageDecorator(aBattercake);
        // 煎饼 加一个鸡蛋 加一个鸡蛋 加一根香肠 销售价格为: 12
        System.out.println(aBattercake.getDesc() + " 销售价格为: " + aBattercake.cost());
    }
}

可以看到当要添加新的功能时,我们可以使用继承,在子类中添加新能的扩展实现。但有时候继承是不可行的,因为有些类是被final修饰的。而且待添加的新功能存在多种组合,使用继承的方式会导致大量子类的的出现。

而装饰者模式则是通过组合的方式来替代继承,为对象添加功能

看一下上述代码的UML图

装饰者模式在JDK和Mybatis中是怎么应用的?


从上图就可以画出装饰者模式的UML图如下

装饰者模式在JDK和Mybatis中是怎么应用的?

Component(组件):组件接口或抽象类定义了全部组件实现类以及所有装饰器实现的行为。

ConcreteComponent(具体组件实现类):具体组件实现类实现了Component接口或抽象类。通常情况下,具体组件实现类就是被装饰器装饰的原始对象,该类提供了Component接口中定义的最基本的功能,其他高级功能或后序添加的新功能,都是通过装饰器的方式添加到该类的对象之上的。

Decorator(抽象装饰器):所有装饰器的父类,它是一个实现了Component接口的类,并在其中封装了一个Component对象,也就是被装饰的对象。而这个被装饰的对象只要是Component类型即可,这就实现了装饰器的组合和复用

ConcreteDecorator(具体的装饰器):该实现类要向被装饰对象添加某些功能

java io包

装饰者模式在JDK和Mybatis中是怎么应用的?

从上图可以看出,InputStream是组件,FileInputStream,ByteArrayInputStream是具体组件实现类,FilterInputStream是抽象装饰器,LineInputStream是具体的装饰器。

InputStream和OutputStream,Reader和Writer体系都用到了装饰者模式,不再概述。

举个例子,我们进行IO操作时,经常写如下代码,你是否意识到这个用到了装饰者模式呢?

BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File("D:/test.txt")));

当我们意识到这个用到装饰器模式时,想增加新功能时,就直接查找是否有相应的具体装饰器即可,或者自己实现一个装饰器,而不是陷入迷茫。

举个例子,我们想把从文件中读入的内容都转为小写时,只要自己继承FilterInputStream,实现相应的功能即可

public class LowerCaseInputStream extends FilterInputStream {

    /*
     * 自己的装饰类,将大写字母转为小写字母
     */

    protected LowerCaseInputStream(InputStream in) {
        super(in);
    }

    @Override
    public int read() throws IOException {
        int c = super.read();
        return (c == -1 ? -1 : Character.toLowerCase((char)c));
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        int result = super.read(b, off, len);
        for (int i=off; i<=off+result; i++) {
            b[i] = (byte)Character.toLowerCase((char)b[i]);
        }
        return result;
    }
}

D:/test.txt的文件内容如下

THIS is JUST for TEST

测试类

public class InputTest {

    public static void main(String[] args) {

        int c;
        try {
            InputStream in = new LowerCaseInputStream(new BufferedInputStream(new FileInputStream("D:/test.txt")));
            while ((c = in.read()) >= 0) {
                //this is just for test
                System.out.print((char)c);
            }
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

Mybatis缓存模块

Mybatis的缓存模块中,使用了装饰器模式的变体,其中将Decorator接口和Componet接口合并为一个Component接口,类间结构如下

装饰者模式在JDK和Mybatis中是怎么应用的?


Mybatis的Cache接口就是上图中的Component


public interface Cache {

  // 省略一部分方法
  String getId();

  void putObject(Object key, Object value);

  Object getObject(Object key);

  Object removeObject(Object key);
}

看一下Cache接口的实现类

装饰者模式在JDK和Mybatis中是怎么应用的?


仔细看包名,由包名就可以看到PerpetualCache扮演着ConcreteComponent(具体组件实现类)的角色,其余的都是装饰类,为什么要弄这么多装饰类呢?

举个例子,我们可以在二级缓存中配置缓存回收策略。

可配置的选项有

LRU:最近最少使用,移除最长时间不被使用的对象
FIFO:先进先出,按对象进入缓存的顺序来移除它们
SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象
WEAK:弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象

再看上面的装饰类和这个配置选项的名字是不是很类似,Mybatis根据你配置的缓存回收策略来选择相应的装饰类,完成扩展功能。

推荐阅读:

不扯概念,用例子演示什么是脏读,不可重复读,幻读?

五分钟读懂工厂模式三兄弟

vim三种模式下的小技巧,提高一半工作效率


Java识堂

一个有干货的公众号

装饰者模式在JDK和Mybatis中是怎么应用的?

原文始发于微信公众号(Java识堂):装饰者模式在JDK和Mybatis中是怎么应用的?