装饰者模式-动态的包装原有对象的行为

公号:码农充电站pro
主页:https://codeshellme.github.io

今天来介绍装饰者模式Decorator Design Pattern)。

假设我们需要给一家火锅店设计一套结账系统,也就是统计顾客消费的总价格。怎样才能设计出一个好的系统呢?

1,结账系统需求分析

既然要设计一个结账系统,当然需要知道火锅店都有哪些食品及食品的价格,假如我们从火锅店老板那里得到以下食品清单:

  • 锅底类:
    • 清汤锅底:5 元
    • 麻辣锅底:7 元
    • 其它
  • 配菜类:
    • 青菜:3 元
    • 羊肉:6 元
    • 其它
  • 饮料类:
    • 可乐:2 元
    • 其它

可以看到,食品共有三大类,分别是:锅底类,配菜类和饮料类,每个大类下边都有很多具体的食品。

为了设计出一个可维护,可扩展,有弹性的系统,应该怎样设计呢?

我们可以这样看待食品之间的关系,将锅底类看作主品,所有其它的都为副品,也就是附加在主品之上的食品。

副品以主品为中心,围绕在主品周围,包裹着主品,一层层的往外扩展

如下图所表达的一样:

在这里插入图片描述

2,装饰者模式

像这种,需要在原来(主品)的基础上,附加其它的东西(副品),这样的业务场景都可以使用装饰者模式

装饰者模式的定义为:动态的给一个对象添加其它功能。从扩展性来说,这种方式比继承更有弹性,更加灵活,可作为替代继承的方案

装饰者模式的优点在于,它能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。也就是不需要通过修改代码,而是通过扩展代码,来完成新的业务需求。

这就非常符合我们所说的设计原则中的开闭原则对扩展开放,对修改关闭。也就是尽量不要修改原有代码,而是通过扩展代码来完成任务。这样做的好处是可以减少对原有系统的修改,从而减少引入 bug 的风险。

装饰者模式的类图如下:

在这里插入图片描述

ConcreteComponent 为被装饰者,Decorator 是所有装饰者的超类。

装饰者和被装饰者有着共同的超类型,这一点很重要,因为装饰者必须能够取代被装饰者。这样,装饰者才能在被装饰者的基础上,加上自己的行为,以增强被装饰者的能力。

一个被装饰者可以被多个装饰者依次包装,这个包装行为是动态的,不限次数的。

3,实现结账系统

那么根据装饰者模式的类图,我们可以设计出火锅店结账系统的类图,如下:

在这里插入图片描述

火锅的锅底作为被装饰者,配菜和饮料作为装饰者。

每个类都有两个方法:

  • describe:返回当前火锅的描述。
  • cost:返回当前火锅的价格。

首先编写 HotPot 类:

class HotPot {
    protected String desc = "HotPot";
    protected double price = 0;

    public String description() {
        return desc;
    }

    public double cost() {
        return price;
    }
 
    public void printMenu() {
        System.out.println("菜单:" + description() + " 消费总价:" + cost());
    }
}

HotPot 类中有两个属性 descprice,还有三个方法 descriptioncostprintMenuprintMenu 用于输出菜单和消费总价。

再编写 SideDish 类:

class SideDish extends HotPot {
    protected HotPot hotpot;
    
    public double cost() {
        return hotpot.cost() + price;
    };

    public String description() {
        return hotpot.description() +" + "+ desc;
    };
}

SideDish 继承了 HotPot,添加了自己的属性 hotpot,并且重写了两个方法 descriptioncost

注意SideDish 类对两个方法 descriptioncost 进行了重写,这非常重要,这体现出了装饰者与被装饰者的区别,装饰者能在被装饰者的基础上附加自己的行为,原因就在这里

编写两个锅底类:

class SoupPot extends HotPot {
    public SoupPot() {
        desc = "Soup";
        price = 5;
    }
}

