探索设计模式的魅力:掌握命令模式-解锁软件设计的‘遥控器’

​🌈 个人主页:danci_

🔥 系列专栏:《设计模式》

💪🏻 制定明确可量化的目标,并且坚持默默的做事。


引言:探索命令模式的奥秘

软件设计领域充满挑战与机遇,命令模式作为关键要素,以优雅方式组织应用程序中的行为和请求。命令模式在现实世界中无处不在,如遥控器按钮或语音助手指令,封装请求或操作为对象,便于灵活处理不同请求、队列、日志及可撤销操作。从简单GUI到复杂企业级事务管理,命令模式均发挥重要作用。
命令模式的魅力在于其提供的松耦合设计方式。它将发起请求的对象(Invoker)与实现请求的对象(Receiver)之间的依赖解耦,引入了具体命令(ConcreteCommand)类来作为二者之间的沟通桥梁。这不仅增强了系统的灵活性,使得命令的添加和扩展变得更为平易,同时也方便了事务逻辑和历史记录的实现,比如撤销/重做功能,这在诸如文本编辑器等需要这类功能的应用程序中至关重要。
本文将深入解析命令模式的精粹,从基本概念入手,阐述其在软件设计中的实际应用,探讨如何高效地实现命令模式,并揭示其背后的设计哲学。我们会通过实例验证命令模式的多功能性,并指出在实现过程中需要注意的常见陷阱和挑战。

文章的结构安排如下:

  • 传统的实现方式
  • 模式讲解
  • 回顾与展望

随着我们走进命令模式的奥秘世界,将揭示如何巧妙地利用它来构建模块化、灵活和可维护性高的软件系统。

文章目录

一、传统实现方式

场景案例

|---------------------------------------------------------------------------------------------------------------------------|
| 一个经典的案例场景是"远程遥控器"(Remote Control)的设计。在这个场景中,我们可以将遥控器上的每个按钮看作是一个命令对象,而遥控器本身则是一个命令的调用者。当用户按下遥控器上的某个按钮时,遥控器就会调用相应的命令对象来执行操作。 |

一坨坨代码实现

一个简单的、直接的实现方法可能是直接在遥控器类中为每一个可能的动作定义一个方法。当一个按钮被按下时,就直接调用对应的方法。这种做法会使得遥控器类和实际执行动作的类高度耦合,因为遥控器类需要知道所有可能的动作以及如何执行这些动作。

java 复制代码
// 遥控器调用者
class RemoteControl {
    private Light light;
    private Television television;

    public RemoteControl(Light light, Television television) {
        this.light = light;
        this.television = television;
    }

    public void pressButton(String button) {
        switch(button) {
            case "lightOn":
                light.turnOn();
                break;
            case "lightOff":
                light.turnOff();
                break;
            case "tvOn":
                television.turnOn();
                break;
            case "tvOff":
                television.turnOff();
                break;
            case "volumeUp":
                television.volumeUp();
                break;
            case "volumeDown":
                television.volumeDown();
                break;
            // 更多可能的按钮动作...
            default:
                System.out.println("Button " + button + " not mapped to an action.");
                break;
        }
    }
}

// 灯光类
class Light {
    public void turnOn() {
        System.out.println("Light is On.");
    }

    public void turnOff() {
        System.out.println("Light is Off.");
    }
}

// 电视类
class Television {
    public void turnOn() {
        System.out.println("Television is On.");
    }

    public void turnOff() {
        System.out.println("Television is Off.");
    }

    public void volumeUp() {
        System.out.println("Television volume turned up.");
    }

    public void volumeDown() {
        System.out.println("Television volume turned down.");
    }
}

// 客户端代码
class Demo {
    public static void main(String[] args) {
        Light light = new Light();
        Television tv = new Television();
        RemoteControl remote = new RemoteControl(light, tv);

        remote.pressButton("lightOn");
        remote.pressButton("tvOn");
        remote.pressButton("volumeUp");
        // ... 更多动作
    }
}

