前言
在软件设计中,我们也有一种类似新房装修的技术可以对已有对象(新房)的功能进行扩展(装修),以获得更加符合用户需求的对象,使得对象具有更加强大的功能。这种技术对应于一种被称之为装饰模式的设计模式,本章将介绍用于扩展系统功能的装饰模式。
装饰器模式概述
装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为,在现实生活中,这种情况也到处存在,例如一张照片,我们可以不改变照片本身,给它增加一个相框,使得它具有防潮的功能,而且用户可以根据需要给它增加不同类型的相框,甚至可以在一个小相框的外面再套一个大相框。
装饰模式是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。
装饰模式结构图:
-
Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。
-
ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。
-
Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。
-
ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。
由于具体构件类和装饰类都实现了相同的抽象构件接口,因此装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。
简单案例
场景:天气太热了,喝点儿冰水解解暑;加点儿柠檬片,让果汁好喝点儿。
先定义一个喝水的接口
public interface Drink {
/**
* 喝水
*/
void drink();
}
写一个接口的实现
public class DrinkWater implements Drink {
@Override
public void drink() {
System.out.println("喝水");
}
}
一个简单的装饰器
public class DrinkDecorator implements Drink {
private final Drink drink;
public DrinkDecorator(Drink drink) {
this.drink = drink;
}
@Override
public void drink() {
System.out.println("先加点儿柠檬片");
drink.drink();
}
}
测试
public class DrinkMain {
public static void main(String[] args) {
Drink drink = new DrinkWater();
drink = new DrinkDecorator(drink);
drink.drink();
}
}
运行结果
先加点儿柠檬片
喝水
一个简单的装饰器模式例子就写完了,这里只是先了解一下装饰器模式。
案例场景
大家应该都喝过奶茶吧,我经常喝的原味奶茶,里面可以添加配料,每增加一种配料,该奶茶的价格就会增加,要求计算一种奶茶的价格。
下面就可以使用装饰器模式来模拟这种场景。
定义抽象构件类
/**
* 奶茶
*/
public interface MilkyTea {
double cost();
}
定义具体构件类
/**
* 珍珠奶茶
*/
public class PearlMilkyTea implements MilkyTea{
@Override
public double cost() {
return 13.0;
}
}
定义抽象装饰类
public abstract class MilkyTeaDecorator implements MilkyTea{
private MilkyTea milkyTea;
public MilkyTeaDecorator(MilkyTea milkyTea){
this.milkyTea = milkyTea;
}
@Override
public double cost() {
return milkyTea.cost();
}
}
定义具体装饰类
/**
* 椰果奶茶
*/
public class CoconutDecorator extends MilkyTeaDecorator{
public CoconutDecorator(MilkyTea milkyTea) {
super(milkyTea);
}
@Override
public double cost() {
System.out.println("添加椰果");
return super.cost() + 2.0;
}
}
/**
* 布丁奶茶
*/
public class PuddingDecorator extends MilkyTeaDecorator{
public PuddingDecorator(MilkyTea milkyTea) {
super(milkyTea);
}
@Override
public double cost() {
System.out.println("添加布丁");
return super.cost() + 1.5;
}
}
测试代码
public class Client {
public static void main(String[] args) {
MilkyTea milkyTea = new PearlMilkyTea();
milkyTea = new CoconutDecorator(milkyTea);
milkyTea = new PuddingDecorator(milkyTea);
System.out.println(milkyTea.cost());
}
}
运行结果
添加布丁
添加椰果
16.5
在客户端代码中,我们先定义了一个MilkyTea类型的具体构件对象milkyTea
,然后将milkyTea
作为构造函数的参数注入到具体装饰类CoconutDecorator中,得到一个装饰之后对象milkyTea
,再将装饰了一次之后的milkyTea
对象注入另一个装饰类PuddingDecorator中实现第二次装饰,得到一个经过两次装饰的对象milkyTea,再调用milkyTea的cost()方法即可得到一杯既加了布丁又加了椰果的珍珠奶茶价格。
装饰器模式优缺点
优点
- 装饰类和被装饰类可以独立发展,不会相互耦合。
- 相比于继承,更加的轻便、灵活。
- 可以动态扩展一个实现类的功能,不必修改原本代码
缺点
- 会产生很多的装饰类,增加了系统的复杂性。
- 这种比继承更加灵活机动的特性,也同时意味着装饰模式比继承易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
装饰器模式适用场景
-
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
-
当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如Java语言中的final类)。
总结
装饰模式降低了系统的耦合度,可以动态增加或删除对象的职责,并使得需要装饰的具体构件类和具体装饰类可以独立变化,以便增加新的具体构件类和具体装饰类。在软件开发中,装饰模式应用较为广泛,例如在JavaIO中的输入流和输出流的设计、javax.swing包中一些图形界面构件功能的增强等地方都运用了装饰模式。