在平时写代码的过程中,一般习惯使用继承。但是继承有时候会出现非常严重的问题,可以通过使用装饰者模式来解决。
一、案例场景
1、有一个咖啡馆,它有一套自己的订单系统,当顾客来咖啡馆的时候,可以通过订单系统来点自己想要的咖啡。他们原先的设计是这样子的:![[设计模式] 装饰者模式 - 图1](/uploads/projects/2book@sjms/19edb28c3cf8b74c87954f410892fd20.png)
2、此时、咖啡馆为了吸引更多的顾客,需要在订单系统中允许顾客选择加入不同调料的咖啡,例如:蒸奶(Steamed Milk)、豆浆(Soy)、摩卡(Mocha,也就是巧克力风味)或覆盖奶泡。星巴兹会根据所加入的调料收取不同的费用。所以订单系统必须考虑到这些调料部分。
下面是他们的第一次尝试:![[设计模式] 装饰者模式 - 图2](/uploads/projects/2book@sjms/68335c3a80ef33c684231c16845821bb.png)
过多的类别冗余,这种设计肯定是不行的。
3、这时,有个人提出了新的方案,利用实例变量和继承,来追踪这些调料。
具体为:先从Beverage基类下手,加上实例变量代表是否加上调料(牛奶、豆浆、摩卡、奶泡……),![[设计模式] 装饰者模式 - 图3](/uploads/projects/2book@sjms/0e900303d2ce748bd6ca8d4ebb9a5061.png)
![[设计模式] 装饰者模式 - 图4](/uploads/projects/2book@sjms/d161f19578cac7b1cf28d7838cdcb027.png)
这种设计虽然满足了现在的需求,但是我们想一下,如果出现下面情况,我们怎么办,
①、调料价钱的改变会使我们更改现有代码。
②、一旦出现新的调料,我们就需要加上新的方法,并改变超类中的cost()方法。
③、以后可能会开发出新饮料。对这些饮料而言(例如:冰茶),某些调料可能并不适合,但是在这个设计方式中,Tea(茶)子类仍将继承那些不适合的方法,例如:hasWhip()(加奶泡)。
④、万一顾客想要双倍摩卡咖啡,怎么办?
很明显,上面的设计并不能够从根本上解决我们所碰到的问题。并且这种设计违反了 开放关闭原则(类应该对扩展开放,对修改关闭)。
那我们怎么办呢?好啦,装饰者可以非常完美的解决以上的所有问题,让我们有一个设计非常nice的咖啡馆。
二、定义
装饰者模式:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
三、实现
1、思路
我们要以饮料为主体,然后在运行时以调料来“装饰”(decorate)饮料。比方说,如果顾客想要摩卡和奶泡深焙咖啡,那么,要做的是:
①、拿一个深焙咖啡(DarkRoast)对象
②、以摩卡(Mocha)对象装饰它
③、以奶泡(Whip)对象装饰它
④、调用cost()方法,并依赖委托(delegate)将调料的价钱加上去。
好了!但是如何“装饰”一个对象,而“委托”又要如何与此搭配使用呢?那就是把装饰者对象当成“包装者”。让我们看看这是如何工作的:![[设计模式] 装饰者模式 - 图5](/uploads/projects/2book@sjms/3d7deb2aa1f6a49be865251869d124a2.png)
![[设计模式] 装饰者模式 - 图6](/uploads/projects/2book@sjms/c241402b4d77cbdbbc903d2bbe9d724d.png)
![[设计模式] 装饰者模式 - 图7](/uploads/projects/2book@sjms/02f5b71103243525c6dc51b979a57c89.png)
2、设计
我们将我们所知道的写下来:
①、装饰者和被装饰对象有相同的超类型。
②、你可以用一个或多个装饰者包装一个对象。
③、既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它。
④、装饰者可以在所委托被装饰者的行为之前与 / 或之后,加上自己的行为,以达到特定的目的。
⑤、对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象。
下面,我们来看一下装饰者模式的类图:![[设计模式] 装饰者模式 - 图8](/uploads/projects/2book@sjms/68c99b309d495c7612d0f645fb957472.png)
利用装饰者模式来实现我们的订单系统的类图:![[设计模式] 装饰者模式 - 图9](/uploads/projects/2book@sjms/090964a962b0b4a130ac536d9f885c34.png)
3、代码实现:
饮料抽象类:
/*** @author fan_rc@suixingpay.com* @description 饮料抽象类* @date 2019/9/17 20:53*/public abstract class Beverage {String description = "Unknown Beverage";public String getDescription() {return description;}/*** cost方法是用来返回饮料的价钱(需在具体类中自己实现)** @return*/public abstract BigDecimal cost();}
深焙咖啡类:
/*** 深焙咖啡类(一种具体的饮料)*/public class DarkRoast extends Beverage {/*** 说明他是DarkRoast饮料*/public DarkRoast() {description = "DarkRoast";}/*** 实现cost方法,用来返回DarkRoast(深焙咖啡)的价格** @return*/@Overridepublic BigDecimal cost() {return new BigDecimal("3.00");}}
低咖啡因咖啡类:
/*** 低咖啡因咖啡类(一种具体的饮料)*/public class Decaf extends Beverage {/*** 说明他是Decaf饮料*/public Decaf() {description = "Decaf";}/*** 实现cost方法,用来返回Decaf(低咖啡因咖啡)的价格** @return*/@Overridepublic BigDecimal cost() {return new BigDecimal("4.00");}}
浓缩咖啡类:
/*** 浓缩咖啡类(一种具体饮料)*/public class Espresso extends Beverage {/*** 说明他是Espresso饮料*/public Espresso() {description = "Espresso";}/*** 实现cost方法,用来返回Espresso(浓缩咖啡)的价格** @return*/@Overridepublic BigDecimal cost() {return new BigDecimal("2.00");}}
调料装饰着抽象类:
/*** @author fan_rc@suixingpay.com* @description 调料装饰着抽象类(继承自饮料抽象类)* @date 2019/9/17 20:56*/public abstract class CondimentDecorator extends Beverage {/*** 所有的调料装饰者都必须重新实现getDescription()方法* 这样才能够用递归的方式来得到所选饮料的整体描述** @return*/public abstract String getDescription();}
摩卡调料类:
/*** 摩卡调料类(继承自CondimentDecorator)*/public class Mocha extends CondimentDecorator {/*** 用一个实例变量记录饮料,也就是被装饰者*/Beverage beverage;/*** 构造器初始化饮料变量** @param beverage*/public Mocha(Beverage beverage) {this.beverage = beverage;}/*** 在原来饮料的基础上添加上Mocha描述(原来的饮料加入Mocha调料,被Mocha调料装饰)** @return*/@Overridepublic String getDescription() {return beverage.getDescription() + ",Mocha";}/*** 在原来饮料的基础上加上Mocha的价格(原来的饮料加入Mocha调料,被Mocha调料装饰)** @return*/@Overridepublic BigDecimal cost() {return new BigDecimal("0.2").add(beverage.cost());}}
豆浆调料类:
/*** 豆浆调料类(继承自CondimentDecorator))*/public class Soy extends CondimentDecorator {/*** 用一个实例变量记录饮料,也就是被装饰者*/Beverage beverage;/*** 构造器初始化饮料变量** @param beverage*/public Soy(Beverage beverage) {this.beverage = beverage;}/*** 在原来饮料的基础上添加上Soy描述(原来的饮料加入Soy调料,被Soy调料装饰)** @return*/@Overridepublic String getDescription() {return beverage.getDescription() + ",Soy";}/*** 在原来饮料的基础上加上Soy的价格(原来的饮料加入Soy调料,被Soy调料装饰)** @return*/@Overridepublic BigDecimal cost() {return new BigDecimal("0.3").add(beverage.cost());}}
奶泡调料类:
/*** 奶泡调料类(继承自CondimentDecorator)*/public class Whip extends CondimentDecorator {/*** 用一个实例变量记录饮料,也就是被装饰者*/Beverage beverage;/*** 构造器初始化饮料变量** @param beverage*/public Whip(Beverage beverage) {this.beverage = beverage;}/*** 在原来饮料的基础上添加上Whip描述(原来的饮料加入Whip调料,被Whip调料装饰)** @return*/@Overridepublic String getDescription() {return beverage.getDescription() + ",Whip";}/*** 在原来饮料的基础上加上Whip的价格(原来的饮料加入Whip调料,被Whip调料装饰)** @return*/@Overridepublic BigDecimal cost() {return new BigDecimal("0.4").add(beverage.cost());}}
咖啡馆(模拟顾客下单):
/*** 咖啡馆(供应咖啡)*/public class StarbuzzCoffee {public static void main(String[] args) {//订一杯Espresso(2.00),不需要调料,打印出它的描述与价钱。Beverage beverage = new Espresso();System.out.println("Description: " + beverage.getDescription() + " $" + beverage.cost());//制造出一个DarkRoast(3.00)对象,用Mocha(0.2)装饰它,用第二个Mocha(0.2)装饰它,用Whip(0.4)装饰它,打印出它的描述与价钱。Beverage beverage2 = new DarkRoast();beverage2 = new Mocha(beverage2);beverage2 = new Mocha(beverage2);beverage2 = new Whip(beverage2);System.out.println("Description: " + beverage2.getDescription() + " $" + beverage2.cost());//再来一杯调料为豆浆(Soy 0.3)、摩卡(Mocha 0.2)、奶泡(Whip 0.4)的Decaf(低咖啡因咖啡 4.00),打印出它的描述与价钱。Beverage beverage3 = new Decaf();beverage3 = new Soy(beverage3);beverage3 = new Mocha(beverage3);beverage3 = new Whip(beverage3);System.out.println("Description: " + beverage3.getDescription() + " $" + beverage3.cost());}}
4、运行结果:
![[设计模式] 装饰者模式 - 图10](/uploads/projects/2book@sjms/38454d3ef02db41b086013ee3ad563de.png)
从以上,我们可以知道,当我们使用继承,导致子类膨胀,我们不想增加很多子类的情况下,将具体功能职责划分,同时继承装饰者超类,动态地给一个对象添加一些额外的职责便实现了我们的装饰者模式。
四、优缺点
- 优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
- 缺点:多层装饰比较复杂。
五、使用场景
1、扩展一个类的功能。
2、动态增加功能,动态撤销。
实际使用:这里我们说一下,在java中I/O便使用了装饰者模式。![[设计模式] 装饰者模式 - 图11](/uploads/projects/2book@sjms/66146080447296de4f57c8bb1ff31b4c.png)
六、装饰者用到的设计原则
1、多用组合,少用继承。
2、对扩展开放,对修改关闭。
转载自: 设计模式之装饰者模式
