命令模式是一种行为设计模式,它将请求或操作封装为对象,使你可以参数化客户端对象,将请求排队、记录请求日志,以及支持可撤销的操作。
落实到编码实现,命令模式用到最核心的实现手段,就是将函数封装成对象。我们知道,在大部分编程语言中,函数是没法作为参数传递给其他函数的,也没法赋值给变量。借助命令模式,我们将函数封装成对象,这样就可以实现把函数像对象一样使用。
命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。
1. 核心概念
命令模式包含以下主要角色:
命令接口 (Command):声明执行操作的接口
具体命令 (ConcreteCommand):实现命令接口,绑定接收者与动作
接收者 (Receiver):知道如何执行与请求相关的操作
调用者 (Invoker):要求命令对象执行请求
客户端 (Client):创建具体命令对象并设置其接收者
2. 模式结构
classDiagram
class Command {
<<interface>>
+Execute() void
+Undo() void
}
class ConcreteCommand {
-receiver: Receiver
-state
+Execute() void
+Undo() void
}
class Receiver {
+Action() void
}
class Invoker {
-command: Command
+SetCommand(Command) void
+ExecuteCommand() void
}
class Client {
+CreateCommand() Command
}
Command <|.. ConcreteCommand
ConcreteCommand --> Receiver
Invoker --> Command
Client --> Receiver
Client --> ConcreteCommand
3. 代码示例
3.1 基本实现
// 命令接口
public interface ICommand
{
void Execute();
void Undo();
}
// 接收者 - 知道如何执行操作
public class Light
{
public void TurnOn() => Console.WriteLine("灯已打开");
public void TurnOff() => Console.WriteLine("灯已关闭");
}
// 具体命令
public class LightOnCommand : ICommand
{
private Light _light;
public LightOnCommand(Light light)
{
_light = light;
}
public void Execute() => _light.TurnOn();
public void Undo() => _light.TurnOff();
}
public class LightOffCommand : ICommand
{
private Light _light;
public LightOffCommand(Light light)
{
_light = light;
}
public void Execute() => _light.TurnOff();
public void Undo() => _light.TurnOn();
}
// 调用者
public class RemoteControl
{
private ICommand _command;
public void SetCommand(ICommand command)
{
_command = command;
}
public void PressButton()
{
_command.Execute();
}
public void PressUndo()
{
_command.Undo();
}
}
// 客户端
class Program
{
static void Main()
{
var light = new Light();
var remote = new RemoteControl();
// 设置开灯命令并执行
remote.SetCommand(new LightOnCommand(light));
remote.PressButton(); // 输出: 灯已打开
// 设置关灯命令并执行
remote.SetCommand(new LightOffCommand(light));
remote.PressButton(); // 输出: 灯已关闭
// 撤销上一步操作
remote.PressUndo(); // 输出: 灯已打开
}
}
3.2 更复杂的例子 - 支持多命令和宏命令
// 宏命令 - 一次执行多个命令
public class MacroCommand : ICommand
{
private List<ICommand> _commands = new List<ICommand>();
public void AddCommand(ICommand command)
{
_commands.Add(command);
}
public void Execute()
{
foreach (var cmd in _commands)
{
cmd.Execute();
}
}
public void Undo()
{
// 反向执行撤销
for (int i = _commands.Count - 1; i >= 0; i--)
{
_commands[i].Undo();
}
}
}
// 使用宏命令的客户端
class AdvancedClient
{
static void Main()
{
var light = new Light();
var tv = new TV(); // 假设有TV类
var partyOn = new MacroCommand();
partyOn.AddCommand(new LightOnCommand(light));
partyOn.AddCommand(new TVOnCommand(tv)); // 假设有TVOnCommand
var remote = new RemoteControl();
remote.SetCommand(partyOn);
Console.WriteLine("--- 派对模式开启 ---");
remote.PressButton(); // 同时打开灯和电视
Console.WriteLine("--- 撤销派对模式 ---");
remote.PressUndo(); // 同时关闭灯和电视
}
}
实现一个游戏场景的例子
using System;
using System.Runtime.CompilerServices;
namespace DesignPatternsPractice.Src.Behavioral.CommandDesignPattern
{
public class CommandDesign
{
/// <summary>
/// 命令模式
/// 命令模式将请求(命令)封装为一个对象,
/// 这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),
/// 并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
///
/// 实现游戏中的控制操作
/// </summary>
static void Main()
{
// 初始化游戏对象
Player Galen = new Player("盖伦", new Vector2(20, 30), 100);
CommandManager commandManager = new CommandManager();
InputHandler inputHandler_Galen = new InputHandler(Galen);
Console.WriteLine("游戏开始!");
Console.WriteLine("控制方式: WASD移动, J(火球术), K(治疗术), U(撤销), R(重做)");
Console.WriteLine("----------------------------------------");
while (true)
{
ICommand command = inputHandler_Galen.HandleInput();
if (command != null)
{
// 命令模式的体现:这里直接调用的命令去执行,解耦了具体实现和调用者
// 直接调用就是player.move(Direction.Up)
commandManager.ExecuteCommand(command);
}
// 特殊按键处理
if (Console.KeyAvailable)
{
var key = Console.ReadKey(true).Key;
switch (key)
{
case ConsoleKey.U:
commandManager.Undo();
break;
case ConsoleKey.R:
commandManager.Redo();
break;
case ConsoleKey.Escape:
return;
}
}
// 模拟游戏帧率
Thread.Sleep(100);
}
}
}
//命令接口
public interface ICommand
{
void Execute();
void Undo();
}
//移动命令
public class MoveCommand : ICommand
{
private Player _player;
private Direction _direction;
private Vector2 _previousPosition; // 以前的位置
public MoveCommand(Player _player, Direction _direction)
{
this._player = _player;
this._direction = _direction;
}
public void Execute()
{
_previousPosition = _player.Position;
_player.Move(_direction);
Console.WriteLine($"{_player.Name} 移动到了 {_player.Position}");
}
public void Undo()
{
_player.Position = _previousPosition;
Console.WriteLine($"{_player.Name} 撤销移动,回到了 {_player.Position}");
}
}
public class SkillCommand : ICommand
{
private Player _player;
private string _skillName; // 技能名称
private int _manaCost; // 法术消耗
public void Execute()
{
if (_player.Mana >= _manaCost)
{
_player.UseSkill(_skillName);
_player.Mana -= _manaCost;
Console.WriteLine($"{_player.Name} 使用了技能 {_skillName},消耗 {_manaCost} 点魔法值");
}
else
{
Console.WriteLine($"{_player.Name} 魔法值不足,无法使用 {_skillName}");
}
}
public void Undo()
{
Console.WriteLine("技能不可被撤销");
}
public SkillCommand(Player player, string skillName, int manaCost)
{
_player = player;
_skillName = skillName;
_manaCost = manaCost;
}
}
/// <summary>
/// 方向
/// </summary>
public enum Direction { Up, Down, Left, Right }
//游戏实体类
public class Player
{
public string Name { get; }
public Vector2 Position { get; set; }
public int Mana { get; set; } // 玩家法术
public Player(string name, Vector2 initialPosition, int initialMana)
{
Name = name;
Position = initialPosition;
Mana = initialMana;
}
public void Move(Direction direction)
{
switch (direction)
{
case Direction.Up:
Position = new Vector2(Position.X, Position.Y + 1);
break;
case Direction.Down:
Position = new Vector2(Position.X, Position.Y - 1);
break;
case Direction.Left:
Position = new Vector2(Position.X - 1, Position.Y);
break;
case Direction.Right:
Position = new Vector2(Position.X + 1, Position.Y);
break;
}
}
public void UseSkill(string skillName)
{
}
}
public struct Vector2
{
public float X { get; set; }
public float Y { get; set; }
public Vector2(float x, float y)
{
X = x;
Y = y;
}
public override string ToString() => $"({X}, {Y})";
}
/// <summary>
/// 命令管理器
/// </summary>
public class CommandManager
{
private readonly Stack<ICommand> _commandHistory = new Stack<ICommand>();
private readonly Stack<ICommand> _redoStack = new Stack<ICommand>();
/// <summary>
/// 命令执行
/// </summary>
/// <param name="command"></param>
public void ExecuteCommand(ICommand command)
{
command.Execute();
_commandHistory.Push(command);
_redoStack.Clear();// 执行新命令清空重做栈
}
/// <summary>
/// 撤销
/// </summary>
public void Undo()
{
if (_commandHistory.Count > 0)
{
var command = _commandHistory.Pop();
command.Undo();
_redoStack.Push(command);
}
}
/// <summary>
/// 重做
/// </summary>
public void Redo()
{
if (_redoStack.Count > 0)
{
var commend = _redoStack.Pop();
commend.Execute();
_commandHistory.Push(commend);
}
}
}
/// <summary>
/// 输入处理器
/// </summary>
public class InputHandler
{
private readonly Player _player;
private readonly Dictionary<ConsoleKey, ICommand> _keyBindings = new Dictionary<ConsoleKey, ICommand>();
public InputHandler(Player player)
{
_player = player;
// 设置按键绑定
_keyBindings[ConsoleKey.W] = new MoveCommand(player, Direction.Up);
_keyBindings[ConsoleKey.S] = new MoveCommand(player, Direction.Down);
_keyBindings[ConsoleKey.A] = new MoveCommand(player, Direction.Left);
_keyBindings[ConsoleKey.D] = new MoveCommand(player, Direction.Right);
_keyBindings[ConsoleKey.J] = new SkillCommand(player, "火球术", 10);
_keyBindings[ConsoleKey.K] = new SkillCommand(player, "治疗术", 15);
}
public ICommand HandleInput()
{
if (Console.KeyAvailable)
{
var key = Console.ReadKey(true).Key;
if (_keyBindings.TryGetValue(key, out var command))
{
return command;
}
}
return null;
}
}
}
4. 命令模式的优点
解耦调用者与接收者:调用者不需要知道接收者的具体实现
可扩展性强:容易添加新命令,不影响现有代码
支持撤销/重做:可以轻松实现命令的撤销和重做功能
支持事务:可以将多个命令组合成一个复合命令
支持日志和排队:可以记录命令历史或将命令放入队列延迟执行
5. 适用场景
需要将操作参数化时
需要支持撤销/重做功能时
需要将操作放入队列中,在不同时间执行时
需要记录操作历史时
需要实现事务系统时
6. 实际应用案例
GUI按钮和菜单项:每个按钮点击对应一个命令对象
事务系统:数据库操作可以封装为命令
宏录制:将用户操作记录为命令序列
多级撤销:文本编辑器中的撤销功能
任务调度:将任务封装为命令放入队列
命令模式通过将请求封装为对象,提供了极大的灵活性和扩展性,是许多复杂系统的基础设计模式之一。