设计模式--装饰器模式 (Decorator)
装饰器模式是一种结构型设计模式,它允许在不修改对象代码的情况下,动态地为其添加新功能。它通过创建一个包装类来“包裹”原始对象,是比继承更灵活的替代方案。
我们来详细、清晰地讲解设计模式中的装饰器模式(Decorator Pattern)。
1. 什么是装饰器模式?
一句话概括:
装饰器模式是一种结构型设计模式,它允许你在不修改现有对象代码的情况下,动态地为对象添加新的功能或职责。它通过创建一个包装(Wrapper)类来实现,这个包装类“包裹”了原始对象。
核心思想:
继承是一种扩展功能的方式,但它是静态的(在编译时确定)。装饰器模式提供了比继承更灵活的替代方案,可以在运行时动态地给对象添加功能。它遵循开闭原则(Open-Closed Principle):对扩展开放,对修改关闭。
2. 一个生动的比喻:喝咖啡
想象一下你去咖啡店点咖啡:
- 核心产品(Component):你先点一杯最基础的咖啡,比如 浓缩咖啡(Espresso) 或 混合咖啡(HouseBlend)。这是你的核心对象。
- 加点料(Decorator):然后,你想要加一些调料,比如 牛奶(Milk)、摩卡(Mocha)、奶泡(Whip)。这些调料就是“装饰器”。
- 动态组合:
- 你可以只点一杯浓缩咖啡。
- 你也可以点一杯浓缩咖啡,然后加上牛奶。
- 你还可以再在加了牛奶的咖啡上再加上奶泡。
- 甚至可以点一杯加了双份摩卡和一份奶泡的混合咖啡。
在这个过程中:
- 咖啡本身没有被改变。
- 调料(装饰器)被一层一层地“包裹”在咖啡外面。
- 每一层装饰器都增加了新的“风味”(功能)和“价格”(职责)。
- 最终你得到的是一个组合了多种功能的对象,但从外部看,它仍然是一杯“咖啡”(拥有共同的接口)。
3. 装饰器模式的结构
装饰器模式通常包含以下四个核心角色:
Component(组件接口)
- 定义了原始对象和装饰器对象的共同接口。客户端代码可以统一地处理这两种对象。
- 咖啡比喻中:
Beverage(饮料)接口,定义了getDescription()和cost()方法。
ConcreteComponent(具体组件)
- 这是被装饰的原始对象,它实现了
Component接口。 - 咖啡比喻中:
Espresso(浓缩咖啡)类,实现了Beverage接口。
- 这是被装饰的原始对象,它实现了
Decorator(抽象装饰器)
- 这是一个抽象类,它也实现了
Component接口。 - 关键点:它内部持有一个
Component对象的引用(通过组合)。 - 它的作用是作为所有具体装饰器的基类,将所有请求转发给它所包裹的
Component对象。 - 咖啡比喻中:
CondimentDecorator(调料装饰器)抽象类。
- 这是一个抽象类,它也实现了
ConcreteDecorator(具体装饰器)
- 这是实际的装饰器类,负责为
Component对象添加新的职责。 - 它会重写父类(Decorator)的方法,在调用被包裹对象(wrapped object)的同样方法之前或之后,加上自己的逻辑。
- 咖啡比喻中:
Milk(牛奶)、Mocha(摩卡)等具体类。
- 这是实际的装饰器类,负责为
4. Java 代码示例(沿用咖啡比喻)
第1步:定义组件接口 (Component)
java
// Component: 饮料接口
public interface Beverage {
String getDescription();
double cost();
}
第2步:创建具体组件 (ConcreteComponent)
java
// ConcreteComponent: 浓缩咖啡
public class Espresso implements Beverage {
@Override
public String getDescription() {
return "Espresso";
}
@Override
public double cost() {
return 1.99;
}
}
// ConcreteComponent: 混合咖啡
public class HouseBlend implements Beverage {
@Override
public String getDescription() {
return "House Blend Coffee";
}
@Override
public double cost() {
return 0.89;
}
}
第3步:创建抽象装饰器 (Decorator)
java
// Decorator: 调料装饰器(抽象类)
public abstract class CondimentDecorator implements Beverage {
// 持有被装饰者的引用
protected Beverage beverage;
public CondimentDecorator(Beverage beverage) {
this.beverage = beverage;
}
// 装饰器需要重新实现getDescription,具体由子类完成
@Override
public abstract String getDescription();
}
注意:getDescription() 在这里被定义为抽象方法,是为了强制子类提供自己的描述。cost() 方法则没有,因为大多数调料的成本计算方式是 自己的价格 + 被包裹饮料的价格,可以在子类中直接实现。
第4步:创建具体装饰器 (ConcreteDecorator)
java
// ConcreteDecorator: 摩卡
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
// 在原有描述上加上自己的描述
return beverage.getDescription() + ", Mocha";
}
@Override
public double cost() {
// 在原有价格上加上自己的价格
return beverage.cost() + 0.20;
}
}
// ConcreteDecorator: 牛奶
public class Milk extends CondimentDecorator {
public Milk(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + ", Milk";
}
@Override
public double cost() {
return beverage.cost() + 0.10;
}
}
// ConcreteDecorator: 奶泡
public class Whip extends CondimentDecorator {
public Whip(Beverage beverage) {
super(beverage);
}
@Override
public String getDescription() {
return beverage.getDescription() + ", Whip";
}
@Override
public double cost() {
return beverage.cost() + 0.15;
}
}
第5步:客户端测试
java
public class CoffeeShop {
public static void main(String[] args) {
// 1. 点一杯最简单的浓缩咖啡
Beverage beverage1 = new Espresso();
System.out.println("Description: " + beverage1.getDescription());
System.out.println("Cost: $" + beverage1.cost());
// 输出:
// Description: Espresso
// Cost: $1.99
System.out.println("--------------------");
// 2. 点一杯混合咖啡,加双份摩卡和一份奶泡
Beverage beverage2 = new HouseBlend(); // 先来一杯基础咖啡
beverage2 = new Mocha(beverage2); // 用Mocha装饰它
beverage2 = new Mocha(beverage2); // 再用Mocha装饰一次
beverage2 = new Whip(beverage2); // 最后用Whip装饰
System.out.println("Description: " + beverage2.getDescription());
System.out.println("Cost: $" + beverage2.cost());
// 输出:
// Description: House Blend Coffee, Mocha, Mocha, Whip
// Cost: $1.44 (0.89 + 0.20 + 0.20 + 0.15)
}
}
5. 优点和缺点
优点
- 高度灵活性:可以在运行时动态地添加或删除功能,比静态继承灵活得多。
- 遵循开闭原则:可以轻松引入新的装饰器,而无需修改现有的组件或装饰器代码。
- 避免类爆炸:如果使用继承,每一种功能的组合都需要一个子类(如
EspressoWithMilk、EspressoWithMocha、EspressoWithMilkAndMocha...),会导致类的数量急剧增加。装饰器模式用少量的类就能实现这些组合。 - 职责单一:每个装饰器类只关心自己的那部分功能,使代码更易于维护。
缺点
- 增加复杂性:会引入许多小的、功能单一的对象,使得代码的结构变得更加复杂,调试时需要穿过多个包装层。
- 对象身份问题:从外部看,装饰后的对象和原始对象是不同的。例如,
beverage2 instanceof HouseBlend会返回false,因为beverage2的最外层类型是Whip。
6. 适用场景
- 当你希望在不影响其他对象的情况下,以动态、透明的方式向单个对象添加功能时。
- 当使用子类继承来扩展功能会导致类的数量失控时。
- 当你无法使用继承来扩展功能时(例如,类被
final修饰)。 - 当一个对象的职责可以被分离,并且可以动态地组合这些职责时。
真实世界的例子
- Java I/O 类库:这是装饰器模式最经典的例子。
FileInputStream是一个具体组件(ConcreteComponent),用于从文件读取字节。BufferedInputStream是一个具体装饰器(ConcreteDecorator),它包裹了InputStream(FileInputStream的父类),并为其添加了缓冲功能,提高了读取性能。ObjectInputStream也是一个装饰器,它包裹了其他InputStream,并添加了反序列化对象的功能。- 你可以这样组合它们:
new ObjectInputStream(new BufferedInputStream(new FileInputStream("data.txt")))。