装饰者模式

装饰者模式

案例

喝奶茶是平时很常见的一件事情,奶茶中有牛奶,珍珠和椰果等等很多的材料,我们可以根据自己的喜爱来进行选择。我们给店家说过我们想要的奶茶材料后,店家根据我们选择的奶茶材料除了需要进行调制以外,最重要的就是根据材料进行算账了,下面我们就来模拟这一过程。

1.各种材料:水

/**
 * 原料水
 */
public class Water {
    private int money = 1;
    public Water() {
        System.out.println("加水,价格:" + money + "元");
    }

    public int getMoney() {
        return this.money;
    }
}

牛奶:

/**
 * 材料牛奶
 */
public class Milk {
    private int money = 2;
    public Milk() {
        System.out.println("加牛奶,价值格:" + money + "元");
    }

    public int getMoney() {
        return money;
    }
}

珍珠:

/**
 * 材料珍珠
 */
public class Pearl {
    private int money = 3;
    public Pearl() {
        System.out.println("加珍珠,价值格:" + money + "元");
    }

    public int getMoney() {
        return money;
    }
}

椰果:

/**
 * 材料椰果
 */
public class Coconut {
    private int money = 3;
    public Coconut() {
        System.out.println("加椰果,价值格:" + money + "元");
    }

    public int getMoney() {
        return money;
    }
}

2.测试:

/**
 * 模拟买一杯珍珠椰果奶茶
 */
public class Main {
    public static void main(String[] args) {
        // 奶茶的默认必备材料:水
        Water water = new Water();
        // 加入牛奶
        Milk milk = new Milk();
        // 加入珍珠
        Pearl pearl = new Pearl();
        // 加入椰果
        Coconut coconut = new Coconut();
        // 计算价格
        int total = water.getMoney() + milk.getMoney() + pearl.getMoney() + coconut.getMoney();
        System.out.println("总价格:" + total + "元");
    }
}

3.模拟结果:

加一份水,价格:1元
加一份牛奶,价值格:2元
加一份珍珠,价值格:3元
加一份椰果,价值格:3元
总价格:9元

我们通过实例化我们选择的材料进行调制奶茶,最后通过相加各种材料的价格得到最终需要进行付款的价格。这里虽然没有什么问题,但是如果我们想要两份珍珠,那么除了要在实例化一个材料以外,还需要重新计算价格。这个计算价格的方式显得不够灵活,我们希望在选择了所有的材料后,就能立马得出总价格是多少。下面就通过装饰者模式来对这一问题进行改善。

模式介绍

装饰模式(结构型模式)指的是在不必改变原类文件使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

可以看到装饰者模式的介绍非常言简意赅,其中的重点就是创建包装对象来动态的扩展功能。

角色构成:

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

这里的具体构件Decorator和抽象装饰类ConcreteDecorator都实现了相同的抽象构件,因此客户端在使用时并不会感觉到对象在装饰前和装时候有什么不同。

UML类图:

decorator

在抽象装饰类Decorator中维护了一个对Component抽象构件对象的引用,并可以通过构造方法或Setter方法将一个Component类型的对象注入进来。同时由于Decorator类实现了抽象构件Component接口,因此需要实现方法method(),但是在Decorator中只是调用原有component对象的method()方法,它没有真正实施装饰,而是提供一个统一的接口,将具体装饰过程交给子类完成。

代码改造

通过上面分析过的这几个角色间的关系,下面就对上面的代码进行改造。

1.首先定义抽象构建类:

/**
 * 抽象构件
 */
public interface Material {
    // 定义计算金额的方法
    int sum();
}

2.具体构件类:

/**
 * 具体构件类角色
 */
public class Water implements Material {
    private int money = 1;

    public Water() {
        System.out.println("加一份水,价格:" + money + "元");
    }

    // 这里实现接口中的方法,返回自身的价格
    public int sum() {
        return this.getMoney();
    }

    public int getMoney() {
        return money;
    }
}

3.抽象装饰类:

/**
 * 抽象装饰类:配料类
 */
public abstract class Batching implements Material {
    protected Material material;

    public Batching(Material material) {
        this.material = material;
    }

    // 实现接口中的方法,实际调用的是具体构件类中的方法
    public int sum() {
        return material.sum();
    }
}

4.三个装饰类:

牛奶类:

/**
 * 具体装饰类
 */
public class Milk extends Batching {
    private int money = 2;

    public Milk(Material material) {
        super(material);
        System.out.println("加一份牛奶,价值格:" + money + "元");
    }

    // 重写抽象类中的方法,主要是做一些额外的操作,在这里是用于返回自身的价格
    public int sum() {
        return super.sum() + this.getMoney();
    }

    public int getMoney() {
        return money;
    }
}

珍珠类:

/**
 * 具体装饰类
 */
public class Pearl extends Batching {
    private int money = 3;