class SpicyPot extends HotPot {
    public SpicyPot() {
        desc = "Spicy";
        price = 7;
    }
}

这两个类都继承HotPot,并分别在构造方法中设置自己的 descprice

再编写三个配菜类:

class VegetablesDish extends SideDish {
    public VegetablesDish(HotPot hotpot) {
        this.hotpot = hotpot;
        desc = "Vegetables";
        price = 3;
    }
}

class MuttonDish extends SideDish {
    public MuttonDish(HotPot hotpot) {
        this.hotpot = hotpot;
        desc = "Mutton";
        price = 6;
    }
}

class ColaDish extends SideDish {
    public ColaDish(HotPot hotpot) {
        this.hotpot = hotpot;
        desc = "Cola";
        price = 2;
    }
}

这三个类都继承 SideDish,并分别在构造方法中设置自己的hotpotdescprice

4,测试结账系统

用如下代码来测试:

// 只有一份清汤锅底
HotPot hotpot = new SoupPot(); // 被装饰者不需装饰者包装也可以使用
hotpot.printMenu();

// 清汤锅底 + 蔬菜
hotpot = new VegetablesDish(hotpot);
hotpot.printMenu();

// 清汤锅底 + 蔬菜 + 羊肉
hotpot = new MuttonDish(hotpot);
hotpot.printMenu();

// 清汤锅底 + 蔬菜 + 羊肉 + 可乐
hotpot = new ColaDish(hotpot);
hotpot.printMenu();

// 清汤锅底 + 蔬菜 + 羊肉 + 可乐 + 蔬菜
hotpot = new VegetablesDish(hotpot);
hotpot.printMenu();

输出如下:

菜单:Soup 消费总价:5.0
菜单:Soup + Vegetables 消费总价:8.0
菜单:Soup + Vegetables + Mutton 消费总价:14.0
菜单:Soup + Vegetables + Mutton + Cola 消费总价:16.0
菜单:Soup + Vegetables + Mutton + Cola + Vegetables 消费总价:19.0

计算总价时,会从最外层的装饰者,朝着被装饰者的方向,依次调用每一层的 cost 方法,直到被装饰者为止。

然后再朝着最外层装饰者的方向,依次计算出每一层的价格,最后得出的价格就是消费总价。

计算过程如下图所示:

在这里插入图片描述

我将完整的装饰者模式代码放在了这里,供大家参考。

5,装饰者模式的使用场景

装饰者模式主要用于,在不修改原有类的前提下,动态的修改原有类的功能。

Java JDK 中大量使用了装饰者模式,尤其是 Java I/O 框架。

Java IO 框架的继承关系如下:

在这里插入图片描述

可以看到,Java IO 框架包含了非常多的类,这对初学者并不是很友好,很难弄明白每个类的作用是什么,也不容易了解设计者的意图。

Java IO 主要分为字节流字符流两大类。我们以 InputStream 为例,画出其类图结构,如下:

在这里插入图片描述

从该图能够看出,Java IO 与我们上文中的装饰者模式的类图基本一模一样,所以 Java IO 其实就是使用了装饰者模式,明白了这一点,再使用它就非常方便了。

6,装饰者模式的缺点

装饰者模式有一个比较明显的缺点,从上文中你也许已经发现了,就是它会引入非常多的小类,这样会让使用者弄不明白类之间的关系。

当了解了装饰者的原理,也就比较容易使用了。

7,总结

从装饰者模式中,能充分的看到开闭原则的使用。利用装饰者模式,可以让我们在不修改原有代码的情况下,扩展原有类的功能。但是也不能过度使用它,因为容易引入非常多的小类。

(本节完。)

推荐阅读:

设计模式之高质量代码

单例模式-让一个类只有一个对象

工厂模式-将对象的创建封装起来

策略模式-定义一个算法族

观察者模式-将消息通知给观察者

欢迎关注作者公众号,获取更多技术干货。

码农充电站pro

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结