命令模式是一种行为设计模式,它将请求或操作封装为对象,使你可以参数化客户端对象,将请求排队、记录请求日志,以及支持可撤销的操作。

落实到编码实现,命令模式用到最核心的实现手段,就是将函数封装成对象。我们知道,在大部分编程语言中,函数是没法作为参数传递给其他函数的,也没法赋值给变量。借助命令模式,我们将函数封装成对象,这样就可以实现把函数像对象一样使用。

命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。

1. 核心概念

命令模式包含以下主要角色:

  1. 命令接口 (Command)​​:声明执行操作的接口

  2. 具体命令 (ConcreteCommand)​​:实现命令接口,绑定接收者与动作

  3. 接收者 (Receiver)​​:知道如何执行与请求相关的操作

  4. 调用者 (Invoker)​​:要求命令对象执行请求

  5. 客户端 (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. 命令模式的优点

  1. 解耦调用者与接收者​:调用者不需要知道接收者的具体实现

  2. 可扩展性强​:容易添加新命令,不影响现有代码

  3. 支持撤销/重做​:可以轻松实现命令的撤销和重做功能

  4. 支持事务​:可以将多个命令组合成一个复合命令

  5. 支持日志和排队​:可以记录命令历史或将命令放入队列延迟执行

5. 适用场景

  1. 需要将操作参数化时

  2. 需要支持撤销/重做功能时

  3. 需要将操作放入队列中,在不同时间执行时

  4. 需要记录操作历史时

  5. 需要实现事务系统时

6. 实际应用案例

  1. GUI按钮和菜单项​:每个按钮点击对应一个命令对象

  2. 事务系统​:数据库操作可以封装为命令

  3. 宏录制​:将用户操作记录为命令序列

  4. 多级撤销​:文本编辑器中的撤销功能

  5. 任务调度​:将任务封装为命令放入队列

命令模式通过将请求封装为对象,提供了极大的灵活性和扩展性,是许多复杂系统的基础设计模式之一。