揭密设计模式:像搭乐高一样构建功能的装饰器模式

揭密设计模式:像搭乐高一样构建功能的装饰器模式

在软件开发中,我们常常会遇到一个问题:如何给一个对象动态地添加新功能,同时又不想修改它的代码?如果直接在原有类上修修补补,代码会变得臃肿复杂,难以维护。

今天,我们就来聊一个能完美解决这个问题的设计模式------装饰器模式 (Decorator Pattern)


什么是装饰器模式?

简单来说,装饰器模式允许你在不改变一个对象原有代码的前提下,为它增加新的职责或功能。 它就像是给一个基础对象"穿上"不同的"配件",每件"配件"都代表一个新功能。


从一杯咖啡开始理解装饰器

为了更好地理解这个模式,我们来想象一下在咖啡店点咖啡的场景。

  1. 基础对象(Base Component) :首先,你有一杯最基础的黑咖啡(BlackCoffee) 。它有自己的价格(比如5元)和描述("黑咖啡")。
  2. 装饰器(Decorator) :现在你想给这杯咖啡加点东西,比如牛奶(Milk)和糖(Sugar) 。我们把这些配料看作是"装饰器"。它们本身也是咖啡的一种,只不过它们的作用是"包裹"另一杯咖啡,并在其基础上增加新的功能。

这个模式的关键在于,无论是黑咖啡还是牛奶、糖,它们都遵循同一个接口(Interface) 。这个接口定义了所有饮料都必须具备的行为,比如 getDescription()(获取描述)和 cost()(获取价格)。


装饰器模式的优势

  1. 符合开闭原则 :我们不需要修改 BlackCoffee 类的代码。如果未来想增加新的配料,比如"奶油",我们只需要新增一个 CreamDecorator 类即可,对原有代码完全无侵入。
  2. 避免"子类爆炸" :如果不用装饰器模式,我们可能需要创建 MilkCoffeeSugarCoffeeMilkSugarCoffee 等大量子类来应对不同的组合。这会让代码变得异常复杂。装饰器模式通过组合而非继承的方式,巧妙地解决了这个问题。
  3. 动态组合功能:用户可以根据需要,在运行时自由组合功能,比如先加奶再加糖,或者只加糖,非常灵活。

UML 类图

为了更直观地理解,我们来看一下装饰器模式的 UML 类图。

  • Beverage:定义了所有对象都必须实现的接口。
  • BlackCoffee:具体组件,提供了最基础的功能。
  • BeverageDecorator:抽象装饰器,继承了接口并持有一个接口的引用。
  • MilkDecoratorSugarDecorator:具体的装饰器,用来添加新功能。

装饰器模式在框架中的应用

除了我们自己手写的代码,装饰器模式在许多成熟的框架中都有广泛应用。

Java I/O 流

Java 的 I/O 流库是装饰器模式最经典的例子之一。InputStreamOutputStream 是最基础的接口。而像 BufferedInputStreamDataInputStreamGZIPInputStream 等类,都是装饰器 。它们通过包裹一个基础的 InputStream,为其添加新的功能,比如提供缓冲、处理基本数据类型、文件解压缩等。

Spring 框架

在 Spring 框架中,装饰器模式与代理模式的思想常常结合使用。当一个 Bean 被 Spring AOP 增强时,Spring 会创建一个代理对象 。这个代理对象实际上就是装饰器,它包裹着原始的 Bean 对象。当方法被调用时,代理对象会先执行一些额外的逻辑(比如事务的开启和提交、日志的记录),然后再将调用转发给原始的 Bean 对象。这种设计使得 Spring 可以在不修改原始业务代码的情况下,为其动态地添加横切关注点(Cross-cutting Concerns),完美体现了装饰器模式的精髓。


装饰器模式与相似模式的对比

最后,为了更精确地理解装饰器模式,我们来把它和两个容易混淆的模式进行比较。

装饰器模式 vs 代理模式

  • 目的不同 :装饰器模式的目的是动态地增强 一个对象的功能。而代理模式的目的是控制对一个对象的访问
  • 联系与区别:虽然两者在结构上相似(都持有一个对目标对象的引用),但其意图不同。在某些情况下(如 Spring AOP),代理既可以作为访问控制的手段,也可以作为功能增强的方式,从而模糊了两者之间的界限。

装饰器模式 vs 组合模式

  • 目的不同 :装饰器模式是为了增强 一个单一对象的功能。而组合模式是为了将对象组织成树形结构,以表示"部分-整体"的层次关系。
  • 结构不同 :在装饰器模式中,装饰器和被装饰者都实现同一个接口 。而在组合模式中,组合对象和叶子对象也实现同一个接口,但组合对象内部持有的是多个接口的引用(一个集合),目的是管理子对象。

