Typescript 命令模式(Command)

标签: 前端 设计模式 命令模式 typescript Command


请仔细阅读下面代码,理解其中的设计理念。

command.jpg

command-client.jpg

命令模式

命令模式: 命令模式是一中封装方法调用的方式,用来将行为请求者和行为实现者解除耦合。

实际场景

在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。

命令模式的结构

  • Client 客户
  • Invoker 调用命令的对象
  • Command 具体的命令
  • Receiver 真正的命令执行对象

命令模式的例子

龟兔赛跑:

  • 喜欢偷懒的兔子每个阶段移动0-60米
  • 持之以恒的乌龟每个阶段移动10-50米
  • 可以向右或者向左移动
  • 每个阶段可以回滚
  • 输出移动日志
  • 支持添加更多参赛者

套用命令模式的结构,我们得到
Command: 向右移动(支持取消操作)、 向左移动(支持取消操作)...
Receiver: 兔子、乌龟、...

定义基本类型

/* command-interface.ts */
// 命令接口
export interface ICommand {
    execute: (step: number) => any
    undo: () => any
}

/* target-interface.ts */
// 参赛队员接口
export interface ITarget {
    name: string
    position: number
}

/* receiver-interface.ts */
// 接收者接口
export interface IRightReceiver {
    moveRight: (step: number) => any;
    rightUndo: () => any;
}

export interface ILeftReceiver {
    moveLeft: (step: number) => any;
    leftUndo: () => any;
}

/* command-class.ts */
// 命令类型
export enum CommandClass {
    GO_RIGHT = 'GO_RIGHT',
    GO_LEFT = 'GO_LEFT',
}

移动命令类

/* left-command.ts */
// 向左移动类
import { ILeftReceiver } from '../receiver-models/receiver-interface';
import { ICommand } from './command-interface';

export class LeftCommand implements ICommand {
    protected receivers: ILeftReceiver[] = [];

    public addReceiver (receiver: ILeftReceiver) {
        this.receivers.push(receiver);
        return this;
    }

    public removeReceiver (receiver: ILeftReceiver) {
        const index = this.receivers.findIndex(item => item === receiver);
        if (index !== -1) {
            this.receivers.splice(index, 1);
        }
    }

    public execute (step: number) {
        this.receivers.map((receiver: ILeftReceiver) => {
            receiver.moveLeft(step);
        });
    }

    public undo () {
        this.receivers.map((receiver: ILeftReceiver) => {
            receiver.leftUndo();
        });
    }
}

/* right-command.ts */
// 向右移动类
import { IRightReceiver } from '../receiver-models/receiver-interface';
import { ICommand } from './command-interface';

export class RightCommand implements ICommand {
    protected receivers: IRightReceiver[] = [];

    public addReceiver (receiver: IRightReceiver) {
        this.receivers.push(receiver);
        return this;
    }

    public removeReceiver (receiver: IRightReceiver) {
        const index = this.receivers.findIndex(item => item === receiver);
        if (index !== -1) {
            this.receivers.splice(index, 1);
        }
    }

    public execute (step: number) {
        this.receivers.map((receiver: IRightReceiver) => {
            receiver.moveRight(step);
        });
    }

    public undo () {
        this.receivers.map((receiver: IRightReceiver) => {
            receiver.rightUndo();
        });
    }
}

接受者类

/* rabbit-receiver.ts */
// 兔子
import { ILeftReceiver, IRightReceiver } from './receiver-interface';
import { ITarget } from './target-interface';

export class RabbitReceiver implements IRightReceiver, ILeftReceiver {
    public rabbit: ITarget;
    private rightLogs: number[] = [];
    private leftLogs: number[] = [];

    constructor (rabbit: ITarget) {
        this.rabbit = rabbit;
    }

    private getPosition () {
        console.log(`Now rabbit is at ${this.rabbit.position}`);
    }

    public moveRight (step: number) {
        const moveStep = parseInt(Math.random() * 60) * step;
        this.rabbit.position += moveStep;
        this.rightLogs.unshift(moveStep);
        console.log(`Rabbit move right ${moveStep}`);
        this.getPosition();
    }

    public rightUndo () {
        this.rabbit.position -= Number(this.rightLogs[0]);
        this.rightLogs.shift();
        this.getPosition();
    }

    public moveLeft (step: number) {
        const moveStep = parseInt(Math.random() * 60) * step;
        this.rabbit.position -= moveStep;
        this.leftLogs.unshift(moveStep);
        console.log(`Rabbit move left ${moveStep}`);
        this.getPosition();
    }

    public leftUndo () {
        this.rabbit.position += Number(this.leftLogs[0]);
        this.leftLogs.shift();
        this.getPosition();
    }
}

