在面向对象设计中,**装饰器模式(Decorator)和适配器模式(Adapter)**是两种极其常用且结构相似的结构型设计模式。它们都通过"包装"一个对象来实现特定目的,但由于设计初衷不同,在实际工程中的应用场景也截然不同。本文将从核心区别、代码示例、工程调用方式以及企业级优化方案四个维度,带你彻底搞懂这两种模式。
一、 核心区别:增强功能 vs 转换接口
用一句话概括它们的本质差异:适配器是为了"兼容"(改变接口),而装饰器是为了"增强"(增加功能)。
- 适配器模式 (Adapter) :解决的是不兼容的问题。当两个接口定义不同、无法直接协同工作时,适配器充当"翻译官",将一个类的接口转换成客户端期望的另一个接口。它的目的是复用现有组件而不修改其源码。
- 装饰器模式 (Decorator) :解决的是灵活扩展的问题。当你不想通过继承来为一个类添加新功能(因为继承会导致类爆炸或破坏封装性)时,装饰器可以在运行时动态地给对象添加额外的职责。它的目的是在不改变原有类的前提下,无缝叠加新功能。
生活中的生动比喻:
- 适配器 = 电源转换插头 / 读卡器:你买了一个国外的电器,插头是两脚圆头的,但你家墙上的插座是三脚扁头的。你需要一个"转换插头"才能插上电。插头的形状变了,但电器的功能没变。
- 装饰器 = 手机贴膜+戴壳+挂绳:你的手机原本只能打电话上网。你给它贴了防窥膜,戴了防摔壳,还挂了个支架。手机的型号没变(还是那个接口),但它现在多出了防偷窥、防摔和支撑的新能力。而且你可以随时把壳摘掉换一个新的,不需要重新买一部手机。
二、 代码示例对比
1. 适配器模式代码示例:接口转换
场景:客户只认识"带吸管杯"的接口,但你手里只有普通的"敞口杯"。你需要把它转成客户能用的样子。
csharp
// 1. 客户期望的接口(Target)
interface CupWithStraw {
void sipFromStraw(); // 用吸管喝
}
// 2. 现有的不兼容组件(Adaptee)
class OpenCup {
public void drinkDirectly() {
System.out.println("直接对着敞口杯喝");
}
}
// 3. 适配器(Adapter):实现新接口,包装旧对象
class StrawAdapter implements CupWithStraw {
private OpenCup openCup;
public StrawAdapter(OpenCup openCup) {
this.openCup = openCup;
}
@Override
public void sipFromStraw() {
// 核心动作:将新接口的调用,翻译成对旧方法的调用
System.out.println("插上吸管...");
openCup.drinkDirectly();
}
}
代码本质 :StrawAdapter 实现了全新的 CupWithStraw 接口,它的存在纯粹是为了让 OpenCup 能够被当成吸管杯来使用。接口变了,功能没变。
2. 装饰器模式代码示例:动态增强
场景:你有一杯基础的美式咖啡,你想灵活地给它加奶、加糖,且随时可以叠加。
java
// 1. 原始接口(Component)
interface Coffee {
String getDesc();
double cost();
}
// 2. 具体基础对象(Concrete Component)
class Americano implements Coffee {
@Override
public String getDesc() { return "美式咖啡"; }
@Override
public double cost() { return 10.0; }
}
// 3. 装饰器基类(Decorator):保持原接口,持有原对象引用
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) { this.coffee = coffee; }
}
// 4. 具体装饰器A(加奶)
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) { super(coffee); }
@Override
public String getDesc() { return coffee.getDesc() + " + 牛奶"; } // 增强描述
@Override
public double cost() { return coffee.cost() + 3.0; } // 增加价格
}
// 5. 具体装饰器B(加糖)
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) { super(coffee); }
@Override
public String getDesc() { return coffee.getDesc() + " + 糖"; }
@Override
public double cost() { return coffee.cost() + 1.0; }
}
代码本质 :所有的装饰器都实现了和原始对象相同的 Coffee 接口。你可以像俄罗斯套娃一样无限嵌套。接口没变,功能增强了。
三、 实际工程中的调用场景写法
理解了原理,我们来看看在实际业务中如何编写客户端调用代码。
1. 适配器模式的调用视角
客户端完全不知道底层是敞口杯,它只认吸管杯接口。
java
public class AdapterClient {
public static void main(String[] args) {
// 1. 创建一个现有的、不兼容的旧对象
OpenCup oldCup = new OpenCup();
// 2. 【核心调用】将旧对象塞进适配器中,伪装成新接口
CupWithStraw adaptedCup = new StrawAdapter(oldCup);
// 3. 客户端愉快地使用新接口调用,毫无违和感
adaptedCup.sipFromStraw();
}
}
输出结果:
插上吸管... 直接对着敞口杯喝
2. 装饰器模式的调用视角
客户端依然在使用原始的 Coffee 接口,但通过嵌套构造,动态赋予了咖啡新的能力。
csharp
public class DecoratorClient {
public static void main(String[] args) {
// 1. 准备一杯基础的美式咖啡
Coffee baseCoffee = new Americano();
// 2. 【核心调用】层层包装,动态增加功能
// 先加奶,再加糖(注意从内向外读:美式 -> 加奶 -> 加糖)
Coffee specialCoffee = new SugarDecorator(
new MilkDecorator(baseCoffee)
);
// 3. 客户端调用的依然是 Coffee 接口,但行为已经增强
System.out.println("描述: " + specialCoffee.getDesc());
System.out.println("价格: " + specialCoffee.cost() + "元");
}
}
输出结果:
描述: 美式咖啡 + 牛奶 + 糖 价格: 14.0元
四、 装饰器的痛点与终极优化方案(Spring AOP)
1. 传统手写装饰器的痛点
观察上面的调用代码:
ini
Coffee specialCoffee = new SugarDecorator(new MilkDecorator(baseCoffee));
如果装饰器层级过深,这种"俄罗斯套娃"式的嵌套写法会导致代码极度臃肿、可读性断崖式下降。在企业级工程中,如果需要为 100 个 Service 批量添加日志、缓存等通用逻辑,手动去写这些包装代码显然是灾难。
2. 优化方案:利用 Spring AOP / 动态代理
核心思想:Spring AOP 就是"全自动的装饰器"。
Spring AOP(面向切面编程)的出现,就是为了让你彻底摆脱手动包装的痛苦。它的底层原理其实就是动态代理 。简单来说:你只管写纯粹的业务代码,Spring 会在程序启动时,自动帮你生成那些带有日志、缓存等功能的"装饰器(代理类)"。
实战演示:
第一步:只写干净的业务代码(目标对象)
typescript
@Service
public class OrderServiceImpl implements OrderService {
public void createOrder() {
System.out.println("执行核心业务:创建订单...");
}
}
第二步:定义一个"规则"(切面 Aspect) 你把需要增强的功能单独抽离出来,告诉 Spring:"只要有人调用 createOrder,你就帮我加上这段逻辑。"
less
@Aspect
@Component
public class LogAspect {
// @Around 相当于装饰器里的前置和后置操作
@Around("execution(* com.example.OrderService.createOrder(..))")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("【前置】准备创建订单...");
Object result = joinPoint.proceed(); // 【核心】执行真正的业务方法
System.out.println("【后置】订单创建成功!");
return result;
}
}
第三步:神奇的事情发生了(动态代理织入) 当你在 Controller 里注入这个服务时:
java
@Autowired
private OrderService orderService;
你以为拿到的是真实的 OrderServiceImpl 吗?错!
Spring 在后台偷偷利用 CGLIB 或 JDK 动态代理技术,生成了一个代理类(Proxy Class) 。这个代理类长得就像我们之前手写的 LogDecorator。当你调用 orderService.createOrder() 时,实际上调用的是那个代理类的方法,代理类再顺便去调用你的真实业务方法。
总结
| 维度 | 手写装饰器模式 | Spring AOP / 动态代理 |
|---|---|---|
| 代码侵入性 | 强(必须显式 new Decorator) |
零侵入(业务代码完全不知道被增强了) |
| 维护成本 | 极高(每加一个功能就要改构造代码) | 极低(只需新增一个 @Aspect 切面) |
| 底层本质 | 静态编写具体的装饰器类 | 运行时由 JVM 动态生成代理类(动态装饰器) |
下次看到 Spring AOP,你只需要在心里把它翻译成:"哦,这是框架在运行时帮我自动套了一层装饰器而已。"这样是不是就豁然开朗了?