装饰器模式(Decorator Pattern)

191 阅读8分钟

点击阅读:设计模式系列文章


1. 装饰模式的引入

当你打开想要喝一杯咖啡时,点开下单小程序,选中了一杯黑咖啡,这时你还可以选择牛奶、摩卡、糖等各种“料”来定制你的咖啡。

如果使用继承方式实现这个下单系统,需要创建子类涵盖下单时所有的可能情况:

  1. 黑咖啡 + 牛奶 子类
  2. 黑咖啡 + 牛奶*2 子类
  3. 黑咖啡 + 三分糖 子类
  4. 黑咖啡 + 七分糖 子类
  5. ...

不仅仅可以选择加一份牛奶,还可以加多份牛奶。还可以加三分糖、七分糖等等。每当我们想要增加一种新的咖啡类型或者一种新的“料”时,就需要额外创建更多新的子类。随着类的数量增加,系统的维护也会变得越来越困难。如果咖啡的种类和“料”的组合非常多,那么类的数量将呈爆炸式增长,导致类爆炸。

使用继承方式实现,在编译时就静态地确定了对象的行为,不能在运行时动态的添加其他行为。

2. 装饰模式定义

装饰器模式(Decorator Pattern) 是一种结构型设计模式,它的核心思想是: 在不改变对象结构的前提下,动态地给对象添加新的功能,装饰器模式提供了比继承更有弹性的替代方案。

  1. 动态地给对象添加额外的职责,而不需要修改原有代码。
  2. 通过组合而非继承的方式扩展对象功能。
  3. 可以透明地包装对象,客户端无需知道装饰的存在。

两种使用场景:

  1. 功能增强型:在原有功能基础上添加新功能,如日志记录、性能监控、缓存等。
  2. 行为修改型:改变对象的某些行为,如数据加密、压缩、格式转换等。

3. 装饰模式实现咖啡下单

  1. Coffee:该接口定义了咖啡可以动态添加的职责。
public interface Coffee {

    String getDescription();

    double cost();
}
  1. BlackCoffee:具体的咖啡种类,黑咖啡,实现了 Coffee 接口。
public class BlackCoffee implements Coffee {

    @Override
    public String getDescription() {
        return "Black Coffee";
    }

    @Override
    public double cost() {
        return 10;
    }
}
  1. 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;
    }
}
  1. 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(具体装饰器):实现具体的装饰功能,给组件添加新的行为。

image.png

  1. Component:定义了一个对象接口,可以给这些对象动态地添加职责,装饰器和被装饰者共有的方法,统一接口。
public interface Component {
    void operation();
}
  1. ConreteComponent:实现 Component 接口的具体类,被装饰者,如黑咖啡,其本身就是一个功能完整的类。
public class ConcreteComponentA extends Component {
    public void operation() {
        sout("ConcreteComponentA");
    }
}
  1. 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();
        }
    }
}
  1. 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. 装饰模式总结

  1. 动态添加功能
    装饰模式通过创建一个新的装饰类来实现功能的动态添加。这个装饰类包含了要添加的新功能,并且它持有一个被装饰对象的引用。这样,装饰类就可以在调用原有功能的基础上,添加新的行为。
  2. 分离了核心职责和装饰功能
    装饰模式有效地将类的核心职责和装饰功能区分开来。核心类只关注于实现其基本功能,而装饰类则负责添加额外的功能。这种分离使得类的设计更加清晰,也更容易理解和维护。
  3. 去除重复的装饰逻辑
    在没有使用装饰模式之前,如果多个类需要添加相同的功能,那么这些功能代码可能会在每个类中重复出现。而使用装饰模式后,可以将这些重复的功能代码提取到一个单独的装饰类中,从而避免代码的重复。
  4. 灵活性和可扩展性
    装饰模式提供了很高的灵活性和可扩展性。可以根据需要创建多个不同的装饰类,每个装饰类都为对象添加了不同的功能。这样,就可以通过组合不同的装饰类来创建具有不同功能的对象。
  5. 遵循开闭原则
    装饰模式允许在不修改现有类代码的情况下添加新的功能,这符合开闭原则的要求。开闭原则要求软件实体应该对扩展开放,对修改关闭。装饰模式通过添加新的装饰类来扩展功能,而不是修改现有类的代码,因此它遵循了开闭原则。
  6. 客户端代码的透明性
    对于客户端代码来说,装饰模式是透明的。客户端代码可以继续使用原有类的接口来与对象进行交互,而不需要知道装饰类的存在。这种透明性使得装饰模式可以很容易地被集成到现有的系统中。

6. 一些问题

Q1: 装饰器模式与代理模式的区别是什么?

  • 装饰器模式:主要目的是增强对象功能,为对象添加新的行为或职责
  • 代理模式:主要目的是控制对象访问,在访问对象时提供额外的处理逻辑
  • 核心区别:装饰器强调"功能增强",代理强调"访问控制"
// 装饰器:功能增强
Coffee decoratedCoffee = new Mocha(new Milk(new Espresso()));
decoratedCoffee.cost(); // 增强了计算逻辑

// 代理:访问控制
Image proxyImage = new ProxyImage("photo.jpg");
proxyImage.display(); // 控制图片加载时机

Q2: 什么时候应该使用装饰器模式?

  1. 需要动态添加功能

    • 场景:在运行时给对象添加额外的功能,而不是编译时确定。
    • 例子:文本编辑器中动态添加加粗、斜体、下划线等格式。
  2. 避免类继承爆炸

    • 场景:如果用继承来扩展功能会导致子类数量急剧增加。
    • 例子:咖啡店的例子,如果用继承,需要为每种组合创建一个类。
  3. 功能可以自由组合

    • 场景:多种功能可以任意组合,且组合顺序可能影响结果。
    • 例子:数据流处理中的加密、压缩、编码等操作。
  4. 不能修改原有类

    • 场景:需要扩展第三方库或遗留代码的功能,但无法修改源码。
    • 例子:为第三方组件添加日志记录、性能监控等功能。

Q3: 装饰器模式在实际开发中的应用?

  1. Java I/O 流
    • 应用:BufferedInputStream、DataInputStream 等都是装饰器
    • 原理:为基础的 InputStream 添加缓冲、数据类型转换等功能
// Java I/O 装饰器示例
InputStream input = new BufferedInputStream(
    new DataInputStream(
        new FileInputStream("file.txt")
    )
);
  1. Spring AOP

    • 应用:通过代理和装饰器为方法添加横切关注点
    • 原理:在方法执行前后添加日志、事务、安全检查等功能
  2. Android View 系统

    • 应用:ViewGroup 可以看作是对 View 的装饰
    • 原理:为基础 View 添加布局、事件处理等功能
  3. 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);
    }
}