/* turtle-receiver.ts */
// 乌龟
import { ILeftReceiver, IRightReceiver } from './receiver-interface';
import { ITarget } from './target-interface';

export class TurtleReceiver implements IRightReceiver, ILeftReceiver {
    public turtle: ITarget;
    private rightLogs: number[] = [];
    private leftLogs: number[] = [];

    constructor (turtle: ITarget) {
        this.turtle = turtle;
    }

    private getPosition () {
        console.log(`Now turtle is at ${this.turtle.position}`);
    }

    public moveRight (step: number) {
        const moveStep = (parseInt(Math.random() * 30) + 10) * step;
        this.turtle.position += moveStep;
        this.rightLogs.unshift(moveStep);
        console.log(`Turtle move right ${moveStep}`);
        this.getPosition();
    }

    public rightUndo () {
        this.turtle.position -= Number(this.rightLogs[0]);
        this.rightLogs.shift();
        this.getPosition();
    }

    public moveLeft (step: number) {
        const moveStep = (parseInt(Math.random() * 30) + 10) * step;
        this.turtle.position -= moveStep;
        this.leftLogs.unshift(moveStep);
        console.log(`Turtle move left ${moveStep}`);
        this.getPosition();
    }

    public leftUndo () {
        this.turtle.position += this.leftLogs[0](this.leftLogs[0]);
        this.leftLogs.shift();
        this.getPosition();
    }
}

invoker

import { CommandClass } from './command-models/command-class';
import { LeftCommand } from './command-models/left-command';
import { RightCommand } from './command-models/right-command';

export class CommandLogModel {
    id: number;
    name: CommandClass;
    step: number;
}

export class MoveInvoker {
    private rightCommand: RightCommand;
    private leftCommand: LeftCommand;

    public logs: CommandLogModel[] = [];

    constructor (commands: { rightCommand: RightCommand, leftCommand: LeftCommand }) {
        this.rightCommand = commands.rightCommand;
        this.leftCommand = commands.leftCommand;
    }

    public moveRight (step: number) {
        console.log(`Do right command with step ${step}`);
        this.rightCommand.execute(step);
        this.logs.unshift({
            id: Date.now(),
            name: CommandClass.GO_RIGHT,
            step: step
        });
    }

    public moveLeft (step: number) {
        console.log(`Do left command with step ${step}`);
        this.leftCommand.execute(step);
        this.logs.unshift({
            id: Date.now(),
            name: CommandClass.GO_LEFT,
            step: step
        });
    }

    public undo () {
        console.log(`Do undo command`);
        if (this.logs.length) {
            const commandLog = this.logs[0];
            switch (commandLog.name) {
                case CommandClass.GO_RIGHT:
                    this.rightCommand.undo();
                    break;
                case  CommandClass.GO_LEFT:
                    this.leftCommand.undo();
                    break;
                default:
            }
            this.logs.shift();
        }
    }
}

客户

import { LeftCommand } from './command-models/left-command';
import { RightCommand } from './command-models/right-command';
import { MoveInvoker } from './move-invoker';
import { RabbitReceiver } from './receiver-models/rabbit-receiver';
import { ITarget } from './receiver-models/target-interface';
import { TurtleReceiver } from './receiver-models/turtle-receiver';

export class Client {
    public rabbit: ITarget = {
        name: 'rabbit',
        position: 0
    };
    public turtle: ITarget = {
        name: 'turtle',
        position: 0
    };
    public invoker: MoveInvoker;

    constructor () {
        const rightCommand = new RightCommand();
        const leftCommand = new LeftCommand();
        rightCommand.addReceiver(new RabbitReceiver(this.rabbit)).addReceiver(new TurtleReceiver(this.turtle));
        leftCommand.addReceiver(new RabbitReceiver(this.rabbit)).addReceiver(new TurtleReceiver(this.turtle));
        this.invoker = new MoveInvoker({rightCommand, leftCommand});
    }

    public moveRight (step: number) {
        this.invoker.moveRight(step);
        return this;
    }

    public moveLeft (step: number) {
        this.invoker.moveLeft(step);
        return this;
    }

    public getLogs () {
        const logs = this.invoker.logs;
        logs.map((log) => console.log(`${log.name}-${log.step}`));
        return this;
    }

    public undo () {
        this.invoker.undo();
        return this;
    }
}
new Client().moveRight(1).moveRight(1).undo().moveRight(2);

执行结果


competition-result.png

命令模式的利弊

利:

  • 将行为请求和行为实现解耦。
  • 新的命令可以很容易添加到系统中去。

弊:

  • 使用命令模式可能会导致某些系统有过多的具体命令类。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容