GoF设计模式——装饰模式

0 阅读10分钟

本文是【GoF设计模式】系列第8篇,更多内容欢迎关注公众号:咖啡八杯

image.png

前言

为什么需要装饰模式?

假设经营一家咖啡店,有基础咖啡和浓缩咖啡两种基底。顾客可以加牛奶、加糖、加珍珠……如果用继承来实现每一种组合,会得到 MilkCoffeeSugarCoffeeMilkSugarCoffeePearlMilkCoffee……每增加一种配料,类的数量就会翻倍。这就是组合爆炸问题。

继承是静态的,编译时就确定了功能组合。但现实中,顾客的需求是动态的——今天加珍珠明天不加,同一个人上午要牛奶下午要燕麦奶。需要一种机制,能在运行时灵活地给对象"叠加"功能,而不是提前把所有组合都写死。

装饰模式正是为此而生:通过组合而非继承,在运行时动态地给对象添加额外功能。被装饰的对象和装饰器实现相同的接口,客户端完全感知不到自己拿到的是原始对象还是被层层包装后的对象。

概念

装饰模式Decorator Pattern是一种结构型设计模式,核心思想是在不改变对象接口的前提下,动态地为对象添加额外职责

装饰模式通过将原始对象放入一个"包装器"(装饰器)中来实现功能增强。装饰器与被装饰对象实现相同的接口,因此可以层层嵌套,形成一条装饰链。

装饰模式的主要角色有:

  • Component(组件) :定义组件和装饰器共同实现的抽象接口
  • ConcreteComponent(具体组件) :被装饰的原始对象,实现 Component 接口
  • Decorator(装饰器) :持有 Component 引用的抽象类,实现 Component 接口,将所有调用委托给被装饰对象
  • ConcreteDecorator(具体装饰器) :在委托调用的基础上添加新功能
classDiagram
    direction BT

    class Component {
        <<interface>>
        +operation()
    }
    class ConcreteComponent {
        +operation()
    }
    class Decorator {
        -component: Component
        +operation()
    }
    class ConcreteDecoratorA {
        +operation()
        +addedBehavior()
    }
    class ConcreteDecoratorB {
        +operation()
        +addedState
    }

    ConcreteComponent ..|> Component : 实现
    Decorator ..|> Component : 实现
    Decorator o--> Component : 持有
    ConcreteDecoratorA --|> Decorator : 继承
    ConcreteDecoratorB --|> Decorator : 继承

ConcreteComponent 是被装饰的原始对象。Decorator 持有一个 Component 引用(可以是 ConcreteComponent,也可以是已经被其他装饰器包装过的对象),并将调用委托给它。ConcreteDecorator 在委托前后添加自己的行为。由于装饰器本身也实现 Component 接口,所以可以层层嵌套,形成装饰链。

可以把装饰模式理解为穿衣服:人(ConcreteComponent)是基础,穿上 T 恤(ConcreteDecoratorA)是第一层装饰,再套上外套(ConcreteDecoratorB)是第二层。每加一层衣服,整体的"功能"就多一层(保暖、防风、好看),但人还是那个人,衣服之间也可以自由搭配、随意增减。

实现

基础实现

装饰模式的标准实现分为以下几个步骤:

  1. 定义 Component 接口,声明组件和装饰器共同实现的方法
  2. 实现 ConcreteComponent,提供基础功能
  3. 定义 Decorator 抽象类,持有 Component 引用并委托调用
  4. 实现 ConcreteDecorator,在委托前后添加新功能
// 步骤1:定义组件接口
interface Component {
    void operation();
}

// 步骤2:实现具体组件
class ConcreteComponent implements Component {
    public void operation() {
        System.out.println("基础功能");
    }
}

// 步骤3:定义抽象装饰器
abstract class Decorator implements Component {
    protected Component component;

    public Decorator(Component component) {
        this.component = component;
    }

    public void operation() {
        component.operation();  // 委托给被装饰对象
    }
}

