命令模式——让程序舒畅执行

● 命令模式介绍

命令模式(Command Pattern),是行为型设计模式之一。命令模式相对于其他的设计模式来说并没有那么多条条框框,其实并不是一个很"规矩"的模式,不过,就是基于一点,命令模式相对于其他的设计模式更为灵活多变。我们接触比较多的命令模式个例无非就是程序菜单命令,如在操作系统中,我们点击"关机"命令,系统就会执行一系列的操作,如先是暂停处理事件,保存系统的一些配置,然后结束程序进程,最后调用内核命令关闭计算机等,对于这一系列的命令,用户不用去管,用户只需要点击系统的关机按钮即可完成如上一系列的命令。而我们的命令模式其实也与之相同,将一系列的方法调用封装,用户只需要调用一个方法执行,那么所有这些被封装的方法就会被挨个调用。

● 命令模式的定义

将一个请求封装成一个对象,从而让用户使用不同的请求把客户端参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。

● 命令模式的使用场景

需要抽象出待执行的动作,然后以参数的形式提供出来------类似于过程设计中的回调机制,而命令模式正是回调机制的一个面向对象的替代品。

在不同的时刻指定、排列和执行请求。一个命令对象可以有与初始请求无关的生存期。

需要支持取消操作。

支持修改日志功能,这样系统崩溃时,这些修改可以被重做一遍。

需要支持事务操作。

● 命令模式的UML类图

UML类图如下图所示。

根据类图可以得出如下一个命令模式的通用模式代码。

接收者类

java 复制代码
/**
 * 接收者类
 */
public class Receiver {

    /**
     * 真正执行具体命令逻辑的方法
     */
    public void action() {
        System.out.println("执行具体操作");
    }
}

抽象命名接口

java 复制代码
/**
 * 抽象命名接口
 */
public interface Command {
    /**
     * 执行具体操作命令
     */
    void execute();
}

具体命令类

java 复制代码
/**
 * 具体命令类
 */
public class ConcreteCommand implements Command {
    private Receiver receiver;//持有一个对接收者对象的引用

    public ConcreteCommand(Receiver receiver) {
        this.receiver = receiver;

    }

    @Override
    public void execute() {
        //调用接收者的相关方法来执行具体逻辑
        receiver.action();
    }
}

请求者类

java 复制代码
/**
 * 请求者类
 */
public class Invoker {
    private Command command;//持有一个对相应命令对象的引用

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

    public void action() {
        //调用具体命令对象的相关方法,执行具体命令
        command.execute();
    }
}

客户类

java 复制代码
/**
 * 客户类
 */
public class Client {
    public static void main(String[] args) {
        //构造一个接受者对象
        Receiver receiver = new Receiver();

        //根据接受者对象构造一个命令对象
        Command command = new ConcreteCommand(receiver);

        //根据具体的对象构造请求者对象
        Invoker invoker = new Invoker(command);

        //执行请求方法
        invoker.action();
    }
}

角色介绍。

Receiver:接受者角色。

该类复杂具体实施或执行一个请求,说得通俗点就是,执行具体逻辑的角色,以开头的"关机"命令操作为例,其接受者角色就是真正执行各项关机逻辑的底层代码。任何一个类都可以成为一个接受者,而在接受者类中封装具体操作逻辑的方法我们则称为行为方法。

Command:命令角色。

定义所有具体命令类的抽象接口。

ConcreteCommand:具体命令角色。

该类实现了 Command 接口,在execute 方法中调用接受者角色的相关方法,在接受者和命令执行的具体行为之间加以肉耦合。而 execute 则通常称为执行方法,如本文开头所示"关机"的操作实现,具体可能还包含很多相关的操作,比如保存数据、关闭文件、结束进程等,如果将这一系列具体的逻辑处理看作接受者,那么调用这些具体逻辑的方法就可以看作是执行方法。

Invoker:请求者角色

该类的职责就是调用命令对象执行具体的请求,相关的方法我们称为行动方法,还是用"关机"为例"关机"这个菜单命令一般就对应一个关机方法,我们点击了"关机"命令后,由这个关机方法去调用具体的命令执行具体的逻辑,这里的"关机"对应的这个方法就可以看做是请求者。

Client:客户端角色

以"关机"的例子来说就相当于人,很好理解不再多说。

这里其实大家可以看到,命令模式的应用其实可以用一句话来概述,就是将行为调用者与实现者解耦,一个简单的例子,我们常会这样来调用代码。

java 复制代码
        Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        mPaint.setShader(new SweepGradient(canvas.getWidth() / 2
                , canvas.getHeight() / 2
                , new int[]{0xffeeadde, 0xffbc4579, 0xffe9c43f, 0xffdccb48, 0xff44adde}
                , null));

这样的逻辑调用再熟悉不过,先是 new 一个 Paint 对象,紧接着调用 Paint 中的 setShader 方法为 Paint 设置一个着色器。但是,如果我们调用逻辑比较复杂或者说调用行为有多种实现时,这种将行为调用者与行为实现者耦合在一起的写法就会很有问题。这时候使用设计模式来解耦合就很有比必要,当然,这里只是作为一个引子给大家思考,不多做分析。