    public Pearl(Material material) {
        super(material);
        System.out.println("加一份珍珠,价值格:" + money + "元");
    }

    // 重写抽象类中的方法,主要是做一些额外的操作,在这里是用于返回自身的价格
    public int sum() {
        return super.sum() + this.getMoney();
    }

    public int getMoney() {
        return money;
    }
}

椰果类:

/**
 * 具体装饰类
 */
public class Coconut extends Batching {
    private int money = 3;

    public Coconut(Material material) {
        super(material);
        System.out.println("加一份椰果,价值格:" + money + "元");
    }

    // 重写抽象类中的方法,主要是做一些额外的操作,在这里是用于返回自身的价格
    public int sum() {
        return super.sum() + this.getMoney();
    }

    public int getMoney() {
        return money;
    }
}

5.测试类:

/**
 * 模拟点一杯珍珠椰果奶茶
 */
public class Main {
    public static void main(String[] args) {
        Coconut coconut = new Coconut(new Pearl(new Milk(new Water())));
        System.out.println("总价格:" + coconut.sum());
    }
}

6.测试结果:

加一份水,价格:1元
加一份牛奶,价值格:2元
加一份珍珠,价值格:3元
加一份椰果,价值格:3元
总价格:9

这里首先是创建了一个抽象的构件类Component,再把Water类当作了一个具体的构件类。同时引入抽象装饰类Batching,在类中维护了一个Component类的实例,使得在实现接口方法时可以调用具体构件类的方法。再把MilkPearlCoconut三个类当作具体的装饰类,重写抽象装饰类Batching类中的方法,同时可以进行扩展,在这里仅仅是返回了各配料类的自身价格,还可以对配料类做打折操作,例如在实现方法sum()中这样写:return super.sum() + this.getMoney()*0.5;,相当于配料打五折。

而且这里再添加其他配料时也和简单,只用在外层再包裹一层配料类的实例就可以了,例如这里添加两份椰果:

public static void main(String[] args) {
    Coconut coconut = new Coconut(new Coconut(new Pearl(new Milk(new Water()))));
    System.out.println("总价格:" + coconut.sum());
}

模式应用

说到装饰着模式其实并不陌生,因为大家都应该听过 Java 中的 IO 就是运用了装饰者模式的思想,下面看一个 IO 读取文件示例:

1.测试类:

public class Main {
    public static void main(String[] args) throws IOException {
        // 1. 从当前文件夹中读取文件为输入流
        InputStream fileInputStream = new FileInputStream("decorator/text.txt");
        // 2 从字节流中读取内容
        // 2.1 定义一个缓冲字节数组
        byte[] buffer = new byte[1024];
        // 2.2 定义读取标志位
        int read;
        // 2.3 循环读取字节到缓冲数组中
        while ((read = fileInputStream.read(buffer)) != -1) {
            // 将读取的字节转换为字符串对象
            String str = new String(buffer, 0, read);
            System.out.println(str);
        }
        // 2.4 文件字节流没有 mark/reset 功能
        System.out.println("markSupported:" + fileInputStream.markSupported());
        System.out.println("---------------------");

        // 3. 将字节流包装为缓冲字节流
        InputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("decorator/text.txt"));
        // 3.1 缓冲字节流支持 mark、reset 的功能
        System.out.println("markSupported:" + bufferedInputStream.markSupported());
        // 3.2 读取第一个字节的数据
        System.out.println((char) bufferedInputStream.read());
        // 3.3 标记当前输入流中的位置
        bufferedInputStream.mark(0);
        // 3.4 读取下三个字节数据
        System.out.println((char) bufferedInputStream.read());
        System.out.println((char) bufferedInputStream.read());
        // 3.5 重置到 2.2.4 中标记的位置
        bufferedInputStream.reset();
        // 3.6 读取下一个字节数据
        System.out.println((char) bufferedInputStream.read());
    }
}

2.测试文件位置如图所示:

decorator-io-readFile

​ 3.测试结果:

开始输出文件内容
hello
world
输出文件结束
markSupported:false
---------------------
markSupported:true
h
e
l
e

从代码中可以看到InputStream是一个接口,FileInputStream是其中一个实现子类,它作为具体构件实现了InputStream中读取数据的方法。而BufferedInputStream扩展了标记/重置功能(如果对这一功能感兴趣,可以参考BufferedInputStream中的mark()和reset()用法,及其中readlimit相关的问题),它作为具体装饰类的角色。而从继承关系中我们可以找到它的父类FilterInputStream,就是一个抽象装饰着角色。下面是这几个类之间的UML类图:

decorator-io-uml

总结

1.主要优点

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

2.主要缺点

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

3.适用场景

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

参考资料

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/decorator
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/13966052.html

原文地址:https://www.cnblogs.com/phoegel/p/13966052.html