TypeScript设计模式之备忘录、命令

看看用TypeScript怎样实现常见的设计模式,顺便复习一下。 学模式最重要的不是记UML,而是知道什么模式可以解决什么样的问题,在做项目时碰到问题可以想到用哪个模式可以解决,UML忘了可以查,思想记住就好。 这里尽量用原创的,实际中能碰到的例子来说明模式的特点和用处。

备忘录模式 Memento

特点:通过保存对象之前的状态来使对象可以恢复到之前的样子。

用处:当对象需要保存/加载某一时刻的状态时可以考虑备忘录模式,如游戏的save/load。

注意:状态过大产生的开销。

备忘录应该经常可以看到,游戏的save/load,photoshop的历史记录,windows的还原点都是这个模式的应用。 使用时也要注意保存的状态过大时产生的开销,保存在硬盘上的还好,如果是运行时保存在内存上的,比如一些复杂对象的undo/redo操作,保存每一个状态都是很大的内存开销,这时就需要做些限制,比方设置一个历史记录栈的最大值来限定内存的使用。

备忘录的例子和下面的命令模式一起写,实现一个支持undo/redo的操作。

命令模式 Command

特点:把请求封装成命令对象,命令对象里包含有接收者,这样client只需要发送命令,接收者就可以做出相关响应或相反的响应。

用处:当需要发送者和接收者解耦时可以考虑命令模式,常用于事件响应,请求排除,undo/redo等。

注意:命令数量爆炸,需要集中维护。

下面用TypeScript简单实现一个命令模式和备忘录模式的undo/redo: 遥控器算是典型的命令模式,按个按钮就可以命令电视做相关响应,假设遥控器有三种功能,开、关和换台。

建个Command、undo/redo、备忘录以及控制接口:

interface Executable{
    execute(param: any);
}

interface UndoRedoable{
    undo(currParam: any, lastParam: any);
    redo(param: any);
}

class MemoItem{
    command: Command;
    param: any;
}
interface Memento{
    currPos: number;  

    set(item: MemoItem);
    get(): MemoItem; 
    getNext(): MemoItem; //找到下一个做redo
    findLastWithSameType(memoItem: MemoItem): MemoItem; // 找出上个同类型的command,得到参数,以这个参数来做undo操作,回到之前的状态
}

interface Controllable{
    channelNum: number;

    open();
    close();
    switchTo(channelNum: number); //换台
}

实现备忘录

class History implements Memento{
    private memoList: Array<MemoItem> = []; // 记住所有command
    static defaultMemoItem: MemoItem = { command: undefined, param: {channelNum: 0} }; // undo到第一步时前面没有command了,返回一个默认command

    currPos: number = 0;  // 当前undo/redo到了哪一个

    get currIndex(): number{  // currPos是从后往前的顺序, currIndex是正向顺序
        return this.memoList.length - this.currPos - 1;
    }

    set(item: MemoItem){  
        if(this.currPos != 0){  // 不是0的话表示已经undo过,往上叠加push前先删除后面的
            this.memoList.splice(this.currIndex + 1);
            this.currPos = 0; // 重置currPos
        }
        this.memoList.push(item);
    }

    get(): MemoItem{
        if(this.currIndex < this.memoList.length){
            return this.memoList[this.currIndex];
        }
        return History.defaultMemoItem;
    }

    getNext(): MemoItem{
        if(this.currIndex + 1 < this.memoList.length){ 
            return this.memoList[this.currIndex+1];
        }
        return History.defaultMemoItem;
    }

    findLastWithSameType(memoItem: MemoItem): MemoItem{// 找出上个同类型的command,得到参数,以这个参数来做undo操作,回到之前的状态
        for(let i = this.currIndex - 1; i >= 0; i--){
            if(memoItem.constructor.name === this.memoList[i].constructor.name){
                return this.memoList[i];
            }
        }

        return History.defaultMemoItem;
    } 
}

undo/redo可以由个专门的管理器来管理,建个undo/redo管理器: 管理器要做的事有

  1. 使用备忘录按顺序记住所有command
  2. undo/redo操作,并记住undo/redo到了哪一步
  3. 当undo/redo到了某一步时,再次有新的command,则在移除这步之后的command后再加新的command
class UndoRedoManager{

    static readonly instance: UndoRedoManager = new UndoRedoManager();

    private history: Memento = new History();

    push(command: Command, param: any){  // command执行时应该push进来
        this.history.set({command, param});
    }

    redo(){
        if(this.history.currPos == 0){ // 表示没undo过,当然redo也没必要了
            return;
        }
        let memoItem = this.history.getNext(); // 取出上次undo过的下一个command并执行
        this.history.currPos--;
        memoItem.command.redo(memoItem.param);
    }

    undo() {
        let memoItem = this.history.get();
        if(memoItem === History.defaultMemoItem){
            return;
        }
        let lastMemoItem = this.history.findLastWithSameType(memoItem); 
        this.history.currPos++;
        memoItem.command.undo(memoItem.param, lastMemoItem.param);
    }
}

抽象个Command, Command需要做到执行命令、撤消上次所做的操作及重做, 这里就可以用上面的UndoRedoManager:

abstract class Command implements Executable, UndoRedoable{

    constructor(protected controller: Controllable) { }

    execute(param: {}){
        UndoRedoManager.push(this, param);
    }

    redo(){
        UndoRedoManager.redo();
    }

    undo(){
       UndoRedoManager.undo(); 
    }
}

接下来分别实现具体的 开、关、换台命令:

class OpenCommand extends Command{

    execute(param: any){
        super.execute(param);
        this.tv.open();
    }

    undo(currParam: any, lastParam: any){
        this.tv.close();
    }
}

class CloseCommand extends Command{

