装饰器模式:给对象“穿衣服”的艺术

说实话,我第一次接触装饰器模式是在看Java I/O源码的时候。当时我盯着new BufferedInputStream(new FileInputStream("test.txt"))这行代码,心里想的是:“这嵌套得也太夸张了吧?”后来我才明白,这种层层包裹的设计,恰恰是装饰器模式的精髓所在。

装饰器模式,说白了就是动态地给一个对象添加额外的职责。它比继承更灵活,因为你可以自由组合各种装饰,而不需要为每一种组合都创建一个子类。

模式结构:四个核心角色

装饰器模式有四个参与者,我习惯把它们比作“装修房子”的过程:

角色 类比 职责
抽象构件(Component) 毛坯房的标准 定义对象的核心接口
具体构件(ConcreteComponent) 毛坯房本身 实现核心功能
抽象装饰(Decorator) 装修公司的模板 持有构件引用,实现统一接口
具体装饰(ConcreteDecorator) 刷墙、铺地板 添加具体的新功能
核心要点:装饰器和被装饰对象实现同一个接口,这是关键。这样装饰器才能“冒充”原对象,一层层嵌套下去。

SVG结构图:装饰器模式的核心逻辑

Component(抽象构件) + operation() ConcreteComponent + operation() Decorator(抽象装饰) - component: Component ConcreteDecoratorA + operation() + addedState ConcreteDecoratorB + operation() + addedBehavior() ConcreteDecoratorC + operation() 实现 实现 继承 继承 继承 持有

Java代码示例:一个简单的咖啡订单系统

我记得有一次帮朋友做一个小型咖啡店的点单系统。需求很简单:顾客点一杯咖啡,可以加牛奶、加糖、加奶油,每种添加都要算钱。如果用继承,你得写MilkCoffeeSugarCoffeeMilkSugarCoffee……组合一多就爆炸了。装饰器模式正好解决这个问题。

// 1. 抽象构件:饮料
public interface Beverage {
    String getDescription();
    double cost();
}

// 2. 具体构件:浓缩咖啡
public class Espresso implements Beverage {
    @Override
    public String getDescription() {
        return "浓缩咖啡";
    }

    @Override
    public double cost() {
        return 15.0;
    }
}

// 3. 抽象装饰:调料装饰器
public abstract class CondimentDecorator implements Beverage {
    protected Beverage beverage;

    public CondimentDecorator(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription();
    }

    @Override
    public double cost() {
        return beverage.cost();
    }
}

// 4. 具体装饰:牛奶
public class Milk extends CondimentDecorator {
    public Milk(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + " + 牛奶";
    }

    @Override
    public double cost() {
        return beverage.cost() + 3.0;
    }
}

// 5. 具体装饰:糖
public class Sugar extends CondimentDecorator {
    public Sugar(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + " + 糖";
    }

    @Override
    public double cost() {
        return beverage.cost() + 1.5;
    }
}

// 6. 使用示例
public class CoffeeShop {
    public static void main(String[] args) {
        Beverage espresso = new Espresso();
        System.out.println(espresso.getDescription() + " ¥" + espresso.cost());

        // 加牛奶
        Beverage milkEspresso = new Milk(espresso);
        System.out.println(milkEspresso.getDescription() + " ¥" + milkEspresso.cost());

        // 加牛奶再加糖
        Beverage milkSugarEspresso = new Sugar(milkEspresso);
        System.out.println(milkSugarEspresso.getDescription() + " ¥" + milkSugarEspresso.cost());
    }
}
运行结果:
浓缩咖啡 ¥15.0
浓缩咖啡 + 牛奶 ¥18.0
浓缩咖啡 + 牛奶 + 糖 ¥19.5

Java I/O流中的装饰器应用

你想想看,Java I/O库为什么设计得那么“嵌套”?其实它就是一个大型的装饰器模式实战现场。InputStream是抽象构件,FileInputStreamByteArrayInputStream是具体构件,而FilterInputStream就是抽象装饰,BufferedInputStreamDataInputStreamPushbackInputStream都是具体装饰。

// 经典嵌套:带缓冲的文件读取
InputStream in = new BufferedInputStream(
    new FileInputStream("data.txt")
);

