设计模式的艺术之道--命令模式

本文介绍了设计模式中的命令模式,旨在通过将请求封装为对象来实现请求发送者与接收者的解耦。内容涵盖了命令模式的定义、情景实例、模式分析,以及如何通过命令模式降低系统耦合度、实现请求的排队和撤销操作。文章还讨论了命令模式在系统设计中的适用场景和优缺点。

设计模式的艺术之道–命令模式

声明:本系列为刘伟老师博客内容总结(http://blog.youkuaiyun.com/lovelion),博客中有完整的设计模式的相关博文,以及作者的出版书籍推荐

本系列内容思路分析借鉴了刘伟老师的博文内容,同时改用C#代码进行代码的演示和分析(Java资料过多 C#表示默哀).

本系列全部源码均在文末地址给出。


本系列开始讲解行为型模式,关注如何将现有类或对象组织在一起形成更加强大的结构。

  • 行为型模式(Behavioral Pattern)
    关注系统中对象之间的交互,研究系统在运行时对象之间的相互通信与协作,进一步明确对象的职责
    不仅仅关注类和对象本身,还重点关注它们之间的相互作用和职责划分
  • 类行为型模式
    使用继承关系在几个类之间分配行为,主要通过多态等方式来分配父类与子类的职责
  • 对象行为型模式
    使用对象的关联关系来分配行为,主要通过对象关联等方式来分配两个或多个类的职责

11种常见的行为型模式
这里写图片描述


命令模式–请求发送者与接收者解耦

现实生活中,相同的开关可以通过不同的电线来控制不同的电器。
开关–请求发送者。
电灯–请求的最终接收者和处理者。
开关和电灯之间并不存在直接耦合关系,它们通过电线连接在一起,使用不同的电线可以连接不同的请求接收者。

1.1定义

-命令模式 (Command Pattern):将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。
- 用不同的请求对客户进行参数化。
- 对请求排队,记录请求日志,支持可撤销操作。

1.2情景实例

问题描述
- 自定义功能键
为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,例如功能键FunctionButton可以用于退出系统(由SystemExitClass类来实现),也可以用于显示帮助文档(由DisplayHelpClass类来实现)
初步思路
功能键类FunctionButton充当请求的发送者,帮助文档处理类HelpHandler充当请求的接收者,在发送者FunctionButton的onClick()方法中将调用接收者HelpHandler的display()方法。

//FunctionButton:功能键类,请求发送者  
class FunctionButton {  
    private HelpHandler help; //HelpHandler:帮助文档处理类,请求接收者 
    //在FunctionButton的onClick()方法中调用HelpHandler的display()方法  
public void onClick() {  
        help = new HelpHandler();  
        help.display(); //显示帮助文档  
    }  
}  

现存缺点(未来变化)
(1)请求发送者和请求接收者之间存在方法的直接调用,耦合度很高,更换请求接收者必须修改发送者的源代码,违反开闭原则
(2)FunctionButton类在设计和实现时功能已被固定,如果增加一个新的请求接收者,如果不修改原有的FunctionButton类,则必须增加一个新的与FunctionButton功能类似的类,这将导致系统中类的个数急剧增加。
(3)用户无法按照自己的需要来设置某个功能键的功能,一个功能键类的功能一旦固定,在不修改源代码的情况下无法更换其功能,系统缺乏灵活性。

如何改进
为了降低功能键与功能处理类之间的耦合度,让用户可以自定义每一个功能键的功能,开发人员使用命令模式来设计“自定义功能键”模块。
命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法。

UML类图
这里写图片描述
关键实例源代码

namespace CommandSample
{
    class FunctionButton
    {
        private Command command;

        public Command Command
        {
            get { return command; }
            set { command = value; }
        }

        public void Click()
        {
            Console.WriteLine("单击功能键!");
            command.Execute();
        }
    }
    abstract class Command
    {
        public abstract void Execute();
    }
    //类似的帮助命令省略 
    class ExitCommand : Command
    {
        private SystemExitClass seObj;

        public ExitCommand()
        {
            seObj = new SystemExitClass();
        }

        public override void Execute()
        {
            seObj.Exit();
        }
    }
    class SystemExitClass
    {
        public void Exit()
        {
            Console.WriteLine("退出系统!");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            FunctionButton fb = new FunctionButton();

            Command command;
            //读取配置文件
            string commandStr = ConfigurationManager.AppSettings["command"];
            //反射生成对象
            command = (Command)Assembly.Load("CommandSample").CreateInstance(commandStr);

            //设置命令对象
            fb.Command = command;
            fb.Click();

            Console.Read();
        }
    }
}

1.3模式分析

动机和意图

  • 如何将请求发送者和接收者完全解耦?
  • 发送者与接收者之间怎么样解除直接引用关系?
  • 发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求?

一般结构

  • 命令模式包含4个角色:
  • Command(抽象命令类):对具体命令的抽象提升,命令中包含公有的执行方法接口。
  • ConcreteCommand(具体命令类)::实现父类的抽象执行方法,并且在内部调用接收者对象的某些方法。
  • Invoker(调用者):客户端或者是需要调用命令发送的按钮或者UI之类。
  • Receiver(接收者):具体的接收者对象,与命令关联,命令类会调用接收者的具体方法。

命令模式的本质是对请求进行封装。
一个请求对应于一个命令,将发出命令的责任和执行命令的责任分开。
命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。

命令模式UML类图
这里写图片描述

改进后的优点

  • 降低了系统的耦合度
  • 新的命令可以很容易地加入到系统中,符合开闭原则
  • 增加一个新的具体请求处理者时无须修改原有系统的代码,只需要在客户端重新建链即可

现存的缺点

  • 使用命令模式可能会导致某些系统有过多的具体命令类

    命令队列实现
    有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。
    命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类来负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者。
    改造上述实例,增加一个命令队列。完整代码见源码下载。

namespace Command_Queue
{
    class CommandQueue
    {
        //定义一个List来存储命令队列
        private List<Command> commands = new List<Command>();

        public void AddCommand(Command command)
        {
            commands.Add(command);
        }

        public void RemoveCommand(Command command)
        {
            commands.Remove(command);
        }

        //循环调用每一个命令对象的Execute()方法
        public void Execute() 
        {
            foreach (object command in commands) 
            {
                ((Command)command).Execute();
            }
        }
    }
     class Invoker
    {
        private CommandQueue commandQueue; //维持一个CommandQueue对象的引用

        //构造注入
        public Invoker(CommandQueue commandQueue)
        {
            this.commandQueue = commandQueue;
        }

        //设值注入
        public void SetCommandQueue(CommandQueue commandQueue)
        {
            this.commandQueue = commandQueue;
        }

        //调用CommandQueue类的Execute()方法
        public void Call()
        {
            Console.WriteLine("开始执行命令");
            commandQueue.Execute();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            CommandQueue cmdqueue = new CommandQueue();

            Command command1 = new HelpCommand();
            Command command2 = new ExitCommand();
            cmdqueue.AddCommand(command1);
            cmdqueue.AddCommand(command2);

            Invoker invoker = new Invoker(cmdqueue);
            invoker.Call();
            Console.Read();
        }
    }
}

撤销操作的实现
在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。除了通过一个逆向操作来实现撤销(Undo)外,还可以通过保存对象的历史状态来实现撤销,后者可使用备忘录模式(Memento Pattern)来实现。(后续讲到)。
通过一个简单案例来理解撤销操作。

菜鸟软件公司欲开发一个简易计算器,该计算器可以实现简单的数学运算,还可以对运算实施撤销操作。

公司开发人员使用命令模式设计了结构图,其中计算器界面类CalculatorForm充当请求发送者,实现了数据求和功能的加法类Adder充当请求接收者,界面类可间接调用加法类中的add()方法实现加法运算,并且提供了可撤销加法运算的undo()方法。
这里写图片描述
// 只是举例说明 不具有通用性 撤销逆操作的实现 应该具体分析

  class CalculatorForm
    {
        private AbstractCommand command;

        public AbstractCommand Command
        {
            get { return command; }
            set { command = value; }
        }

        public void Compute(int value)
        {
            int i = Command.Execute(value);
            Console.WriteLine("执行运算,运算结果为:" + i);
        }

        public void Undo()
        {
            int i = Command.Undo();
            Console.WriteLine("执行撤销,运算结果为:" + i);
        }
    }
    abstract class AbstractCommand
    {
        public abstract int Execute(int value);
        public abstract int Undo();
    }
        class AddCommand : AbstractCommand
    {
        private Adder adder = new Adder();
        private int value;

        public override int Execute(int value)
        {
            this.value = value;
            return adder.Add(value);
        }

        public override int Undo()
        {
            return adder.Add(-value);
        }
    }
        class Adder
    {
        private int num = 0;

        public int Add(int value)
        {
            num += value;
            return num;
        }
    }
       public class Program
    {
        static void Main(string[] args)
        {
            CalculatorForm form = new CalculatorForm();
            AbstractCommand command;
            command = new AddCommand();
            form.Command = command;

            form.Compute(10);
            form.Compute(5);
            form.Compute(10);
            form.Undo();

            Console.Read();
        }
    }

思考:如果连续调用“form.undo()”两次,预测客户端代码的输出结果。
适用场景
(1) 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
(2) 系统需要在不同的时间指定请求、将请求排队和执行请求(命令队列)
(3)系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作(命令队列)
(4)系统需要将一组操作组合在一起形成宏命令(命令队列)

实例源代码
GitHub地址
百度云地址:链接: https://pan.baidu.com/s/1gf1YW2B 密码: aewy

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值