请注意,虽然这种方法可以实现功能,但它违反了设计原则,如开闭原则(OCP)和单一职责原则(SRP)。每新增一个设备或者一个功能,都需要修改RemoteControl类的代码,这使得RemoteControl类随着时间推进和功能增加变得越来越庞大和难以维护。

有何问题

上述实现设计存在以下问题:

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 高耦合: 遥控器类(RemoteControl)直接依赖于特定的设备类(如Light和Television),并且需要知道它们的接口和实现细节。这违背了设计模式中强调的依赖倒置原则,即高层模块不应该依赖于底层模块,它们都应该依赖于抽象。 2. 违反开闭原则 (OCP): 如果需要添加新的设备或者动作,例如让遥控器控制空调,就必须修改RemoteControl类的pressButton方法,添加新的case分支。开闭原则指出软件实体应该对扩展开放,对修改关闭。上述设计并不满足这一原则。 3. 违反单一职责原则 (SRP): RemoteControl类不仅是调用者,还决定了逻辑如何执行。这给了RemoteControl多个变化的原因,比如设备接口改变或者控制逻辑改变。 4. 可维护性差: 随着功能的增多,pressButton方法会变得越来越长,充满了各种case语句,这会让代码难以维护。 5. 可扩展性差: 向遥控器中添加新功能变得复杂,需要修改现有代码并有可能引入新的错误。 6. 重复代码: 如果多个按钮需要执行类似的逻辑,这就可能产生代码重复。 |

综上所述,遥控器的这种设计缺乏灵活性和可维护性。使用命令模式可以解决这些缺点,因为命令模式将请求封装成对象,与执行操作的对象解耦,增加新命令无需修改已有的代码,只需定义新的命令对象。这样增强了代码的可维护性、扩展性,且令代码更加清晰。

二、命令模式讲解

解决上述问题的方法是使用命令模式。

命令模式的定义

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

核心思想

|-------------------------------------------------|
| 将请求或操作封装成对象,并通过调用这些对象来执行操作,从而实现了客户端和接收者的解耦。 |

结构图及说明

说明

  • **命令接口(Command Interface):**这是一个抽象接口,定义了执行命令的通用方法。具体的命令类需要实现这个接口。
  • **具体命令类(Concrete Command Classes):**这些类实现了命令接口,并封装了具体的操作或请求。它们通常持有一个对接收者的引用,并实现了在调用时执行相应操作的方法。
  • **接收者(Receiver):**接收者对象知道如何执行与命令相关的操作。具体命令类在调用时会与接收者对象交互,执行相应的操作。
  • **调用者(Invoker):**调用者对象负责调用命令对象。它通常持有一个或多个命令对象的引用,并提供一个方法来执行这些命令。调用者不需要知道命令的具体实现细节,只需要调用命令对象的方法即可。
  • **客户端(Client):**客户端创建具体的命令对象,并将其传递给调用者对象。客户端还可以设置接收者对象,并将其传递给命令对象。客户端通过调用调用者的方法来间接执行命令。

命令模式的结构允许将请求或操作封装成独立的对象,从而实现了请求调用者和请求接收者之间的解耦。这种设计模式使得请求可以被排队、撤销、重做以及事务处理等,提高了系统的灵活性和可扩展性。同时,通过引入命令接口和具体命令类,也使得新的命令可以很容易地加入到系统中,而无需修改现有的代码。

示例代码

首先,我们需要定义一个Command接口,所有的具体命令类都需要实现这个接口:

java 复制代码
public interface Command {  
    void execute();  
}

接下来,我们定义两个具体的命令类ConcreteCommandA和ConcreteCommandB,它们实现了Command接口:

java 复制代码
public class ConcreteCommandA implements Command {  
    private Receiver receiver;  
  
    public ConcreteCommandA(Receiver receiver) {  
        this.receiver = receiver;  
    }  
  
    @Override  
    public void execute() {  
        receiver.action();  
    }  
}  
  
public class ConcreteCommandB implements Command {  
    private Receiver receiver;  
  
    public ConcreteCommandB(Receiver receiver) {  
        this.receiver = receiver;  
    }  
  
    @Override  
    public void execute() {  
        receiver.anotherAction();  
    }  
}

