设计模式(四) -- 装饰者模式

定义

装饰者模式:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性,装饰者模式也体现了开闭原则。

即:在不必改变原类文件和使用继承的情况下,动态的扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

简言之装饰者模式的简单实现方式:装饰者中存放一个被装饰对象的引用(属性)

  1. 接口A,
  2. 被装饰对象:一个实现类B,即被装饰对象。
  3. 装饰者抽象类:装饰者抽象类C实现A,并在其中存放一个A的引用。
  4. 具体装饰类:具体的装饰者实现类D继承C,完成对被装饰对象的装饰。

举例

需求:

现在有一个星巴克订单项目:
在这里插入图片描述
比如我可以点Espresso+豆浆+2份牛奶,然后计算它的价格,我也可以只点一份Espresso,然后计算它的价格。

解决方案1:

做一个抽象类,里面有一个cost抽象方法,然后将所有的组合穷举成类。
在这里插入图片描述
问题:

  1. 这样设计会造成类爆炸
  2. 如果添加一种调料,又要添加很多类
解决方案2:

将是否添加调料设计成布尔型,放到Drink类中,在单点咖啡中通过控制true和fasle来判断是否添加,这样做大大减少了类的数量
在这里插入图片描述
问题:如果新增调料时,代码的维护量还是很大。

解决方案3:装饰者模式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码:

Drink抽象类:

public abstract class Drink {
    public String des; // 藐视

    private int price; // 价格 单位:分

    // 计算费用的抽象方法
    public abstract int cost();

	// 省略getset方法
}

单点咖啡的父类:

public class Coffee extends Drink {
    @Override
    public int cost() {
        return super.getPrice();
    }
}

各个单点咖啡类:即被装饰类

public class Espresso extends Coffee{
    public Espresso() {
        setDes("意大利咖啡");
        setPrice(100);
    }
}

public class LongBlack extends Coffee{
    public LongBlack() {
        setDes("LongBlack");
        setPrice(200);
    }
}

public class ShortBlack extends Coffee{
    public ShortBlack() {
        setDes("ShortBlack");
        setPrice(300);
    }
}

添加类的父类:即装饰类:

public class Decorator extends Drink {
    private Drink drink;

    public Decorator(Drink drink) { // 组合
        this.drink = drink;
    }

    @Override
    public int cost() {
        // getPrice 自己价格,drink.cost,单点咖啡的价格
        return super.getPrice() + drink.cost();
    }

    public String getDes() {
        // drink.getDes():被装饰者信息
        return super.getDes() + " " + super.getPrice() + " " + drink.getDes();
    }
}

添加类:

public class Chocolate extends Decorator{
    public Chocolate(Drink drink) {
        super(drink);
        setDes("巧克力");
        setPrice(10);
    }
}

public class Milk extends Decorator{
    public Milk(Drink drink) {
        super(drink);
        setDes("牛奶");
        setPrice(20);
    }
}

public class Soy extends Decorator{
    public Soy(Drink drink) {
        super(drink);
        setDes("豆浆");
        setPrice(30);
    }
}

订单生成类:

public class StarBucks {
    public static void main(String[] args) {
        // 装饰者模式下的订单: 2份巧克力+1一份牛奶的LongBlack
        // 点一份LongBlack
        Drink order = new LongBlack();
        System.out.println("费用1=" + order.cost());
        System.out.println("描述1=" + order.getDes());

        // order加入一份牛奶
        order = new Milk(order);
        System.out.println("费用2=" + order.cost());
        System.out.println("描述2=" + order.getDes());

        // 再加入一份牛奶
        order = new Milk(order);
        System.out.println("费用3=" + order.cost());
        System.out.println("描述3=" + order.getDes());

        // 再加入一份巧克力
        order = new Chocolate(order);
        System.out.println("费用4=" + order.cost());
        System.out.println("描述4=" + order.getDes());
    }
}

运行结果:
在这里插入图片描述
内部使用了递归调用,第二次加入牛奶时,此时order时Milk,order中的drink存放的是第一次加入的milk,第一次加入的milk中的drink是LongBlack

装饰者模式在IO中的使用

IO的UML图:
在这里插入图片描述
InputStream是一个顶层的接口,文章开头就说,装饰器模式是继承关系的一种替代方案,看一下为什么:

  1. InputStream假设这里写了两个实现类,FileInputStream,ObjectInputStream分别表示文件字节输入流,对象字节输入流
  2. 现在我要给这两个输入流加入一点缓冲功能以提高输入流效率,使用继承的方式,那么就写一个BufferedInputStream,继承FileInputStream,ObjectInputStream,给它们加功能
  3. 现在我有另外一个需求,需要给这两个输入流加入一点网络功能,那么就写一个SocketInputStream,继承继承FileInputStream,ObjectInputStream,给它们加功能

