参考:《HeadFirst设计模式》
1.关于命令模式
命令模式是一种行为型
模式。
命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。
本文以游戏快捷键
为场景来学习命令模式
:
- 在MMORPG游戏中,游戏操作有:角色前进、角色跳跃、释放技能:火球术、释放技能:潜行术、打开背包界面、打开技能界面等等。
- 需求一:通过按键进行快捷操作。例如:按下
空格
键,则角色跳跃;按下W
键,则角色前进。 - 需求二:可以替换快捷键。例如:默认
角色跳跃
的快捷键是空格
,用户可以自定义为回车
键。 - 需求三:可以自定义宏命令,一个宏命令可以进行多个操作。例如:按下按键
Q
,则依次进行:角色前进、释放技能:潜行术、释放技能:火球术。
2.按键
无论如何实现三个需求,我们先来实现按键本身。
游戏按键:枚举:KeyEnum
/**
* <p>按键(部分)</P>
*
* @author hanchao
*/
public enum KeyEnum {
/**
* 空格键
*/
KEY_SPACE,
/**
* 按键:回车
*/
KEY_ENTER,
/**
* 按键:A
*/
KEY_A,
/**
* 按键:B
*/
KEY_B,
/**
* 按键:V
*/
KEY_V,
/**
* 按键:W
*/
KEY_W,
/**
* 按键:1
*/
KEY_1,
/**
* 按键:2
*/
KEY_2,
/**
* 按键:无
*/
KEY_NULL
}
2.游戏操作
无论如何实现三个需求,我们先来实现游戏操作本身。
游戏操作:角色相关:Role
/**
* <p>游戏角色</P>
*
* @author hanchao
*/
@Slf4j
public class Role {
/**
* 前进
*/
public static void forward() {
log.info("角色前进");
}
/**
* 跳跃
*/
public static void jump() {
log.info("角色跳跃");
}
}
游戏操作:技能相关:SkillHandler
/**
* <p>技能管理器</P>
*
* @author hanchao
*/
@Slf4j
public class SkillHandler {
/**
* 释放技能-五火球神术
*/
public static void fireBall() {
log.info("释放技能:火球术...酝酿1秒钟...砰! 砰!! 砰!!!");
}
/**
* 释放技能-潜行
*/
public static void sneak() {
log.info("释放技能:潜行术...蒙面...抛出烟雾弹...不见了!");
}
}
游戏操作:界面相关:UIHandler
/**
* <p>游戏界面</P>
*
* @author hanchao
*/
@Slf4j
public class UiHandler {
/**
* 打开背包界面
*/
public static void showPack() {
log.info("打开背包界面,背包中现在有:2个面包,3棵草药,5块矿石...");
}
/**
* 打开技能界面
*/
public static void showSkill() {
log.info("打开技能界面,已掌握的技能有:暗影之舞,剑刃风暴,神圣之光...");
}
}
3.实现方式:if-else/switch
实现思路:
- 遍历所有按键,然后进行相应的处理。这种实现方式,能够满足第一条需求。
- 对于第二、三条需求,实现起来比较麻烦。
4.实现方式:命令模式
实现命令模式的关键在于:将命令/请求封装为一个对象
。
在我们的例子中,控制角色向前
、释放技能:火球术
、打开背包界面
等等,都是一种命令/请求
。
这些命令可以抽象为一个抽象类:命令Command
。
4.1.命令抽象:Command
/**
* <p>命令:执行、撤销</P>
*
* @author hanchao
*/
public interface Command {
/**
* 执行命令
*/
void execute();
}
4.2.普通命令实现
命令实现:角色跳跃:RoleJumpCommand
角色前进:RoleForwardCommand实现方式类似。
/**
* <p>命令:跳跃</P>
*
* @author hanchao
*/
public class RoleJumpCommand implements Command {
/**
* 执行命令
*/
@Override
public void execute() {
Role.jump();
}
}
命令实现:释放技能:火球术:ReleaseFireBallCommand
释放技能:潜行术:ReleaseSneakCommand实现方式类似。
/**
* <p>命令:释放技能:火球术</P>
*
* @author hanchao
*/
public class ReleaseFireBallCommand implements Command {
/**
* 执行命令
*/
@Override
public void execute() {
SkillHandler.fireBall();
}
}
命令实现:打开背包界面:ShowPackCommand
打开技能界面:ShowSkillCommand实现方式类似。
/**
* <p>命令:打开背包界面</P>
*
* @author hanchao
*/
public class ShowPackCommand implements Command {
/**
* 执行命令
*/
@Override
public void execute() {
UiHandler.showPack();
}
}
4.3.默认命令实现
命令实现:默认命令:DefaultCommand
可能存在这种情况:某个按键并未关联任何命令,如果按下此键,应该不会产生任何效果。
为了统一处理上述情况,定义一种默认命令,这样就不必专门去进行非空判断。
/**
* <p>默认命令</P>
*
* @author hanchao
*/
@Slf4j
public class DefaultCommand implements Command {
/**
* 执行命令
*/
@Override
public void execute() {
log.info("什么也没有发生");
}
}
4.4.宏命令实现
命令实现:宏命令:MacroCommand
所谓宏命令,就是一次按键,产生多个游戏操作。其实,宏命令本身也是一种命令。
为了实现多个游戏操作
的需求,我们可以通过集合
来实现。
/**
* <p>宏命令</P>
* <p>
* 需求三:可以自定义宏命令,一个宏命令可以进行多个操作。
*
* @author hanchao
*/
@AllArgsConstructor
public class MacroCommand implements Command {
/**
* 宏命令列表
*/
private List<Command> commandList;
/**
* 执行命令
*/
@Override
public void execute() {
//依次执行命令
for (Command command : commandList) {
command.execute();
}
}
}
4.5.三个需求的实现
下面编写客户代码。
首先,定义按键管理器``KeyManager`,主要关注点:按键初始化:设置默认按键。
/**
* <p>调用者:按键管理器</P>
*
* @author hanchao
*/
@Slf4j
public class KeyManager {
/**
* 假设共计32种游戏操作
*/
private static final int MAX_SIZE = 32;
/**
* 快捷键列表
*/
private static Map<KeyEnum, Command> commandMap = new HashMap<>(MAX_SIZE);
static {
//按键初始化:设置默认按键
commandMap.put(KeyEnum.KEY_W, new RoleForwardCommand());
commandMap.put(KeyEnum.KEY_SPACE, new RoleJumpCommand());
commandMap.put(KeyEnum.KEY_B, new ShowPackCommand());
commandMap.put(KeyEnum.KEY_V, new ShowSkillCommand());
commandMap.put(KeyEnum.KEY_1, new ReleaseFireBallCommand());
commandMap.put(KeyEnum.KEY_2, new ReleaseSneakCommand());
commandMap.put(KeyEnum.KEY_NULL, new DefaultCommand());
}
}
4.5.1.需求一的实现
需求一:通过按键进行快捷操作。例如:按下空格
键,则角色跳跃;按下W
键,则角色前进。
在按键管理器``KeyManager中定义按键
press()`方法,实现需求一。
/**
* 需求一:通过按键进行快捷操作。
*/
public static void press(KeyEnum key) {
//如果旧的按键为空,则置为空按键
if (Objects.isNull(key)) {
key = KeyEnum.KEY_NULL;
}
log.info("按下了「{}」", key.name());
//执行此按键
Command command = commandMap.get(key);
if (Objects.isNull(command)){
command = new DefaultCommand();
}
command.execute();
log.info("----------");
}
4.5.2.需求二的实现
需求二:可以替换快捷键。例如:默认角色跳跃
的快捷键是空格
,用户可以自定义为回车
键。
在按键管理器``KeyManager中定义设置自定义按键
setCustomKey()`方法,实现需求二。
/**
* 需求二:可以替换快捷键。
*/
public static void setCustomKey(Command command, KeyEnum oldKey, KeyEnum newKey) {
//如果旧的按键为空,则置为空按键
if (Objects.isNull(oldKey)) {
oldKey = KeyEnum.KEY_NULL;
}
//如果新的按键为空,则置为空按键
if (Objects.isNull(newKey)) {
newKey = KeyEnum.KEY_NULL;
}
log.info("将「{}」操作的快捷键进行替换:{} --> {}", command.getClass().getSimpleName(), oldKey.name(), newKey.name());
//将旧按键绑定到默认操作
if (!Objects.equals(oldKey, KeyEnum.KEY_NULL)) {
commandMap.put(oldKey, new DefaultCommand());
}
//将新按键绑定到当前操作
commandMap.put(newKey, command);
}
4.5.3.需求三的实现
需求三:可以自定义宏命令,一个宏命令可以进行多个操作。例如:按下按键Q
,则依次进行:角色前进、释放技能:潜行术、释放技能:火球术。
其实之前已经实现了,在章节4.4.中,实现了宏命令,宏命令也是命令。
4.6.运行效果
测试代码:GameClientDemo
/**
* <p>客户端</P>
*
* @author hanchao
*/
public class GameClientDemo {
public static void main(String[] args) {
//默认按键
KeyManager.press(KeyEnum.KEY_W);
KeyManager.press(KeyEnum.KEY_SPACE);
System.out.println("==============================================================================================================");
//将角色跳跃的快捷键进行替换
KeyManager.setCustomKey(new RoleJumpCommand(), KeyEnum.KEY_SPACE, KeyEnum.KEY_ENTER);
KeyManager.press(KeyEnum.KEY_SPACE);
KeyManager.press(KeyEnum.KEY_ENTER);
System.out.println("==============================================================================================================");
//自定义宏命令:角色前进、释放技能:潜行术、释放技能:火球术,并将其绑定在按键A
KeyManager.press(KeyEnum.KEY_A);
List<Command> commandList = Lists.newArrayList(
new RoleForwardCommand(), new ReleaseSneakCommand(), new ReleaseFireBallCommand()
);
KeyManager.setCustomKey(new MacroCommand(commandList), null, KeyEnum.KEY_A);
KeyManager.press(KeyEnum.KEY_A);
}
}
测试结果
2019-07-28 11:47:50,919 INFO - 按下了「KEY_W」
2019-07-28 11:47:50,923 INFO - 角色前进
2019-07-28 11:47:50,923 INFO - ----------
2019-07-28 11:47:50,923 INFO - 按下了「KEY_SPACE」
2019-07-28 11:47:50,923 INFO - 角色跳跃
2019-07-28 11:47:50,923 INFO - ----------
==============================================================================================================
2019-07-28 11:47:50,923 INFO - 将「RoleJumpCommand」操作的快捷键进行替换:KEY_SPACE --> KEY_ENTER
2019-07-28 11:47:50,923 INFO - 按下了「KEY_SPACE」
2019-07-28 11:47:50,923 INFO - 什么也没有发生
2019-07-28 11:47:50,923 INFO - ----------
2019-07-28 11:47:50,923 INFO - 按下了「KEY_ENTER」
2019-07-28 11:47:50,923 INFO - 角色跳跃
2019-07-28 11:47:50,923 INFO - ----------
==============================================================================================================
2019-07-28 11:47:50,923 INFO - 按下了「KEY_A」
2019-07-28 11:47:50,923 INFO - 什么也没有发生
2019-07-28 11:47:50,924 INFO - ----------
2019-07-28 11:47:50,941 INFO - 将「MacroCommand」操作的快捷键进行替换:KEY_NULL --> KEY_A
2019-07-28 11:47:50,941 INFO - 按下了「KEY_A」
2019-07-28 11:47:50,942 INFO - 角色前进
2019-07-28 11:47:50,943 INFO - 释放技能:潜行术...蒙面...抛出烟雾弹...不见了!
2019-07-28 11:47:50,943 INFO - 释放技能:火球术...酝酿1秒钟...砰! 砰!! 砰!!!
2019-07-28 11:47:50,943 INFO - ----------
5.关于命令操作的取消
在命令模式的定义中,有这么一句话:对请求排队或记录请求日志,以及支持可取消的操作。
当然在游戏快捷键
这个例子中,状态取消是不合适的,总不能让已经释放的火球术再返回来吧。
但是在有些场景是有必要存在的,比方说word文档的回撤
操作等。
那么,如何实现取消操作
呢?
- 在抽象命令类中定义
取消cancel()操作
。 - 在所有命令实现类中实现
取消cancel()操作
。 - 在客户端类中,定义
已执行命令栈Stack
,当一个命令被执行时,将其入栈;当一个命令被取消时,将其出栈。
6.实际应用
- 熔断器HystrixCommand
- CQRS
7.总结
最后以UML类图来总结本文的游戏快捷键
场景以及命令模式
。