然后,我们需要定义一个Receiver类,这个类将执行实际的操作:

java 复制代码
public class Receiver {  
    public void action() {  
        System.out.println("Executing action in Receiver");  
    }  
  
    public void anotherAction() {  
        System.out.println("Executing another action in Receiver");  
    }  
}

接下来,我们定义一个Invoker类,这个类将负责调用命令:

java 复制代码
public class Invoker {  
    private Command command;  
  
    public Invoker(Command command) {  
        this.command = command;  
    }  
  
    public void setCommand(Command command) {  
        this.command = command;  
    }  
  
    public void executeCommand() {  
        command.execute();  
    }  
}

最后,我们可以在客户端代码中使用这些类:

java 复制代码
public class Client {  
    public static void main(String[] args) {  
        Receiver receiver = new Receiver();  
  
        Command commandA = new ConcreteCommandA(receiver);  
        Command commandB = new ConcreteCommandB(receiver);  
  
        Invoker invoker = new Invoker(commandA);  
        invoker.executeCommand();  // 输出 "Executing action in Receiver"  
  
        invoker.setCommand(commandB);  
        invoker.executeCommand();  // 输出 "Executing another action in Receiver"  
    }  
}

在这个示例中,Command接口定义了一个execute方法,ConcreteCommandA和ConcreteCommandB类实现了这个接口,并在execute方法中调用了Receiver对象的不同方法。Invoker类持有一个Command对象,并提供了executeCommand方法来执行命令。在客户端代码中,我们创建了一个Receiver对象和两个命令对象,并使用Invoker对象来执行这些命令。

使用命令模式重构案例

假设原有的实现中包含的是一个简单的遥控器类RemoteControl,用于控制Light(开/关灯)和Television(开/关电视)两种设备。我们将使用命令模式实现它,首先定义命令接口,然后创建几个具体的命令类,之后实现调用者即遥控器类,最后实现接收者类。

  1. 定义命令接口 (Command):
java 复制代码
public interface Command {
    void execute();
}
  1. 实现具体命令类 (ConcreteCommands):
java 复制代码
// Light On Command
public class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.on();
    }
}

// Light Off Command
public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) {
        this.light = light;
    }

    public void execute() {
        light.off();
    }
}

// Television On Command
public class TelevisionOnCommand implements Command {
    private Television television;

    public TelevisionOnCommand(Television television) {
        this.television = television;
    }

    public void execute() {
        television.on();
    }
}

// Television Off Command
public class TelevisionOffCommand implements Command {
    private Television television;

    public TelevisionOffCommand(Television television) {
        this.television = television;
    }

    public void execute() {
        television.off();
    }
}
  1. 实现调用者 (Invoker):
java 复制代码
public class RemoteControl {
    private Command[] onCommands;
    private Command[] offCommands;

    public RemoteControl() {
        onCommands = new Command[2];
        offCommands = new Command[2];
    }

    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void pressOnButton(int slot) {
        onCommands[slot].execute();
    }

    public void pressOffButton(int slot) {
        offCommands[slot].execute();
    }
}
  1. 实现接收者 (Receivers):
java 复制代码
// Light class
public class Light {
    public void on() {
        System.out.println("Light is on.");
    }

    public void off() {
        System.out.println("Light is off.");
    }
}

// Television class
public class Television {
    public void on() {
        System.out.println("Television is on.");
    }

    public void off() {
        System.out.println("Television is off.");
    }
}
  1. 使用命令模式
java 复制代码
public class Client {
    public static void main(String[] args) {
        RemoteControl remote = new RemoteControl();

        Light light = new Light();
        Television tv = new Television();

        Command lightOn = new LightOnCommand(light);
        Command lightOff = new LightOffCommand(light);
        Command tvOn = new TelevisionOnCommand(tv);
        Command tvOff = new TelevisionOffCommand(tv);

        remote.setCommand(0, lightOn, lightOff);
        remote.setCommand(1, tvOn, tvOff);

        // Turn on the light and tv
        remote.pressOnButton(0);
        remote.pressOnButton(1);

        // Turn off the light and tv
        remote.pressOffButton(0);
        remote.pressOffButton(1);
    }
}