这样就导致两个问题:

  1. 因为我要给哪个类加功能就必须继承它,比如我要给FileInputStream,ObjectInputStream加上缓冲功能、网络功能就得扩展出2*2=4个类,更多的以此类推,这样势必导致类数量不断膨胀
  2. 代码无法复用,给FileInputStream,ObjectInputStream加入缓冲功能,本身代码应该是一样的,现在却必须继承完毕后把一样的代码重写一遍,多此一举,代码修改的时候必须修改多个地方,可维护性很差

所以,这个的时候我们就想到了一种解决方案:

  1. 在要扩展的类比如BufferedInputStream中持有一个InputStream的引用,在BufferedInputStream调用InputStream中的方法,这样扩展的代码就可以复用起来
  2. 将BufferedInputStream作为InputStream的子类,这样客户端只知道我用的是InputStream而不需要关心具体实现,可以在客户端不知情的情况下,扩展InputStream的功能,加上缓冲功能

这就是装饰器模式简单的由来,一切都是为了解决实际问题而诞生。下一步,根据UML图,我们来划分一下装饰器模式的角色。

IO的装饰器角色:
  1. InputStream是抽象类,类似上面例子中的Drink
  2. ByteArrayInputStream、FileInputStream、ObjectInputStream、PipedInputStream都是具体构建角色,是InputStream的子类,类似之前的具体的单点咖啡如LongBlack、ShortBlack,是被装饰对象
    如FileInputStream的声明
public
class FileInputStream extends InputStream
{
  1. FilterInputStream无疑就是一个装饰角色,因为FilterInputStream实现了InputStream内的所有抽象方法并且持有一个InputStream的引用,就如同之前的Decorator装饰者。
public
class FilterInputStream extends InputStream {
    /**
     * The input stream to be filtered.
     */
    protected volatile InputStream in;

    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
  1. 具体装饰角色就是InflaterInputStream、BufferedInputStream、DataInputStream;类似之前的Milk、Soy
IO流举例:
public static void main(String[] args) throws Exception
{
    File file = new File("D:/aaa.txt");
    InputStream in0 = new FileInputStream(file);
    InputStream in1 = new BufferedInputStream(new FileInputStream(file)); 
    InputStream in2 = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
}

我们这里实例化出了三个InputStream的实现类:

  1. in0这个引用指向的是new出来的FileInputStream,这里简单构造出了一个文件字节输入流
  2. in1这个引用指向的是new出来的BufferedInputStream,它给FileInputStream增加了缓冲功能,使得FileInputStream读取文件的内容保存在内存中,以提高读取的功能
  3. in2这个引用指向的是new出来的DataInputStream,它也给FileInputStream增加了功能,因为它有DataInputStream和BufferedInputStream两个附加的功能

同理,我要给ByteArrayInputStream、ObjectInputStream增加功能,也可以使用类似的方法,整个过程中,最重要的是要理解几个问题:

  1. 哪些是具体构建角色、哪些是具体装饰角色,尤其是后者,区分的关键就是,角色中是否持有顶层接口的引用
  2. 每个具体装饰角色有什么作用,因为只有知道每个具体装饰角色有什么作用后,才可以知道要装饰某个功能需要用哪个具体装饰角色
  3. 使用构造方法的方式将类进行组合,给具体构建角色加入新的功能

装饰者模式的优缺点:

优点
  1. 装饰器模式与继承关系的目的都是要扩展对象的功能,但是装饰器模式可以提供比继承更多的灵活性。装饰器模式允许系统动态决定贴上一个需要的装饰,或者除掉一个不需要的装饰。继承关系是不同,继承关系是静态的,它在系统运行前就决定了
  2. 通过使用不同的具体装饰器以及这些装饰类的排列组合,设计师可以创造出很多不同的行为组合
缺点
  1. 由于使用装饰器模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。但是另一方面,由于使用装饰器模式会产生比使用继承关系更多的对象,更多的对象会使得查错变得困难,特别是这些对象看上去都很像。

装饰者模式与静态代理模式的区别:

我觉得其实他们可以看成是一样的,但是,其他的地方都说是代理模式是控制对象的访问,装饰者是对被装饰对象进行功能的增强。我觉得其实都一样,装饰者也可以实现静态代理的功能。
可以看看这篇:装饰者和静态代理区别

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值