"结构型设计模式"这六个字,凡是学编程的应该都不陌生。它和创建型、行为型并称设计模式三大类,而结构型这七个模式——适配器、桥接、组合、装饰器、外观、享元、代理——恰恰是日常开发里出场率最高、也最容易让人犯迷糊的一组。我见过太多人临到期末或者做大作业时才来突击,结果把装饰器和代理搞混,把适配器和桥接分不清,代码写出来丑得没法看,答辩时被老师一句"你为什么不用xxx模式"问住,直接卡壳。
这篇东西我也不打算写成教科书式的罗列,就按我实际做项目和带新人时总结的思路来拆:每个模式解决什么问题、在什么场景下真的该用、代码该怎么落,以及期末作业和大作业里最容易踩的坑。目标是你看完能直接把结构型模式用起来,而不是背完就忘。
1. 结构型设计模式的整体认知:它到底在解决什么
1.1 为什么叫"结构型":从类的拼装到系统的骨架
想理解结构型模式,先忘掉那些拗口的定义,回到一个最朴素的问题:写代码时,类和数据都有了,怎么把它们"搭"起来才最合理?
创建型模式管的是"对象怎么new出来",行为型模式管的是"对象之间怎么通信"。而结构型模式卡在中间,管的是类和对象的组合方式。你可以把系统想象成一栋房子,创建型是烧砖、和水泥,行为型是房子里的人怎么活动,结构型就是墙体、承重柱、门窗这些骨架——它决定了砖和水泥怎么咬合,才让房子既结实又灵活。
结构型模式里有一部分用继承来实现,比如适配器继承目标接口;有一部分用组合来实现,比如装饰器层层包裹。继承是白盒复用,组合是黑盒复用,后者的耦合度更低,这也是为什么现代开发更推崇组合优先于继承。你在做设计模式大作业时,如果发现某个结构型模式硬要用继承去做,多半是思路偏了。
1.2 七个模式一张表:先建立整体地图
我每次讲结构型模式,都先让学生记住下面这张表。它不是用来背的,是用来在拿到需求时快速定位选型的:
| 模式 | 一句话本质 | 核心解决 | 典型场景 |
|---|---|---|---|
| 适配器 | 翻译接口 | 老接口和新接口对不上 | 兼容旧系统、对接第三方SDK |
| 桥接 | 分离抽象与实现 | 两个维度独立变化 | 跨平台UI控件、多种数据库驱动 |
| 组合 | 树形结构统一处理 | 部分和整体无法分清 | 文件系统、组织架构、菜单 |
| 装饰器 | 动态叠加功能 | 继承爆炸,需要运行时增强 | IO流、日志增强、权限校验 |
| 外观 | 统一门面 | 子系统太复杂,调用方不需要知道细节 | 接口聚合、SDK封装 |
| 享元 | 共享内部状态 | 大量细粒度对象浪费内存 | 字符串常量池、连接池、棋类游戏 |
| 代理 | 控制访问 | 不想直接操作真实对象 | 懒加载、远程调用、AOP |
记这张表有个窍门:别按名字记,按"它让你偷了什么懒"记。适配器让你不用改旧代码,桥接让你不用写爆炸性继承,装饰器让你不用为每个功能组合建一个类……想清楚痛点,模式自然就记住了。
1.3 期末和大作业场景:选型比实现更重要
结合搜索词里频繁出现的"设计模式期末""设计模式大作业",我得说句实话:作业被扣分,多半不是模式实现错了,而是选型选错了。比如需求是"给不同的报表添加导出Excel、导出PDF的能力",这明显是装饰器模式,你非要上继承,每个报表类配一个导出子类,三张报表两种格式就要六个类,老师看一眼就知道你没学透。
选型的正确姿势是:先看需求里有没有"维度"的概念,再看有没有"整体部分"的概念,然后看有没有"接口不匹配"的概念。需求提到"统一访问入口"就考虑外观,提到"对象创建开销大"就考虑享元,提到"不能直接访问目标对象"就考虑代理。这套判断流程我在第三、四、五节会具体演示。
2. 适配器模式与桥接模式:接口的翻译官和维度的拆解师
2.1 适配器模式:别让新旧代码互相嫌弃
适配器模式的目标很直白——让接口不兼容的类能一起工作。它分两种:类适配器用多继承实现,对象适配器用组合实现。Java不支持多继承,所以Java里绝大多数是对象适配器;C++可以多继承,但实际工程里也推荐对象适配器,因为更灵活。
我举个实际的工作场景。之前接一个老项目的需求,数据库要从MySQL迁移到Oracle,老代码里全是mysql_query($conn, $sql)这类直连写法。我不可能把几百处调用全改了,正确做法是定义一个DatabaseInterface,暴露出query、execute、connect三个方法,再写一个MysqlAdapter和一个OracleAdapter去适配不同类型的底层连接。这样上层业务代码一行不用动,新数据库的驱动直接被适配器接进来。
光说概念太虚,给一段能跑的核心示例,Java版对象适配器:
// 目标接口:业务代码真正依赖的抽象 public interface DatabaseInterface { void connect(String url); ResultSet query(String sql); void execute(String sql); } // 被适配者:第三方MySQL驱动,有自己的一套API public class MysqlDriver { public void open(String url) { /* 建立连接 */ } public Object run(String sql) { return new ResultSet(); } public void update(String sql) { /* 更新 */ } } // 适配器:把MysqlDriver翻译成DatabaseInterface public class MysqlAdapter implements DatabaseInterface { private MysqlDriver driver = new MysqlDriver(); @Override public void connect(String url) { driver.open(url); // 内部仍然是调用老接口 } @Override public ResultSet query(String sql) { return (ResultSet) driver.run(sql); } @Override public void execute(String sql) { driver.update(sql); } }看到没有,适配器做的就是一个"翻译"的动作:外部只知道DatabaseInterface,内部把请求转成MysqlDriver能理解的调用。这就是典型的对象适配器,它持有被适配者的引用,而不是继承它。
实操时有一个细节必须注意:适配器不要塞入业务逻辑。我见过有人嫌麻烦,把SQL拼接、结果集处理也写进适配器,导致适配器越来越臃肿。适配器只做接口转换,其他一概不管,这是它的本分。
2.2 桥接模式:两个维度,别用继承硬凑
桥接模式是我在作业里见少用对频率最高的模式,也是面试里最容易和适配器混淆的模式。区分它们的口诀很简单:适配器是让你已有的代码继续能用,桥接是让未来的变化不会互相牵制。
桥接模式的核心是"把抽象部分和实现部分分离,让它们可以独立变化"。英语里有句描述最形象——Decouple an abstraction from its implementation。举例:消息系统有普通消息和加急消息,发送方式有邮件和短信。如果全部用继承,就是普通邮件、普通短信、加急邮件、加急短信四个类。再加一个"延迟消息"维度呢?六个类;再加"微信发送"呢?九个类。继承爆炸就是从这里来的。
正确解法是用桥接,抽象的"消息类型"和"发送方式"各出一条继承链,中间通过组合搭一座桥,C++代码如下:
// 实现部分:发送方式 class MessageSender { public: virtual void send(const std::string& content) = 0; virtual ~MessageSender() = default; }; class EmailSender : public MessageSender { public: void send(const std::string& content) override { std::cout << "[Email] " << content << std::endl; } }; class SmsSender : public MessageSender { public: void send(const std::string& content) override { std::cout << "[SMS] " << content << std::endl; } }; // 抽象部分:消息类型 class Message { protected: MessageSender* sender; public: Message(MessageSender* s) : sender(s) {} virtual void sendMessage(const std::string& content) = 0; }; class UrgentMessage : public Message { public: UrgentMessage(MessageSender* s) : Message(s) {} void sendMessage(const std::string& content) override { sender->send("[加急]" + content); } }; // 使用:两个维度任意组合 // EmailSender email; // UrgentMessage msg(&email); // msg.sendMessage("系统宕机了");桥接模式的精髓在构造函数上——Message持有一个MessageSender引用,消息类型和发送方式各自扩展,互不干扰。以后加一个WechatSender,直接实现MessageSender就好,完全不用碰Message这条链。
我调试桥接代码时常遇到一个问题:有人为了让代码显得高级,把桥接用到根本不需要的地方。比如两个维度里一个维度永远不变,那桥接就是过度设计。桥接的价值前提是两个维度至少有一方变化频繁,否则纯属浪费。作业里选型时,先问自己:这个系统真的有两个独立的扩展方向吗?没有就别硬上。
2.3 适配器和桥接的区分指南(含面试版回答)
作业答辩或者面试时,被问到"适配器和桥接有什么区别"是大概率事件。我给一个能直接套用的回答框架:
先说共同点,它们都在解决"类之间关系复杂"的问题,都倾向于用组合取代继承。然后说本质区别:适配器是为了兼容,它的目标是把不匹配的接口修好,让双方能对接,通常是事后补救;桥接是为了设计,它在编码之前就规划好两个维度各自独立扩展,是事前预防。
再补一个判断技巧:如果拿到需求时"接口不兼容"已经是个既定事实,这是适配器;如果需求里存在两个变化方向,且你不希望它们互相绑架,这是桥接。有了这个判断技巧,期末卷子上的场景题基本不会丢分。
3. 组合模式与装饰器模式:树形结构的统一与功能的动态叠加
3.1 组合模式:让叶子节点和枝干节点用同一种方式对待
组合模式解决的是"部分与整体"的递归关系。文件系统里一个文件夹下面有文件也有子文件夹,管理后台里一个部门下面有员工也有子部门,GUI里一个面板里面有按钮也有子面板——这些都是天然的树形结构。
组合模式最核心的设计决策是:让叶子节点和容器节点实现同一个抽象接口。这样调用方就不用关心自己处理的是单个对象还是整个树,一个display()方法既能显示文件也能显示整个目录树。
Java版的核心结构是这样:
// 抽象构件:统一叶子与容器的接口 public abstract class FileSystemNode { protected String name; protected String path; public FileSystemNode(String name, String path) { this.name = name; this.path = path; } public abstract void display(int depth); } // 叶子构件:文件,没有子节点 public class FileNode extends FileSystemNode { private long size; public FileNode(String name, String path, long size) { super(name, path); this.size = size; } @Override public void display(int depth) { System.out.println(" ".repeat(depth) + "📄 " + name + " (" + size + "KB)"); } } // 容器构件:文件夹,内部可以装文件也可以装子文件夹 public class DirectoryNode extends FileSystemNode { private List<FileSystemNode> children = new ArrayList<>(); public DirectoryNode(String name, String path) { super(name, path); } public void add(FileSystemNode node) { children.add(node); } public void remove(FileSystemNode node) { children.remove(node); } @Override public void display(int depth) { System.out.println(" ".repeat(depth) + "📁 " + name); for (FileSystemNode child : children) { child.display(depth + 1); // 递归调用 } } }写组合模式时有一个极其容易翻车的点:递归的终止条件没想清楚。display方法在DirectoryNode里递归遍历子节点,但FileNode的display不会再递归——这就是天然终止条件,叶子节点就是递归树的边界。如果你把递归条件写在调用方,而不是让每个节点自己负责自己的行为,组合模式就白写了。
另一个实操经验是:容器节点的add和remove方法是否要定义在抽象接口里,业界有"透明式"和"安全式"两种流派。透明式把add、remove也放进抽象类,调用方操作方便,但叶子节点调用add会抛异常;安全式只在容器里定义add、remove,接口更安全,但调用方需要instanceof判断类型。我的建议是:学习作业用透明式,展示组合的优势更直观;工程代码用安全式,避免接口污染。
3.2 装饰器模式:叠加功能像套娃,但每个娃都能单独用
装饰器模式是结构型模式里最"生动"的一个——它就是把功能像套娃一样一层层套起来。Java里的IO流是最经典的教科书案例:new BufferedReader(new FileReader("test.txt")),就是先用FileReader读字节,再套上BufferedReader加缓冲。
装饰器模式解决的核心痛点是继承爆炸。以前面加急消息的例子继续,如果既要加急又要延迟又要密文,用继承得写多少排列组合类?装饰器则完全不同——每个功能做一个装饰器类,然后按需组合:new 加密(new 延迟(new 加急(msg))),三个装饰器,任意组合。
C++版装饰器模式的骨架:
// 抽象组件 class Message { public: virtual std::string getContent() = 0; virtual ~Message() = default; }; // 具体组件:原始消息 class BaseMessage : public Message { private: std::string content; public: BaseMessage(const std::string& c) : content(c) {} std::string getContent() override { return content; } }; // 装饰器基类:持有Message引用,这也是装饰器的关键 class MessageDecorator : public Message { protected: Message* wrapper; public: MessageDecorator(Message* m) : wrapper(m) {} std::string getContent() override { return wrapper->getContent(); } }; // 具体装饰器:加密 class EncryptDecorator : public MessageDecorator { public: EncryptDecorator(Message* m) : MessageDecorator(m) {} std::string getContent() override { std::string s = wrapper->getContent(); // 模拟加密,实际用真正的加密算法 std::string result = ""; for (char c : s) result += (char)(c + 1); return "[加密]" + result; } }; // 具体装饰器:附加日志 class LogDecorator : public MessageDecorator { public: LogDecorator(Message* m) : MessageDecorator(m) {} std::string getContent() override { std::string s = wrapper->getContent(); std::cout << "[日志]" << s << std::endl; return s; } }; // 使用:套娃式组合 // Message* msg = new LogDecorator(new EncryptDecorator(new BaseMessage("Hello"))); // std::cout << msg->getContent() << std::endl;关于装饰器,我有一条反复强调的纪律:装饰器和原始组件必须实现同一个接口。这是装饰器能"套娃"的前提。如果某天你发现装饰器里要加一堆新方法,而原始组件没有这些方法,那就说明你的层次结构设计错了,应该回去重新建模。
另一个常见问题是和代理模式混淆。我后面会专门讲代理,这里先给一个最简单的分辨方法:装饰器是为调用方添加功能,代理是限制访问。同样是包装一层,装饰器允许你调用新加的方法,代理则往往什么都不加,只是控制原方法的入口。
3.3 组合模式与装饰器模式的异同辨析
两种模式在形式上都涉及树形结构和递归,很多人学完就乱,我就直接做个对比。
它们最大的区别在意图上:组合模式想让你忽略单个对象和组合对象的差别,统一对待;装饰器模式想让你无中生有地叠加能力,动态扩展。组合模式中,父节点持有子节点的引用,所以天然是树;装饰器模式中,装饰器只持有一层引用,套多层时看起来像链表,但和树没关系。
还有一个场景判断法:需求里出现"部分-整体"语义,比如菜单里嵌套菜单,就用组合;需求里出现"增强""附加""临时加一个功能",比如给汉堡加芝士加牛肉,就用装饰器。
4. 外观模式与享元模式:系统的门面担当和内存的抠门专家
4.1 外观模式:给调用方一个傻瓜式入口
外观模式可能是七个模式里最容易理解的,一句话:你不需要知道子系统内部怎么运作,只需要一个统一入口。就像你去餐厅吃饭,不需要知道后厨有几个厨师、用了什么灶,只需要对服务员点单就行。
工程里最常见的外观模式应用是SDK封装。一个视频处理SDK,内部可能要初始化播放器、加载解码器、管理字幕、处理音轨,如果让外部调用全部内部模块,调用方得写一大堆初始化代码。正确的做法是提供一个VideoFacade,把init()、play()、pause()、stop()这些粗粒度方法暴露出去,内部再去协调各个子系统。
写外观模式时最需要克制的是:不要在外观类里写太多业务逻辑。外观只是把复杂调用串起来,业务规则应该留在子系统内部。很多人写着写着把外观类写成了一个上帝类,所有逻辑全塞进去,结果子系统之间的耦合转移到了外观类里,这跟没做封装没有区别。
外观模式和中介者模式也容易混,但它们在本质上是不同的:外观模式是单向的,子系统并不知道外观的存在;中介者模式是双向的,多个对象通过中介者通信。如果你发现外观类需要反向调用子系统的细节,那这个方案就要重新考虑。
4.2 享元模式:把重复创建的代价降到最低
享元模式的核心思想是共享,只适合处理大量相似的细粒度对象。最经典的例子是String常量池:同样的字符串字面量在内存中只有一份,所有引用都指向它。游戏里的子弹、树、粒子效果,如果用new创建几千个对象,内存直接爆掉;用享元模式,相同状态的物体共享同一份数据,只有状态不同的部分才单独存储。
享元模式把对象的状态分成两类,这个划分是整个模式能否成功的关键:内部状态(Intrinsic State)存在享元对象内部,永不变化,可以被共享;外部状态(Extrinsic State)由客户端持有,使用对象时再传入。
我用一个俄罗斯方块的例子来说明。如果一个方块算一类,那I、L、J、T、S、Z、O七种方块是固定不变的,这些是内部状态;但每个方块的位置、旋转角度、颜色方案是变化的,这些是外部状态。
// 享元工厂:负责创建和管理享元对象 public class TetrisBlockFactory { private static final Map<String, TetrisBlock> blockMap = new HashMap<>(); public static TetrisBlock getBlock(String type) { TetrisBlock block = blockMap.get(type); if (block == null) { block = new TetrisBlock(type, new int[][]{/* 初始化形状矩阵 */}); blockMap.put(type, block); System.out.println("创建新的方块类型:" + type); } return block; } } // 享元对象:内部状态是type和形状,外部状态是位置x,y public class TetrisBlock { private final String type; private final int[][] shape; public TetrisBlock(String type, int[][] shape) { this.type = type; this.shape = shape; } public void display(int x, int y) { // x, y是外部状态 System.out.println(type + "方块绘制在(" + x + ", " + y + ")"); } } // 使用:每个方块对象可以反复使用 // TetrisBlock lBlock = TetrisBlockFactory.getBlock("L"); // lBlock.display(10, 20); // lBlock.display(11, 20); // 同一个对象,不同位置我在C++课程设计里见过一个反面案例:有学生要渲染一千个雪花,给每个雪花单独new了一个对象,每个对象里都存了形状、颜色、大小这些完全相同的属性。用享元模式重写之后,所有雪花共享同一个形状数据,只存各自的位移和透明度,内存占用从十几MB直接降到几百KB,这个优化效果在期末答辩时是非常有说服力的。
享元模式的陷阱是线程安全。共享意味着并发访问,如果享元对象的内部状态被外部修改,整个系统的数据就乱了。所以享元的内部状态必须是只读或不可变的,需要修改的状态一律作为外部状态传入。
4.3 外观+享元的经典组合:缓冲池设计
设计模式从来不是孤立使用的。外观和享元经常合作——数据库连接池就是典型。连接本身被共享(享元),池的对外接口是一个简单的getConnection()(外观)。外部调用方完全不知道池子里有多少连接、连接如何创建销毁,只需要拿连接、用、归还。这个组合思路在你做大作业时特别值得展示,既能体现对单一模式的理解,又能体现模式组合的能力,这是高分作业和普通作业的分水岭。
5. 代理模式:不直接动手,找个中间人
5.1 代理模式的四种经典应用场景
代理模式的结构和装饰器几乎一模一样,都是持有一个真实对象的引用,然后包装一层。区别在于目的。代理模式的目的不是增加新功能,而是控制对真实对象的访问。根据控制的方式不同,我归纳为四类:
- 远程代理:隐藏对象存在于不同地址空间的事实。RPC框架的客户端就是一个代理,调用本地方法其实是发网络请求到远端执行。
- 虚拟代理:延迟对象的创建。大图加载时可以先用一个小图片代理,真正的图片在后台慢慢加载。
- 保护代理:控制访问权限。某些页面只有登录用户才能访问,代理里做权限校验。
- 智能引用:在访问对象时附加额外操作,比如引用计数、日志记录,这其实就是AOP的雏形。
期末大作业里,保护代理是最容易展示的场景。比如一个文档管理器,普通用户只能read(),不能write();管理员两个都能操作。用代理模式,真实文档对象完全不知道权限的存在,代理在调用前拦截判断。
5.2 Java动态代理:作业里最亮眼的加分项
静态代理是每个类手写一个代理类,一个真实类配一个代理类,很死板。Java的InvocationHandler可以实现在运行时动态生成代理类,代码量小,可维护性强,而且答辩时导师看了就知道你是真的理解代理,不是背概念。核心代码如下:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; // 真实主题接口 interface DocumentService { void read(String user); void write(String user, String content); } // 真实主题实现 class DocumentServiceImpl implements DocumentService { @Override public void read(String user) { System.out.println(user + "读取了文档"); } @Override public void write(String user, String content) { System.out.println(user + "写入了:" + content); } } // 动态代理处理器 class AccessInvocationHandler implements InvocationHandler { private Object target; public AccessInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String user = (String) args[0]; // 保护代理:write操作需要admin权限 if (method.getName().equals("write") && !"admin".equals(user)) { throw new SecurityException("用户 " + user + " 没有写入权限"); } // 原样转发给真实对象 return method.invoke(target, args); } } // 客户端获取代理对象 public class ProxyDemo { public static void main(String[] args) { DocumentService realService = new DocumentServiceImpl(); DocumentService proxy = (DocumentService) Proxy.newProxyInstance( realService.getClass().getClassLoader(), realService.getClass().getInterfaces(), new AccessInvocationHandler(realService) ); proxy.read("zhangsan"); // proxy.write("zhangsan", "hello"); // 运行时会抛出SecurityException proxy.write("admin", "hello"); } }这个示例把保护代理和动态代理结合了。有一个必须记牢的坑:Proxy.newProxyInstance的第二个参数是接口数组,它要求真实对象实现了接口,而且代理对象只能强转成接口类型。如果你忘了传接口数组,或者真实对象没实现接口,运行时会直接抛异常。
5.3 代理 vs 装饰器 vs 适配器:三兄弟一次性分清
这三个模式结构都像"包一层",容易混,我直接给一个终极分辨方法,这也是面试的高频题。
- 看接口:适配器的接口和目标接口讨论的是不同的事物,比如USB接口和Type-C接口,它们天生不匹配;装饰器和代理的接口与被包装对象完全一致,讨论的是同一件事。
- 看目的:适配器的目的是兼容,让原本不匹配的两方协作;装饰器的目的是增强,给原本的对象加新能力;代理的目的是控制,限制对原本对象的访问。
- 看生命周期:适配器往往在系统集成时一次性定义好;装饰器可以动态地在运行时任意组合套娃;代理通常绑定一个具体的真实对象,职责单一。
做个表更直观:
| 对比维度 | 适配器 | 装饰器 | 代理 |
|---|---|---|---|
| 接口 | 转换接口 | 与目标接口一致 | 与目标接口一致 |
| 核心目的 | 兼容 | 增强 | 控制 |
| 创建时机 | 集成期 | 运行期动态组合 | 运行期或编译期 |
| 双方关系 | 原本不认识 | 认识且同一接口 | 认识且同一接口 |
去面试或答辩时,能把这个表流利地讲出来,面试官对你的理解深度评估会上一个台阶。
6. 常见问题与排查技巧实录:作业答辩里最容易翻的车
6.1 选型错误:把七个模式当万能药乱套
我审过不少设计模式课程作业,最大的问题不是代码bug,而是用错了地方。有学生为了凑模式数量,把外观模式用到只有两个类的系统上,把享元模式用到总共才创建十个对象的地方,还把代理和装饰器叠着上——结果代码长度翻倍,可读性崩了。
给一个简洁的选型checklist:
- 需求里有没有"接口不兼容"?有 → 适配器
- 需求里有没有"两个维度独立变化"?有 → 桥接
- 需求里有没有"部分-整体"的树形关系?有 → 组合
- 需求里有没有"动态添加功能"或"功能排列组合"?有 → 装饰器
- 需求里有没有"子系统很复杂,我只想给一个简单入口"?有 → 外观
- 需求里有没有"大量相似对象导致内存压力"?有 → 享元
- 需求里有没有"不能直接访问,需要中间控制"?有 → 代理
这七个问题按顺序过一遍,选型基本不会错。记住:设计模式的目的是让代码更简单、更清晰,而不是让代码看起来更复杂。如果引入模式后代码更难懂了,那一定是你的用法出了问题。
6.2 实现细节里的四个高频bug
代码层面的问题也有规律可循,我列四个最常见的,都是我真实踩过或者帮别人调过的:
- 装饰器构造函数忘记调用父类初始化。子类装饰器继承了
MessageDecorator,但构造函数里忘了传入Message引用,导致空指针。C++里直接段错误,Java里是NullPointerException。排查方式很简单:所有装饰器的第一行代码应该是构造函数的参数传递。 - 组合模式忘记递归终止条件。
display里只写了遍历逻辑,忘了判断是叶子节点还是容器节点,结果无限递归,栈溢出。这个bug的特征是运行时报StackOverflowError,而且报错栈里能看到自己调用自己。 - 适配器方法的返回类型对不上。被适配者的方法返回
void,但目标接口要求返回ResultSet,适配器里憋不出来,只能到处找替代方案。做适配器前先画一张接口映射表,把每个方法的源返回类型和目标返回类型都写清楚再动手。 - 代理对象忘记实现接口。Java动态代理第二参必须传接口列表,很多人从网上抄代码,第三个参数抄成了真实对象的实例而不是
InvocationHandler。结果编译没问题,运行就抛ClassCastException。
6.3 答辩时被问到"为什么用这个模式"怎么答
期末答辩或面试,被问"为什么用xxx模式"几乎是必考题。我给一个万能回答模板,但不是让你去背,而是帮助你建立表达框架:
先说场景("我们的需求是……"),再说痛点("如果不用这个模式,会遇到……问题"),最后说这个模式带来的收益("用了之后,可以独立扩展/动态调整/复用已有代码……")。如果还能补一句这个模式的代价("但它引入了更多的类,增加了系统复杂度"),那就更稳了——这说明你真的理解了设计模式是Trade-off,不是银弹。
6.4 一份期末考试速查口诀
最后送一份我整理的速查口诀,适合复习到最后一晚的同学:
适配器管兼容,桥接管分离;组合管树形,装饰器管增强;外观管聚合,享元管共享;代理管控制。七个模式的意图各一句,考试时看到场景题,先对号入座,再写UML图,最后补关键代码,保证拿分。
我个人在实际教学中发现,结构型模式学得好不好,不看会不会背定义,要看拿到一个新需求时能不能快速锁定模式。这也是为什么我一直强调要理解每个模式背后的"痛点"。等你真正用结构型模式做完一次大作业,你会发现它们不是七个孤立的概念,而是一套组合拳——适配器接旧系统,外观封装门面,装饰器动态加能力,代理控制访问,各司其职,整个系统的架构才立得起来。遇到拿不准的场景,按第三节的checklist多练几次,慢慢就有感觉了。