
1. 结构型设计模式从“对象拼装”看软件架构的底层逻辑做开发这些年我越来越觉得设计模式不是背出来的而是“踩坑踩出来的”。尤其是结构型设计模式它解决的核心问题非常朴素如何把类和对象组织成更大的结构同时保持系统灵活、易扩展。换句话说创建型模式管的是“对象怎么来”行为型模式管的是“对象之间怎么协作”而结构型模式管的是“对象和类怎么拼在一起”。很多初学者容易忽略这部分觉得适配器、代理、装饰器这些东西看起来简单甚至觉得有些模式“用不上”。但等你真正维护过一个几百个类互相引用的老项目或者被一段改一处崩三处的代码折磨过之后就会明白结构型模式的价值——它不是让你写出更炫的代码而是让你在需求不断变化时少改代码多留余地。这一篇我打算把七种经典的结构型设计模式——适配器、桥接、组合、装饰器、外观、享元、代理逐一拆开讲清楚。每种模式我都会结合实际使用场景、代码示例、以及在项目里常见的误用情况来说明。不论你是刚学设计模式的学生还是工作中已经写过不少业务的开发者这篇都能给你一些新的参考视角。提示这七个模式虽然都归在“结构型”这个大类里但它们各自解决的问题差异非常大。学习的时候千万不要只记 UML 图而是要把每种模式对应的“痛点”记清楚这样才能在写代码时自然而然地用出来。2. 适配器模式让不兼容的接口“凑合”到一起2.1 适配器要解决什么问题适配器模式可能是结构型模式里最直白的一个。生活里的例子就是电源转换头——你从国外买了一个两脚插头的电器国内的插座是三孔的你没法直接插于是买一个转换头把两脚转成三脚电器就能用了。适配器模式做的事情就是这个把一个类的接口转换成客户端期望的另一个接口让原本因为接口不匹配而无法协作的类能够一起工作。在代码里这类场景几乎天天见。比如你接手一个老系统里面有一个LegacyReportGenerator类它输出报表的方法是generateReport(String format)而且格式参数用的是自己的枚举。但业务方现在要求统一走一个新的报表服务接口ReportService.generate(ReportFormat format)那请问你怎么办直接改老类的代码老类可能被几十个地方调用改完风险非常大。不改新接口又调不动它。这时候适配器就是最合适的选择。写一个ReportAdapter implements ReportService内部持有LegacyReportGenerator的实例在generate方法里把新的格式枚举转换成老的枚举再调用老类的方法。2.2 类适配器与对象适配器适配器模式在实现上有两种方式类适配器和对象适配器。现在主流推荐的是对象适配器因为它是基于组合实现的耦合度更低。类适配器通过多继承实现在 Java 这种单继承语言里意味着适配器必须继承目标接口对应的抽象类再实现接口灵活性很差。而对象适配器是持有一个被适配者的引用通过调用被适配者的方法实现功能结构上更清晰也更容易测试。以一个简单的支付对接为例。系统里定义了统一的支付接口public interface PaymentGateway { void pay(String orderId, double amount); }老系统里有一个第三方的支付宝支付类方法名完全对不上public class AlipayOldSdk { public void doAlipayPay(String bizOrderNo, BigDecimal money) { // 支付宝老SDK的支付逻辑 } }对象适配器写起来就是这样public class AlipayAdapter implements PaymentGateway { private final AlipayOldSdk alipayOldSdk; public AlipayAdapter(AlipayOldSdk alipayOldSdk) { this.alipayOldSdk alipayOldSdk; } Override public void pay(String orderId, double amount) { // 转换参数格式 BigDecimal money BigDecimal.valueOf(amount); alipayOldSdk.doAlipayPay(orderId, money); } }这样业务层只依赖PaymentGateway接口完全不知道底层调用的是支付宝老 SDK。将来换成微信支付只需要再写一个WechatPayAdapter业务代码一行都不用动。2.3 适配器模式的使用心得我在项目里见过不少人把适配器当成“万能胶”什么接口不匹配都硬写一个适配器结果到处都是 Adapter 类反而把系统搞得更乱。适配器模式适用的场景应该满足两个条件一是被适配的类你不方便改或者不想改二是接口不匹配的问题主要出在方法签名层面而不是出在业务逻辑层面。如果两边的方法行为本身就天差地别那适配器只是在掩盖设计问题你更应该回头审视是不是领域模型建错了。还有一个细节写适配器时要注意异常处理。老接口往往会抛出一些受检异常或者自定义异常而新接口的规范可能只允许抛出统一的业务异常。适配器里要做一层异常转换避免底层的异常细节泄漏到上层。否则你辛辛苦苦做的接口隔离最后被一个NullPointerException全毁了。3. 桥接模式把“抽象”和“实现”分离成两条可独立变化的线3.1 为什么要拆开抽象与实现桥接模式可能是结构型模式里最不容易理解的一个因为它不像适配器那样有一个非常直观的现实对应物。它的核心思想是将抽象部分与它的实现部分分离使它们都可以独立地变化。什么叫做“抽象部分”和“实现部分”我用一个例子来说明。假设你正在做一个消息发送系统消息可以从两个维度变化按类型分有普通消息、加急消息、特急消息按发送渠道分有短信、邮件、站内信。如果使用继承来设计你会得到NormalSmsMessage、NormalEmailMessage、UrgentSmsMessage、UrgentEmailMessage、SpecialUrgentSmsMessage、SpecialUrgentEmailMessage……这才两个维度就产生了六个类。如果再加一个渠道“微信”就再增加三个子类。这种爆炸式增长的根源在于我们把两个独立的维度强行绑在了同一条继承链上。桥接模式的思路是把“消息类型”和“发送渠道”拆成两条独立的继承链然后通过组合的方式把两者桥接起来。3.2 一个具体的桥接实现在上面消息系统的例子里可以抽象出两个角色Message作为抽象部分MessageSender作为实现部分。public interface MessageSender { void send(String content, String target); } public class SmsSender implements MessageSender { Override public void send(String content, String target) { System.out.println(通过短信发送给 target content); } } public class EmailSender implements MessageSender { Override public void send(String content, String target) { System.out.println(通过邮件发送给 target content); } }然后是抽象部分public abstract class Message { protected MessageSender sender; public Message(MessageSender sender) { this.sender sender; } public abstract void send(String content, String target); } public class NormalMessage extends Message { public NormalMessage(MessageSender sender) { super(sender); } Override public void send(String content, String target) { sender.send([普通] content, target); } } public class UrgentMessage extends Message { public UrgentMessage(MessageSender sender) { super(sender); } Override public void send(String content, String target) { sender.send([加急] content, target); } }使用时自由组合Message msg new UrgentMessage(new SmsSender()); msg.send(您的验证码是 123456, 13800000000);以后要增加“微信渠道”只需要新增WechatSender要增加“定时消息”只需要新增TimedMessage。两边互不影响。这就是桥接模式最大的价值——维度独立扩展组合爆炸消失。3.3 桥接模式与接口设计的本质从更本质的层面看桥接模式是在实践一个设计原则优先使用对象组合而不是类继承。继承虽然能实现代码复用但它的复用粒度是“类”而且继承关系在编译期就固定了运行时无法改变。组合的粒度是“对象”运行时可以动态替换灵活性要高得多。所以当你发现一个类的继承层次开始变得又宽又深而且不同分支之间的组合数量明显超过预期的子类数量时就应该停下来想想是不是有两个维度被硬绑在一条继承链上了这时候引入桥接模式往往会让你重新梳理出更清晰的设计。不少人对桥接和适配器的区别有疑惑。我的理解很简单适配器是为了让“已有的东西”能被用起来解决的是兼容问题桥接是为了让“未来的东西”能被扩展出来解决的是演化问题。适配器通常在被适配类不方便修改时使用桥接则是在系统设计初期就要考虑好的结构划分。4. 组合模式用树形结构处理“部分与整体”的关系4.1 文件系统就是天然的组合模式组合模式的核心是将对象组合成树形结构以表示“部分-整体”的层次结构使得客户端对单个对象和组合对象的使用具有一致性。这句话翻译成人话就是不管操作的是一个文件夹还是一个文件调用方式应该是一样的。文件系统是最经典的例子。文件夹里可以放文件也可以放子文件夹子文件夹里又可以放文件或更小的文件夹。在客户端看来双击一个文件是打开双击一个文件夹也是打开进入操作入口是一致的。如果设计代码时把File和Folder当成两种完全不同的类型那客户端就要不停地instanceof判断代码会非常难看。组合模式的做法是定义一个FileSystemNode抽象类或接口让File和Folder都实现它。Folder里持有ListFileSystemNode并且list()、delete()等方法会递归调用子节点的对应方法。public abstract class FileSystemNode { protected String name; public FileSystemNode(String name) { this.name name; } public abstract void display(int depth); } public class File extends FileSystemNode { public File(String name) { super(name); } Override public void display(int depth) { System.out.println(-.repeat(depth) name); } } public class Folder extends FileSystemNode { private ListFileSystemNode children new ArrayList(); public Folder(String name) { super(name); } public void add(FileSystemNode node) { children.add(node); } Override public void display(int depth) { System.out.println(-.repeat(depth) name /); for (FileSystemNode node : children) { node.display(depth 2); } } }客户端调用的时候根本不需要关心当前节点是文件还是文件夹Folder root new Folder(root); root.add(new File(readme.txt)); Folder src new Folder(src); src.add(new File(Main.java)); src.add(new File(Utils.java)); root.add(src); root.display(0);4.2 组合模式的透明性与安全性组合模式在接口设计上有一个经典的权衡究竟应不应该在抽象节点里定义add、remove这些管理子节点的方法如果在抽象类里定义了这些方法叶子节点比如File就必须实现它们但文件没有子节点add方法只能抛异常或者空实现这就是“透明方式”。好处是客户端不需要区分叶子节点和组合节点缺点是叶子节点会暴露出它并不支持的方法不符合接口隔离原则。如果不在抽象类里定义add、remove而是在Folder类里单独提供这些方法这就是“安全方式”。好处是接口设计更合理缺点是客户端想添加子节点时必须先把节点强转成Folder失去了透明性。实际上纯透明或纯安全都很难完全做到。我自己的实践偏向于如果树形结构很稳定树深度比较浅用安全方式更稳妥如果树形结构复杂客户端经常需要递归遍历用透明方式能减少很多分支判断。具体选哪种要看你更看重“调用的统一”还是“接口的干净”。4.3 组合模式在真实项目中的典型用法组合模式在真实项目里的地位非常高。除了文件系统它还大量用于GUI 组件树一个 Panel 里可以放 Button也可以再放一个 Panel、组织架构树部门下可以有员工也可以有子部门、菜单系统菜单项可以是叶子也可以是子菜单、解析语法树表达式由操作符和操作数组成操作数也可能是子表达式。我做过一个权限管理的需求菜单和权限点要构成一棵树前端要根据菜单树渲染页面后端要根据权限树做鉴权。如果为树里的每个层级单独写一套数据结构和处理方法工作量会翻倍而且后续加层级时改动面很大。后来用组合模式统一了节点处理逻辑树的遍历、搜索、过滤都写在一个递归方法里新增一种节点类型只需要实现统一接口就行整体改动量非常小。5. 装饰器模式给对象“穿衣服”而不是疯狂造子类5.1 为什么不建议用继承来扩展功能装饰器模式的结构在 GoF 书里不算复杂但理解它能解决什么问题比画 UML 图重要得多。它的核心是动态地给一个对象添加一些额外的职责而且比生成子类的方式更灵活。你可能会问给类加功能直接用继承不就行了吗子类里加个方法多简单。问题在于继承是静态的而且会带来类爆炸。举个最经典的例子——咖啡店点单系统。假设咖啡的基础类是Coffee它有cost()方法返回价格。现在顾客可以加牛奶、加糖、加摩卡、加奶泡。如果每个组合都用一个子类表示那么CoffeeWithMilk、CoffeeWithSugar、CoffeeWithMilkAndMocha……需要的子类数量会随着可选配料的增加呈指数级增长。这显然不可接受。装饰器模式的思路是不通过继承来扩展功能而是把每个配料都做成一个“装饰器”它包装一个咖啡对象在调用cost()时先调用被包装对象的cost()再加上自己的价格。5.2 手写一个咖啡订单系统还是用上面的例子直接看代码。先定义基础的饮料抽象类public abstract class Beverage { protected String description 未知饮料; public String getDescription() { return description; } public abstract double cost(); } public class Espresso extends Beverage { public Espresso() { description 浓缩咖啡; } Override public double cost() { return 15.0; } } public class HouseBlend extends Beverage { public HouseBlend() { description 综合咖啡; } Override public double cost() { return 12.0; } }然后定义装饰器抽象类它本身继承自Beverage同时持有另一个Beverage的引用public abstract class CondimentDecorator extends Beverage { protected Beverage beverage; public CondimentDecorator(Beverage beverage) { this.beverage beverage; } Override public abstract String getDescription(); }再加两个具体装饰器public class Milk extends CondimentDecorator { public Milk(Beverage beverage) { super(beverage); } Override public String getDescription() { return beverage.getDescription() 加奶; } Override public double cost() { return beverage.cost() 3.0; } } public class Mocha extends CondimentDecorator { public Mocha(Beverage beverage) { super(beverage); } Override public String getDescription() { return beverage.getDescription() 加摩卡; } Override public double cost() { return beverage.cost() 5.0; } }点单的时候可以一层一层地装饰Beverage beverage new Espresso(); beverage new Milk(beverage); beverage new Mocha(beverage); System.out.println(beverage.getDescription()); System.out.println(beverage.cost());输出结果为“浓缩咖啡加奶加摩卡”价格 23.0。5.3 装饰器与继承、静态代理的对比把装饰器模式和继承放在一起比较区别很清楚继承是在编译期决定好类关系装饰器是在运行时通过对象组合动态组装行为。所以装饰器的扩展性要好得多新增一个配料只需要新增一个装饰器类不会影响已有的类。但装饰器模式也有一个很实际的毛病会产生大量的小类。一个中等规模系统里用了十几个装饰器光类定义就能铺满一屏。而且装饰器之间如果有顺序依赖排查问题会很痛苦。比如水煮鱼里先放盐还是先放辣椒结果一样但有的装饰器对操作顺序很敏感这时候还是要谨慎使用。另外装饰器和代理模式在代码结构上长得很像都是持有另一个对象的引用并在调用前后做一些事情。区别在于装饰器的目的是增强功能它和原始对象是“IS-A”的关系代理的目的是控制访问它和原始对象是“HAS-A”的关系。装饰器最终返回的是包装后的对象客户端通常还会把它当成原始类型来用代理则是替客户端管理对原始对象的访问权限和生命周期。5.4 Java I/O 里的装饰器实战Java 的 I/O 类库是装饰器模式最典型的应用。BufferedInputStream装饰FileInputStream给文件输入流加上缓冲功能DataInputStream再装饰BufferedInputStream又加上了读取基本数据类型的功能。这种层层包装的方式让每个类只专注一项职责组合使用就能构造出各种功能的输入流。InputStream in new DataInputStream(new BufferedInputStream(new FileInputStream(data.txt)));学习装饰器的时候把 Java I/O 作为一个实际样板去研究比看任何教程都管用。你可以自己写一个简单的UpperCaseInputStream继承FilterInputStream在read()方法里把字符转成大写体会一下“包装”到底是怎么实现的。6. 外观模式给复杂系统开一扇“简约门”6.1 外观模式的核心价值外观模式可能是结构型模式里最“反直觉”的一个因为它是唯一一个主动增加封装层来简化接口的模式。它的定义是为子系统中的一组接口提供一个统一的、更高层的接口从而让子系统更容易使用。理解外观模式的关键在于一个现实很多系统内部是复杂的但使用方其实只想做一件简单的事。比如你启动一个电脑需要按电源键然后主板加电、CPU复位、内存自检、显卡初始化、硬盘引导、加载操作系统……这一大串动作对用户来说只需要按一个电源键就够了。电源键就是“外观”它屏蔽了后面所有复杂的启动流程。在代码里我见过一个典型的反面教材一个影院系统的客户端代码要看电影需要依次调用投影仪打开、音响打开、灯光调暗、播放器播放、屏幕放下……五个六个步骤每个步骤还要处理异常。如果调用方每次都把这六个步骤写一遍代码会极度冗长且容易遗漏。外观模式就是把这六个步骤封装到一个HomeTheaterFacade.watchMovie()方法里调用方只需要一行代码。6.2 外观模式与“最少知识原则”外观模式背后体现的是面向对象设计中的最少知识原则也叫迪米特法则一个对象应该尽量少地了解其他对象。如果一个类为了完成工作需要跟五个类交互那它对这五个类的内部细节都了如指掌耦合度自然很高。通过外观类把这五个类的交互过程收拢起来调用方只需要跟外观类打交道整个系统的耦合度就明显降低了。这里要注意一个使用边界外观类不应该承载业务逻辑它只是把调用过程编排好。如果外观类里开始出现“如果 A 成功就执行 B否则执行 C”这类业务判断那它就不是外观类而是一个业务服务类了。业务逻辑应该下沉到子系统内部的领域对象里外观类只需要做“组织编排”。6.3 外观模式和适配器怎么区分因为外观模式也是在“包一层”所以经常被拿来跟适配器模式对比。一句话区分适配器改变的是接口的形状外观改变的是接口的粒度。适配器把 A 接口的形状转换成 B 接口的形状让两个已有的东西能接上外观是把一堆散落的接口调用汇聚成一个更高层的接口让使用方更省事。举个不太恰当但容易理解的例子适配器是“转换插头”让你的电器能插进不匹配的插座外观是“总开关”一拉闸全屋通电不用逐个去开灯。前者解决兼容后者解决简化。7. 享元模式用“共享”扛住海量对象的内存压力7.1 为什么对象多了会出问题享元模式的核心是运用共享技术有效地支持大量细粒度的对象。它是结构型模式里最贴近性能优化的一种。核心思想说起来很简单如果系统里有大量相似的对象而这些对象的某些属性是相同的那么这些相同的属性可以被抽取出来共享而不是每个对象都保存一份。典型场景是文字编辑器。一个文档里有成千上万个字符如果每个字符都单独创建一个对象每个对象里都存字体、字号、颜色等格式信息内存开销会非常大。但其实同一个文档里很大一部分字符的格式是相同的。享元模式的思路是把“字符本身”和“字符的格式”分开字符对象内部只存字符值固有状态字体字号颜色等格式通过外部传入外部状态。7.2 用享元模式设计一个文字编辑器来看这个文字编辑器的简化实现。先是享元类它包含字符的固有状态——就是字符本身public class CharacterFlyweight { private final char character; public CharacterFlyweight(char character) { this.character character; } public void display(int fontSize, String color) { System.out.println(字符: character , 字号: fontSize , 颜色: color); } }然后是享元工厂它负责维护字符对象的池子。同一个字符只创建一次后续请求直接复用public class CharacterFactory { private static final MapCharacter, CharacterFlyweight pool new HashMap(); public static CharacterFlyweight getCharacter(char c) { CharacterFlyweight flyweight pool.get(c); if (flyweight null) { flyweight new CharacterFlyweight(c); pool.put(c, flyweight); } return flyweight; } }客户端使用的时候字体颜色这些外部状态由客户端传入CharacterFlyweight c1 CharacterFactory.getCharacter(A); c1.display(12, 黑色); CharacterFlyweight c2 CharacterFactory.getCharacter(A); c2.display(14, 红色); System.out.println(c1 c2); // true两个引用指向同一个对象c1和c2是同一个对象只是展示时传入了不同的外部状态这就大幅减少了内存中的对象数量。7.3 享元模式的适用条件与坑享元模式不是万能的它有一个硬性前提对象必须有可分离的固有状态和外部状态。如果一个对象的几乎所有属性在不同场景下都不同强行抽取共享状态不仅没有收益还会让代码变得极难理解。我在项目中真正用到享元模式的场景是游戏开发里的子弹和粒子效果。一局对战里可能有几千发子弹同时在屏幕上移动每个子弹对象如果都保存完整的材质、模型、动画序列引用内存根本扛不住。把这些渲染相关的数据作为固有状态放进享元池每个子弹对象只保存位置、速度、方向这些变化的数据整体内存占用立刻下降了一个量级。还有一个容易被忽略的坑享元对象因为被大量共享一旦它的内部状态被不小心修改了影响范围会被放大很多倍。所以享元对象的固有状态一定要设计成不可变的final并且不要在享元对象里保存会变化的数据。否则就是给生产环境埋雷。8. 代理模式对象的“守门人”和“替身”8.1 代理模式的基本结构代理模式是七个结构型模式里应用最广泛、面试出镜率最高的一个。它的核心是为其他对象提供一种代理以控制对这个对象的访问。用一个“替身”挡在客户端和真实对象之间客户端不直接操作真实对象而是通过代理来间接操作。代理模式的基本结构包含三个角色抽象主题Subject、真实主题RealSubject、代理Proxy。抽象主题定义真实对象和代理共有的接口代理持有真实对象的引用。客户端面对的是代理代理控制对真实对象的访问时机和访问方式。public interface Image { void display(); } public class RealImage implements Image { private final String filename; public RealImage(String filename) { this.filename filename; loadFromDisk(); } private void loadFromDisk() { System.out.println(加载高清图片: filename); } Override public void display() { System.out.println(显示图片: filename); } } public class ImageProxy implements Image { private RealImage realImage; private final String filename; public ImageProxy(String filename) { this.filename filename; } Override public void display() { if (realImage null) { realImage new RealImage(filename); } realImage.display(); } }注意看ImageProxy在display()被调用时才真正创建RealImage对象这就是虚拟代理的典型用法——延迟加载节省资源。8.2 代理模式的四种常见形态代理模式在实际应用中演化出了多种形态最常见的有四种。远程代理代理对象代表远程机器上的真实对象客户端调用代理的方法代理通过网络把请求转发给远程服务。RPC 框架、WebService 客户端里基本都是这种模式。对调用方来说它以为自己调的是本地对象实际上请求已经跨网络了。虚拟代理真实对象的创建开销很大时用代理延迟创建。图片懒加载、大文件读取加载都属于这一类。上面ImageProxy的例子就是虚拟代理。保护代理控制访问权限只有满足条件的客户端才能调用真实对象的方法。比如在管理系统中普通用户不能调用删除接口代理会先做权限校验通过后才转发给真实对象。智能引用在访问真实对象时附加一些额外操作比如引用计数、线程安全控制、访问日志记录。Spring AOP 本质上就是一种智能引用——在方法调用前后织入切面逻辑。8.3 静态代理、动态代理与 CGLIB在 Java 生态里代理模式还有一个实现技术层面的重要分支静态代理和动态代理。静态代理就是上面示例那种方式代理类和真实类在编译期就确定了关系。一个代理类只能代理一种接口如果接口很多代理类也会很多。动态代理则是在运行时动态生成代理类不需要为每个接口手写代理类。JDK 自带java.lang.reflect.Proxy但它有一个限制只能代理实现了接口的类。如果目标类没有实现任何接口就要借助 CGLIB通过生成目标类的子类来实现代理。// JDK 动态代理示例 public class LoggingProxyHandler implements InvocationHandler { private final Object target; public LoggingProxyHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(调用前日志: method.getName()); Object result method.invoke(target, args); System.out.println(调用后日志: method.getName()); return result; } } Image proxy (Image) Proxy.newProxyInstance( Image.class.getClassLoader(), new Class[]{Image.class}, new LoggingProxyHandler(new RealImage(photo.jpg)) );Spring 的 AOP 就是基于这套机制实现的。当你用Transactional、Async这类注解时Spring 实际上创建了一个代理对象在调用目标方法之前开启事务或线程在方法返回之后再提交事务或封装返回值。这里有一个我在实际开发中踩过的坑Spring 默认使用 JDK 动态代理代理对象和真实对象是兄弟关系。如果你在类内部用this调用被Transactional修饰的方法事务是不生效的因为this是真实对象而不是代理对象。必须注入代理对象来调用或者用AopContext.currentProxy()。这个细节排查起来非常隐蔽不熟悉代理原理的人往往折腾很久都找不到原因。8.4 代理模式与装饰器模式的区别不管是面试还是实际设计代理模式和装饰器模式都很容易搞混。它们的代码结构几乎一样都是持有一个目标对象的引用都在方法调用前后做些事情。我的区分办法是看意图装饰器关注的是“增强功能”代理关注的是“控制访问”。装饰器给对象添加新的行为比如给咖啡加奶加糖代理控制对象的访问比如延迟加载、权限检查。装饰器里的目标对象通常由外部传入代理里的目标对象往往由代理自己创建或者从容器中获取。从客户端视角看装饰器返回的是“增强后的对象”代理遮挡的是“你本不该直接操作的对象”。9. 结构型模式怎么选一个实战向的判断清单把七种结构型模式全部过完一遍之后最关键的一个问题浮出水面面对实际的业务需求我到底该用哪种模式我先把你可能遇到的场景用一张对照表整理出来。场景特征首选模式已有类的接口和客户端期望的接口不匹配且老类不方便修改适配器一个类有两个独立变化的维度继承导致类数量爆炸桥接需要处理树形结构且客户端不关心叶子节点还是容器节点组合需要动态、灵活地给对象增加功能避免子类过多装饰器子系统内部很复杂只想给调用方暴露一个简单入口外观大量细粒度对象导致内存紧张且对象有可共享的固有状态享元需要控制真实对象的访问方式、访问时机或访问权限代理但真实业务不会像表格里这么干净很多时候需求会同时命中好几种模式。比如你要给一个报表系统做权限控制、又要加缓存、又不想改原来的报表类这时候代理和装饰器就可能同时出现——代理控制权限装饰器加缓存。设计模式从来不是单选题它们是积木可以组合使用。我的实际经验是不要为了用模式而用模式。如果你能用一段简单的if-else解决问题那就用if-else如果两个类之间接口不匹配先看看能不能直接改一边的接口改不了再用适配器如果类数量还没到失控的程度桥接可以先缓缓。设计模式的引入应该让代码结构更简单而不是更绕。一个粗暴的评判标准是加了模式之后如果一个新同学看不懂这段代码了那你要慎重考虑是不是过度设计。10. 从“记住模式”到“理解设计”结构型模式的进阶建议学到后面你会发现结构型模式的很多思想是相通的它们都在反复强调几个核心原则面向接口编程、组合优于继承、封装变化点。适配器、外观、代理都是在“加中间层”桥接、装饰器、组合都是在“用组合替代继承”享元则是在“提炼共享状态”。一旦看透这一层你记住的不再是七个模式的名称和 UML 图而是一整套处理对象组织关系的方法论。我在团队带新人的时候会给一个比较实在的建议先找两个开源项目把里面用到的设计模式逐个标出来。比如读 Spring 源码时你会发现BeanFactory体系里的各种getBean实现其实涉及了抽象工厂、模板方法、代理模式看 MyBatis 源码时SqlSession对外暴露的简洁 API背后就是外观模式。把模式放进真实代码里去理解比单纯看书深刻得多。另外我强烈建议你自己动手写一遍而不是只看示例代码。用七种模式分别去实现一个小需求比如用适配器对接两个不同版本的 SDK用装饰器给日志系统加上加密压缩功能用代理给一个查询接口加缓存。写的过程中你会踩到各种示例代码里不会出现的坑比如类型擦除导致的代理失效、享元状态被意外修改、组合树递归时栈溢出……这些坑踩得越多你对设计模式的理解就越扎实。我个人在实际项目里最常用的是代理、装饰器和组合因为它们几乎能用在任何业务模块里。适配器和桥接的频率稍低但一旦用上往往解决的都是一整类问题而不是单个点上的问题。享元和外观则取决于业务形态数据量大、子系统多的项目里收益会很明显。如果你正在准备面试或者期末复习别把重点放在背诵模式的定义上多想想“什么场景下我会用这个模式”以及“这个模式的坑在哪里”。面试官真正想听的是你有没有在真实项目里遇到过类似问题以及你是怎么解决的。能说出“在这里用代理是为了延迟加载大对象但代理类要小心生命周期管理避免内存泄漏”的人和只会背“代理模式为其他对象提供一种代理以控制对这个对象的访问”的人在面试官眼里完全是两个级别。最后再分享一个判断代码设计是否合理的小技巧写代码的时候如果发现自己在不停地复制粘贴或者每次新增需求都要改很多现有的类那就说明你的类结构很可能不符合“开闭原则”。回头看一眼是不是该引入某个结构型模式了。如果发现一段代码反复修改都改不对且每次改总会留下新 bug那么大概率是抽象层次出了问题试着用桥接模式把变化点拆开往往会有豁然开朗的感觉。模式不是银弹但它确实能让你在设计的十字路口多几条清晰的路可以走。