设计模式-装饰者模式

314 阅读4分钟

JDK中,使用到了很多设计模式,例如装饰者模式、观察者模式,它广泛存在于Java I/O中,说实话,刚学习Java I/O时有点懵,他的类是在太多了,可供选择的就十多种,还有很多叫不上名字的IO类,当时一度怀疑自己不是学编程的选手。

今天重点所说的就是装饰者模式,也算是一个总结吧

场景

这个场景很简单,为某冷饮店制造一个收费系统。

他的业务是这样的,主要的为饮品收费,其他的收费项目为配料收费,例如在点了饮品的基础上增加了珍珠、椰果等配料,然后计算总的费用。

问题解决

大家的想法可能是这样:当购买哪一种类的饮料时,就创建出来这个对象,如果想另外增加配料,再创建出来配料类的对象,最后计算出价格。但是怎么实现呢,要讲弹性,以方便日后的扩展

最初可以设计出一个抽象类,将饮料抽象出来

public abstract class Beverage {
    public String getDescription() {
        return "unknown beverage";
    }
    
    public abstract float cost();
}

接下来就是围绕这个类展开,从而引出装饰者模式。在往下进行之前,先说一个设计原则:

对修改关闭,对扩展开放(开放封闭原则)

新创建的饮料可以完全继承自Beverage类,并且实现其中的cost()和覆盖getDescription(),我们创建两个饮料类拿铁(Latte)和卡布奇诺(Cappuccino)

public class Latte extends Beverage{
    @Override
    public float cost() {
        return 12.1f;
    }

    @Override
    public String getDescription() {
        return "Latte";
    }
}
public class Cappuccino extends Beverage{
    @Override
    public float cost() {
        return 7.21f;
    }

    @Override
    public String getDescription() {
        return "Cappuccino";
    }
}

在装饰者模式中,这两类被称为被装饰者,而这个场景中,真正的装饰者是调料,我们看一下调料的抽象类

public abstract class Flavour extends Beverage {
    @Override
    public String getDescription() {
        return "Flavour";
    }
}

另外再去实现两种调料珍珠(Pearl)、椰肉(Coconut)

public class Pearl extends Flavour{

    private Beverage beverage;

    public Pearl(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public float cost() {
        return beverage.cost() + 2.1f;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + " Pearl";
    }
}
public class Coconut extends Flavour{

    private Beverage beverage;

    public Coconut(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public float cost() {
        return beverage.cost() + 4.2f;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + " Coconut";
    }
}

目前我们就完成了所有所需的类,UML类图如下

其中PearlCoconut装饰者类,负责装饰LatteCappuccino,且装饰者与被装饰者应该有相同的类型,即Beverage

如果我想点一份Latte,并且加上Pearl配料,应该如何计算金钱呢

public class Consumption {
    public static void main(String[] args) {
        Latte latte = new Latte();

        Pearl pearl = new Pearl(latte);

        System.out.println("total cost: " + pearl.cost());
        System.out.println(pearl.getDescription());
    }
}

最后打印出

total cost: 14.200001
Latte Pearl

这样的扩展是否很灵活呢,通过装饰者对被装饰者的装饰,在不修改原有代码的前提下,完成了如此具有弹性的功能。

此时我想点一份Cappuccino加两份Coconut

public class Consumption {
    public static void main(String[] args) {
        Cappuccino cappuccino = new Cappuccino();

        Coconut coconut = new Coconut(cappuccino);
        coconut = new Coconut(coconut);

        System.out.println("total cost: " + coconut.cost());
        System.out.println(coconut.getDescription());
    }
}

输出

total cost: 15.61
Cappuccino Coconut Coconut

I/O类中的装饰者模式

以输入流举例(输出流也是同理),InputStream可以看成是抽象的组件,FilterInputStream可以看成是抽象装饰者,而她下面的类就可以看成是具体的装饰者,他们都是对以及InputStream的装饰

既然说的这么细了,那就进到FilterInputStream的源码中看一看

public class FilterInputStream extends InputStream {
   	// 被装饰的对象
    protected volatile InputStream in;

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }

    public int read() throws IOException {
        // 调用被装饰的对象方法
        return in.read();
    }
    
    // 部分省略
}

他和咱们上面饮品的例子结构差不多吧

Java I/O也引出来了装饰者模式的一个缺点,利用装饰者模式,常常造成设计中有大量的小类,数量实在太多,可能会给使用此API的程序员造成困扰。

我们在知道了Java I/O类的原理后,可以自己动手写一个Java I/O类的API,功能是将文件中的全部大写转换为小写

public class LowerFileInputStream extends FileInputStream {

    private FileInputStream fileInputStream;

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

    @Override
    public int read(byte[] b) throws IOException {
        int read = super.read(b);

        for (int i = 0; i < read; i++) {
            b[i] = (byte) Character.toLowerCase((char) b[i]);
        }

        return read;
    }

    public LowerFileInputStream(String name) throws FileNotFoundException {
        super(name);
        fileInputStream = new FileInputStream(name);
    }
}

测试

public static void main(String[] args) throws Exception {
    LowerFileInputStream lowerFileInputStream = new LowerFileInputStream("lower.txt");

    int read = 0;

    StringBuilder stringBuilder = new StringBuilder();

    while((read = lowerFileInputStream.read()) != -1) {
        stringBuilder.append((char)read);
    }

    lowerFileInputStream.close();

    System.out.println(stringBuilder.toString());
}

输出

# 原始文件内容
I am Programmer

# 输出内容
i am programmer

总结

本篇文章以某饮料公司的消费系统为场景,引出了装饰者模式,并且得到了一个重要的设计原则:

类只对扩展开放,对修改关闭(开放封闭原则)

还总结出Java IO中存在着大部分的装饰者模式,同时也指出了装饰者模式的不足之处。

最后自己动手实现了一个依靠装饰者模式自定义的IO类。