    execute(param: any){
        super.execute(param);
        this.tv.close();
    }

    undo(currParam: any, lastParam: any){
        this.tv.open();
    }
}

class SwitchCommand extends Command{

    execute(param: any){
        super.execute(param);
        this.tv.switchTo(param.channelNum);
    }

    undo(currParam: any, lastParam: any){
        this.tv.switchTo(lastParam.channelNum);
    }
}

最后来实现 电视和遥控器,遥控器通常只有一个开关按钮,要么开要么关,另外遥控器可以撤消到上次选的频道,也可以取消撤消,重新回到当前的: 电视只需要做具体的事就可以了,遥控器也不需要知道命令是谁在执行,只管发命令就好,这就是命令模式的好处。

class TV implements Controllable{

    open(){
        console.log('open');
    }

    close(){
        console.log('close');
    }

    switchTo(channelNum: number){
        console.log(`switch to channel: ${channelNum}`);
    }
}

class Controller {

    isOn: boolean = false;

    constructor(private openCmd: Command, private closeCmd: Command, private switchCmd: Command){
    }

    onOff(){
        if(this.isOn){
            this.isOn = false;
            this.closeCmd.execute(null);
        } else {
            this.isOn = true;
            this.openCmd.execute(null);
        }
    }

    switchTo(channelNum: number){
        this.switchCmd.execute({channelNum: channelNum});
    }

    undo(){
        UndoRedoManager.instance.undo(); //只需要调用UndoRedoManager做undo/redo就可以了,不需要管具体的细节
    }

    redo(){
        UndoRedoManager.instance.redo();
    }
}

来看看成果: 先定个执行顺序, 打开电视 -> 3频道 -> 4频道 -> 7频道 -> 撤消 -> 撤消 -> 重做 -> 11频道 -> 12频道 -> 撤消 -> 撤消 -> 关电视

预期结果: open -> 3 -> 4 -> 7 -> 4 -> 3 -> 4 -> 11 -> 12 -> 11 -> 4 -> close 从11回到4是因为在push 11频道时的command是4,也就是7已经被删掉了。

看看具体执行结果:

let tv = new TV();
let controller = new Controller(new OpenCommand(tv), new CloseCommand(tv), new SwitchCommand(tv));

controller.onOff();
controller.switchTo(3);
controller.switchTo(4);
controller.switchTo(7);
controller.undo();
controller.undo();
controller.redo();
controller.switchTo(11);
controller.switchTo(12);
controller.undo();
controller.undo();
controller.onOff();

执行结果: open switch to channel: 3 switch to channel: 4 switch to channel: 7 switch to channel: 4 switch to channel: 3 switch to channel: 4 switch to channel: 11 switch to channel: 12 switch to channel: 11 switch to channel: 4 close

完全一样,没问题。

本来想写简单一点,不知不觉就写多了,undo/redo还是偏复杂了一些,而且这还只是最基本的架子,很多东西不严谨,有兴趣的朋友可以自己研究下,建议只针对用户常用的部分做undo/redo,保持系统的简单。

命令模式的优点已经清楚了,缺点也比较明显,一个操作就是一个命令,项目大的话命令会非常多,也是个麻烦的点。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏腾讯移动品质中心TMQ的专栏

【探索式测试基础系列】初恋的味道

在学习探索式测试的过程中,也会有酸甜苦辣,只有了解它的人才知道这种味道。不妨和探索测试一起再回味一下初恋的味道。

1.5K10
来自专栏Golang语言社区

[Go语言]采用Go语言作为服务端编程语言的建议书

按:这是我给公司(部门)写的使用推广Go语言的建议书,给领导看了以后,领导同意使用Go语言对一些服务器程序进行改写并部署到外网进行验证。希望这篇文章能够给同样在...

3887
来自专栏老九学堂

【休息室】一张图看懂Java的垃圾回收机制

? 新手程序员第一次做项目的过程…… ? 代码写好了,咱们来测试吧…… ? 一张图看懂 Java 多线程阻塞机制…… ? Bug多了,总有一个会把你坑了…… ...

3367
来自专栏腾讯移动品质中心TMQ的专栏

探索式测试基础系列--初恋的味道

一、探索式测试基础系列 1、背景 在移动互联网时代,敏捷开发是主流的开发流程,功能的快速迭代让我们面临的问题就是如何应对各种需求变更,如何提升测试效率,要解决以...

2108
来自专栏雪胖纸的玩蛇日常

老男孩Python全栈开发(92天全)视频教程 自学笔记02

2024
来自专栏Golang语言社区

[Go语言]采用Go语言作为服务端编程语言的建议书

按:这是我给公司(部门)写的使用推广Go语言的建议书,给领导看了以后,领导同意使用Go语言对一些服务器程序进行改写并部署到外网进行验证。希望这篇文章能够给同样在...

5018
来自专栏带你撸出一手好代码

框架是什么

「框架」一词在编程术语中使用的频繁程度绝对排前五, 框架的数量也成百上千倍于编程语言, 任何一门编程语言都会搭配上一定数据的框架用以提升开发软件产品的效率。 随...

2926
来自专栏IT大咖说

VMware云管平台运维管理

摘要 跨 SDDC 和多云环境从应用到基础架构的智能 IT 运维管理。与 vRealize Log Insight 和 vRealize Business fo...

4255
来自专栏技术之路

重构学习-重构原则

什么是重构: 视上下文重构有两个不同的定义,第一个定义是名词形式 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本...

1735
来自专栏Java学习网

每个程序员都需要学习 JavaScript 的7个理由

每个程序员都需要学习 JavaScript 的7个理由 最近在和招聘经理交流现在找一个好的程序员有多难的时候,我渐渐意识到了现在编程语言越来越倾重于JavaS...

2239

扫码关注云+社区