// 步骤4:实现具体装饰器
class ConcreteDecoratorA extends Decorator {
    public ConcreteDecoratorA(Component component) {
        super(component);
    }

    public void operation() {
        super.operation();  // 先调用被装饰对象的功能
        System.out.println("附加功能A");  // 再添加新功能
    }
}

class ConcreteDecoratorB extends Decorator {
    public ConcreteDecoratorB(Component component) {
        super(component);
    }

    public void operation() {
        super.operation();
        System.out.println("附加功能B");
    }
}

// 客户端使用:层层装饰
Component component = new ConcreteComponent();
component = new ConcreteDecoratorA(component);  // 包装第一层
component = new ConcreteDecoratorB(component);  // 包装第二层
component.operation();
// 输出:
// 基础功能
// 附加功能A
// 附加功能B

引入一个例子:「去咖啡店点单,一杯基础咖啡可以自由加配料——加牛奶、加糖、加珍珠,每加一种配料价格和描述都会叠加,但咖啡本身没变。」

// 组件接口:饮品
interface Beverage {
    String getDescription();
    int getCost();
}

// 具体组件:基础咖啡
class BasicCoffee implements Beverage {
    public String getDescription() {
        return "基础咖啡";
    }

    public int getCost() {
        return 10;
    }
}

// 抽象装饰器:配料基类
abstract class ToppingDecorator implements Beverage {
    protected Beverage beverage;

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

// 具体装饰器:牛奶
class MilkDecorator extends ToppingDecorator {
    public MilkDecorator(Beverage beverage) {
        super(beverage);
    }

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

    public int getCost() {
        return beverage.getCost() + 3;
    }
}

// 具体装饰器:糖
class SugarDecorator extends ToppingDecorator {
    public SugarDecorator(Beverage beverage) {
        super(beverage);
    }

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

    public int getCost() {
        return beverage.getCost() + 1;
    }
}

// 具体装饰器:珍珠
class PearlDecorator extends ToppingDecorator {
    public PearlDecorator(Beverage beverage) {
        super(beverage);
    }

    public String getDescription() {
        return beverage.getDescription() + " + 珍珠";
    }

    public int getCost() {
        return beverage.getCost() + 2;
    }
}

// 客户端:自由组合配料
Beverage order = new BasicCoffee();
order = new MilkDecorator(order);   // 加牛奶
order = new SugarDecorator(order);  // 加糖
order = new PearlDecorator(order);  // 加珍珠

System.out.println(order.getDescription());  // 基础咖啡 + 牛奶 + 糖 + 珍珠
System.out.println(order.getCost());          // 16

总结

装饰模式本质上是一种"包装"机制——用与被装饰对象相同接口的装饰器层层包裹,在不改变原始对象的情况下动态添加功能。

什么时候用

  • 需要在运行时动态地给对象添加功能,而不是在编译时通过继承确定
  • 功能组合种类繁多,用继承会导致类爆炸
  • 需要透明地增强对象功能,客户端无需感知装饰过程

什么时候不用

  • 功能固定不变,继承更简单直接
  • 需要修改对象的核心行为而非添加额外职责
  • 装饰层数过深导致调试困难

简单记忆

装饰解决"动态叠加功能"的问题,是给对象"一层层穿衣服"。功能固定用继承,功能可变用装饰。

装饰 vs 代理 vs 适配器:三个结构型模式都"包了一层对象",结构相似但意图不同:

模式接口关系核心意图
装饰目标接口 = 被包装对象接口增强功能,动态叠加
代理目标接口 = 被包装对象接口控制访问,附加访问前后逻辑
适配器目标接口 ≠ 被包装对象接口转换接口,让不兼容的类协同

口诀对比:装饰增功能,代理控访问,适配改接口。

常见误区

