15、C#设计模式 - 命令模式

命令模式(Command Pattern)

命令模式属于行为型模式,它尝试将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。

在该设计模式中,请求以命令的形式包裹在对象中并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象请求执行。

角色:

1、 抽象命令(Command);

定义命令的接口,声明命令执行的方法;

2、 具体命令(ConcreteCommand);

命令接口实现对象,需要维持对接收者的引用,并调用接收者的功能来完成命令要执行的操作;

3、 接收者(Receiver);

真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能;

4、 调用者(Invoker);

要求命令对象执行请求,需要维持命令对象的引用,可以持有很多的命令对象。

示例:

 

命名空间CommandPattern中包含Command基类、发票开具命令类CreateCommand、发票作废命令类CancelCommand、发票打印命令类PrintCommand、命令参数基类CommandArgs、发票开具命令参数类CommandArgs、发票作废命令参数类CancelArgs、发票打印命令参数类PrintArgs、接收者类Receiver和调用者类Invoker。本命尝试通过客户端调用不同的参数化发票命令来使调用者调用不同的功能。

namespace CommandPattern
public abstract class Command {

    protected Receiver _receiver = null;

    protected CommandArgs _commandArgs = null;

    public Command(Receiver receiver, CommandArgs commandArgs) {
        this._receiver = receiver;
        this._commandArgs = commandArgs;
    }

    public abstract void Action();

}

抽象命令基类,包含Action动作执行命令,并且维持对接受者和命令参数的引用。

public class CreateCommand : Command {

    public CreateCommand(Receiver receiver, CommandArgs commandArgs)
        : base(receiver, commandArgs) {

    }

    public override void Action() {
        _receiver.CommandArgs = _commandArgs;
        (_receiver as CreateReceiver)?.CreateInvoice();
    }

}

这是发票开具命令,由于基类维持了对调用者的引用,所以在Action方法中通过调用CreateInvoice方法来开具一张发票。

public class CancelCommand : Command {

    public CancelCommand(Receiver receiver, CommandArgs commandArgs)
        : base(receiver, commandArgs) {

    }

    public override void Action() {
        _receiver.CommandArgs = _commandArgs;
        (_receiver as CancelReceiver)?.CancelInvoice();
    }

}

这是发票作废命令,由于基类维持了对调用者的引用,所以在Action方法中通过调用CancelInvoice方法来作废一张发票。

public class PrintCommand : Command {

    public PrintCommand(Receiver receiver, CommandArgs commandArgs)
        : base(receiver, commandArgs) {

    }

    public override void Action() {
        _receiver.CommandArgs = _commandArgs;
        (_receiver as PrintReceiver)?.PrintInvoice();
    }

}

这是发票打印命令,由于基类维持了对调用者的引用,所以在Action方法中通过调用PrintInvoice方法来打印一张发票。

public class CommandArgs {

    public string InvoiceType { get; set; }

}
public class CreateArgs : CommandArgs {

    public DateTime BillingDate { get; set; }

}
public class CancelArgs : CommandArgs {

    public string InvoiceCode { get; set; }
    public int InvoiceNumber { get; set; }
    public string CancelReason { get; set; }
    public string CancelMan { get; set; }
    public DateTime CancelDate { get; set; }

}
public class PrintArgs : CommandArgs {

    public string InvoiceCode { get; set; }
    public int InvoiceNumber { get; set; }

}

参数化的命令参数基类CommandArgs和它的3个具体实现类。实际开发过程中可以将参数化命令信息封装在具体命令类中,本例为了更好的扩展性,将参数化命令信息抽象出来。

public class Invoker {

    private Command _command = null;

    public Invoker(Command command) {
        this._command = command;
    }

    public void Execute() {
        _command.Action();
    }

}

调用者类Invoker,实际开发中这个应为具体的调用类。例如我们需要从MQ获取实时数据,并根据从MQ获取到的JSON数据来处理不同的命令,那么这个调用者类应该为MQ所在的管理类(假如名为ActiveMQManager)。这时我们需要在ActiveMQManager类中维持对命令基类的引用,并在收到不同的JSON数据时解析出相应命令和命令参数信息,然后执行命令中的Action方法。

public abstract class Receiver {

    public CommandArgs CommandArgs { get; set; }

    protected const string LINE_BREAK =
        "-------------------------" +
        "-------------------------";
    //文章排版需要,故折成2行

}
public class CreateReceiver : Receiver {

