Skip to content

第76课:结构型模式

🎯 学习目标

  • 理解结构型模式解决的是“类和对象如何组合”的问题。
  • 掌握适配器、装饰器、代理、外观、组合、桥接、享元模式的核心思想。
  • 能识别 Java IO、动态代理、Spring AOP、网关层、树形结构中的结构型模式。
  • 能判断组合与继承的取舍。
  • 避免把简单调用链包装成过度复杂的模式。

📖 一、结构型模式解决什么问题

业务系统会不断接入新接口、新组件、新协议。结构型模式关注:

text
接口不兼容怎么适配?
不改原类如何增强功能?
如何隐藏复杂子系统?
如何把对象组合成树?
如何用代理控制访问?

它们的共同目标是降低耦合,让系统结构更容易扩展。


📖 二、适配器模式

适配器把一个已有接口转换成调用方期望的接口。

场景:系统内部统一使用 PaymentClient,但不同支付 SDK 接口不一致。

java
public interface PaymentClient {
    PayResult pay(PayCommand command);
}

第三方 SDK:

java
public class LegacyAlipaySdk {
    public String doPay(String account, int amountInCent) {
        return "SUCCESS";
    }
}

适配器:

java
public class AlipayAdapter implements PaymentClient {
    private final LegacyAlipaySdk sdk;

    public AlipayAdapter(LegacyAlipaySdk sdk) {
        this.sdk = sdk;
    }

    @Override
    public PayResult pay(PayCommand command) {
        String status = sdk.doPay(command.account(), command.amountInCent());
        return new PayResult("SUCCESS".equals(status));
    }
}

适配器适合隔离第三方 SDK,避免外部接口污染内部业务代码。


📖 三、装饰器模式

装饰器在不修改原对象的情况下动态增强功能。

Java IO 是经典例子:

java
InputStream input = new BufferedInputStream(
    new FileInputStream("data.txt")
);

FileInputStream 提供基本读取能力,BufferedInputStream 增加缓冲能力。

业务示例:

java
public interface MessageSender {
    void send(String message);
}

public class SmsSender implements MessageSender {
    public void send(String message) {
        // 发送短信
    }
}

public class LoggingMessageSender implements MessageSender {
    private final MessageSender delegate;

    public LoggingMessageSender(MessageSender delegate) {
        this.delegate = delegate;
    }

    public void send(String message) {
        log.info("send message start");
        delegate.send(message);
        log.info("send message success");
    }
}

装饰器强调“增强同一个接口的能力”,增强后仍然可以当作原接口使用。


📖 四、代理模式

代理控制对目标对象的访问,可以做权限、缓存、事务、远程调用等。

java
public interface UserService {
    UserDTO getById(Long id);
}

public class UserServiceImpl implements UserService {
    public UserDTO getById(Long id) {
        return userRepository.findById(id);
    }
}

代理:

java
public class CachedUserServiceProxy implements UserService {
    private final UserService target;
    private final Cache<Long, UserDTO> cache;

    public UserDTO getById(Long id) {
        return cache.get(id, target::getById);
    }
}

Spring AOP、事务、缓存注解都大量使用代理思想。

java
@Transactional
public void createOrder(OrderCommand command) {
    // Spring 通过代理在方法前后开启/提交/回滚事务
}

📖 五、外观模式

外观模式为复杂子系统提供一个简单入口。

java
@Service
public class OrderFacade {
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final DeliveryService deliveryService;

    public OrderResult placeOrder(OrderCommand command) {
        inventoryService.reserve(command);
        paymentService.pay(command);
        deliveryService.createDelivery(command);
        return OrderResult.success();
    }
}

Controller 不应该直接编排大量底层服务:

text
Controller -> Facade -> 多个领域服务/基础服务

外观模式能让接口层保持简单,但不要把 Facade 写成上帝类。


📖 六、组合模式

组合模式把对象组织成树,让调用方统一处理单个对象和组合对象。

典型场景:

text
菜单树
组织架构
文件目录
评论树
权限节点
表达式树

示例:

java
public interface MenuNode {
    String name();
    void print(int depth);
}

public class MenuItem implements MenuNode {
    private final String name;

    public void print(int depth) {
        System.out.println(" ".repeat(depth) + name);
    }
}

public class MenuGroup implements MenuNode {
    private final String name;
    private final List<MenuNode> children = new ArrayList<>();

    public void add(MenuNode node) {
        children.add(node);
    }

    public void print(int depth) {
        System.out.println(" ".repeat(depth) + name);
        for (MenuNode child : children) {
            child.print(depth + 2);
        }
    }
}

调用方不需要区分叶子节点和组合节点。


