五、状态模式
1、概述
状态设计模式是一种行为型设计模式,它允许对象在其内部状态发生时改变其行为,这种模式可以消除大量的条件语句,并将每个状态的行为封装到单独的类中。
状态模式的主要组成部分如下:
1)上下文(Context):上下文通常包含一个具体状态的引用,用于维护当前状态,上下文委托给当前对象处理状态相关行为。
2)抽象状态(State):定义一个接口,用于封装与上下文的特定状态相关的行为。
3)具体状态(Concrete State):实现抽象状态接口,为具体状态定义行为。每个具体状态类对应一个状态。
简单示例,假设要模拟一个简易的电视遥控器,具有开启、关闭和调整音量的功能。
假设不使用设计模式:
public class TV {
private boolean isOn;
private int volume;
public TV() {
isOn = false;
volume = 0;
}
public void turnOn() {
// 如果是开启状态
if (isOn) {
System.out.println("TV is already on.");
// 否则打开电视
} else {
isOn = true;
System.out.println("Turning on the TV.");
}
}
public void turnOff() {
if (isOn) {
isOn = false;
System.out.println("Turning off the TV.");
} else {
System.out.println("TV is already off.");
}
}
public void adjustVolume(int volume) {
if (isOn) {
this.volume = volume;
System.out.println("Adjusting volume to: " + volume);
} else {
System.out.println("Cannot adjust volume, TV is off.");
}
}
}
public class Main {
public static void main(String[] args) {
TV tv = new TV();
tv.turnOn();
tv.adjustVolume(10);
tv.turnOff();
}
}
该例子中我们状态比较少,所以代码看起来也不是很复杂,但是状态如果变多了就会变得不好控制,比如增加换台,快捷键,静音等功能。
使用状态设计模式之后:
首先定义抽象状态接口TVState,将每一个修改状态的动作抽象成一个接口:
public interface TVState {
void turnOn();
void turnOff();
void adjustVolume(int volume);
}
接下来为每个具体的状态创建类,实现TVState接口,例如:创建TVOnState 和 TVOffState 类:
// 在on状态下,去执行以下各种操作
public class TVOnState implements TVState {
@Override
public void turnOn() {
System.out.println("TV is already on.");
}
@Override
public void turnOff() {
System.out.println("Turning off the TV.");
}
@Override
public void adjustVolume(int volume) {
System.out.println("Adjusting volume to: " + volume);
}
}
// 在关机的状态下执行以下的操作
public class TVOffState implements TVState {
@Override
public void turnOn() {
System.out.println("Turning on the TV.");
}
@Override
public void turnOff() {
System.out.println("TV is already off.");
}
@Override
public void adjustVolume(int volume) {
System.out.println("Cannot adjust volume, TV is off.");
}
}
接下来定义上下文类TV:
public class TV {
// 当前状态
private TVState state;
public TV() {
state = new TVOffState();
}
public void setState(TVState state) {
this.state = state;
}
public void turnOn() {
// 打开
state.turnOn();
// 设置为开机状态
setState(new TVOnState());
}
public void turnOff() {
// 关闭
state.turnOff();
// 设置为关机状态
setState(new TVOffState());
}
public void adjustVolume(int volume) {
state.adjustVolume(volume);
}
}
最后通过以下方式使用这些类:
public class Main {
public static void main(String[] args) {
TV tv = new TV();
tv.turnOn();
tv.adjustVolume(10);
tv.turnOff();
}
}
这个例子展示了状态模式的基本结构和用法。通过使用状态模式,我们可以更好地组织和管理与特定状态相关的代码。当状态较多时,这种模式的优势就会凸显出来,同时我们在代码时,因为我们会对每个状态进行独立封装,所以也会简化代码编写。
2、有限状态机
有限状态机,英文翻译时Flinite State Machine,缩写为FSM,简称状态机,比较官方的说法是:有限状态机是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。状态机有3个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也成为转移条件(Transition Condition)。事件触发状态的转移及动作的执行,不过,动作不是必须的,也可能只转移状态,不执行任何动作。
2.1 分支法
如何实现状态机,总结了三种方式。其中,最简单直接的实现方式是,参照状态转移图,将每一个状态转移。原模原样的直译成代码。这样编写的代码会包含大量的 if - else或 switch-case 分支判断逻辑,甚至是嵌套的分支判断逻辑。所以我们把这种方法暂且命名为分支法:
下面是一个使用if-else 语句实现的马里奥形态变化的代码示例:
public class Mario {
private MarioState state;
public Mario() {
state = MarioState.SMALL;
}
public void handleEvent(Event event) {
MarioState newState = state;
// 处理吃蘑菇事件
if (event == Event.MUSHROOM) {
if (state == MarioState.SMALL) {
newState = MarioState.BIG;
}
// 处理吃火花事件
} else if (event == Event.FIRE_FLOWER) {
if (state == MarioState.BIG) {
newState = MarioState.FIRE;
}
// 处理遇到小怪事件
} else if (event == Event.ENEMY_ATTACK) {
if (state == MarioState.BIG) {
newState = MarioState.SMALL;
} else if (state == MarioState.FIRE) {
newState = MarioState.BIG;
} else if (state == MarioState.SMALL) {
newState = MarioState.DEAD;
}
// 处理掉坑事件
} else if (event == Event.FALL_INTO_PIT) {
newState = MarioState.DEAD;
}
System.out.printf("从 %s 变为 %s%n", state, newState);
state = newState;
}
}
public class MarioDemo {
public static void main(String[] args) {
Mario mario = new Mario();
mario.handleEvent(Event.MUSHROOM); // 变为大马里奥
mario.handleEvent(Event.FIRE_FLOWER); // 变为火焰马里奥
mario.handleEvent(Event.ENEMY_ATTACK); // 变为死亡马里奥
}
}
2.2 查表法
该种方法略,不怎么用,可以自己查资料。
2.3 状态模式
在查表法的代码实现中,事件触发的动作只是简单的状态或者数值,所以,我们用一个 MarioState类型的二维数组 TRANSITION_TABLE 就能表示,二维数组中的值表示出发事件后的新状态。但是,如果要执行的动作并非这么简单,而是一系列复杂的逻辑操作(比如加减分数、处理位置信息等等),我们就没法用如此简单的二维数组来表示了。这也就是说,查表法的实现方式有一定局限性。
虽然分支逻辑的实现方式不存在这个问题,但它又存在前面讲到的其他问题,比如分支判断逻辑较多,导致代码可读性和可维护性不好等。实际上,针对分支逻辑法存在的问题,我们可以使用状态模式来解决。
状态模式通过将事件触发的状态转移和动作执行,拆分到不同的状态类中,来避免分支判断逻辑。我们还是结合代码来理解这句话。利用状态模式,我们来补全 MarioStateMachine 类,补全后的代码如下所示。
以下是一个使用 Java 实现的简化版马里奥形态变化的案例代码,我为代码添加了中文注释以便理解:
// 定义事件枚举类型
enum Event {
// 吃蘑菇,吃火花,遇到小怪,调入深坑
MUSHROOM, FIRE_FLOWER, ENEMY_ATTACK, FALL_INTO_PIT
}
// 定义马里奥状态接口
interface MarioState {
void handleEvent(Event event);
}
// 实现死亡马里奥状态
class DeadMario implements MarioState {
private Mario mario;
public DeadMario(Mario mario) {
this.mario = mario;
}
@Override
public void handleEvent(Event event) {
System.out.println("马里奥已死亡,无法处理事件");
}
}
// 实现小马里奥状态
class SmallMario implements MarioState {
private Mario mario;
public SmallMario(Mario mario) {
this.mario = mario;
}
@Override
public void handleEvent(Event event) {
switch (event) {
case MUSHROOM:
System.out.println("变为大马里奥");
mario.setState(new BigMario(mario));
break;
case FIRE_FLOWER:
System.out.println("小马里奥不能直接变为火焰马里奥");
break;
case ENEMY_ATTACK:
System.out.println("小玛丽奥去世了");
mario.setState(new DeadMario(mario));
break;
case FALL_INTO_PIT:
System.out.println("小玛丽奥去世了");
mario.setState(new DeadMario(mario));
break;
}
}
}
// 实现大马里奥状态
class BigMario implements MarioState {
private Mario mario;
public BigMario(Mario mario) {
this.mario = mario;
}
@Override
public void handleEvent(Event event) {
switch (event) {
case MUSHROOM:
System.out.println("保持大马里奥");
break;
case FIRE_FLOWER:
System.out.println("变为火焰马里奥");
mario.setState(new FireMario(mario));
break;
case ENEMY_ATTACK:
System.out.println("变为小马里奥");
mario.setState(new SmallMario(mario));
break;
case FALL_INTO_PIT:
System.out.println("马里奥去世了");
mario.setState(new DeadMario(mario));
break;
}
}
}
// 实现火焰马里奥状态
class FireMario implements MarioState {
private Mario mario;
public FireMario(Mario mario) {
this.mario = mario;
}
@Override
public void handleEvent(Event event) {
switch (event) {
case MUSHROOM:
System.out.println("保持火焰马里奥");
break;
case FIRE_FLOWER:
System.out.println("保持火焰马里奥");
break;
case ENEMY_ATTACK:
System.out.println("变为大马里奥");
mario.setState(new BigMario(mario));
break;
case FALL_INTO_PIT:
System.out.println("马里奥去世了");
mario.setState(new DeadMario(mario));
break;
}
}
}
// 定义马里奥类,作为状态的上下文
class Mario {
private MarioState state;
public Mario() {
state = new SmallMario(this);
}
public void setState(MarioState state) {
this.state = state;
}
public void handleEvent(Event event) {
state.handleEvent(event);
}
}
// 测试类
public class MarioDemo {
public static void main(String[] args) {
Mario mario = new Mario();
mario.handleEvent(Event.MUSHROOM); // 变为大马里奥
mario.handleEvent(Event.FIRE_FLOWER); // 变为火焰马里奥
mario.handleEvent(Event.ENEMY_ATTACK); // 变为大马里奥
}
}
在这个简化示例中,我们定义了 MarioState 接口以及实现了 DeadMario 、SmallMario 、 BigMario 和 FireMario 类,分别表示马里奥的四种形态。每个形态类实现了 handleEvent 方法,用于处理不同的游戏事件并根据有限状态机规 则进行状态转换。
Mario 类作为状态的上下文,用于管理和切换马里奥的状态。它有一个 setState方法,用于更新当前状态。 handleEvent 方法将事件传递给当前状态,以便根据事件执行相应的状态转换。
在 MarioDemo 测试类中,我们创建了一个 Mario 实例,并通过调用handleEvent 方法模拟游戏中的事件。通过运行这个测试类,你可以观察到马里奥根据有限状态机的规则在不同形态之间切换。
这个简化示例展示了如何使用有限状态机来实现马里奥角色的形态变化。在实际游戏开发中,你可能需要考虑更多的事件和状态,以及与游戏引擎或框架集成的方式。不过,这个示例可以帮助你理解有限状态机在游戏中的应用。
六、迭代器模式
迭代器模式,它用来遍历集合对象,不过,很多编程语言都将迭代器作为一个基础的类库,直接提供出来了。在平时的开发中,特别是业务开发,我们直接使用即可,很少会自己去实现一个迭代器。不过弄懂原理能帮助我们更好的使用这些工具类。
不怎么用,省略,需要可以自行查询。
七、访问者模式
1、概述和原理
访问者设计模式(Visitor Pattern)是一种行为型设计模式,它允许你再不修改现有类结构的情况下,为类添加新的操作,这种模式可以实现良好的解耦和扩展性,尤其适用于现有类层次结构中添加新的功能的情况。
访问者模式主要包含以下角色:
1. 访问者(Visitor):定义一个访问具体元素的接口,为每种具体元素类型声明一个访问操作。
2. 具体访问者(ConcreteVisitor):实现访问者接口,为每种具体元素提供具体的访问操作实现。
3. 元素(Element):定义一个接口,声明接受访问者的方法。
4. 具体元素(ConcreteElement):实现元素接口,提供接受访问者的具体实现。
5. 对象结构(ObjectStructure):包含一个元素集合,提供一个方法以遍历这些元素并让访问者访问它们。
以下是一个简单的访问者模式示例:
假设我们有一个表示计算机组件的类层次结构(如 CPU、内存和硬盘等),我们需要为这些组件实现一个功能,比如展示它们的详细信息。使用访问者模式,我们可以将【展示详细信息】的功能与【组件类】分离,从而实现解耦和扩展性。
1、不同的元素(被访问的对象)可以接收不同的访问者。
2、不同的访问者会对不同的被访问者产生不同的行为。
3、如果想要扩展,则独立重新实现访问者接口,产生一个新的具体访问者就可以了。
4、他实际解耦的是【被访问者】和【对被访问者的操作】。
简单理解就是不同的访问者,到了同一个被访问对象的家里会干不同的事。这个【事】就是行为,通过访问者模式,我们可以将行为和对象分离解耦,如下图。
// 访问者接口
interface ComputerPartVisitor {
// 访问 Computer 对象
void visit(Computer computer);
// 访问 Mouse 对象
void visit(Mouse mouse);
// 访问 Keyboard 对象
void visit(Keyboard keyboard);
}
// 具体访问者
class ComputerPartDisplayVisitor implements ComputerPartVisitor {
// 访问 Computer 对象
@Override
public void visit(Computer computer) {
System.out.println("Displaying Computer.");
}
// 访问 Mouse 对象
@Override
public void visit(Mouse mouse) {
System.out.println("Displaying Mouse.");
}
// 访问 Keyboard 对象
@Override
public void visit(Keyboard keyboard) {
System.out.println("Displaying Keyboard.");
}
}
// 元素接口
interface ComputerPart {
// 接受访问者的访问
void accept(ComputerPartVisitor computerPartVisitor);
}
// 具体元素
class Computer implements ComputerPart {
// 子元素数组
ComputerPart[] parts;
public Computer() {
// 初始化子元素数组
parts = new ComputerPart[]{new Mouse(), new Keyboard()};
}
// 接受访问者的访问
@Override
public void accept(ComputerPartVisitor computerPartVisitor) {
// 遍历所有子元素并接受访问者的访问
for (int i = 0; i < parts.length; i++) {
parts[i].accept(computerPartVisitor);
}
// 访问 Computer 对象本身
computerPartVisitor.visit(this);
}
}
// 具体元素:鼠标
class Mouse implements ComputerPart {
// 接受访问者的访问
@Override
public void accept(ComputerPartVisitor computerPartVisitor) {
// 访问 Mouse 对象
computerPartVisitor.visit(this);
}
}
// 具体元素:键盘
class Keyboard implements ComputerPart {
// 接受访问者的访问
@Override
public void accept(ComputerPartVisitor computerPartVisitor) {
// 访问 Keyboard 对象
computerPartVisitor.visit(this);
}
}
// 客户端代码
public class VisitorPatternDemo {
public static void main(String[] args) {
// 创建一个 Computer 对象
ComputerPart computer = new Computer();
// 创建一个具体访问者
ComputerPartVisitor visitor = new ComputerPartDisplayVisitor();
// 让 Computer 对象接受访问者的访问
computer.accept(visitor);
}
}
在这个示例中,我们定义了一个表示计算机组件的类层次结构,包括 Computer 、Mouse 和 Keyboard 。这些类实现了 ComputerPart 接口,该接口声明了一个接受访问者的方法。我们还定义了一个 ComputerPartVisitor 接口,用于访问这些计算机组件,并为每种组件类型声明了一个访问操作。
ComputerPartDisplayVisitor 类实现了 ComputerPartVisitor 接口,为每种计算机组件提供了展示详细信息的功能。在客户端代码中,我们创建了一个Computer 对象和一个 ComputerPartDisplayVisitor 对象。当我们调用computer.accept() 方法时,计算机的所有组件都会被访问者访问,并显示相应的详细信息。
这个示例展示了如何使用访问者模式将功能与类结构分离,实现解耦和扩展性。如果我们需要为计算机组件添加新功能,只需创建一个新的访问者类,而无需修改现有的组件类。这使得在不影响现有代码的情况下,为系统添加新功能变得容易。
// 添加一个更新计算机部件的访问者实现
class ComputerPartUpdateVisitorImpl implements ComputerPartVisitor {
// 访问 Computer 对象并执行更新操作
@Override
public void visit(Computer computer) {
System.out.println("Updating Computer.");
}
// 访问 Mouse 对象并执行更新操作
@Override
public void visit(Mouse mouse) {
System.out.println("Updating Mouse.");
}
// 访问 Keyboard 对象并执行更新操作
@Override
public void visit(Keyboard keyboard) {
System.out.println("Updating Keyboard.");
}
}
// 客户端代码,几乎不用任何修改
public class VisitorPatternDemo {
public static void main(String[] args) {
// 创建一个 Computer 对象
ComputerPart computer = new Computer();
// 创建一个具体访问者
ComputerPartVisitor visitor = new ComputerPartUpdateVisitorImpl();
// 让 Computer 对象接受访问者的访问
computer.accept(visitor);
}
}
访问者模式可以算是 23 种经典设计模式中最难理解的几个之一。因为它难理解、难实现,应用它会导致代码的可读性、可维护性变差,所以,访问者模式在实际的软件开发中很少被用到,在没有特别必要的情况下,建议你不要使用访问者模式。
2、使用场景
2.1 抽象语法树
访问者模式在实际项目中的一个常见使用场景是处理抽象语法树(AST)。例如,在编译器或解释器中,我们需要处理不同类型的语法结构,如声明、表达式、循环等。 使用访问者模式,我们可以将处理这些结构的功能与结构类分离,实现解耦和扩展性。
以下是一个简单的示例,展示了如何使用访问者模式处理抽象语法树
// AST 节点基类
abstract class AstNode {
// 接受访问者的方法
abstract void accept(AstVisitor visitor);
}
// 访问者接口
interface AstVisitor {
// 访问表达式节点的方法
void visit(ExpressionNode node);
// 访问数字节点的方法
void visit(NumberNode node);
// 访问加法节点的方法
void visit(AdditionNode node);
// 省略其他节点
}
// 数字节点,表示一个整数值
class NumberNode extends AstNode {
int value;
// 构造方法,接收一个整数作为值
NumberNode(int value) {
this.value = value;
}
// 实现基类的 accept 方法,接受访问者
void accept(AstVisitor visitor) {
visitor.visit(this);
}
}
// 加法节点,表示两个子节点的相加
class AdditionNode extends AstNode {
AstNode left;
AstNode right;
// 构造方法,接收两个子节点
AdditionNode(AstNode left, AstNode right) {
this.left = left;
this.right = right;
}
// 实现基类的 accept 方法,接受访问者
void accept(AstVisitor visitor) {
visitor.visit(this);
}
}
// 表达式节点,包含一个子节点
class ExpressionNode extends AstNode {
AstNode node;
// 构造方法,接收一个子节点
ExpressionNode(AstNode node) {
this.node = node;
}
// 实现基类的 accept 方法,接受访问者
void accept(AstVisitor visitor) {
visitor.visit(this);
}
}
class AstEvaluator implements AstVisitor {
int result;
AstEvaluator() {
result = 0;
}
void visit(ExpressionNode node) {
node.node.accept(this);
}
void visit(NumberNode node) {
result = node.value;
}
void visit(AdditionNode node) {
node.left.accept(this);
int leftValue = result;
node.right.accept(this);
int rightValue = result;
result = leftValue + rightValue;
}
}
最后,我们可以使用这个访问者类计算一个简单的 AST
public class Main {
public static void main(String[] args) {
// 创建一个简单的 AST:(2 + 3)
AstNode ast = new ExpressionNode(
new AdditionNode(
new NumberNode(2),
new NumberNode(3)
)
);
// 创建一个访问者实例
AstEvaluator evaluator = new AstEvaluator();
// 使用访问者计算 AST 的结果
ast.accept(evaluator);
// 输出计算结果
System.out.println("AST 的结果是: " + evaluator.result);
}
}