【愚公系列】2023年10月 二十三种设计模式(十四)-命令模式(Command Pattern)
🏆 作者简介,愚公搬代码
🏆《头衔》:华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,阿里云专家博主,腾讯云优秀博主,掘金优秀博主,51CTO博客专家等。
🏆《近期荣誉》:2022年CSDN博客之星TOP2,2022年华为云十佳博主等。
🏆《博客内容》:.NET、Java、Python、Go、Node、前端、IOS、Android、鸿蒙、Linux、物联网、网络安全、大数据、人工智能、U3D游戏、小程序等相关领域知识。
🏆🎉欢迎 👍点赞✍评论⭐收藏
🚀前言
设计模式(Design Pattern)是软件开发领域的宝贵经验,是多人反复借鉴和广泛应用的代码设计指导。它们是一系列经过分类和归纳的代码组织方法,旨在实现可重用性、可维护性和可理解性。使用设计模式,我们能够编写高质量的代码,使其更易于他人理解,并提供了代码可靠性的保证。
毫无疑问,设计模式对个人、团队和整个系统都带来了显著的益处。它们将代码开发提升到工程化水平,为软件工程构建提供了坚实的基础,就如同大厦的一块块精巧的砖石一样。在项目中明智地应用设计模式可以完美地解决各种复杂问题。每种设计模式都有相应的原理和最佳实践,它们描述了我们日常开发中不断遇到的问题,以及这些问题的核心解决方法。正是因为这种实用性和通用性,设计模式才能在软件开发中广泛地得以应用。设计模式是构建稳健、可扩展和可维护软件的关键工具,为开发者们提供了解决问题的智慧和指导。
🚀一、命令模式(Command Pattern)
当谈及命令模式时,我们涉及到一种行为型设计模式,它的目的在于将一个请求封装成一个对象,从而能够以不同的请求来参数化客户端。
具体来说,在这种设计模式中,请求会被包裹成一个命令对象,并传递给调用者。调用者会寻找适合处理该命令的对象,并将命令传递给相应的对象以执行。
这种模式的优势在于可以实现松耦合,允许将请求的发起者与请求的执行者解耦。这也使得可以将多个命令排队、延迟执行或者撤销。
需要注意的是,命令模式适用于各种场景,尤其在需要对请求进行排队、记录日志、撤销或重做等情况下十分有用。同时,它也为系统的扩展提供了便利,能够方便地添加新的命令和处理方式。
🚀二、使用步骤
🔎1.角色
🦋1.1 抽象命令(Command)
在命令模式(Command Pattern)中,抽象命令(Command)是一个关键概念,它具有以下作用:
封装请求:抽象命令封装了一个请求或操作,包括操作的接收者(执行者)以及执行操作所需的参数。这样,客户端可以将请求作为命令对象进行参数化,而无需了解请求的具体细节。
解耦请求发送者和接收者:通过将请求封装在命令对象中,抽象命令将请求发送者与请求的接收者解耦。发送者只需要知道如何调用命令,而不需要知道具体的接收者或执行细节。这降低了系统中不同部分的耦合度。
支持撤销和重做:抽象命令通常定义了执行操作和撤销操作的接口。这允许系统能够追踪执行的命令,以便在需要时撤销或重做它们。这在实现撤销和重做功能时非常有用。
支持命令队列:命令模式可以用于构建命令队列,即将多个命令对象存储在队列中,按顺序执行它们。这对于实现任务调度和日志记录等功能非常有用。
支持事务性操作:抽象命令还可用于实现事务,即一系列相关的操作要么全部执行成功,要么全部回滚。如果其中一个命令失败,可以撤销之前的命令。
增强可扩展性:通过引入抽象命令,系统变得更加灵活和可扩展。新的命令可以很容易地添加到系统中,而无需修改现有的客户端代码。
抽象命令在命令模式中充当了请求的中介,将请求的发起者和接收者解耦,同时支持一系列附加功能,如撤销、重做、队列和事务等。这使得命令模式成为一种强大的设计模式,可用于多种场景,提高了系统的灵活性和可维护性。
🦋1.2 具体命令(Concrete Command)
在命令模式(Command Pattern)中,具体命令(Concrete Command)是一种具体的命令对象,它实现了抽象命令(Command)接口,并且负责将一个具体的请求绑定到接收者(执行者)上,以执行该请求。以下是具体命令的概念和作用:
概念:
- 具体命令是命令模式中的一种具体实现,通常对应于系统中的一个具体操作或请求。
- 具体命令实现了抽象命令接口中的方法,包括执行(
execute
)和撤销(undo
)操作。 - 具体命令通常持有一个对接收者对象的引用,负责将请求委派给接收者来执行实际操作。
作用:
- 将请求与接收者解耦:具体命令的主要作用是将一个请求从请求发送者(客户端)解耦,并将其绑定到接收者上。这使得请求发送者无需了解请求的具体执行方式,只需通过具体命令来发送请求。
- 执行请求操作:具体命令实现了执行(
execute
)方法,该方法包含了实际的操作逻辑。当具体命令的execute
方法被调用时,它会将请求委派给接收者来执行请求的实际操作。 - 支持撤销操作:具体命令通常也实现了撤销(
undo
)方法,允许系统能够撤销之前执行的操作。这对于实现撤销和重做功能非常有用,以及处理事务性操作。 - 具体化请求:具体命令将抽象的请求具体化,即将请求的参数和执行方式封装到一个具体的对象中,以便传递和执行。
具体命令是命令模式的核心组成部分,它负责将请求与接收者解耦,并提供了执行和撤销请求的方法。通过使用具体命令,可以实现灵活的命令调用和支持一系列与请求相关的附加功能,如撤销、队列、事务等。这使得命令模式非常有用,特别是在需要构建可扩展和可维护的系统时。
🦋1.3 接收者(Receiver)
在命令模式(Command Pattern)中,接收者(Receiver)是负责执行与请求相关的实际操作的对象。它具体执行了命令对象所代表的请求。以下是接收者的概念和作用:
概念:
- 接收者是命令模式中的一个组件,负责具体执行一个或多个操作。它知道如何执行与命令相关的操作,但不知道命令的高级策略和结构。
- 接收者可能是任何对象,例如:灯(对于“打开”或“关闭”命令)、文档(对于“保存”或“打印”命令)或其他任何可以执行特定操作的对象。
作用:
- 执行具体操作:接收者的主要职责是执行与请求相关的具体操作。当命令对象的
execute
方法被调用时,它通常会调用接收者的一个或多个方法来执行请求的实际操作。 - 解耦命令与实际操作:通过将接收者与命令对象分离,系统中的请求发送者和请求的实际执行者被解耦。这意味着发送请求的对象不需要知道请求如何以及在哪里执行,它只需要知道如何发送请求。
- 支持命令的扩展:由于接收者负责具体执行操作,新增或修改命令时,可以不修改接收者的代码。这有助于命令的扩展和修改。
- 封装细节:接收者封装了与命令相关的执行细节。这使得命令对象和请求发送者可以不关心如何实现请求,从而更加关注高级策略和流程。
- 执行具体操作:接收者的主要职责是执行与请求相关的具体操作。当命令对象的
接收者在命令模式中扮演着关键角色,它负责执行与命令相关的实际操作,为命令模式提供了强大的执行能力。通过将接收者与命令对象解耦,命令模式可以在不改变发送者或接收者代码的前提下,灵活地新增、修改或删除命令。这提高了系统的灵活性和可维护性,使得命令模式成为一种强大且广泛使用的设计模式。
🦋1.4 调用者(Invoker)
在命令模式(Command Pattern)中,调用者(Invoker)是一个对象,负责将具体的命令对象与其执行者(接收者)连接起来,并在需要时触发命令的执行。以下是调用者的概念和作用:
概念:
- 调用者是命令模式中的一个组件,它持有一个或多个命令对象,并在某个时刻触发这些命令的执行。
- 调用者通常不了解命令对象的具体细节,只知道如何触发命令的执行。
作用:
- 触发命令执行:调用者的主要作用是在适当的时机触发命令对象的执行。这通常通过调用命令对象的
execute
方法来实现。 - 解耦发送者和接收者:调用者将请求发送者与请求接收者解耦。请求发送者只需将命令对象交给调用者,而不需要了解命令是如何执行的。
- 支持撤销和重做:通过调用者,系统可以轻松地维护已执行的命令历史,以便支持撤销(undo)和重做(redo)操作。调用者可以保持命令对象的引用,以便在需要时执行撤销操作。
- 支持批处理和队列:调用者可以将多个命令对象组织成队列或批处理,并在合适的时机一次性执行它们。这对于任务调度和执行多个命令非常有用。
- 增强可扩展性:调用者通过将命令对象与具体的执行者解耦,使系统更容易扩展。新的命令可以轻松地添加到调用者中,而无需修改已有的客户端代码。
- 触发命令执行:调用者的主要作用是在适当的时机触发命令对象的执行。这通常通过调用命令对象的
调用者在命令模式中充当了一个协调者的角色,它负责管理命令对象并触发它们的执行。通过使用调用者,命令模式能够实现请求的发送者与接收者的解耦,支持一系列附加功能,如撤销、重做、批处理和队列。这提高了系统的灵活性和可维护性,使得命令模式成为一种有力的设计模式。
🔎2.示例
命名空间CommandPattern中包含Command基类、发票开具命令类CreateCommand、发票作废命令类CancelCommand、发票打印命令类PrintCommand、命令参数基类CommandArgs、发票开具命令参数类CommandArgs、发票作废命令参数类CancelArgs、发票打印命令参数类PrintArgs、接收者类Receiver和调用者类Invoker。本命尝试通过客户端调用不同的参数化发票命令来使调用者调用不同的功能。
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.优点
命令模式(Command Pattern)是一种行为型设计模式,它具有多种优点,可以帮助构建更灵活、可扩展和可维护的软件系统。以下是命令模式的一些主要优点:
解耦发送者和接收者:
- 命令模式将请求发送者(客户端代码)与请求接收者(执行者)解耦。发送者不需要知道具体的接收者,只需要知道如何发送命令即可。
- 这种解耦性使系统更加灵活,允许在不影响客户端代码的情况下修改、扩展或替换接收者对象。
支持撤销和重做:
- 命令模式可以轻松地支持撤销(undo)和重做(redo)操作。每个命令对象通常包含了执行和撤销两个方法,使得系统能够回滚到之前的状态。
- 这对于实现撤销历史记录或支持事务性操作非常有用。
支持队列和批处理:
- 命令模式可以将命令对象组织成队列或批处理,以便一次性执行多个命令。
- 这对于任务调度、命令日志记录和实现事务性操作等场景非常有用。
增强可扩展性:
- 新的命令可以轻松地添加到系统中,而不需要修改已有的客户端代码。这使得系统更容易扩展,符合开放封闭原则。
- 命令模式支持在运行时动态配置和组合命令对象,从而实现复杂的操作序列。
增强可维护性:
- 命令模式将每个具体命令封装成一个独立的对象,使得命令的创建、修改和测试变得更加容易。
- 客户端代码可以保持简洁,因为它只需与抽象命令接口交互,而不需要了解具体命令的实现细节。
支持日志和审计:
- 命令模式可以用于记录命令的执行历史,以实现日志和审计功能。这有助于跟踪系统的操作记录。
命令模式是一种非常有用的设计模式,它提供了一种松耦合的方式来处理请求和操作,并支持一系列附加功能,如撤销、重做、批处理和队列。通过将命令封装成对象,可以增强系统的可维护性、可扩展性和可重用性,使得它在各种应用场景中都具有广泛的适用性。
🔎2.缺点
虽然命令模式(Command Pattern)具有许多优点,但也存在一些缺点,需要在使用时考虑。以下是一些常见的命令模式的缺点:
增加类的数量:
- 命令模式引入了许多具体命令类,每个命令对应一个类。这可能导致类的数量急剧增加,特别是在有大量不同命令的系统中。
- 过多的类可能会增加代码的复杂性和维护成本。
内存消耗:
- 每个命令对象都需要占用一定的内存,尤其是在需要大量命令对象时,可能会导致内存消耗较高。
- 对于资源有限的嵌入式系统或移动设备,需要谨慎使用。
复杂性:
- 对于简单的应用场景,引入命令模式可能会增加不必要的复杂性。命令模式更适用于需要处理复杂命令交互和撤销/重做功能的系统。
执行速度:
- 命令模式可能引入了额外的方法调用和对象创建,这可能会对系统的执行速度产生轻微的影响。
- 在对性能要求极高的应用中,需要谨慎使用。
理解成本:
- 对于新的开发人员或团队成员来说,理解命令模式和命令对象之间的关系可能需要一些时间和培训。
- 在团队中广泛采用命令模式时,需要确保团队成员都能够理解和正确使用该模式。
不适用于所有场景:
- 命令模式并不是适用于所有应用场景的设计模式。对于简单的、直接的操作,可能不需要引入命令模式,因为它会增加额外的复杂性。
虽然命令模式在许多情况下是有用的,但它也不是一种适用于所有情况的解决方案。在使用命令模式时,需要权衡其优点和缺点,并根据具体的需求和情境来决定是否使用该模式。如果命令模式能够提供所需的灵活性和可维护性,那么它通常是一个有价值的选择。
🔎3.使用场景
命令模式(Command Pattern)适用于许多场景,特别是在需要解耦请求发送者和请求接收者,支持撤销和重做操作,以及实现队列、批处理和日志记录等功能时,它特别有用。以下是一些适合使用命令模式的常见场景:
撤销和重做功能:
- 当需要在应用程序中实现撤销和重做操作时,可以使用命令模式。每个命令对象可以存储执行和撤销操作,以便在需要时回滚操作历史。
菜单和工具栏:
- 在图形用户界面中,可以使用命令模式来管理菜单项、工具栏按钮和快捷键的操作。每个命令对象对应一个具体的操作。
任务调度:
- 命令模式可用于实现任务调度系统,将任务抽象为命令对象,然后按照一定的策略执行这些任务。
日志记录:
- 命令模式可以用于记录系统的操作历史,以实现日志记录和审计功能。每个命令对象执行时可以生成日志条目。
批处理:
- 当需要按照一定的顺序批量执行一系列操作时,命令模式可以将这些操作封装为命令对象,并按顺序执行它们。
遥控器:
- 在遥控器应用程序中,可以使用命令模式来管理不同的遥控按钮,每个按钮对应一个命令对象,以执行特定的操作。
数据库事务:
- 在数据库管理中,命令模式可以用于管理数据库事务。每个数据库操作可以封装为一个命令对象,然后批量执行或回滚事务。
网络请求队列:
- 在网络编程中,命令模式可用于创建和管理网络请求队列。每个网络请求可以表示为一个命令对象,以便按顺序执行或重试失败的请求。
游戏开发:
- 在游戏开发中,命令模式可用于处理用户输入、动作和游戏操作。每个用户输入可以映射到一个命令对象,以执行相应的操作。
命令模式在需要将请求封装成独立对象、支持撤销和重做、实现队列和批处理等情况下非常有用。它有助于提高系统的灵活性、可维护性和可扩展性,使代码更加模块化和清晰。因此,在这些场景中考虑使用命令模式
🚀感谢:给读者的一封信
亲爱的读者,
我在这篇文章中投入了大量的心血和时间,希望为您提供有价值的内容。这篇文章包含了深入的研究和个人经验,我相信这些信息对您非常有帮助。
如果您觉得这篇文章对您有所帮助,我诚恳地请求您考虑赞赏1元钱的支持。这个金额不会对您的财务状况造成负担,但它会对我继续创作高质量的内容产生积极的影响。
我之所以写这篇文章,是因为我热爱分享有用的知识和见解。您的支持将帮助我继续这个使命,也鼓励我花更多的时间和精力创作更多有价值的内容。
如果您愿意支持我的创作,请扫描下面二维码,您的支持将不胜感激。同时,如果您有任何反馈或建议,也欢迎与我分享。
再次感谢您的阅读和支持!
最诚挚的问候, “愚公搬代码”
- 点赞
- 收藏
- 关注作者
评论(0)