📖 七、桥接模式

桥接模式把两个变化维度拆开,避免类爆炸。

场景:通知渠道和消息类型都在变化。

text
渠道:短信、邮件、站内信
类型:验证码、营销、告警

如果用继承,可能出现:

text
SmsCodeMessage
SmsMarketingMessage
EmailCodeMessage
EmailMarketingMessage

桥接写法:

java
public interface MessageChannel {
    void send(String content);
}

public abstract class Message {
    protected final MessageChannel channel;

    protected Message(MessageChannel channel) {
        this.channel = channel;
    }

    public abstract void send();
}

public class AlertMessage extends Message {
    public AlertMessage(MessageChannel channel) {
        super(channel);
    }

    public void send() {
        channel.send("[告警] 服务异常");
    }
}

渠道和消息类型可以独立扩展。


📖 八、享元模式

享元模式复用大量相同或相似的小对象,降低内存占用。

Java 中的例子:

text
String 常量池
Integer 缓存 -128 到 127
线程池复用线程
数据库连接池复用连接

业务示例:棋盘上的棋子类型共享。

java
public record PieceType(String name, String color) {
}

public class PieceFactory {
    private final Map<String, PieceType> cache = new ConcurrentHashMap<>();

    public PieceType get(String name, String color) {
        return cache.computeIfAbsent(name + ":" + color, key -> new PieceType(name, color));
    }
}

享元适合对象数量巨大且内部状态可共享的场景。


⚠️ 九、常见陷阱

1. 适配器里混入业务逻辑

适配器应该转换接口和数据格式,不应该承载复杂业务规则。

2. 代理和装饰器混淆

装饰器强调增强功能,代理强调控制访问。代码结构可能相似,但意图不同。

3. 外观类变成上帝类

Facade 只做编排,不应该吞掉所有业务逻辑。

4. 组合树递归过深

树结构要考虑深度限制、循环引用和批量查询,避免递归栈溢出或 N+1 查询。


🆚 十、Java vs C 对比

维度CJava
适配函数指针、包装函数接口 + 适配器类
装饰结构体嵌套、函数包装同接口包装对象
代理手写转发函数动态代理、CGLIB、Spring AOP
组合struct 指针树接口 + List 子节点

Java 的接口、多态和动态代理让结构型模式更自然,Spring 框架中尤其常见。


💡 十一、最佳实践

  • 接第三方系统时优先使用适配器隔离外部模型。
  • 横切增强优先考虑代理或 AOP,不要散落到业务代码里。
  • Controller 复杂编排可以引入 Facade,但不要让它变成业务黑洞。
  • 树形结构要注意递归深度和查询次数。
  • 优先组合而不是继承,除非存在明确的 is-a 关系。
  • 设计模式要表达意图,类名可以直接体现 Adapter、Proxy、Facade。

🎓 小结

结构型模式关注“对象如何连接”。它们能让旧接口适配新系统、让对象获得额外能力、让复杂子系统暴露简单入口、让树形结构被统一处理。

学习结构型模式时,不要只背 UML,重点看它如何降低耦合和隔离变化。


🧭 十二、模式选择速查

问题优先考虑
第三方接口和内部接口不一致适配器
不改原类但要增强功能装饰器
要控制访问、事务、缓存、权限代理
Controller 调用链太复杂外观
菜单、组织、评论等树结构组合
两个维度都在变化桥接
大量重复小对象占内存享元

判断时先问“变化点是什么”。如果没有变化点,只是为了让代码看起来高级,就不需要模式。


📌 十三、Spring 中的结构型模式

Spring 项目里经常能看到这些模式:

text
HandlerAdapter        适配器
BeanPostProcessor     装饰/扩展思想
Transactional Proxy   代理
Cache Proxy           代理
Spring MVC Dispatcher 外观
Security FilterChain  责任链,但结构上也有组合

示例:@Transactional 不是魔法。调用方以为自己在调用 Service,实际调用的是 Spring 生成的代理对象。代理对象在方法前后开启事务、提交事务或回滚事务。

这也是为什么同一个类内部自调用事务方法可能失效:

java
public void outer() {
    inner(); // 没有经过代理
}

@Transactional
public void inner() {
}

理解代理模式能帮助你理解很多 Spring 行为。


🔍 十四、自测问题

text
适配器和外观有什么区别?
装饰器和代理代码结构相似,意图区别是什么?
Java IO 为什么是装饰器模式的经典例子?
Spring AOP 为什么属于代理思想?
组合模式适合哪些树形业务?
桥接模式解决什么类爆炸问题?
享元模式为什么要区分内部状态和外部状态?
为什么 Facade 不应该变成上帝类?