行为型设计模式
行为型模式关注对象之间的职责分配和通信方式。
- 责任链模式(Chain of Responsibility):将请求沿处理链传递,直到有对象处理它为止。
- 命令模式(Command):将请求封装为对象,从而参数化客户、排队请求或记录日志。
- 解释器模式(Interpreter):定义语言的文法并解释该语言中的句子(适用于简单语法规则)。
- 迭代器模式(Iterator):提供一种顺序访问聚合对象元素的方法,而不暴露其内部表示。
- 中介者模式(Mediator):用一个中介对象封装一系列对象交互,降低耦合度。
- 备忘录模式(Memento):在不破坏封装性的前提下,捕获并外部化对象的内部状态以便恢复。
- 观察者模式(Observer):定义对象间一对多依赖关系,当一个对象状态改变时,所有依赖者自动更新。
- 状态模式(State):允许对象在其内部状态改变时改变其行为,看起来像改变了类。
- 策略模式(Strategy):定义一系列算法,把它们封装起来,并且使它们可以互相替换。
- 模板方法模式(Template Method):在一个方法中定义算法骨架,而将具体步骤延迟到子类中实现。
- 访问者模式(Visitor):表示一个作用于某对象结构中各元素的操作,可在不改变元素类的前提下定义新操作。
责任链模式
责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。
责任链模式通过将多个处理器(处理对象)以链式结构连接起来,使得请求沿着这条链传递,直到有一个处理器处理该请求为止。
责任链模式允许多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求。
什么意思,处理者有很多,他们组成一个链,客户端将请求沿着处理者链传递,直到请求被处理为止。
想一想,类处理机制中的类加载器是不是也是这种责任链机制,而且在JavaScript中,事件从最具体的元素开始,逐级向上传播,是不是也是这种机制
实现方式也比较清晰
- 定义处理者接口:所有处理者必须实现同一个接口。
- 创建具体处理者:实现接口的具体类,包含请求处理逻辑和指向链中下一个处理者的引用。
那么,责任链模式有两种经典的实现模式
- 链式引用:每个 Handler 持有
next引用 - 集中管理:由一个“链管理者”统一调度
主要涉及到如下角色
| 角色 | 说明 |
|---|---|
| Handler(处理器接口) | 定义处理请求的接口,通常包含 handleRequest()
和指向下一个处理器的引用(但在你的例子中略有不同) |
| ConcreteHandler(具体处理器) | 实现 Handler,判断是否能处理请求;若不能,转发给链中下一个 |
| Client(客户端) | 发起请求,但不知道哪个对象会处理 |
还是以一个例子来说明,例子以集中管理模式来说明,它更灵活,而且支持按优先级排序!
请求定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29/** Request. */
public class Request {
// 请求类型
private final RequestType requestType;
// 对请求的描述
private final String requestDescription;
// 是否被处理
private boolean handled;
// 全参构造
public Request(final RequestType requestType, final String requestDescription) {
this.requestType = Objects.requireNonNull(requestType);
this.requestDescription = Objects.requireNonNull(requestDescription);
}
/** Mark the request as handled. */
public void markHandled() {
this.handled = true;
}
public String toString() {
return getRequestDescription();
}
}handled标志防止重复处理
请求类型枚举,也就是系统支持的请求种类,随便看看就行
1
2
3
4
5public enum RequestType {
DEFEND_CASTLE, // 防守城堡
TORTURE_PRISONER, // 折磨囚犯
COLLECT_TAX // 收税
}处理器接口:
RequestHandler.java,这是责任链中每个节点必须实现的契约1
2
3
4
5
6public interface RequestHandler {
boolean canHandleRequest(Request req); // 判断能否处理
int getPriority(); // 优先级(数字越小越优先?看实现)
void handle(Request req); // 执行处理
String name(); // 名称(用于日志)
}具体处理器,例子中按照
OrcSoldier``OrcCommander``OrcOfficer排列的
可以看到他们都有自己的优先级,这是责任链中的请求执行顺序
链的构建与调度:
OrcKing.java,不是传统链式引用,而是集中式调度1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class OrcKing {
private List<RequestHandler> handlers;
public OrcKing() {
buildChain();
}
private void buildChain() {
handlers = Arrays.asList(
new OrcCommander(),
new OrcOfficer(),
new OrcSoldier()
);
}
public void makeRequest(Request req) {
handlers.stream()
.sorted(Comparator.comparing(RequestHandler::getPriority)) // 按优先级排序
.filter(handler -> handler.canHandleRequest(req)) // 找出能处理的
.findFirst() // 取第一个(最高优先级)
.ifPresent(handler -> handler.handle(req)); // 处理它
}
}- 链不是通过
next指针连接,而是存在List中,每次请求都重新排序 + 过滤,选出最合适的处理者,允许同类型请求由不同角色处理,findFirst()确保只处理一次
- 链不是通过
客户端
- 可以看到,实现了发送者(King)与接收者(Soldier/Commander/Officer)的解耦
命令模式
命令模式将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
我感觉这么说很别扭,实际上,命令模式中我们将请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化;例如对请求排队或记录请求日志,以及支持可撤销的操作。
几乎所有使用命令在控制台操作的程序都使用了这种设计模式
- 把“动作请求”变成“对象”
- 解耦请求的发起者(Invoker)和执行者(Receiver)
这样对支持 undo/redo、宏命令、事务等高级功能很方便
实现方式如下
- 定义命令接口:所有命令必须实现的接口。
- 创建具体命令:实现命令接口的具体类,包含执行请求的方法。
- 调用者:持有命令对象并触发命令的执行。
- 接收者:实际执行命令的对象。
命令模式的经典角色
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| Command(命令接口) | 声明执行操作的接口 | Runnable(Java 内置函数式接口) |
| ConcreteCommand(具体命令) | 绑定 Receiver 和 Action | goblin::changeSize,
goblin::changeVisibility(方法引用) |
| Invoker(调用者) | 负责调用命令对象 | Wizard |
| Receiver(接收者) | 真正执行业务逻辑的对象 | Goblin(继承自 Target) |
| Client(客户端) | 创建命令并配置 Invoker 和 Receiver | App.main() |
还是以具体例子来描述
接收者
抽象接收者,封装了可被操作的状态,并且提供了具体操作方法
具体接收者
调用者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27public class Wizard {
private final Deque<Runnable> undoStack = new LinkedList<>();
private final Deque<Runnable> redoStack = new LinkedList<>();
// 执行命令 + 入栈
public void castSpell(Runnable runnable) {
runnable.run(); // 执行命令
undoStack.offerLast(runnable); // 记录用于撤销
}
// 利用栈结构实现撤销/重做
public void undoLastSpell() {
if (!undoStack.isEmpty()) {
var spell = undoStack.pollLast();
redoStack.offerLast(spell);
spell.run(); // 注意:这里直接 re-run,依赖命令的“可逆性”
}
}
public void redoLastSpell() {
if (!redoStack.isEmpty()) {
var spell = redoStack.pollLast();
undoStack.offerLast(spell);
spell.run();
}
}
}Runnable本身包含了所有执行所需信息,所以调用命令的时候可以直接使用方法引用,直接调用命令具体实现的方法命令接口和具体命令通过 Java 方法引用来简化掉了
goblin::changeSize是一个Runnable对象,它捕获了goblin实例和changeSize方法这也就实现了把命令当成对象来处理,而且支持各种执行者的高级操作
客户端在交互的时候也完全不知道 Goblin 的内部细节,它一直在和 Invoker(Wizard)交互
解释器模式
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。
解释器模式给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
什么意思,我定义一种语言的文法表示,并创建一个解释器,该解释器能够解释该语言中的句子
所以说,解释器模式用于构建一个能够解释特定语言或文法的句子的解释器。
就当某一特定类型的问题或者需求频繁出现,并且可以通过设计一种简单的语言来表达这些问题的实例和解决过程时,使用这一设计模式非常有用。
想一下,这种模式在 SQL 解析、符号处理引擎等,是不是用到了他们,而且解释器模式广泛的用于编译器设计,将源代码解释为目标代码。
实现方式也比较清晰
- 定义文法:明确语言的终结符和非终结符。一般来说,将语言的每个符号(token)表示为一个类
- 构建语法树:根据语言的句子构建对应的语法树结构来表示整个表达式
- 创建环境类:包含解释过程中所需的全局信息,通常是一个HashMap。通过递归解释 语法树 节点得到最终结果
只不过,通常需要注意如下内容
- 终结符与非终结符:定义语言的文法结构。
- 环境类:存储解释过程中需要的外部环境信息。
注意,解释器模式在 Java 中不是首选,如果遇到适用场景,可以考虑使用如expression4J之类的库来代替。
那么,还是以一个例子来讲解,可以看出,这是一个后缀表达式的计算器
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| AbstractExpression(抽象表达式) | 声明解释操作的接口 | Expression |
| TerminalExpression(终结符表达式) | 实现文法中终结符的解释(如数字) | NumberExpression |
| NonterminalExpression(非终结符表达式) | 实现文法中非终结符的解释(如 +, -,
*) |
PlusExpression, MinusExpression,
MultiplyExpression |
| Context(上下文) | 包含解释器之外的全局信息(本例未显式使用) | ——(隐含在栈中) |
| Client(客户端) | 构建抽象语法树并调用解释器 | App.main() |
抽象表达式,它是所有表达式的基类,提供了一个解释操作的入口方法
休止符表达式类
在后缀表达式中,数字部分就是休止符,他们不能被再分解了,需要结合之前的符号直接返回结果,而单个数字的解析就是返回自身值
非休止符表达式类
以加法为例,它会在解释时递归调用子表达式的
interpret()而这又形成了组合模式的设计模式
客户端
它负责:解析输入字符串,构建表达式对象树AST,调用根节点的
interpret()
那么,趁着这个再复习一下后缀表达式的求值
遇到数字 → 创建
NumberExpression,压入栈遇到操作符 → 弹出两个表达式(先弹出的是右操作数!)
构造
OperatorExpression(left, right)调用其
interpret()得到结果将结果包装为
NumberExpression压回栈
迭代器模式
迭代器模式(Iterator Pattern) 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。
- 解耦集合(Collection)与遍历算法(Traversal)
- 统一遍历接口:无论底层是数组、链表、树还是图,客户端都用
hasNext()/next()遍历 - 支持多种遍历方式:同一集合可有多个不同迭代器(如正序、倒序、按类型过滤)
提供一种统一的方法来遍历不同的聚合对象。而且因此被迭代对象内部的细节,理解还是很好理解的,但是实现方式有点麻烦
- 定义迭代器接口:包含
hasNext()和next()等方法,用于遍历元素。规定了遍历元素的方法。 - 创建具体迭代器:实现迭代器接口,定义如何遍历特定的聚合对象。实现了迭代器接口,包含遍历逻辑。
- 聚合类:定义一个接口用于返回一个迭代器对象。
Java集合框架中的迭代器用于遍历集合元素。它就使用了这种设计模式,屏蔽了各个集合的细节
那么,迭代器模式通常有如下角色,还是以例子来讲解
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| Iterator(迭代器接口) | 定义访问和遍历元素的接口 | com.iluwatar.iterator.Iterator<T> |
| ConcreteIterator(具体迭代器) | 实现 Iterator 接口,跟踪当前位置 | BstIterator<T>,
TreasureChestItemIterator |
| Aggregate(聚合接口) | 定义创建迭代器的接口(本例未显式定义) | ——(由 TreasureChest 直接提供) |
| ConcreteAggregate(具体聚合) | 实现 Aggregate,返回 ConcreteIterator | TreasureChest(BST 的 root 节点隐含聚合) |
| Client(客户端) | 通过 Iterator 遍历聚合对象 | App.main() |
首先,二叉搜索树的中序迭代器中,按照左 → 根 → 右遍历,其中的对于一个二叉搜索树的节点的聚合对象为,其中表示方法也是整个树由 root 节点隐式表示
1 | public class TreeNode<T extends Comparable<T>> { |
对于它的迭代器接口就是这样,只关注这两个方法
对于二叉搜索树的中序遍历的具体迭代器,它的实现为
1 | public class BstIterator<T extends Comparable<T>> implements Iterator<TreeNode<T>> { |
那么,这个例子还有一个特殊类型的过滤迭代器,差不多就是这样
TreasureChest存储多种物品(药水、戒指、武器)- 客户端希望只遍历特定类型的物品
对于它,具体对象如下,封装了物品集合,而且提供工厂方法创建迭代器
元素对象不看了,直接来看具体的迭代器实现
1 | public class TreasureChestItemIterator implements Iterator<Item> { |
这样,在客户端调用的时候,对于这两种完全不同的集合类型,他们都可以使用 hasNext 和 next 方法来处理对集合的迭代,很舒服的向下封装了
集合只管存储,迭代器只管遍历,新增遍历方式根本不用修改集合
Java 的 for (Item item : chest)
语法糖背后就是迭代器模式,要支持 for-each,类必须实现
Iterable
接口,所以说,这个是自定义类型是实现迭代的关键,背后是迭代器模式的设计
中介者模式
中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性,属于行为型模式。
中介者模式定义了一个中介对象来封装一系列对象之间的交互。中介者使各对象之间不需要显式地相互引用,从而使其耦合松散,且可以独立地改变它们之间的交互。
就是通过引入一个中介对象来封装和协调多个对象之间的交互,从而降低对象间的耦合度。
一般当需求设计到解决对象间复杂的一对多关联问题,避免对象之间的高度耦合,简化系统结构,会经常使用这个设计模式。
而且一般情况下,此时系统中多个类相互耦合,形成网状结构时。通过中介者模式,就会转换成星状图通信
实现方式比较清楚
- 定义中介者接口:规定中介者必须实现的接口。
- 创建具体中介者:实现中介者接口,包含协调各同事对象交互的逻辑。
- 定义同事类:各个对象不需要显式地相互引用,而是通过中介者来进行交互。
例如,中国加入WTO后,各国通过WTO进行贸易,简化了双边关系。
想象一个微信群:群成员(Colleague)不直接私聊,而是都在群里发言(通过群主/群规 = Mediator 协调)。
中介者模式包含以下几个主要角色,还是以一个具体例子来说
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| Mediator(中介者接口) | 定义同事对象到中介者的接口 | Party |
| ConcreteMediator(具体中介者) | 实现 Mediator 接口,协调 Colleague 交互 | PartyImpl |
| Colleague(同事类基类) | 每个 Colleague 知道自己的 Mediator | PartyMemberBase |
| ConcreteColleague(具体同事类) | 与其他 Colleague 通信时通过 Mediator | Hobbit, Wizard, Rogue,
Hunter |
| Client(客户端) | 创建 Mediator 和 Colleague,并建立关联 | App.main() |
中介者接口
1
2
3
4public interface Party {
void addMember(PartyMember member); // 添加成员
void act(PartyMember actor, Action action); // 处理某个成员的动作
}定义了
具体中介者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class PartyImpl implements Party {
private final List<PartyMember> members = new ArrayList<>();
public void act(PartyMember actor, Action action) {
for (var member : members) {
if (!member.equals(actor)) { // 不通知自己
member.partyAction(action); // 广播给其他成员
}
}
}
public void addMember(PartyMember member) {
members.add(member);
member.joinedParty(this); // 通知成员:你已加入本队
}
}对中介者能力的具体实现
同事类基类
每个成员持有中介者引用,对于关键操作,也就是中介者的核心能力,交给中介者来做,实现同事类只与中介者通信
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public abstract class PartyMemberBase implements PartyMember {
protected Party party; // 每个成员持有中介者引用
public void joinedParty(Party party) {
this.party = party;
LOGGER.info("{} joins the party", this);
}
public void partyAction(Action action) {
LOGGER.info("{} {}", this, action.getDescription());
}
public void act(Action action) {
if (party != null) {
LOGGER.info("{} {}", this, action);
party.act(this, action); // 通过中介者广播
}
}
}具体同事类,所有交互逻辑继承自
PartyMemberBase
动作定义不展开细说了,就一枚举
客户端
同事类之间零依赖,只依赖中介者,而且所有交互逻辑在
PartyImpl.act() 中,易于修改,好处不言而喻
但是如果交互逻辑复杂,PartyImpl
会变得庞大,而且中介者成为“上帝对象”,可能违反单一职责,此时就需要根据组合模式什么的进行拆分了
备忘录模式
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象,备忘录模式属于行为型模式。
在不破坏封装性的前提下,捕获一个对象的内部状态,并允许在对象之外保存和恢复这些状态。这样以后就可将该对象恢复到原先保存的状态。
使用这个设计模式通常就是当需要提供一种撤销机制,允许用户回退到之前的状态时。
来看实现方式
- 创建备忘录类:用于存储和封装对象的状态。一般是存储发起人的状态信息。
- 创建发起人角色:负责创建备忘录,并根据需要恢复状态。
- 创建备忘录管理类(可选):负责管理所有备忘录对象。
最早围棋的悔棋就是这种设计模式,它提供一种撤销操作的功能。而且游戏存档保存游戏进度,允许玩家加载之前的存档,大部分都使用了这种设计模式。
但是,问题很明显,如果对象的状态复杂,保存状态可能会占用较多资源。牛魔的例如博德之门3存档在我电脑里达到了惊人了8.1G
备忘录模式包含以下几个主要角色:
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| Originator(发起人) | 创建备忘录 & 恢复状态的对象 | Star |
| Memento(备忘录) | 存储 Originator 的内部状态 | StarMementoInternal(实现
StarMemento) |
| Caretaker(管理者) | 负责保存/管理备忘录(但不能修改) | App.main() 中的
Stack<StarMemento> |
发起人
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class Star {
private StarType type;
private int ageYears;
private int massTons;
// ... 构造函数、timePasses() 等
// 创建备忘录(保存当前状态)
StarMemento getMemento() {
var state = new StarMementoInternal();
state.setAgeYears(ageYears);
state.setMassTons(massTons);
state.setType(type);
return state;
}
// 恢复状态(从备忘录)
void setMemento(StarMemento memento) {
var state = (StarMementoInternal) memento;
this.type = state.getType();
this.ageYears = state.getAgeYears();
this.massTons = state.getMassTons();
}
}- 内部类
StarMementoInternal:作为真正的备忘录实现
- 内部类
备忘录是
StarMemento.java+ 上面的内部类而
StarMemento.java接口是一个窄接口,Caretaker 只能持有此接口,无法读取/修改状态
而实现者这个宽接口,只能在 Originator 可见
这是双重接口(Dual Interface) 的经典应用:
- 对 Caretaker:窄接口(只能传递)
- 对 Originator:宽接口(可读写)
管理者,在这里是客户端
Stack<StarMemento>用栈结构实现“后进先出”的撤销
可以看出这个例子是描述的恒星的演化,还挺有意思的
备忘录模式提供状态恢复能力,但是资源消耗大,而且频繁创建备忘录影响性能,双刃剑了
命令模式的撤销这种逆向操作,一般都是相对简单的情况下,能够直接用命令撤销,而且只能是针对上个版本的情况下,而备忘录模式就比较复杂和自由了,因为它复制的是指定版本的快照
我们智研平台对于 wiki 就使用了备忘录模式
给对象一个“后悔药”,让它能随时回到过去的状态,而无需暴露自己的秘密。
观察者模式
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生改变时,其所有依赖者都会收到通知并自动更新。
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
也就是,当一个对象状态改变时,所有依赖于它的对象都会得到通知并自动更新。保持对象间的低耦合和高协作性
- 解耦 Subject(主题)和 Observer(观察者)
- 广播通信:Subject 不关心谁在观察,只负责通知
- 动态订阅:观察者可随时加入/退出
就好像微信公众号(Subject) vs 订阅用户(Observer)。公众号发文章,所有订阅者自动收到。
实现方式也是两边接口两边实现
- 定义观察者接口:包含一个更新方法。
- 创建具体观察者:实现观察者接口,定义接收到通知时的行为。
- 定义主题接口:包含添加、删除和通知观察者的方法。
- 创建具体主题:实现主题接口,管理观察者列表,并在状态改变时通知它们。
观察者模式的主要角色如下,还是以一个例子说明
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| Subject(主题/被观察者) | 管理观察者列表,状态变化时通知 | Weather, GenWeather |
| Observer(观察者) | 定义接收通知的接口 | WeatherObserver, Race |
| ConcreteSubject(具体主题) | 实现 Subject,维护状态 | Weather, GenWeather |
| ConcreteObserver(具体观察者) | 实现 Observer,响应通知 | Orcs, Hobbits, GenOrcs,
GenHobbits |
传统实现
来看观察者接口
- 所有观察者必须实现此接口,此时,
update()是通知入口
对于具体的观察者
- 实现
update()方法,定义自己的响应逻辑,而且他们完全不知道Weather的内部细节
那么,这个例子描述的是天气预报,那么被观察者就是Weather.java
首先,它维护了一个观察者列表,然后对应的是add/remove,也就是订阅和退订,状态变更时广播。通过notifyObservers()
遍历调用 update()
泛型增强实现
再看使用泛型对其增强实现,泛型观察者基类:Observable.java
- 那么其中涉及到了一个泛型递归
S= 具体主题类型(如GenWeather)O= 具体观察者类型(如GenOrcs)A= 通知参数类型(如WeatherType)
- 使用了并发编程中常用的
CopyOnWriteArrayList来保证线程安全 - 强类型回调:
update(S subject, A argument)提供主题引用,避免了传统实现中update()参数类型不明确的问题。
泛型观察者接口:Observer.java
回调方法接收主题实例和参数,功能更强
对于具体的主题
- 继承
Observable观察者基类,指定泛型参数 - 无需重复实现观察者管理逻辑
对于具体的观察者
- 实现
update(GenWeather, WeatherType),直接获得主题引用
其中,Race 接口是一个窄接口,仅声明并且被持有,无法做出改动
两者的客户端使用都是一样的,只不过泛型版类型更安全
客户端只与主题交互,观察者自动响应
状态模式
在状态模式(State Pattern)中,类的行为是基于它的状态改变的,这种类型的设计模式属于行为型模式。
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。
状态模式允许对象在内部状态改变时改变其行为,使得对象在不同的状态下有不同的行为表现。通过将每个状态封装成独立的类,可以避免使用大量的条件语句来实现状态切换。
这个说的太抽象了,我来说的清楚一点
就是它允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
我的状态从睡觉变成了苏醒,那么我就需要执行起床这个一个动作
- 将状态相关的行为封装到独立的状态类中
- 状态转换由状态类自身或上下文管理
- 避免庞大的条件语句(if-else / switch)
状态模式的角色如下
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| Context(上下文) | 持有当前状态的引用,将客户端请求委托给状态对象 | Mammoth |
| State(状态接口) | 定义状态相关行为的接口 | State |
| ConcreteState(具体状态) | 实现 State 接口,封装特定状态的行为 | PeacefulState, AngryState |
状态接口如下,对于一个状态,需要进入状态触发的方法和查询当前状态的行为表现的方法
具体状态如下,以和平状态为例子
1 | public class PeacefulState implements State { |
- 持有
Mammoth引用:状态对象可以访问上下文 - 状态之间不会相互影响
上下文就是Mammoth
1 | public class Mammoth { |
changeStateTo()封装切换细节,进入状态涉及到了更新引用和触发onEnterState(),这说明了什么,说明了行为委托,对象行为随状态改变
对于客户端,直接使用上下文就可以实现状态切换额外附带的一堆动作
- 客户端只与
Mammoth交互,完全不知道内部状态的具体细节 - 通过简单的方法调用触发状态切换
也就是说,状态模式的核心就是将 if-else/switch 转为多态,而且符合开闭原则,新增状态只需添加新类,不修改现有代码
策略模式
在策略模式(Strategy Pattern)中一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式定义了一系列算法或策略,并将每个算法封装在独立的类中,使得它们可以互相替换。通过使用策略模式,可以在运行时根据需要选择不同的算法,而不需要修改客户端代码。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
策略模式的意图就是将每个算法封装起来,使它们可以互换使用。
- 将“做什么”(策略)与“怎么做”(执行者)分离,这样就能运行时切换算法了
解决在多种相似算法存在时,使用条件语句(如if…else)导致的复杂性和难以维护的问题。
这个就比较清晰了说的,而且和状态模式比较像,只不过是一个是根据状态改变行为,一个是运行时切换策略或算法
| 特性 | 状态模式 | 策略模式 |
|---|---|---|
| 目的 | 对象行为随内部状态改变 | 封装算法族,运行时切换 |
| 状态转换 | 通常由 Context 或 State 自身触发 | 由 Client 显式设置 |
| 耦合度 | State 通常持有 Context 引用 | Strategy 通常不持有 Context 引用 |
| 行为变化 | 隐式(状态自动切换) | 显式(Client 选择策略) |
实现方式如下
- 定义策略接口:所有策略类都将实现这个统一的接口。规定了所有策略类必须实现的方法。
- 创建具体策略类:每个策略类封装一个具体的算法或行为。实现了策略接口,包含具体的算法实现。
- 上下文类:包含一个策略对象的引用,并通过该引用调用策略。
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| Strategy(策略接口) | 定义所有支持的算法的公共接口 | DragonSlayingStrategy |
| ConcreteStrategy(具体策略) | 实现 Strategy 接口的具体算法 | MeleeStrategy, ProjectileStrategy,
SpellStrategy |
| Context(上下文) | 持有 Strategy 引用,调用其算法 | DragonSlayer |
| Client(客户端) | 配置 Context 使用哪个 Strategy | App.main() |
策略接口如下
- 标记
@FunctionalInterface的原因是成为函数式接口,这样就能被 Lambda 表达式实现 - 单一方法
execute():定义策略的核心行为
具体策略类,这些实现都一样,挑一个来说,每个策略封装一种屠龙算法
上下文类,DragonSlayer.java,它持有策略引用,而且支持动态的切换
1 | public class DragonSlayer { |
goToBattle()不直接实现逻辑,而是调用strategy.execute(),这就是算法与使用解耦的实现,通过切换策略来切换算法
客户端使用了三种方式
1 | public static void main(String[] args) { |
枚举策略:LambdaStrategy.java就是几种策略的枚举,不说了
这就是策略模式的精髓:让算法成为可插拔的组件,赋予对象运行时改变行为的能力。
模板方法模式
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
模板方法模式(Template Method Pattern) 是一种行为型设计模式,它在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义该算法的某些特定步骤。
关键是在父类中定义了算法的骨架,并允许子类在不改变算法结构的前提下重定义算法的某些特定步骤。
解决在多个子类中重复实现相同的方法的问题,通过将通用方法抽象到父类中来避免代码重复。
一般情况下,当存在一些通用的方法,可以在多个子类中共用时。
实现方式
- 定义抽象父类:包含模板方法和一些抽象方法或具体方法。抽象方法代表算法中的可变部分。具体方法代表算法的不变部分。
- 实现子类:继承抽象父类并实现抽象方法,不改变算法结构。
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| AbstractClass(抽象类) | 定义模板方法和抽象步骤 | StealingMethod |
| ConcreteClass(具体子类) | 实现抽象步骤,完成算法 | HitAndRunMethod, SubtleMethod |
抽象类(模板定义):
StealingMethod.java1
2
3
4
5
6
7
8
9
10
11
12
13
14public abstract class StealingMethod {
// === 抽象步骤(由子类实现) ===
protected abstract String pickTarget();
protected abstract void confuseTarget(String target);
protected abstract void stealTheItem(String target);
// === 模板方法(final 禁止子类重写) ===
public final void steal() {
var target = pickTarget();
LOGGER.info("The target has been chosen as {}.", target);
confuseTarget(target);
stealTheItem(target);
}
}steal()是模板方法:- 定义了偷窃的固定流程:选目标 → 迷惑目标 → 偷窃物品
final关键字:确保算法骨架不被子类篡改
- 三个抽象方法:
pickTarget():选择目标confuseTarget():迷惑目标stealTheItem():执行偷窃protected访问权限:仅限子类访问
具体子类
他们都都遵循
steal()定义的三步走,不同的策略实现了对应的实现上下文使用者:
HalflingThief.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class HalflingThief {
private StealingMethod method;
public HalflingThief(StealingMethod method) {
this.method = method;
}
public void steal() {
method.steal(); // 调用模板方法
}
public void changeMethod(StealingMethod method) {
this.method = method; // 支持切换策略
}
}这里也使用了组合模式
客户端使用起来就可以很灵活了
模板方法模式和策略模式这俩互相都有着对面的影子
| 特性 | 模板方法模式 | 策略模式 |
|---|---|---|
| 实现方式 | 继承(Inheritance) | 组合(Composition) |
| 变化点 | 算法的特定步骤 | 整个算法 |
| 控制权 | 父类控制流程,子类实现细节 | Context 完全委托给 Strategy |
| 灵活性 | 较低(需通过继承扩展) | 高(运行时动态切换) |
| 代码复用 | 通过父类复用骨架 | 通过接口复用契约 |
| 你的代码体现 | StealingMethod 的 steal() 流程固定 |
HalflingThief 通过 changeMethod() 切换
StealingMethod |
访问者模式
在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。
通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。
根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。
什么意思,访问者模式将算法与对象结构分离。通过这种方式,可以在不修改对象结构的情况下定义新的操作。
解决在稳定数据结构和易变操作之间的耦合问题,使得操作可以独立于数据结构变化。
当需要对一个对象结构中的对象执行多种不同的且不相关的操作时,尤其是这些操作需要避免”污染”对象类本身,会使用这个设计模式
它的实现方式也是双接口双实现
- 定义访问者接口:声明一系列访问不同元素的方法,一个访问方法对应数据结构中的一个元素类。
- 创建具体访问者:实现访问者接口,为每个访问方法提供具体实现,包含对每个元素类的访问逻辑。
- 定义元素接口:声明一个接受访问者的方法。
- 创建具体元素:实现元素接口,每个具体元素类对应数据结构中的一个具体对象,提供给访问者访问的入口。
那么,访问者模式的角色有如下,最后一个还是用例子来描述
| 角色 | 说明 | 在你例子中的对应 |
|---|---|---|
| Visitor(访问者接口) | 声明访问每个具体元素的方法 | UnitVisitor |
| ConcreteVisitor(具体访问者) | 实现 Visitor 接口,定义具体操作 | SoldierVisitor, SergeantVisitor,
CommanderVisitor |
| Element(元素接口) | 声明接受访问者的方法 | Unit(抽象类) |
| ConcreteElement(具体元素) | 实现 Element 接口,调用 Visitor 的具体方法 | Soldier, Sergeant,
Commander |
| ObjectStructure(对象结构) | 管理元素集合,提供高层遍历接口 | Commander(作为根节点) |
访问者接口
1
2
3
4
5public interface UnitVisitor {
void visit(Soldier soldier);
void visit(Sergeant sergeant);
void visit(Commander commander);
}- 为每种具体元素声明一个
visit()方法,这样根据参数类型区分不同操作
- 为每种具体元素声明一个
具体访问者
- 每个访客只关心特定类型的元素,对不关心的元素提供空实现
元素基类 Unit
1
2
3
4
5
6
7
8
9
10
11
12public abstract class Unit {
private final Unit[] children;
public Unit(Unit... children) {
this.children = children;
}
// 接受访问者(模板方法)
public void accept(UnitVisitor visitor) {
Arrays.stream(children).forEach(child -> child.accept(visitor));
}
}Unit可以包含子Unit(形成树形结构)accept()方法:提供遍历子节点的通用逻辑- 注意:这里没有调用
visitor.visit(this)!这个调用在子类中完成
具体元素
最后对于客户端,只需调用根节点的
accept(),就能按照指挥官 → 军士 → 士兵这样的树形结构访问,这样,同一结构可应用不同算法
这样,通过 UnitVisitor 接口和 accept()
方法的分发,使得同一军队结构可以轻松应用不同的问候策略,而无需修改任何单位类。因为面对不同的对象,它会使用不同的方法
当你的对象结构稳定但操作多变时,让操作成为可插拔的访客,而不是硬编码在对象内部。





