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管理器:
管理器要做的事有
- 使用备忘录按顺序记住所有command
- undo/redo操作,并记住undo/redo到了哪一步
- 当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,保持系统的简单。
命令模式的优点已经清楚了,缺点也比较明显,一个操作就是一个命令,项目大的话命令会非常多,也是个麻烦的点。
TypeScript设计模式之备忘录、命令的更多相关文章
- 乐在其中设计模式(C#) - 备忘录模式(Memento Pattern)
原文:乐在其中设计模式(C#) - 备忘录模式(Memento Pattern) [索引页][源码下载] 乐在其中设计模式(C#) - 备忘录模式(Memento Pattern) 作者:webabc ...
- 折腾Java设计模式之备忘录模式
原文地址:折腾Java设计模式之备忘录模式 备忘录模式 Without violating encapsulation, capture and externalize an object's int ...
- 设计模式:备忘录(Memento)模式
设计模式:备忘录(Memento)模式 一.前言 备忘录模式用于保存和恢复对象的状态,相信大家看过我前面的拙作就会想到原型模式也能保存一个对象在某一个时刻的状态,那么两者有何不同的呢?原型模式保存 ...
- C#设计模式:备忘录模式(Memento Pattern)
一,C#设计模式:备忘录模式(Memento Pattern) 1.发起人角色(Originator):记录当前时刻的内部状态,负责创建和恢复备忘录数据.负责创建一个备忘录Memento,用以记录当前 ...
- 北风设计模式课程---备忘录(Memento)模式
北风设计模式课程---备忘录(Memento)模式 一.总结 一句话总结: 备忘录模式也是一种比较常用的模式用来保存对象的部分用于恢复的信息,和原型模式有着本质的区别,广泛运用在快照功能之中.同样的使 ...
- js设计模式——7.备忘录模式
js设计模式——7.备忘录模式 /*js设计模式——备忘录模式*/ // 备忘类 class Memento { constructor(content) { this.content = conte ...
- 设计模式之备忘录模式(Memento)
备忘录模式(Memento) 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可将该对象恢复到原先保存的状态. Originator(发起人):负责创建一个备忘录 ...
- 【TS】358- 浅析 TypeScript 设计模式
点击上方"前端自习课"关注,学习起来~ 作者:DD菜 https://zhuanlan.zhihu.com/p/43283016 设计模式就是软件开发过程中形成的套路,就如同你在玩 ...
- java设计模式之备忘录模式
备忘录模式 备忘录模式是一种软件设计模式:在不破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可将该对象恢复到原先保存的状态.一听到备忘录这个字的时候想起了小小时打的游 ...
随机推荐
- Angular - - 脏值检查及其相关
今天突然就想写写$digest和$apply,这些都是脏值检查的主体内容. 先以普通js来做一个简单的监控例子吧: var div = ducoment.getElementById("my ...
- 用Linux命令行获取本机外网IP地址
引言:目前获取ip的方法中,ifconfig和ip获取函数得到的都是内网ip.有时候需要获取外网ip,目前通用的做法,是向外部服务器发送请求,解析外部服务器响应,从而得到的自己的外网ip.linux下 ...
- bootstrap 基础表单 内联表单 横向表单
bootstrap 基础表单 内联表单 横向表单 <!DOCTYPE html> <html> <head> <title></title> ...
- [html5]学习笔记一 新增的非主体结构元素
html新增加的非主体结构元素,主要是用来表示附加信息的,包括header,footer,hgroup,address元素. 1.header元素 header元素是一种具有引导和导航作用的结构元素, ...
- ABP入门系列(10)——扩展AbpSession
ABP入门系列目录--学习Abp框架之实操演练 源码路径:Github-LearningMpaAbp 一.AbpSession是Session吗? 1.首先来看看它们分别对应的类型是什么? 查看源码发 ...
- 关于data-xxx属性大小写不敏感,不识别大写的几点总结
1.所有标签属性,没有大小写区分,都是小写,如:data-userID,在输出后会变成data-userid,前者只能获取到'undefined'. 2.dataset 自动把 - 转换为驼峰.类似的 ...
- 访问量分类统计(QQ,微信,微博,网页,网站APP,其他)
刚准备敲键盘,突然想起今天已经星期五了,有点小兴奋,一周又这么愉快的结束,又可以休息了,等等..我好像是来写Java博客的,怎么变成了写日记,好吧,言归正传. 不知道大家有没有遇到过这样的需求:统计一 ...
- C#操作XML方式
前言 前一篇XML读取,现在咱们继续XML操作相关 C#中也有三种操作(增.删.改.查)XML文件方法如下: 使用 XmlDocument(DOM模式) 使用 XmlTextWriter(流模式) 使 ...
- GCD 多线程 ---的记录 iOS
先写一个GCD static UserInfoVoModel *userInfoShare = nil; +(instancetype)shareUserInfoVoModel { static di ...
- 【easyui】之treegrid的分页
easyui官网给的treegrid的分页是相当的复杂,我们来简化一下! 首先treegrid 分页和 datagrid一样需要设置一系列参数! 如下: depTreeGrid=$("#de ...