这样,我们使用命令模式实现了遥控器类。添加新命令只需创建新的Command类,然后使用setCommand方法将其设为特定按钮的操作。无需修改远程控制器的内部逻辑,符合开闭原则。

克服的问题

使用命令模式实现遥控器场景能够解决的问题包括:

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1. 高耦合: 在不使用命令模式的情况下,遥控器需要直接调用具体设备的操作方法,这造成了调用者和接收者之间的高耦合。使用命令模式后,遥控器只需要知道如何触发命令对象,而具体的设备操作被封装在命令对象中,降低了耦合性。 2. 开闭原则 (OCP): 开闭原则强调系统设计应该对扩展开放,对修改封闭。在命令模式中,若要引入新的命令或动作,无需修改遥控器的代码,只需要创建新的命令对象即可。这样遥控器的功能扩展不需要修改既有代码,保持了系统的开放性与封闭性。 3. 单一职责原则 (SRP): 单一职责原则要求一个类应该只有一个引起变化的原因。在命令模式中,遥控器的职责仅限于发出请求,而命令对象负责定义具体的操作行为,这样就将两者的责任区分开来,满足了SRP。 4. 可维护性: 在没有命令模式的遥控器实现中,交织的命令逻辑使得代码难以理解和维护。命令模式将命令逻辑封装在单独的对象中,使得遥控器和设备的代码更加清晰,提高了可维护性。 5. 可扩展性: 不使用命令模式,每增加一个操作或设备都可能需要修改遥控器的代码。命令模式使得增加新的命令或设备变得容易,只需定义新的命令类,无需改动遥控器或其他命令类。 6. 代码复用: 没有命令模式的遥控器实现可能会导致很多重复代码,比如多个类似设备的操作可能非常相似。命令模式提高代码复用性,因为可以通过继承或组合来复用命令类代码,从而减少重复。 |

命令模式通过对命令的抽象和封装提供了一种清晰而灵活的方式来解耦调用者和接收者,使代码变得更加模块化,易于理解、维护和扩展。

三、回顾与展望

优点

  • **解耦发送者和接收者:**命令模式将发起请求的对象(发送者)和执行请求的对象(接收者)分离开来。这样,发送者只需要知道如何发送请求,而不需要知道请求的具体实现细节,从而降低了系统的耦合度。
  • **扩展性:**当需要引入新命令时,不需要改变现有的代码,只需增加新的命令类。这符合开闭原则,对于扩展是开放的,对于修改是封闭的。
  • **重用性:**你可以重用这些命令,因为命令本身与其操作的上下文是独立的。
  • **灵活性:**命令模式可以支持撤销(undo)和重做(redo)操作,因为每个命令都能够记录它的历史状态,或者可以提供撤销其所做操作的方式。
  • 可以将一组简单的命令组合成复杂的命令。
  • 可以方便地实现请求的记录和队列化。

缺点

  • **类膨胀:**对于每一个操作都需要创建一个特定的命令类,随着应用程序命令增多,会产生大量类,使系统变得更加复杂。
  • **增加复杂性:**对于简单的操作,使用命令模式可能会过于复杂,它会引入许多额外的类和对象,增加了系统的复杂度。
  • **性能考虑:**如果在某些性能敏感的系统中,增加这样一个额外的抽象层可能会影响性能。

命令模式在需要将发起操作和执行操作解耦时表现很好,在菜单选择、队列请求、事务操作以及作业排队等场景下特别有用。然而,适当的使用是关键,因为在不需要这种级别灵活性的简单场景中,它可能会导致不必要的复杂性。

应用场景