// 再加一层:读取基本数据类型
DataInputStream dataIn = new DataInputStream(
    new BufferedInputStream(
        new FileInputStream("data.bin")
    )
);

// 再加一层:解压缩
InputStream zipIn = new GZIPInputStream(
    new BufferedInputStream(
        new FileInputStream("data.gz")
    )
);

我曾经在做一个日志分析系统时,需要从压缩文件中读取数据,还要做缓冲和行号统计。当时我直接用了三层装饰:LineNumberReader装饰BufferedReader,再装饰InputStreamReader,最后套上GZIPInputStream。代码写出来非常干净,每一层只负责一件事。

避坑指南:我曾经犯过一个错误——在装饰器链中忘记调用父类的operation()方法。结果装饰器只执行了自己的逻辑,原始对象的功能完全丢失了。记住:装饰器一定要调用super.operation()component.operation(),否则就是“只装修不盖房”。

C++代码示例:同样的咖啡系统

#include <iostream>
#include <memory>
#include <string>

// 抽象构件
class Beverage {
public:
    virtual std::string getDescription() const = 0;
    virtual double cost() const = 0;
    virtual ~Beverage() = default;
};

// 具体构件
class Espresso : public Beverage {
public:
    std::string getDescription() const override {
        return "浓缩咖啡";
    }
    double cost() const override {
        return 15.0;
    }
};

// 抽象装饰
class CondimentDecorator : public Beverage {
protected:
    std::unique_ptr<Beverage> beverage;
public:
    CondimentDecorator(std::unique_ptr<Beverage> b) 
        : beverage(std::move(b)) {}
};

// 具体装饰:牛奶
class Milk : public CondimentDecorator {
public:
    using CondimentDecorator::CondimentDecorator;
    std::string getDescription() const override {
        return beverage->getDescription() + " + 牛奶";
    }
    double cost() const override {
        return beverage->cost() + 3.0;
    }
};

// 具体装饰:糖
class Sugar : public CondimentDecorator {
public:
    using CondimentDecorator::CondimentDecorator;
    std::string getDescription() const override {
        return beverage->getDescription() + " + 糖";
    }
    double cost() const override {
        return beverage->cost() + 1.5;
    }
};

int main() {
    auto espresso = std::make_unique<Espresso>();
    std::cout << espresso->getDescription() 
              << " ¥" << espresso->cost() << std::endl;

    auto milkEspresso = std::make_unique<Milk>(std::move(espresso));
    std::cout << milkEspresso->getDescription() 
              << " ¥" << milkEspresso->cost() << std::endl;

    auto milkSugarEspresso = std::make_unique<Sugar>(std::move(milkEspresso));
    std::cout << milkSugarEspresso->getDescription() 
              << " ¥" << milkSugarEspresso->cost() << std::endl;

    return 0;
}
C++注意点:使用std::unique_ptr管理装饰器链中的对象生命周期,避免内存泄漏。移动语义(std::move)确保所有权正确转移。

装饰器模式 vs 继承:什么时候用哪个?

我个人习惯这样判断:

  • 用继承:如果功能扩展是静态的、固定的,比如“所有咖啡都必须有热水”,那就用继承。
  • 用装饰器:如果功能组合是动态的、可选的,比如“今天加牛奶,明天加糖,后天两个都要”,那就用装饰器。

举个例子:如果你要做一个“加密+压缩+日志”的数据传输管道,用装饰器模式可以自由组合:new LoggingDecorator(new CompressionDecorator(new EncryptionDecorator(new FileDataSource())))。如果用继承,你得写EncryptedCompressedLoggedFileDataSource这种又臭又长的类名,而且每增加一种组合就要新建一个类。

实战建议:装饰器模式特别适合以下场景:
• 需要给对象动态添加功能,且功能可以撤销
• 功能组合数量巨大,无法用继承穷举
• 不希望修改原有类的代码(开闭原则)

嗯,装饰器模式就讲到这里。它不像单例模式那么“网红”,但绝对是实用派。下次你看到Java I/O里那些层层嵌套的代码,应该就能会心一笑了——原来都是装饰器在“穿衣服”啊。


公众号:蓝海资料掘金营,微信deep3321