  • 误区:装饰器和代理结构一样,可以混用 → 意图不同:装饰是增强功能、层层叠加;代理是控制访问、通常一对一
  • 误区:装饰器只能包装一层 → 装饰器可以层层嵌套,这是它的核心优势
  • 误区:装饰模式需要修改被装饰对象 → 装饰器完全透明,被装饰对象无需任何改动

练习题目

奶茶定制系统

题目描述:小明经营一家奶茶店,提供两种基础饮品,顾客可以在基础饮品上添加多种加料,同一种加料可以多次添加,每次独立计价。请使用装饰模式实现这个奶茶定制系统。

基础饮品:

  • 编号1:原味奶茶,价格8元,描述 MilkTea
  • 编号2:柠檬茶,价格6元,描述 LemonTea

加料(装饰者):

  • 编号1:珍珠,加价+2元,描述追加, Pearl
  • 编号2:椰果,加价+1元,描述追加, Coconut
  • 编号3:芋圆,加价+3元,描述追加, Taro

输入描述:第一行输入一个整数 T,表示订单数量。每个订单占多行:

  • 第一行:一个整数,基础饮品编号(1 或 2)
  • 第二行:一个整数 M(0 ≤ M ≤ 10),表示加料数量
  • 接下来 M 行:每行一个整数,加料编号(1、2 或 3)

输出描述:对每个订单输出两行:

  • 第一行:饮品完整描述
  • 第二行:总价

输入示例

2
1
3
1
2
1
2
1
3

输出示例

MilkTea, Pearl, Coconut, Pearl
13
LemonTea, Taro
9

解题思路:奶茶定制系统是装饰模式的典型应用。基础饮品是具体组件,加料是具体装饰者。每个装饰者包装基础饮品并添加价格和描述。同一种加料可以多次添加,每次独立计价,体现装饰模式动态组合的特点。

import java.util.*;

// 组件接口
interface Tea {
    String getDesc();
    int getPrice();
}

// 具体组件:原味奶茶
class MilkTea implements Tea {
    public String getDesc() { return "MilkTea"; }
    public int getPrice() { return 8; }
}

// 具体组件:柠檬茶
class LemonTea implements Tea {
    public String getDesc() { return "LemonTea"; }
    public int getPrice() { return 6; }
}

// 抽象装饰类
abstract class Decorator implements Tea {
    protected Tea tea;
    public Decorator(Tea tea) { this.tea = tea; }
}

// 具体装饰者:珍珠
class PearlDecorator extends Decorator {
    public PearlDecorator(Tea tea) { super(tea); }
    public String getDesc() { return tea.getDesc() + ", Pearl"; }
    public int getPrice() { return tea.getPrice() + 2; }
}

// 具体装饰者:椰果
class CoconutDecorator extends Decorator {
    public CoconutDecorator(Tea tea) { super(tea); }
    public String getDesc() { return tea.getDesc() + ", Coconut"; }
    public int getPrice() { return tea.getPrice() + 1; }
}

// 具体装饰者:芋圆
class TaroDecorator extends Decorator {
    public TaroDecorator(Tea tea) { super(tea); }
    public String getDesc() { return tea.getDesc() + ", Taro"; }
    public int getPrice() { return tea.getPrice() + 3; }
}

// 客户端代码
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        while (n-- > 0) {
            int type = sc.nextInt();
            Tea tea;
            if (type == 1) {
                tea = new MilkTea();
            } else {
                tea = new LemonTea();
            }
            int m = sc.nextInt();
            while (m-- > 0) {
                int t = sc.nextInt();
                if (t == 1) {
                    tea = new PearlDecorator(tea);
                } else if (t == 2) {
                    tea = new CoconutDecorator(tea);
                } else {
                    tea = new TaroDecorator(tea);
                }
            }
            System.out.println(tea.getDesc());
            System.out.println(tea.getPrice());
        }
    }
}

扩展:实际项目中的装饰模式

Java I/O 流体系

Java I/O 是装饰模式最经典的教科书案例。InputStream 是抽象组件,FileInputStream 是具体组件,BufferedInputStreamDataInputStreamGZIPInputStream 等都是装饰器。每一层包装添加一种能力:缓冲、类型读取、解压缩……

// 层层装饰:文件 → 缓冲 → 解压缩
InputStream is = new GZIPInputStream(
    new BufferedInputStream(
        new FileInputStream("data.gz")));

