结构型设计模式
结构型设计模式关注处理类或对象的组合,形成更大的结构。
- 适配器模式(Adapter):将一个类的接口转换成客户期望的另一个接口,使原本不兼容的类能一起工作。
- 桥接模式(Bridge):将抽象部分与实现部分分离,使它们可以独立变化。
- 组合模式(Composite):将对象组合成树形结构以表示“部分-整体”的层次关系,统一处理单个对象和组合对象。
- 装饰器模式(Decorator):动态地给对象添加额外职责,比继承更灵活。
- 外观模式(Facade):为子系统提供一个统一的高层接口,简化客户端使用。
- 享元模式(Flyweight):通过共享技术有效支持大量细粒度对象的复用。
- 代理模式(Proxy):为其他对象提供一种代理以控制对这个对象的访问。
适配器模式
对象适配器模式
适配器模式(Adapter Pattern)如其名,充当两个不兼容接口之间的桥梁
它通过一个中间件(适配器)将一个类的接口转换成客户期望的另一个接口,使原本不能一起工作的类能够协同工作。
举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
希望创建一个可复用的类,与多个不相关的类一起工作,这些类可能没有统一的接口。通过接口转换,将一个类集成到另一个类系中。
一般情况下,按照前面的设计原则,适配器模式推荐使用依赖关系,而不是继承,以保持灵活性。
那么还是以案例来讲,一般来说,适配器模式的角色如下
| 角色 | 对应代码 | 说明 |
|---|---|---|
| Target(目标接口) | RowingBoat |
客户端期望的接口。定义了 row() 方法。 |
| Adaptee(被适配者) | FishingBoat |
已有但接口不兼容的类。它有 sail() 方法,而不是
row()。 |
| Adapter(适配器) | FishingBoatAdapter |
实现 Target 接口,并持有 Adaptee 的引用。在 row()
中调用 sail()。 |
| Client(客户端) | Captain |
只知道如何使用 RowingBoat(即调用
row())。 |
首先,来看目标接口,也就是客户端期望的接口
- 所有能“划船”的东西都必须实现这个接口
然后,被适配者FishingBoat.java,这是一个
已有的、无法修改的类,它通过 sail()
航行,而不是
row()。所以说,客户端Captain无法直接使用它,因为没有
row() 方法。
所以说,为了让客户端能使用
FishingBoat.java,需要一个适配器,来充当中间的转换角色
- 它实现了 Target 接口
RowingBoat。内部持有一个 需要被适配的 实例,在row()方法中,委托调用boat.sail()。
这样,从客户端角度看,它得到了一个“会划船”的对象,实际上底层是“航行”的渔船。
这是 对象适配器(Object Adapter),通过 组合(Composition) 使用 Adaptee。
最后是客户端
Captain 完全不知道 FishingBoat
的存在。他只依赖于 RowingBoat 接口。
然后 App 入口可以选择这两种船,这成功让 Captain 驾驶了 FishingBoat,尽管两者原本不兼容
最后,适配器模式应谨慎使用,特别是在详细设计阶段,它更多地用于解决现有系统的问题。
类适配器模式
类适配器通过多重继承实现,我不推荐用
当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
在 类适配器(Class Adapter) 中:
- 适配器类通过继承被适配类(Adaptee),同时实现目标接口(Target)。
- 这种方式利用了多重继承的语义,在 Java 中就是通过单继承 + 接口
- 因此,类适配器 = 继承 Adaptee + 实现 Target
继续上面的例子,所以,根据上面的例子,我们需要让让
FishingBoat
变成可继承的类,创建新的适配器类,继承
FishingBoat 并 实现
RowingBoat
1 | package com.iluwatar.adapter; |
替换 FishingBoatAdapter.java 为
1 | public class FishingBoatAdapter extends FishingBoat implements RowingBoat { |
- 这里没有组合,而是直接继承,并通过
sail()调用父类行为。
App.java 不用动,因为客户端只依赖
RowingBoat
接口,无论是对象适配器还是类适配器,都实现了它:
1 | var captain = new Captain(new FishingBoatAdapter()); |
但是很明显,看上去,类适配器很不灵活
接口适配器模式
接口适配器几乎同类适配器,只不过是多实现接口,改一下接口内的默认方法就可以,不展开说了
桥接模式
桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
这表达的还有人类吗。。。。。。实际上、
桥接模式 将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- 解耦抽象(Abstraction)与实现(Implementation)。
- 传统继承会导致类爆炸(例如:2 种武器 × 2 种附魔 = 4 个子类;3×3=9…)。
- 桥接模式通过 组合(Composition) 替代继承,让抽象和实现可以 独立扩展。
这种模式涉及到一个作为桥接的接口,使得实体类的功能独立于接口实现类,这两种类型的类可被结构化改变而互不影响。
桥接模式的目的是将抽象与实现分离,使它们可以独立地变化。它通过组合的方式,而不是继承的方式,将抽象和实现的部分连接起来。
想象一个遥控器(抽象)和电视(实现)。
- 遥控器有“开/关/音量”等抽象操作。
- 电视有“三星/LG/索尼”等具体实现。
- 桥接模式让你可以 任意组合 遥控器和电视,而不需要为每种组合写一个新类。
还是通过例子说,描述这个东西太困难了
| 角色 | 对应代码 | 说明 |
|---|---|---|
| Abstraction(抽象) | Weapon 接口 |
定义高层控制接口(wield, swing,
unwield)。 |
| Refined Abstraction(扩充抽象) | Sword, Hammer |
具体武器类型,持有 Enchantment 实现。 |
| Implementor(实现者) | Enchantment 接口 |
定义底层实现接口(onActivate, apply,
onDeactivate)。 |
| Concrete Implementor(具体实现者) | FlyingEnchantment,
SoulEatingEnchantment |
具体的附魔效果实现。 |
首先来看,实现者这个接口 Enchantment.java,它定义
附魔行为的契约,规定所有附魔效果必须实现这三个方法。
对于FlyingEnchantment.java &
SoulEatingEnchantment.java,他们都是Enchantment的两种完全不同的具体的实现,所以,他们彼此独立,可单独修改或新增
对于,Weapon它是一个抽象,是一个接口,它定义了武器的契约,但是其中需要附魔,因为包含一个
getEnchantment() 方法来暴露其具体实现
那么,对于持有某种附魔的特定武器类型,就可以像Sword.java这样编写
每个武器持有一个 Enchantment
实例,通过构造函数注入,武器的具体行为由 自身逻辑 +
附魔逻辑
组合而成,但是,这个附魔的行为如果不这么写,它就需要实现的附魔这个接口,然后对具体的附魔进行实现,但是在这里,没有继承附魔,而是
组合了附魔这一行为
那么,客户端,造各种附魔的武器就很简单了,狠狠的传入就可以了,任意组合武器和附魔也是很简单的事情了
如果用继承,你需要:
SwordWithFlyingEnchantmentSwordWithSoulEatingEnchantmentHammerWithFlyingEnchantmentHammerWithSoulEatingEnchantment- …(n 种武器 × m 种附魔 = n×m 个类!)
和适配器模式相反,桥接模式推荐在 设计阶段 就使用它解耦抽象与实现,提前规划,避免类爆炸
桥接模式本质上是高内聚低耦合,Weapon 不关心
Enchantment
的具体实现,新增武器或附魔无需修改现有代码,传入不同参数就可
组合模式
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。
那么,一组对象当成一个单一的对象??这是如何实现的,抽象工厂模式是不是跟这个差不多
实际上,它是一种将对象组合成树状的层次结构的模式,用来表示“部分-整体”的关系,使用户对单个对象和组合对象具有一致的访问性。
如果希望用户忽略组合对象与单个对象的不同,让用户将统一地使用组合结构中的所有对象。使用这个模式是无比合适的
例如,windows 的文件管理器就使用了组合模式,文件系统在文件是叶子,文件夹是容器,这是两个不同的东西,但是我们操作他们的时候,貌似没什么不一样
组合模式的主要优点有:
- 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
- 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
但是它的设计很复杂了。。。。。。。而且几乎很不容易用继承的方法来增加构件的新功能
还是用一个例子讲解如何使用和实现组合模式
| 角色 | 对应代码 | 说明 |
|---|---|---|
| Component(组件) | LetterComposite (抽象类) |
定义所有对象(单个/组合)的通用接口(add,
print)。 |
| Leaf(叶子) | Letter |
表示单个字符,不能再分解。 |
| Composite(容器) | Word, Sentence |
表示组合对象(单词由字母组成,句子由单词组成)。 |
首先,我们要是希望让单一对象和组合对象能视同为一种类型,那么需要有类似的一种接口LetterComposite.java,来规定所有对象的通用方法
- 可以看到,这是一个后序遍历,
print()方法是 递归的,在后面,无论children是Letter还是Word,都调用print()。
然后来看叶子节点,也就是单个对象
那么,组合对象是这样的
什么意思,可以看到,Word 把自己构造成了是一个多个 Letter 的 List,然后也实现了对应的 printThisBefore 方法,在单词前加空格
那么,对于继续组合的对象,也就是 Sentence,它肯定也是多个 Word 的组合对象
Sentence 类似 Word 那样,把自己构造成了存放 Word 的 一个 List,它重写
printThisAfter() 在句子末尾加标点。
那么对象
Messgae,完全不知道内部结构,它只知道,只需调用
print(),就能递归打印
1 | package com.iluwatar.composite; |
Sentence(根) →Word(枝) →Letter(叶)- 调用
sentence.print(),自动递归渲染整个句子,并处理格式(空格、句号),App 完全把 Sentence,Word,Letter 通过 Message 视同无异了
假设我们不用组合模式,而是用传统面向对象的方式,那么大概需要为每种类型写独立的打印方法,不能像这样不能写一个通用方法处理同时
Letter/Word/Sentence,如果新增
Paragraph,必须修改所有相关代码
组合模式就很松耦合了,Sentence 不关心子节点是
Word,还是未来的什么内容
装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
装饰器模式 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
动态的将新功能附加到对象上。这样在对象扩展上,比继承灵活的多
装饰器模式通过将对象包装在装饰器类中,以便动态地修改其行为。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
- 用组合替代继承 来扩展对象功能。
- 装饰器(Decorator)和被装饰对象(Component)实现 同一接口。
- 可以 动态地、透明地 给对象添加职责,甚至可以 叠加多个装饰器。
想象一杯咖啡:
- 基础款:
SimpleCoffee - 加奶:
MilkDecorator(coffee) - 加糖:
SugarDecorator(milkCoffee) - 加奶加糖:
SugarDecorator(MilkDecorator(coffee))
每一步都是在原有对象上“包裹”一层新功能,而不是创建一个新的
MilkSugarCoffee 类。
那么,还是用例子讲解,这个设计模式比适配器,桥接,组合什么的还是好理解很多的
| 角色 | 对应代码 | 说明 |
|---|---|---|
| Component(组件) | Troll 接口 |
定义所有 Troll(基础/装饰后)的通用行为(attack,
getAttackPower, fleeBattle)。 |
| Concrete Component(具体组件) | SimpleTroll |
最基础的 Troll 实现。 |
| Decorator(装饰器) | ClubbedTroll |
持有 Troll 引用,并在其基础上添加新行为。 |
| Client(客户端) | App |
使用 Troll 接口,无需关心具体是
SimpleTroll 还是 ClubbedTroll。 |
Troll.java是基础组件,定义了 Troll 的
核心契约。无论是否被装饰,都需要实现其中的方法
然后,最基础的 Troll 实现是这样的
那么,装饰器ClubbedTroll.java就在基础的 Troll
上增加新功能
然后客户端就可以
这样就实现了动态添加功能:同一个
SimpleTroll 实例,被装饰后行为改变。
如果不使用装饰器模式,会发生什么?
大概率是继承
1
2
3
4
5
6
7
8
9
10class SimpleTroll { /* ... */ }
// 为每种武器创建一个子类
class ClubbedTroll extends SimpleTroll {
public void attack() { /* 原攻击 + 挥舞棍棒 */ }
public int getAttackPower() { return super.getAttackPower() + 10; }
}
class SwordedTroll extends SimpleTroll { /* ... */ }
class ArmoredTroll extends SimpleTroll { /* 防御力提升 */ }这样首先的问题就是不灵活,而且,这样的话需要写很多个子类,而且这样类在写完之后,如果需要修改装备,会出现违反开闭原则的问题
那么,直观感受一下装饰器模式
- 基础:
SimpleTroll - 装饰:
new ClubbedTroll(simpleTroll) - 叠加:
new ArmoredTroll(new ClubbedTroll(simpleTroll))(如果实现ArmoredTroll)
这就是 装饰器模式的精髓:用对象组合的方式,动态地、透明地扩展对象功能,避免继承带来的僵化和膨胀。
总之就是这样
外观模式
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。
它向现有的系统添加一个接口,来隐藏系统的复杂性。
当然,它也可以为子系统中的一组接口提供一个一致的界面。外观模式定义了一个高层接口,使得这一子系统更加容易使用。
什么意思,外观模式意图就是为一个复杂的子系统提供一个一致的高层接口。这样,客户端代码就可以通过这个简化的接口与子系统交互,而不需要了解子系统内部的复杂性。
想象你去餐厅吃饭:
- 没有服务员(外观):你需要自己去厨房告诉厨师做什么菜、去仓库拿餐具、去收银台付款…
- 有服务员(外观):你只需对服务员说“我要一份牛排”,剩下的都由他协调完成。
这是编写 Spring Boot 和接入外部 API 的时候最常用的设计模式
主要解决的问题就非常明显
- 降低客户端与复杂子系统之间的耦合度。
- 简化客户端对复杂系统的操作,隐藏内部实现细节。
那么实现方式也比较简单,就是定义一个类(外观),作为客户端与子系统之间的中介。外观类将复杂的子系统操作封装成简单的方法。然后这个Facade类提供高层接口,简化客户端与子系统的交互。子系统类实现具体的业务逻辑,被Facade类调用。
依旧例子
| 角色 | 对应代码 | 说明 |
|---|---|---|
| Facade(外观) | DwarvenGoldmineFacade |
提供简化的高层接口(startNewDay,
digOutGold, endDay)。 |
| Subsystem Classes(子系统类) | DwarvenGoldDigger, DwarvenCartOperator,
DwarvenTunnelDigger |
复杂的子系统实现,继承自 DwarvenMineWorker。 |
| Client(客户端) | App |
只与 DwarvenGoldmineFacade
交互,完全不知道子系统的存在。 |
首先,有一个子系统基础类,DwarvenMineWorker,所有子系统都继承了这一实现
1 | package com.iluwatar.facade; |
- 代码中定义了矮人工人的 通用行为,其中有两个抽象方法需要具体工种来实现
被子系统基础基类实现的三个实现类如下,这三个类共同构成了子系统,例如DwarvenGoldDigger
- 这些类就是对基类的相关实现,代表了外观模式中的复杂细节
外观类DwarvenGoldmineFacad.java,针对子系统中的三个类,进行了统一处理
- 其中,makeActions 就是针对工人类型去让他们做对应的事情,这是对工种的抽象,然后三个高层接口的实现是对工人需要做的事情的抽象
这样客户端在调用的时候就 无需知道有哪些工人,也 无需知道每个工人的具体动作。
客户端只需要 3 行代码就能完成一整天的金矿运作。完全不知道背后有三种工人负责三种不同的事情,而且也不知道一个工人他有五种动作
如果不这样写,那么客户端 必须知道所有工种类 和 每天的操作流程,难以维护
那么外观模式就使客户端被大大简化,客户端只需与 Facade 交互,而且客户端与子系统完全隔离。子系统变化不影响客户端,新增/修改子系统只需改 Facade。
享元模式
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。它提供了减少对象数量从而改善应用所需的对象结构的方式。
享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。
这样就能通过共享对象来减少创建大量相似对象时的内存消耗。
也就是说,当程序需要大量相似对象时,将对象的 不变部分(内部状态) 提取出来共享。
区分内部状态与外部状态:
- 内部状态(Intrinsic State):对象可共享的部分
- 外部状态(Extrinsic State):对象不可共享的部分,一般由客户端传入。
实现方式如下
- 定义享元接口:创建一个享元接口,规定可以共享的状态。
- 创建具体享元类:实现该接口的具体类,包含内部状态。
- 使用享元工厂:创建一个工厂类,用于管理享元对象的创建和复用。
那么,还是用一个例子
| 角色 | 对应代码 | 说明 |
|---|---|---|
| Flyweight(享元) | Potion 接口及其实现类 (HealingPotion,
HolyWaterPotion 等) |
不可变的共享对象,只包含内部状态(药水类型)。 |
| Concrete Flyweight(具体享元) | HealingPotion, HolyWaterPotion,
PoisonPotion 等 |
具体的药水实现,drink() 方法不依赖外部状态。 |
| Flyweight Factory(享元工厂) | PotionFactory |
创建并管理享元对象池,确保相同类型的药水只创建一次。 |
| Client(客户端) | AlchemistShop |
通过工厂获取药水,并调用其方法。 |
享元接口被定义为如下
- 也就是说,在这个例子中,对象们只有一个可以被共享的内部状态
drink(),而且所有药水实现都必须提供drink()方法。
那么,关于上述药水这个享元对象的如下具体享元如下,例如
- 注意,
drink()方法 不依赖任何实例变量,行为完全由类型决定。因此,这样的一个类,创建出来的实例,就可以被无限次复用
emmmm,这是一个枚举享元类型的键,工厂根据 PotionType
决定返回哪个共享实例。
然后就是享元工厂,内部维护一个
享元对象池Map<PotionType, Potion>,createPotion()
方法会先检查缓存 Map,不存在才创建
客户端药水商店这样使用这些享元
1 | public class AlchemistShop { |
可以看到,对象之间,同一类型的对象哈希码相同,说明是同一对象
代理模式
在代理模式(Proxy Pattern)中,一个类代表另一个类的功能,代理模式通过引入一个代理对象来控制对原对象的访问。
代理对象在客户端和目标对象之间充当中介,负责将客户端的请求转发给目标对象,同时可以在转发请求前后进行额外的处理。
也就是说,代理模式的意图是为其他对象提供一种代理以控制对这个对象的访问。
- 引入一个代理对象,作为客户端与目标对象之间的中介。
- 代理对象 持有对真实对象的引用,并通常会在调用真实对象前后 添加额外逻辑。
- 客户端 直接与代理交互,完全不知道真实对象的存在。
Java 的动态代理和静态代理特性几乎严格遵守了这个设计模式
当需要在访问一个对象时进行一些控制或额外处理时。访问某些对象时可能遇到问题,代理模式几乎是必须的,它的特性决定了这里面七个,就它用的最多
但是实现方式比较简单
- 增加中间层:创建一个代理类,作为真实对象的中间层。
- 代理与真实对象组合:代理类持有真实对象的引用,并在访问时进行控制。
想象你请律师打官司:
- 你(客户端) ↔︎ 律师(代理) ↔︎ 法院(真实对象)
- 律师会先检查你的材料是否齐全(前置检查),再帮你提交给法院。
- 如果材料不全,律师会直接拒绝你,根本不会联系法院。
还是以一个例子来说明
| 角色 | 对应代码 | 说明 |
|---|---|---|
| Subject(主题接口) | WizardTower |
定义通用接口(enter)。 |
| Real Subject(真实主题) | IvoryTower |
真正执行业务逻辑的对象。 |
| Proxy(代理) | WizardTowerProxy |
控制对 IvoryTower
的访问,添加额外逻辑(人数限制)。 |
| Client(客户端) | App |
只与 WizardTower
接口交互,不知道背后是代理还是真实对象。 |
首先,对于主题接口,是这样定义的
- 定义了 进入高塔的契约。所有“高塔”(无论真实或代理)都必须实现此接口。
真实对象如下IvoryTower.java
虽然真实的高塔对象实现了进入这个核心逻辑,但是缺乏访问控制,这时候不方便改这个对象,可以引入我们的代理对象,代理对象如下WizardTowerProxy.java
- 代理对象持有对真实高塔的引用,将核心方法 enter() 委托给了真实的高塔对象,然后自己内部存在一些逻辑,添加了访问控制逻辑
那么此时客户端就
完美的不修改原对象,通过代理,实现了对原真实对象的控制和逻辑添加,而且客户端
完全不知道 IvoryTower 的存在,只和 proxy
交互
不使用代理模式如果,直接修改
IvoryTower,很明显,会违反单一职责,而且无法复用
在客户端做检查就会违反封装,如果规则变了,,所有客户端都要改
代理模式是最简单的实现开闭原则的一种设计模式
简单梳理一下我遇到过的代理模式
| 类型 | 说明 |
|---|---|
| 保护代理 | 控制对敏感对象的访问(如权限检查) |
| 虚拟代理 | 延迟初始化重量级对象(如图片加载) |
| 远程代理 | 代表远程对象(如 RPC) |
| 智能引用代理 | 添加额外操作(如引用计数、日志) |