● 命令模式的简单实现

命令模式总体来说并不难,只是相对比较烦琐,你想想一个简单的调用关系被解耦成多个部分,必定会增加类的复杂度,但是即便如此,命令模式的结构依然清晰。大家小时候应该玩过俄罗斯方块游戏,这里以古老的俄罗斯方块游戏为例,看看我们在命令模式下是如何操控俄罗斯方块变化的。一般来说,俄罗斯方块游戏中都有4个按钮,两个左右移动的安排,一个快速下落的按钮,还有一个变化方块形状的按钮,这是计较经典的游戏原型。一个玩游戏的人相当于我们的客户端,而游戏上的4个按钮就相当于请求者,或者也可以称为调用者,执行具体按钮命令的逻辑方法可以看作是命令角色,当然,游戏内部具体是怎么实现的我们不知道,也不在这里探讨,仅作例子分析,最后真正执行处理具体逻辑的则是游戏本身,你可以看作是各种机器码计算处理来执行的具体逻辑,这里我们将它看作为接受者角色。逻辑分析比较清楚了,我们来将其"翻译"成代码,首先是我们的接收者,这里以俄罗斯方块游戏本身作为接受者角色。

接受者角色,俄罗斯方块游戏。

java 复制代码
/**
 * 接受者角色 俄罗斯方块游戏
 */
public class TetrisMachine {
    /**
     * 真正处理"向左"操作的逻辑代码
     */
    public void toLeft() {
        System.out.println("向左");
    }

    /**
     * 正在处理"向右"操作的逻辑代码
     */
    public void toEight() {
        System.out.println("向右");
    }

    /**
     * 正常处理"快速下落"操作的逻辑代码
     */
    public void fastToBottom() {
        System.out.println("快速下落");
    }

    /**
     * 真正处理"改变形状"操作的逻辑代码
     */
    public void transform() {
        System.out.println("改变形状");
    }
}

TetrisMachine 类整个命令模式中唯一处理具体代码逻辑的地方,其他的类都是直接或间接地调用到该来的方法,这就是接收者角色,处理具体的逻辑。如上文我们所说,接收者类只是一个普通的类,任何类都可以作为接收者。接下来我们定义一个接口作为命令角色的抽象。

命令者抽象,定义执行方法。

java 复制代码
/**
 * 命令者抽象 定义执行方法
 */
public interface Command {
    /**
     * 命令执行
     */
    void execute();
}

然后就是4个具体命令:向左移、向右移、掉落和变换。

具体命令,向左移的命令类。

java 复制代码
/**
 * 具体命令 向左移的命令类
 */
public class LeftCommand implements Command {
    //持有一个接收者俄罗斯方块游戏对象的引用
    private TetrisMachine machine;

    public LeftCommand(TetrisMachine machine) {
        this.machine = machine;
    }

    @Override
    public void execute() {
        //调用游戏机里的具体方法执行操作
        machine.toLeft();
    }
}

具体命令者,向右移的命令类。

java 复制代码
/**
 * 具体命令者 向右移的命令类
 */
public class RigthCommand implements Command {
    //持有一个接收者俄罗斯方块游戏对象的引用
    private TetrisMachine machine;

    public RigthCommand(TetrisMachine machine) {
        this.machine = machine;
    }

    @Override
    public void execute() {
        //调用游戏机里的具体方法执行操作
        machine.toRight();
    }
}

具体命令者,快速落下的命令类。

java 复制代码
/**
 * 具体命令者,快速落下的命令类。
 */
public class FallCommand implements Command {
    //持有一个接收者俄罗斯方块游戏对象的引用
    private TetrisMachine machine;

    public FallCommand(TetrisMachine machine) {
        this.machine = machine;
    }

    @Override
    public void execute() {
        //调用游戏机里的具体方法执行操作
        machine.fastToBottom();
    }
}

具体命令者,改变形状的命令类。

java 复制代码
/**
 * 具体命令者,改变形状的命令类。
 */
public class TransformCommand implements Command {
    //持有一个接收者俄罗斯方块游戏对象的引用
    private TetrisMachine machine;

    public TransformCommand(TetrisMachine machine) {
        this.machine = machine;
    }

    @Override
    public void execute() {
        //调用游戏机里的具体方法执行操作
        machine.transform();
    }
}

从程序中可以看到,命令者角色类中的方法名称与 TetrisMachine 接受者角色类中的方法名可以不一样,两者之间仅是一种弱耦合。对于请求者,这里我们以一个 Buttons 类来表示,命令由按钮来执行。

请求者类,命名由按钮发起。

java 复制代码
/**
 * 请求者类,命名由按钮发起。
 */