    public void CreateInvoice() {
        var args = CommandArgs as CreateArgs;
        if (args == null) throw new InvalidOperationException();
        Console.WriteLine("Create Invoice!");
        Console.WriteLine(
            $"InvoiceType is {args.InvoiceType},{Environment.NewLine}" +
            $"BillingDate is {args.BillingDate.ToString("yyyy-MM-dd HH:mm:ss")}!");
        Console.WriteLine(LINE_BREAK);
    }

}
public class CancelReceiver : Receiver {

    public void CancelInvoice() {
        var args = CommandArgs as CancelArgs;
        if (args == null) throw new InvalidOperationException();
        Console.WriteLine("Cancel Invoice!");
        Console.WriteLine(
            $"InvoiceCode is {args.InvoiceCode},{Environment.NewLine}" +
            $"InvoiceNumber is {args.InvoiceNumber},{Environment.NewLine}" +
            $"InvoiceType is {args.InvoiceType},{Environment.NewLine}" +
            $"CancelReason is {args.CancelReason},{Environment.NewLine}" +
            $"CancelMan is {args.CancelMan},{Environment.NewLine}" +
            $"CancelDate is {args.CancelDate.ToString("yyyy-MM-dd HH:mm:ss")}!");
        Console.WriteLine(LINE_BREAK);
    }

}
public class PrintReceiver : Receiver {

    public void PrintInvoice() {
        var args = CommandArgs as PrintArgs;
        if (args == null) throw new InvalidOperationException();
        Console.WriteLine("Print Invoice!");
        Console.WriteLine(
            $"InvoiceCode is {args.InvoiceCode},{Environment.NewLine}" +
            $"InvoiceNumber is {args.InvoiceNumber},{Environment.NewLine}" +
            $"InvoiceType is {args.InvoiceType}!");
        Console.WriteLine(LINE_BREAK);
    }

}

接收者基类Receiver和它的3个具体接收者类,需要维持对命令参数基类的引用,以便我们可以获取相应信息。接收者基类并不是命令模式必须的,但考虑到里氏替换原则和开闭原则,我们引入接收者基类并在不同的实现类里解耦不同的命令操作。

public class Program {

    private static Receiver _receiver = null;

    public static void Main(string[] args) {
        _receiver = new CreateReceiver();
        Command command = new CreateCommand(
            _receiver, new CreateArgs {
                InvoiceType = "004",
                BillingDate = DateTime.UtcNow
            });

        var invoker = new Invoker(command);
        invoker.Execute();

        _receiver = new CancelReceiver();
        command = new CancelCommand(
            _receiver, new CancelArgs {
                InvoiceCode = "310987289304",
                InvoiceNumber = 34156934,
                InvoiceType = "007",
                CancelReason = "Invoice missing!",
                CancelMan = "Iori",
                CancelDate = DateTime.UtcNow
            });

        invoker = new Invoker(command);
        invoker.Execute();

        _receiver = new PrintReceiver();
        command = new PrintCommand(
            _receiver, new PrintArgs {
                InvoiceCode = "310987289304",
                InvoiceNumber = 34156934,
                InvoiceType = "026"
            });

        invoker = new Invoker(command);
        invoker.Execute();

        Console.ReadKey();
    }

}

以上是为了测试本案例所编写的代码,通过不同的命令并提供额外的参数化命令信息来执行不同的功能。以下是这个案例的输出结果:

Create Invoice!
InvoiceType is 004,
BillingDate is 2018-07-19 05:34:45!
--------------------------------------------------
Cancel Invoice!
InvoiceCode is 310987289304,
InvoiceNumber is 34156934,
InvoiceType is 007,
CancelReason is Invoice missing!,
CancelMan is Iori,
CancelDate is 2018-07-19 05:34:45!
--------------------------------------------------
Print Invoice!
InvoiceCode is 310987289304,
InvoiceNumber is 34156934,
InvoiceType is 026!
--------------------------------------------------

优点:

1、 降低对象之间的耦合度,通过参数化的命令信息来驱动程序的运行;
2、 新的命令可以很容易地加入到系统中;
3、 可以比较容易地设计一个组合命令;
4、 调用同一方法实现不同的功能;

缺点:

使用命令模式可能会导致某些系统有过多的具体命令类,导致子类膨胀。

使用场景:

1、 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互;
2、 系统需要在不同的时间指定请求、将请求排队和执行请求;
3、 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作;