设计模式-装饰器模式(java)

前言

装饰器模式(Decorator Pattern)属于结构型设计模式。它的核心思想是:在不修改原有对象结构的前提下,通过包装对象,为原对象动态增加额外职责。

它解决的不是“能不能加功能”的问题,而是“如何在不破坏原有代码稳定性的前提下,把新功能优雅地加上去”的问题。

装饰器模式的典型认知误区:遇到”需要给某个已有类增加功能”时,第一反应总是继承——继承后重写方法,把新逻辑写进去。这在功能单一、不再迭代的场景下没有问题,但一旦需要横向扩展多种功能,就会催生出大量子类,最终维护成本失控。

装饰器模式提供了另一条路:不改变原有类,不继承原有类,而是用一个”包装类”在运行时动态叠加新功能。

本文代码案例:https://github.com/likerhood/CodeDesignWork/tree/main/codedesign8.0-0 和 8.0-1 、8.0-2


一、核心定义

装饰器模式,也叫 Decorator、Wrapper,是一种通过“组合”替代“继承”来扩展对象行为的模式。

它的核心定义可以拆成三句话理解:

  1. 装饰器和被装饰对象实现同一个接口

  2. 这样客户端使用时不需要关心拿到的是原始对象,还是被包装后的增强对象。

  3. 装饰器内部持有一个同接口对象的引用
    这个引用可以指向原始对象,也可以指向另一个装饰器,所以装饰器可以一层套一层。

  4. 装饰器在调用原对象方法前后添加额外逻辑
    原有能力不变,新增能力通过外层包装完成。

一句话总结:
装饰器模式不是直接改被装饰对象 ,也不是简单继承它,而是用一个新的对象包住它,在不破坏原对象的情况下增强它。

image-20260517140613462


二、标准体系结构图

标准结构说明:

角色 作用
Component 定义原对象和装饰器共同遵守的接口
ConcreteComponent 被装饰的原始对象,提供基础功能
Decorator 抽象装饰器,内部保存一个 Component 引用
ConcreteDecorator 具体装饰器,负责增加额外行为
Client 负责组装对象,可以自由决定是否加装饰、加几层装饰

关键点在于:Decorator 实现了 Component,同时又持有一个 Component
这就形成了一个非常灵活的结构:

1
2
Component component = new ConcreteComponent();
component = new ConcreteDecorator(component);

客户端最终调用的还是 component.operation(),但实际执行时已经经过了装饰器增强。

你熟悉的 new BufferedReader(new FileReader("")) 就是装饰器模式的经典体现:一层嵌套一层,字节流转字符流再转缓冲流,原有对象完全不受影响。


三、场景推演

业务背景: ERP 系统使用 SSO(单点登录)组件统一管理登录验证。

初期: 系统只需验证登录 ticket 是否合法,验证通过即可访问所有资源。

演进: 随着团队扩大,加入了运营、营销、数据等不同岗位人员,需要对不同用户的访问方法范围做精细化管控——不是每个登录成功的用户都能访问所有接口。

问题: SSO 是一个通用组件,不能直接在里面添加特定业务的访问控制逻辑。需要在不修改 SSO 组件的前提下,为其叠加”方法访问范围校验”功能。

image-20260517140355233

💡 补充说明:什么是 SSO

SSO(Single Sign-On,单点登录)的作用是让用户只登录一次,就可以访问多个相互信任的系统(如公司内部的 OAERPCRM、财务、人事系统等)。

如果没有 SSO,访问每个系统都要单独登录;有了 SSO,登录交由统一认证中心处理。用户登录成功后拿到凭证(如 Ticket 或 Token),之后访问其他业务系统只需携带该凭证即可。

简而言之,SSO 解决的是:“你是谁?你有没有登录?”

但它不直接解决:“你登录之后能访问什么?你有没有权限调用某个方法?”****

两种解法对比:

解法 做法 代价
继承重写 LoginSsoDecorator extends SsoInterceptor,在 preHandle重复 SSO 校验逻辑,再追加权限逻辑 SSO 逻辑改了要同步改所有子类;子类职责不清晰
装饰器模式 LoginSsoDecorator extends SsoDecorator(HandlerInterceptor),通过 super.preHandle() 委托 SSO 执行,只关注权限扩展 零代码冗余,职责清晰,新增扩展类型只加新装饰类