Java 代码实现

接下来,我们用 Java 来实现这个咖啡店的例子。

1. 统一接口:Beverage

首先,定义所有饮料都必须实现的接口。

java 复制代码
// Beverage.java
public interface Beverage {
    String getDescription();
    double cost();
}

2. 具体组件:BlackCoffee

然后,我们创建最基础的饮料类,它实现了 Beverage 接口。

java 复制代码
// BlackCoffee.java
public class BlackCoffee implements Beverage {
    @Override
    public String getDescription() {
        return "黑咖啡";
    }

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

3. 抽象装饰器:BeverageDecorator

为了让所有装饰器都具有统一的结构,我们创建一个抽象装饰器类。它也实现了 Beverage 接口,并持有一个对 Beverage 对象的引用。所有的具体装饰器都将继承这个抽象类。

java 复制代码
// BeverageDecorator.java
public abstract class BeverageDecorator implements Beverage {
    protected Beverage beverage;

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

    @Override
    public abstract String getDescription();

    @Override
    public abstract double cost();
}

4. 具体装饰器:MilkDecorator 和 SugarDecorator

现在,我们创建具体的装饰器类。它们继承 BeverageDecorator 并重写方法,在原有功能上添加新的职责。

java 复制代码
// MilkDecorator.java
public class MilkDecorator extends BeverageDecorator {

    public MilkDecorator(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        // 在原有描述上添加 "加奶"
        return beverage.getDescription() + ",加奶";
    }

    @Override
    public double cost() {
        // 在原有价格上加上牛奶的费用
        return beverage.cost() + 3.0;
    }
}
java 复制代码
// SugarDecorator.java
public class SugarDecorator extends BeverageDecorator {

    public SugarDecorator(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        // 在原有描述上添加 "加糖"
        return beverage.getDescription() + ",加糖";
    }

    @Override
    public double cost() {
        // 在原有价格上加上糖的费用
        return beverage.cost() + 1.0;
    }
}

5. 客户端代码:如何使用

最后,我们来看看如何将这些组件组合起来,构造出我们想要的咖啡。

java 复制代码
// Main.java
public class Main {
    public static void main(String[] args) {
        // 1. 来一杯纯黑咖啡
        Beverage blackCoffee = new BlackCoffee();
        System.out.println("描述: " + blackCoffee.getDescription() + ",价格: " + blackCoffee.cost());

        // 2. 来一杯加奶的黑咖啡
        Beverage milkCoffee = new MilkDecorator(blackCoffee);
        System.out.println("描述: " + milkCoffee.getDescription() + ",价格: " + milkCoffee.cost());

        // 3. 来一杯加奶又加糖的黑咖啡
        Beverage milkSugarCoffee = new SugarDecorator(milkCoffee);
        System.out.println("描述: " + milkSugarCoffee.getDescription() + ",价格: " + milkSugarCoffee.cost());
    }
}

运行结果:

makefile 复制代码
描述: 黑咖啡,价格: 5.0
描述: 黑咖啡,加奶,价格: 8.0
描述: 黑咖啡,加奶,加糖,价格: 9.0

结语

装饰器模式是一种强大且灵活的设计模式。它通过"包裹"而非修改的方式,让我们可以像搭乐高积木一样,动态地为对象添加和组合功能。当你的系统需要灵活扩展、避免大量子类时,不妨考虑一下这个精妙的模式。

相关推荐
小安同学iter12 小时前
Spring Cloud Gateway 网关(五)
java·开发语言·spring cloud·微服务·gateway
码农小C13 小时前
idea2025.1.5安装+pj
java·开发语言·apache
David爱编程13 小时前
synchronized 全解析:从用法到底层原理的全面剖析
java·后端
yzx99101313 小时前
Java视觉跟踪入门:使用OpenCV实现实时对象追踪
java·开发语言·人工智能·opencv
黄雄进14 小时前
Spring Ioc —— 集合类型的依赖注入
java·后端·spring
瓯雅爱分享14 小时前
Java提供高效后端支撑,Vue呈现直观交互界面,共同打造的MES管理系统,含完整可运行源码,实现生产计划、执行、追溯一站式管理,提升制造执行效率
java·mysql·vue·软件工程·源代码管理
Joker—H14 小时前
【Java】Redis(中间件)
java·开发语言·经验分享·redis·中间件
小蒜学长14 小时前
基于Hadoop的可视化城市宜居指数分析(代码+数据库+LW)
java·大数据·数据库·hadoop·spring boot·后端