关键点:每一层装饰器都持有 InputStream 引用,对外暴露相同的 read() 接口。关闭流时只需关闭最外层,内层会自动关闭。这种设计让 I/O 功能可以自由组合,不需要为每种组合写一个类。

Servlet HttpServletRequestWrapper

Servlet API 提供了 HttpServletRequestWrapper,这是一个典型的装饰器基类。它实现了 HttpServletRequest 接口,将所有方法委托给原始请求。开发者继承它,重写需要增强的方法即可。

// 字符编码装饰器:解决中文乱码
class EncodingRequest extends HttpServletRequestWrapper {
    private String encoding;

    public EncodingRequest(HttpServletRequest request, String encoding) {
        super(request);
        this.encoding = encoding;
    }

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        if (value != null) {
            try {
                value = new String(value.getBytes("ISO-8859-1"), encoding);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        return value;
    }
}

// Filter 中使用装饰器
request = new EncodingRequest(request, "UTF-8");

关键点:Filter 中用装饰器包装原始请求,后续所有 getParameter() 调用都会自动转码。多个 Filter 可以层层包装,每个 Filter 负责一种增强(编码、安全、日志)。

Spring Security 的安全请求装饰

Spring Security 使用装饰模式增强 HttpServletRequest,在请求对象上叠加安全能力——getRemoteUser() 返回当前登录用户,isUserInRole() 检查权限。

// Spring Security 内部:用装饰器包装请求
HttpServletRequest secureRequest =
    new SecurityContextHolderAwareRequestWrapper(originalRequest, authentication);

// 业务代码无感知地调用安全方法
String user = request.getRemoteUser();
boolean isAdmin = request.isUserInRole("ADMIN");

关键点:业务代码拿到的 HttpServletRequest 实际上是被装饰过的对象,但调用方式与原始请求完全一致。装饰器在底层拦截方法调用,从 SecurityContext 中获取认证信息。

Collections.unmodifiableList 不可变包装

Collections.unmodifiableList() 返回一个只读视图,底层数据还是同一个 List,但所有修改操作都会抛出异常。这是一个"限制功能"的装饰器,与"添加功能"方向相反,但原理相同。

List<String> mutable = new ArrayList<>(Arrays.asList("A", "B", "C"));
List<String> readOnly = Collections.unmodifiableList(mutable);

readOnly.get(0);      // 正常:A
readOnly.add("D");    // 抛出 UnsupportedOperationException

关键点unmodifiableList 持有原始 List 的引用,所有读操作委托给原始 List,写操作直接拒绝。这不是增强功能,而是"限制功能",但同样体现了装饰模式"透明包装"的思想。

自定义日志装饰器

在没有 AOP 框架的场景下,可以用装饰模式为方法调用添加日志记录。装饰器在调用前后记录时间戳、参数、返回值,对业务代码完全透明。

interface UserService {
    User findById(Long id);
}

class UserServiceImpl implements UserService {
    public User findById(Long id) {
        return userDao.findById(id);
    }
}

// 日志装饰器
class LoggingUserService implements UserService {
    private UserService target;
    private Logger logger = Logger.getLogger("UserService");

    public LoggingUserService(UserService target) {
        this.target = target;
    }

    public User findById(Long id) {
        logger.info("findById called with id=" + id);
        long start = System.currentTimeMillis();
        User result = target.findById(id);
        logger.info("findById returned in " + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}

// 使用
UserService service = new LoggingUserService(new UserServiceImpl());
service.findById(1L);  // 自动记录日志

关键点:日志装饰器实现了与业务类相同的接口,在不修改业务代码的情况下添加日志能力。可以随时去掉装饰器(换回原始对象),日志功能即消失。

现在可能还用不到这些,但等遇到"想给一个对象加点功能又不想动它"的时候,会突然发现:"这不就是装饰模式吗?"——那时候就真的懂了。

技术交流 & 更多原创内容,关注公众号:咖啡八杯