1. 装饰模式的引入
当你打开想要喝一杯咖啡时,点开下单小程序,选中了一杯黑咖啡,这时你还可以选择牛奶、摩卡、糖等各种“料”来定制你的咖啡。
如果使用继承方式实现这个下单系统,需要创建子类涵盖下单时所有的可能情况:
- 黑咖啡 + 牛奶 子类
- 黑咖啡 + 牛奶*2 子类
- 黑咖啡 + 三分糖 子类
- 黑咖啡 + 七分糖 子类
- ...
不仅仅可以选择加一份牛奶,还可以加多份牛奶。还可以加三分糖、七分糖等等。每当我们想要增加一种新的咖啡类型或者一种新的“料”时,就需要额外创建更多新的子类。随着类的数量增加,系统的维护也会变得越来越困难。如果咖啡的种类和“料”的组合非常多,那么类的数量将呈爆炸式增长,导致类爆炸。
使用继承方式实现,在编译时就静态地确定了对象的行为,不能在运行时动态的添加其他行为。
2. 装饰模式定义
装饰器模式(Decorator Pattern) 是一种结构型设计模式,它的核心思想是: 在不改变对象结构的前提下,动态地给对象添加新的功能,装饰器模式提供了比继承更有弹性的替代方案。
- 动态地给对象添加额外的职责,而不需要修改原有代码。
- 通过组合而非继承的方式扩展对象功能。
- 可以透明地包装对象,客户端无需知道装饰的存在。
两种使用场景:
- 功能增强型:在原有功能基础上添加新功能,如日志记录、性能监控、缓存等。
- 行为修改型:改变对象的某些行为,如数据加密、压缩、格式转换等。
3. 装饰模式实现咖啡下单
- Coffee:该接口定义了咖啡可以动态添加的职责。
public interface Coffee {
String getDescription();
double cost();
}
- BlackCoffee:具体的咖啡种类,黑咖啡,实现了 Coffee 接口。
public class BlackCoffee implements Coffee {
@Override
public String getDescription() {
return "Black Coffee";
}
@Override
public double cost() {
return 10;
}
}
- CoffeeDecorator:咖啡装饰抽象类,实现了 Coffee,并包含了一个 Coffee 接口的引用。
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public String getDescription() {
if (coffee != null) {
return coffee.getDescription();
}
return "";
}
public double cost() {
if (coffee != null) {
return coffee.cost();
}
return 0;
}
}
- MilkCoffee:具体的装饰对象,起到给具体的 Coffee 对象添加额外的功能,如牛奶咖啡、三分糖咖啡。
public class MilkCoffee extends CoffeeDecorator{
public MilkCoffee(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + " + milk";
}
@Override
public double cost() {
return coffee.cost() + 2;
}
}
public class SugarCoffee extends CoffeeDecorator{
public SugarCoffee(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return super.getDescription() + " + sugar";
}
@Override
public double cost() {
return super.cost() + 1;
}
}
客户端调用:
Coffee blackCoffee = new BlackCoffee();
System.out.println(blackCoffee.getDescription() + " : " + blackCoffee.cost() + "¥");
Coffee milkCoffee = new MilkCoffee(blackCoffee);
System.out.println(milkCoffee.getDescription() + " : " + milkCoffee.cost() + "¥");
Coffee sugarCoffee = new SugarCoffee(milkCoffee);
System.out.println(sugarCoffee.getDescription() + " : " + sugarCoffee.cost() + "¥");
输出结果:
Black Coffee : 10.0¥
Black Coffee + milk : 12.0¥
Black Coffee + milk + sugar : 13.0¥
4. 装饰模式结构
- Component(抽象组件):定义对象的接口,可以给这些对象动态地添加职责。
- ConcreteComponent(具体组件):实现抽象组件接口的具体对象,是被装饰的原始对象。
- Decorator(抽象装饰器):持有一个组件对象的引用,并实现与组件接口一致的接口。
- ConcreteDecorator(具体装饰器):实现具体的装饰功能,给组件添加新的行为。
- Component:定义了一个对象接口,可以给这些对象动态地添加职责,装饰器和被装饰者共有的方法,统一接口。
public interface Component {
void operation();
}
- ConreteComponent:实现 Component 接口的具体类,被装饰者,如黑咖啡,其本身就是一个功能完整的类。
public class ConcreteComponentA extends Component {
public void operation() {
sout("ConcreteComponentA");
}
}
- Decorator:装饰抽象类,实现了 Component,并包含了一个 Component 接口的引用。
public abstract class Decorator extends Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
public void operation() {
if(component != null) {
component.operation();
}
}
}
- ConreteDecorator:具体的装饰对象,给具体的 Component 添加额外的功能,如给黑咖啡加牛奶。每一个装饰类都有具体的装饰效果。
public class ConcreteDecoratorA extends Decorator {
private String desciption;
public void operation() {
super.operation();
description = "ConcreteDecoratorA";
sout(description);
}
}
public class ConcreteDecoratorB extends Decorator {
public void operation() {
super.operation();
addBehavior();
}
private void addBehavior() {
sout("B 的独有操作");
}
}
客户端调用:
ConreteComponent c = new ConreteComponent();
ConcreteDecoratorA d1 = new ConcreteDecoratorA();
ConcreteDecoratorB d2 = new ConcreteDecoratorB();
d1.setComponent(c);
d2.setComponent(d1);
d2.operation();
5. 装饰模式总结
- 动态添加功能
装饰模式通过创建一个新的装饰类来实现功能的动态添加。这个装饰类包含了要添加的新功能,并且它持有一个被装饰对象的引用。这样,装饰类就可以在调用原有功能的基础上,添加新的行为。 - 分离了核心职责和装饰功能
装饰模式有效地将类的核心职责和装饰功能区分开来。核心类只关注于实现其基本功能,而装饰类则负责添加额外的功能。这种分离使得类的设计更加清晰,也更容易理解和维护。 - 去除重复的装饰逻辑
在没有使用装饰模式之前,如果多个类需要添加相同的功能,那么这些功能代码可能会在每个类中重复出现。而使用装饰模式后,可以将这些重复的功能代码提取到一个单独的装饰类中,从而避免代码的重复。 - 灵活性和可扩展性
装饰模式提供了很高的灵活性和可扩展性。可以根据需要创建多个不同的装饰类,每个装饰类都为对象添加了不同的功能。这样,就可以通过组合不同的装饰类来创建具有不同功能的对象。 - 遵循开闭原则
装饰模式允许在不修改现有类代码的情况下添加新的功能,这符合开闭原则的要求。开闭原则要求软件实体应该对扩展开放,对修改关闭。装饰模式通过添加新的装饰类来扩展功能,而不是修改现有类的代码,因此它遵循了开闭原则。 - 客户端代码的透明性
对于客户端代码来说,装饰模式是透明的。客户端代码可以继续使用原有类的接口来与对象进行交互,而不需要知道装饰类的存在。这种透明性使得装饰模式可以很容易地被集成到现有的系统中。
6. 一些问题
Q1: 装饰器模式与代理模式的区别是什么?
- 装饰器模式:主要目的是增强对象功能,为对象添加新的行为或职责
- 代理模式:主要目的是控制对象访问,在访问对象时提供额外的处理逻辑
- 核心区别:装饰器强调"功能增强",代理强调"访问控制"
// 装饰器:功能增强
Coffee decoratedCoffee = new Mocha(new Milk(new Espresso()));
decoratedCoffee.cost(); // 增强了计算逻辑
// 代理:访问控制
Image proxyImage = new ProxyImage("photo.jpg");
proxyImage.display(); // 控制图片加载时机
Q2: 什么时候应该使用装饰器模式?
-
需要动态添加功能
- 场景:在运行时给对象添加额外的功能,而不是编译时确定。
- 例子:文本编辑器中动态添加加粗、斜体、下划线等格式。
-
避免类继承爆炸
- 场景:如果用继承来扩展功能会导致子类数量急剧增加。
- 例子:咖啡店的例子,如果用继承,需要为每种组合创建一个类。
-
功能可以自由组合
- 场景:多种功能可以任意组合,且组合顺序可能影响结果。
- 例子:数据流处理中的加密、压缩、编码等操作。
-
不能修改原有类
- 场景:需要扩展第三方库或遗留代码的功能,但无法修改源码。
- 例子:为第三方组件添加日志记录、性能监控等功能。
Q3: 装饰器模式在实际开发中的应用?
- Java I/O 流
- 应用:BufferedInputStream、DataInputStream 等都是装饰器
- 原理:为基础的 InputStream 添加缓冲、数据类型转换等功能
// Java I/O 装饰器示例
InputStream input = new BufferedInputStream(
new DataInputStream(
new FileInputStream("file.txt")
)
);
-
Spring AOP
- 应用:通过代理和装饰器为方法添加横切关注点
- 原理:在方法执行前后添加日志、事务、安全检查等功能
-
Android View 系统
- 应用:ViewGroup 可以看作是对 View 的装饰
- 原理:为基础 View 添加布局、事件处理等功能
-
Web 开发中的中间件
- 应用:Express.js、Koa.js 等框架的中间件机制
- 原理:为请求处理添加认证、日志、CORS 等功能
// Android 装饰器示例
public class BorderDecorator extends ViewGroup {
private View decoratedView;
private Paint borderPaint;
public BorderDecorator(Context context, View view) {
super(context);
this.decoratedView = view;
this.borderPaint = new Paint();
borderPaint.setColor(Color.BLACK);
borderPaint.setStyle(Paint.Style.STROKE);
addView(view);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 为被装饰的 View 添加边框
canvas.drawRect(0, 0, getWidth(), getHeight(), borderPaint);
}
}