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

来源:互联网 发布:ubuntu 10.04 163 源 编辑:程序博客网 时间:2024/05/19 02:00

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

声明:本系列为刘伟老师博客内容总结(http://blog.csdn.net/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