四、实战案例

4.1 需求分析

系统原本已经有一个通用的 SSO 单点登录拦截器,它只负责判断用户是否已经登录成功。最开始这完全没有问题,因为需求很简单:

1
用户请求进入系统 ➔ 校验是否登录 ➔ 登录成功则放行(失败则拦截)

随着业务变复杂,不同用户访问 ERP 系统时,不仅要判断“是否登录”,还要判断“是否有权限访问某个方法”。新的业务流程变成了:

1
用户请求进入系统 ➔ SSO 登录校验 ➔ 登录成功后继续做方法权限校验 ➔ 均通过才放行

痛点分析:为什么不直接改?

面对新增的权限校验需求,常规思路会带来明显的代码腐化:

  • 如果直接修改 SsoInterceptor:会把业务权限逻辑侵入到通用登录组件中,破坏它原本的稳定性。
  • 如果继承 SsoInterceptor 并重写方法:看似扩展了功能,但极易导致子类中重新复制一遍父类的登录校验逻辑,再追加权限校验。一旦后续 SSO 校验规则发生变化,多个子类都要跟着改,维护成本极高。

更合理的方式是:保留原有 SSO 登录校验能力,在它外面再包装一层权限校验能力。 这正是装饰器模式发挥价值的地方。

在本案例中,各个角色的分工如下表所示:

角色分类 对应类名 职责说明
统一接口 HandlerInterceptor 拦截器的顶级标准规范
原始组件 SsoInterceptor 仅负责基础的 SSO 登录校验
抽象装饰器 SsoDecorator 负责包装原始拦截器对象
具体装饰器 LoginSsoDecorator 负责增加具体的方法权限校验逻辑

最终客户端使用时,可以这样优雅地组装:

1
new LoginSsoDecorator(new SsoInterceptor());

这行代码的语义非常清晰:用“权限校验装饰器”,去包装“原始 SSO 登录拦截器”。

实际执行流程为:

  1. 先执行 SsoInterceptor 的登录校验。
  2. 登录成功后,再执行 LoginSsoDecorator 的权限校验。

4.2 架构图

4.2.1 普通方式架构图

使用的是继承方式。

image-20260517141848628

这个版本的问题是:LoginSsoDecorator 虽然继承了 SsoInterceptor,但它并没有复用父类的 preHandle,而是把父类里的登录校验逻辑又写了一遍。

也就是说,代码从:

1
SsoInterceptor 负责登录校验

变成了:

1
LoginSsoDecorator 同时负责登录校验 + 权限校验

短期看能跑,长期看容易失控。
如果后面继续增加“部门校验”“数据范围校验”“接口频率校验”,就可能不断出现新的子类,或者在一个类里堆越来越多逻辑。


4.2.2 装饰器模式架构图

image-20260517162521053

核心变化是:

1
new LoginSsoDecorator(new SsoInterceptor())

这句代码表达的意思是:

1
用 LoginSsoDecorator 包装 SsoInterceptor

执行时先走装饰器:

1
boolean success = super.preHandle(request, response, handler);

super.preHandle(...) 实际会调用被包装对象:

1
handlerInterceptor.preHandle(request, response, handler);

也就是:

1
2
3
LoginSsoDecorator
-> SsoDecorator
-> SsoInterceptor

这样 SsoInterceptor 继续只做登录校验,LoginSsoDecorator 只在登录成功后增加权限校验。


4.3 时序图

4.3.1 普通代码时序图

这个版本中,所有流程都在 LoginSsoDecorator 一个类里完成。

问题是:虽然类名叫 LoginSsoDecorator,但它并不是严格意义上的装饰器。 因为它没有包装一个 SsoInterceptor 对象,而是通过继承重写了全部流程。


4.3.2 装饰器模式时序图

它把两件事分开了:

职责
SsoInterceptor 判断是否登录
LoginSsoDecorator 判断登录后是否有方法权限
SsoDecorator 连接原始对象和装饰对象

4.5 代码分析

4.5.1 普通代码(if-else/硬编码)

tutorials-12.0-1 的核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class LoginSsoDecorator extends SsoInterceptor {

private static Map<String, String> authMap = new ConcurrentHashMap<String, String>();

static {
authMap.put("huahua", "queryUserInfo");
authMap.put("doudou", "queryUserInfo");
}

@Override
public boolean preHandle(String request, String response, Object handler) {

String ticket = request.substring(1, 8);
boolean success = ticket.equals("success");

if (!success) return false;

String userId = request.substring(8);
String method = authMap.get(userId);

return "queryUserInfo".equals(method);
}
}

这个实现的优点是简单直接:

1
登录校验 -> 权限校验 -> 返回结果

但缺点也非常明显:

  1. 重复了 SSO 登录校验逻辑

    SsoInterceptor 已经有 ticket 校验,但子类没有复用,而是重新写了一遍。

  2. 继承关系表达不准确

    LoginSsoDecorator extends SsoInterceptor 看起来是扩展 SSO,但实际是覆盖 SSO 的核心流程。

  3. 扩展能力差

    如果继续增加更多校验,比如角色、部门、数据权限、接口限流,就会让这个类越来越膨胀。

  4. 不利于运行时组合
    继承在编译期就确定了父子关系,不能像装饰器一样灵活组装:

1
new ADecorator(new BDecorator(new SsoInterceptor()))

4.5.2 装饰器模式代码

tutorials-12.0-2 的核心代码分成两部分。

第一部分是抽象装饰器 SsoDecorator

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class SsoDecorator implements HandlerInterceptor {

private HandlerInterceptor handlerInterceptor;

public SsoDecorator(HandlerInterceptor handlerInterceptor) {
this.handlerInterceptor = handlerInterceptor;
}

public boolean preHandle(String request, String response, Object handler) {
return handlerInterceptor.preHandle(request, response, handler);
}
}

这一层的重点不是业务逻辑,而是结构设计。

它通过构造函数接收一个 HandlerInterceptor

1
public SsoDecorator(HandlerInterceptor handlerInterceptor)

所以它可以包装:

1
new SsoInterceptor()

也可以包装另一个装饰器:

1
new LoginSsoDecorator(new OtherDecorator(new SsoInterceptor()))

第二部分是具体装饰器 LoginSsoDecorator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LoginSsoDecorator extends SsoDecorator {

public LoginSsoDecorator(HandlerInterceptor handlerInterceptor) {
super(handlerInterceptor);
}

@Override
public boolean preHandle(String request, String response, Object handler) {
boolean success = super.preHandle(request, response, handler);
if (!success) return false;

String userId = request.substring(8);
String method = authMap.get(userId);

return "queryUserInfo".equals(method);
}
}

这段代码最关键的是:

1
boolean success = super.preHandle(request, response, handler);

它不是自己重新做登录校验,而是把登录校验交回被包装的 SsoInterceptor
只有登录成功后,才执行自己的权限校验逻辑。

测试代码:

1
2
3
LoginSsoDecorator ssoDecorator = new LoginSsoDecorator(new SsoInterceptor());
String request = "1successhuahua";
boolean success = ssoDecorator.preHandle(request, "ewcdqwt40liuiu", "t");

这段组装关系非常重要:

1
LoginSsoDecorator 包装 SsoInterceptor

最终含义是:

1
先执行 SSO 登录校验,再执行登录后的权限校验

总结

装饰器模式解决的不是“能不能加功能”的问题,而是“如何优雅地加功能”的问题。

在本案例中,tutorials-12.0-1 通过继承完成了功能扩展,但登录校验和权限校验耦合在一起,还重复了父类逻辑;tutorials-12.0-2 使用装饰器模式后,SsoInterceptor 继续负责原始登录校验,LoginSsoDecorator 只负责新增权限校验,职责更清楚,扩展也更灵活。

继承是“我变成你的一种特殊类型”,装饰器是“我包住你,并在你原有能力外面再加一层能力”。