public class Buttons {
    private LeftCommand leftCommand;//向左移动的命令对象引用
    private RightCommand rightCommand;//向右移动的命令对象引用
    private FallCommand fallCommand;//快速落下的命令对象引用
    private TransformCommand transformCommand;//变换形状的命令对象引用

    /**
     * 设置向左移动的命令对象
     *
     * @param leftCommand 向左移动的命令对象
     */
    public void setLeftCommand(LeftCommand leftCommand) {
        this.leftCommand = leftCommand;
    }

    /**
     * 设置向右移动的命令对象
     *
     * @param rightCommand 向右移动的命令对象
     */
    public void setRightCommand(RightCommand rightCommand) {
        this.rightCommand = rightCommand;
    }

    /**
     * 设置快速下落的命令对象
     *
     * @param fallCommand 快速下落的命令对象
     */
    public void setFallCommand(FallCommand fallCommand) {
        this.fallCommand = fallCommand;
    }

    /**
     * 设置变换形状的命令对象
     *
     * @param transformCommand 变换形状的命令对象
     */
    public void setTransformCommand(TransformCommand transformCommand) {
        this.transformCommand = transformCommand;
    }

    /**
     * 按下按钮向左移动
     */
    public void toLeft() {
        leftCommand.execute();
    }

    /**
     * 按下按钮向右移动
     */
    public void toRight() {
        rightCommand.execute();
    }

    /**
     * 按下按钮快速下落
     */
    public void fall() {
        fallCommand.execute();
    }

    /**
     * 按下按钮改变状态
     */
    public void transform() {
        transformCommand.execute();
    }
}

客户类

java 复制代码
/**
 * 客户类
 */
public class Player {
    public static void main(String[] args) {
        //首先要有俄罗斯方块游戏
        TetrisMachine machine = new TetrisMachine();

        //根据游戏我们构造4种命令
        LeftCommand leftCommand = new LeftCommand(machine);
        RightCommand rightCommand = new RightCommand(machine);
        FallCommand fallCommand = new FallCommand(machine);
        TransformCommand transformCommand = new TransformCommand(machine);

        //按钮可以执行不同的命令
        Buttons buttons = new Buttons();
        buttons.setLeftCommand(leftCommand);
        buttons.setRightCommand(rightCommand);
        buttons.setFallCommand(fallCommand);
        buttons.setTransformCommand(transformCommand);

        //具体按下哪个按钮玩具说了算
        buttons.toLeft();
        buttons.toRight();
        buttons.fall();
        buttons.transform();
    }
}

或许大家在看了这一长篇代码后心存疑虑,明明就是一个简单的逻辑,为什么要做的如此复杂呢?对于大部分开发者来说更愿意解释如下的代码。

java 复制代码
/**
 * 客户类
 */
public class Player {
    public static void main(String[] args) {
        //首先要有俄罗斯方块游戏
        TetrisMachine machine = new TetrisMachine();

        //要实现怎样的控制方式,我们就直接调用相关的函数就行
        machine.toLeft();
        machine.toRight();
        machine.fastToBottom();
        machine.transform();
    }
}

调用逻辑做得如此复杂,这是因为来发起来方便,每次我们增加或许修改游戏功能只需修改

TetrisMachine 类就行了,然后对应地改一改 Player 类,一切都很方便。但是,对开发者自己来说是方便了,那么,如果有一天开发者不再负责这个项目了呢?这样的逻辑留给后来者,没人觉得会方便。实际模式有一条重要的原则;对修改关闭对扩展开发,大家可以细细体会。

除此之外,使用命令模式的另一个好处是可以实现命令记录功能,如在上例中,我们在请求者 Buttons 里使用一个数据结构来存储执行过的命令对象,以此可以方便地知道刚刚执行过哪些命令动作,并可以在需要时恢复,具体代码大家可自行尝试,这里不再给出。

在命令模式中,其充分体现了几乎所有设计模块的通病,就是类的膨胀,大量衍生类的创建,这是一个不可避免的问题,但是,其给我们带来的好处也非常多,更弱的耦合性、灵活性的控制性以及更好的扩展性,不过,在实际开发过程中是不是需要采用命令模式还是需要斟酌。

相关推荐
冷琴19965 分钟前
基于java+springboot的酒店预定网站、酒店客房管理系统
java·开发语言·spring boot
daiyang123...31 分钟前
IT 行业的就业情况
java
追光天使39 分钟前
【Mac】和【安卓手机】 通过有线方式实现投屏
android·macos·智能手机·投屏·有线
爬山算法1 小时前
Maven(6)如何使用Maven进行项目构建?
java·maven
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛1 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
吹老师个人app编程教学1 小时前
详解Java中的BIO、NIO、AIO
java·开发语言·nio
爱学的小涛1 小时前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio
北极无雪1 小时前
Spring源码学习:SpringMVC(4)DispatcherServlet请求入口分析
java·开发语言·后端·学习·spring
小雨cc5566ru1 小时前
uniapp+Android智慧居家养老服务平台 0fjae微信小程序
android·微信小程序·uni-app