装饰者模式
1、定义
装饰者模式 动态地将责任附加到对象上,若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
在学习装饰者模式之前,如果我们想要扩展一个类的功能,可以通过继承这个类然后扩展它的方法来实现,但是这种方式是静态的,也就是说我们要事先将要扩展的功能写入到继承的类中,我们无法在运行时动态地添加新的功能,而装饰者模式可以做到这一点,下面我们就通过一个案例来详细介绍一下装饰者模式。
2、装饰者模式案例
2.1、需求
假设我们想要开发一套系统为奶茶店计算每一款奶茶的价格,奶茶有多种类型,每种奶茶也可以自由添加各种配料,最终的价格是由奶茶的价格加上各种配料的价格计算得出的,我们来思考一下该如何实现?
2.2、第一种实现方式
我们思考这样一种实现方式,先定义一个接口Drink表示所有的饮料的抽象,其中只包含一个表示价格的方法cost(),之后我们定义一些饮料类来实现这个接口,因为每种饮料的价格不同,所以它们都需要实现cost()方法。这种方式乍看之下没什么问题,但是我们没有考虑到加上配料的情况,例如我们实现了一个红茶类RedTea,它只包含红茶价格的实现,如果我们想点一个冰淇淋红茶,那么我们就要再定义一个冰淇淋红茶类,如果我们又想点一个红茶玛奇朵,那又需要新定义一个红茶玛奇朵类,以此类推,我们需要定义的Drink实现类会越来越多,代码也会越来越臃肿,显然这不是一个很好的方案。
如果单纯通过继承Drink接口无法实现我们的需求,我们是否可以换一个思路,我们将Drink改为一个类,其中定义一系列的布尔类型的变量表示是否有此种配料,再定义一个计算价格的方法cost(),在方法中我们根据是否加了配料来动态地计算价格,我们根据此想法写出以下的代码:
public class Drink {
/**珍珠*/
boolean pearl;
/**椰果*/
boolean coconut;
/**冰淇淋*/
boolean iceCream;
String desc;
public float cost() {
float cost = 0.0f;
//如果有珍珠,价格加1
if(hasPearl()) {
cost += 1;
}
//如果有椰果,价格加0.5
if(hasCoconut()) {
cost += 0.5;
}
//如果有冰淇淋,价格加3
if(hasIceCream()) {
cost += 3;
}
return cost;
}
public boolean hasPearl() {
return pearl;
}
public void setPearl(boolean pearl) {
this.pearl = pearl;
}
public boolean hasCoconut() {
return coconut;
}
public void setCoconut(boolean coconut) {
this.coconut = coconut;
}
public boolean hasIceCream() {
return iceCream;
}
public void setIceCream(boolean iceCream) {
this.iceCream = iceCream;
}
public String getDesc() {
return desc;
}
}
我们可以看到,在Drink类中,我们定义了三种配料珍珠、椰果和冰淇淋,在cost()方法中,我们根据是否有此配料来增加相应的价格,这样,对于每一种饮料,我们都能够动态地计算其价格了。在Drink类中,我们还定义了一个desc属性,用于表示饮料的名称。接下来,我们就可以写几个饮料类来继承Drink类来实现我们的功能了:
/**红茶*/
public class RedTea extends Drink {
public float cost() {
float cost = super.cost();
cost += 10;
return cost;
}
public RedTea() {
desc = "红茶";
}
}
/**绿茶*/
public class GreenTea extends Drink {
public GreenTea() {
desc = "绿茶";
}
public float cost() {
float cost = super.cost();
cost += 11;
return cost;
}
}
/**奶茶*/
public class MilkTea extends Drink {
public MilkTea() {
desc = "奶茶";
}
public float cost() {
float cost = super.cost();
cost += 15;
return cost;
}
}
我们定义了RedTea、GreenTea和MilkTea三个饮料类,并分别重写了cost()方法,cost()方法中首先是调用Drink的cost()方法来计算配料的价格,然后再加上饮料的价格得到的就是最终的售价。现在我们就可以写一个测试方法来测试上面的功能了:
public static void main(String[] args) {
RedTea redTea = new RedTea();
redTea.setIceCream(true);
System.out.println(redTea.getDesc() + "价格:" + redTea.cost());
GreenTea greenTea = new GreenTea();
System.out.println(greenTea.getDesc() + "价格:" + greenTea.cost());
MilkTea milkTea = new MilkTea();
milkTea.setPearl(true);
milkTea.setCoconut(true);
System.out.println(milkTea.getDesc() + "价格:" + milkTea.cost());
}
我们先定义了一个红茶对象,并将冰淇淋设为true,也就是说向红茶中添加了冰淇淋,然后我们定义了一个绿茶对象,里面什么都没有添加,最后我们定义了一个奶茶对象,向其中添加了珍珠和椰果,最后程序的运行结果如下:
红茶价格:13.0
绿茶价格:11.0
奶茶价格:16.5
可以看到,我们能够正确计算出每一款奶茶的价格,最终的运行结果是符合我们预期的,但是我们再仔细想一想,这种实现方式是否存在问题?
- 如果我们要更改调料的价格,例如我们想要将冰淇淋的价格下调为2元,那么就必须要修改现有的Drink类;
- 如果我们想要新增一种调料布丁,那么也需要修改Drink类,向其中添加代码;
- 我们每一种饮料都是继承自Drink类的,也就是说每一种饮料都会继承相应的调料布尔值,不管它是否真的需要这些调料,假设我们要新增一种饮料苏打水,我们还是要继承是否有冰淇淋方法,虽然苏打水根本不需要冰淇淋;
- 我们无法添加多份调料,例如,如果顾客点了两个冰淇淋球的红茶,那现有的实现就无法计算出正确的价格了。
我们可以通过装饰者模式来解决上面这些问题。
2.3、装饰者模式实现
在介绍装饰者模式之前,我们先尝试思考一下有没有什么好的方式能够动态地计算奶茶的价格,假设顾客点了一份绿茶加珍珠和椰果,我们可以先定义一个绿茶对象,然后先使用珍珠对象去装饰绿茶,再使用椰果对象去装饰绿茶,最后通过调用cost()方法,依赖委托将调料价格加上去,这样就完成了饮料价格的计算,光凭描述还是很难理解,我们通过实际编码来讲解一下,首先我们先设计一下类图:
可以看到,我们先定义了一个接口Drink表示饮料,然后我们定义了RedTea、GreenTea和MilkTea,它们实现了Drink接口,同时它们也是我们需要动态新增行为的类,我们可以动态地为它们新增一些功能。我们还定义了一个DrinkIngredients类,它表示饮料调料抽象类,它实际上是一个装饰者,值得注意的是它也实现了Drink接口,在这里我们使用继承并不是为了继承Drink接口的行为,而是为了把饮料和调料能够更有弹性地组合在一起,使我们使用起来更加方便,另外DrinkIngredients类还有一个Drink属性,这里的Drink属性是被装饰者,我们可以通过Drink属性来委托计算价格。DrinkIngredients类也有三个继承者:Pearl、Coconut和IceCream。接下来我们通过编码实现我们的功能,首先我们定义Drink接口,它是所有类的超类:
public interface Drink {
public float cost();
public String getDesc();
}
Drink接口中只有两个抽象方法cost()和getDesc(),然后我们定义几个组件:
/**红茶*/
public class RedTea implements Drink {
@Override
public float cost() {
return 10;
}
@Override
public String getDesc() {
return "红茶";
}
}
/**绿茶*/
public class GreenTea implements Drink {
@Override
public float cost() {
return 11;
}
@Override
public String getDesc() {
return "绿茶";
}
}
/**奶茶*/
public class MilkTea implements Drink {
@Override
public float cost() {
return 15;
}
@Override
public String getDesc() {
return "奶茶";
}
}
这几个类都实现了Drink接口,因为它们是被装饰者装饰的组件,所以它们的方法实现也比较简单,在cost()方法中返回的是这种饮料的价格,无需处理是否有调料,getDesc()方法返回的是这种饮料的名称。接下来我们看一下DrinkIngredients抽象类:
public abstract class DrinkIngredients implements Drink{
Drink drink;
String desc;
}
这个类只包含了Drink属性和desc属性,用它作为所有装饰者的超类,最后我们来定义几个装饰者:
/**珍珠*/
public class Pearl extends DrinkIngredients {
public Pearl(Drink drink) {
this.drink = drink;
}
@Override
public float cost() {
return drink.cost() + 1f;
}
@Override
public String getDesc() {
return drink.getDesc() + ",珍珠";
}
}
/**椰果*/
public class Coconut extends DrinkIngredients {
public Coconut(Drink drink) {
this.drink = drink;
}
@Override
public float cost() {
return drink.cost() + 0.5f;
}
@Override
public String getDesc() {
return drink.getDesc() + ",椰果";
}
}
/**冰淇淋*/
public class IceCream extends DrinkIngredients {
public IceCream(Drink drink) {
this.drink = drink;
}
@Override
public float cost() {
return drink.cost() + 3f;
}
@Override
public String getDesc() {
return drink.getDesc() + ",冰淇淋";
}
}
这几个装饰者都在构造函数中初始化了Drink属性,这样就使被装饰者就被记录到了实例变量中了,另外在cost()方法中,我们首先把调用委托给被装饰对象,以计算饮料的价钱,然后再加上实际调料的价格,这样就得到了最终的价格。现在我们可以写一个测试类来测试我们的功能了:
public static void main(String[] args) {
Drink redTea = new IceCream(new RedTea());
System.out.println(redTea.getDesc() + "价格:" + redTea.cost());
Drink greenTea = new GreenTea();
System.out.println(greenTea.getDesc() + "价格:" + greenTea.cost());
Drink milkTea = new Pearl(new Pearl(new Coconut(new MilkTea())));
System.out.println(milkTea.getDesc() + "价格:" + milkTea.cost());
}
可以看到,我们首先定义了一个红茶类,并向其中添加了一份冰淇淋,然后我们定义了一个绿茶类,其中不添加任何调料,最后我们定义了一份奶茶,向其中添加了一份椰果和两份珍珠,我们使用getDesc()列出了每种饮料的原料和调料信息,最终我们的程序运行结果如下:
红茶,冰淇淋价格:13.0
绿茶价格:11.0
奶茶,椰果,珍珠,珍珠价格:17.5
可以看到,程序正确执行,另外,这种实现方式也解决了我们第一种实现方式中存在的问题:如果我们需要更改价格,我们无需修改Drink类,我们只需修改相应的调料类即可,代码改动影响性最小;如果我们想新增一种调料,我们也无需修改Drink类,只需要新增一个调料的实现即可;对于无需指定调料的饮料,我们也不需要强制继承超类的方法;我们可以自由添加调料,不管需要几份。
2.4、装饰者模式的实际使用
学习完了装饰者模式之后,我们可以再次回想一下以前学习Java IO时,里面定义了很多很多的类,这些类的名字都是差不多的,大多像XXXInputStream、XXXOutputStream、XXXWriter和XXXReader,而且声明一个IO对象的时候往往会很长,其实Java IO类也使用了装饰者模式,下图是《Head First 设计模式》中画的类图:
可以看到,Java IO中的设计和我们之前使用装饰者模式的设计是类似的,这里只画出了InputStream的设计,实际上OutputStream、Writer和Reader都是类似的。
3、总结
通过上面的介绍,我们对装饰者模式有了一定的了解,通过装饰者模式,我们设计了一系列的装饰类,这样我们就可以动态地为对象添加新的功能,这样设计比继承要更有弹性。
装饰者模式反映了装饰的组件类型,实际上,它们是具有相同的类型,都经过接口或者继承实现的。装饰者可以在被装饰者的行为前面与/或后面加上自己的行为,甚至将被装饰者的行为整个取代掉,而达到特定的目的。我们可以使用无数个装饰者来包装一个组件。当然,装饰者对于组件的客户是透明的,除非客户程序以来组件的具体类型,例如,如果我们指定需要使用FileInputStream作为我们的IO流,那么我们是无法使用装饰者模式的。
当然,装饰者模式也有不足之处,最大的问题就是使用装饰者模式会出现很多小的对象,如果过度使用,会让程序变得很复杂,例如Java IO中我们定义一个IO对象时往往会定义很长的一串名字,这样增加的代码了复杂度。