命令模式作为一种行为设计模式,是处理请求和操作执行解耦的强大工具。以下是命令模式的一些典型应用场景:

  • **GUI按钮和菜单操作:**图形用户界面(GUI)程序中,按钮行为和菜单命令通常使用命令模式实现。每个按钮或菜单项都有不同的行为和请求,命令模式允许将这些行为封装成具体的命令对象,使得你可以根据需要动态地为按钮分配不同的功能。
  • **撤销/重做功能:**在文本编辑器、图像编辑软件或数据库事务管理等应用程序里实现撤销(Undo)和重做(Redo)功能。命令对象可以用来记录发生的行为及其状态,可以在必要时回滚或重播这些行为。
  • **操作队列:**在需要排队多个请求并按顺序执行它们的场景中,如线程池、任务管理器或后台作业调度。命令模式可以将所有的请求封装成命令对象并添加到队列中,独立于请求本身。
  • **日志请求:**在需要记录和存储用户操作的系统中,可以使用命令模式记录每个操作的详细信息,当系统崩溃或出错时能够通过日志重放用户的操作以恢复到之前的状态。
  • **智能家居或物联网(IoT)控制:**智能家居应用中将用户的操作(如打开灯光、设置温度等)抽象为命令,这些命令可以远程或定时执行,命令模式使得新增更多功能(如新的设备控制)变得简单。
  • **异步任务执行:**在需要将请求处理的工作委托给后台线程执行的系统中,可以将这些请求作为命令放入工作队列中,由后台服务进行处理。
  • **事务行为管理:**在需要实现事务的系统中(比如需要保证一系列操作要么全部成功要么全部失败),命令对象能够用于封装所有事务的操作,并在发生错误时进行回滚。

未来展望

随着软件设计的不断进步,命令模式作为一种行为设计模式,其在分离命令的定义与调用、组织复杂命令结构等方面的优势将会保持其重要性。未来的发展趋势和可能的应用方向可从以下几个方面进行探讨:
与新兴技术的融合
命令模式很可能会与当前逐渐普及的技术像云计算、物联网(IoT)以及人工智能(AI)产生更紧密的结合。例如,在智能家居领域,命令模式可以用来设计一个集中的控制系统,用以发送和调度对各种家电的控制命令;在云服务中,命令模式可以协助构建更为灵活的服务调用机制,促进分布式系统的命令传递和执行。

可扩展性与微服务架构
随着微服务架构的流行,命令模式的可扩展性将得到进一步重视。微服务架构依赖于服务的精细化管理和交互,而利用命令模式能够有效地对服务间传递的命令进行封装处理,使得服务间的通信更加的稳定与易于维护。

丰富的行为组合与软件自动化
命令模式也可以在增强软件自动化方面发挥重要作用。由于它支持撤销、重做等操作,这使得自动化脚本或框架在执行过程中可以更灵活地控制事务。同时,命令的组合能力可以促使开发者创造出更加复杂和智能化的宏命令,这些宏命令可以应对快速变化的业务逻辑和工作流程。

增强人机交互体验
随着VR(虚拟现实)、AR(增强现实)等交互技术的发展,命令模式有潜力在未来的人机交互系统中扮演关键角色。在这些系统中,用户的各种指令可以通过命令模式进行封装,从而提供更为直观和流畅的交互体验。

面向可持续发展的软件设计
环境和可持续发展成为全球关注的热点,软件设计同样需要考虑绿色计算。命令模式由于其高度解耦命令发起者与执行者的特性,有助于构建能够轻松适应能效优化和资源管理策略的软件系统。

结语
命令模式,作为一种成熟和灵活的设计模式,对于软件工程师来说是必不可少的工具之一。随着科技的发展和计算模型的演变,命令模式的应用场景将会越发广泛,其在设计模式中的地位也将日益凸显。
继续深入学习和实践命令模式。务求不仅理解其基本概念和结构,还要通过实际项目练习来把握其精髓,灵活运用到各种复杂场景中。不断探索命令模式与其他设计模式的结合使用,也是拓宽设计思路、提升软件质量的重要途径。
记住,技术总是在变,但设计模式提供的是解决问题的思想和方法。持续学习,不断实践,让我们一同推动软件设计的边界向前延伸。

相关推荐
神仙别闹13 分钟前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭37 分钟前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫1 小时前
泛型(2)
java
超爱吃士力架1 小时前
邀请逻辑
java·linux·后端
南宫生1 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石1 小时前
12/21java基础
java
李小白661 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp2 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶2 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗2 小时前
常用类晨考day15
java