js设计模式系列之(三)命令模式

命令模式

望文生义,所谓的命令模式其实就是:
发出一定的指令,然后由对象接受并且执行

需要强调一点,就是对于命令的发出者来说,他并不知道该命令是给谁,执行效果是怎样,他只管发出命令就行。听到这里感觉和发布订阅者模式有异曲同工的效果。 但事实上,他们两者应用的场景还是有比较大的区分。不仅写法上有不同,而且执行的过程也有所不同。要知道在命令模式里面执行的效果是1对1,而在订阅者模式里面是1对>=1的.

你在逼逼什么?

哦,说明白一点。 就和上课一样。 老师进教室了,首先说:“上课!”. 接着,你们的monitor 会立马接上: “起立!”。 然后,你们就会异口同声的说:”老湿好~”。 没错,分析一下。 当老师说上课的时候,他并不会知道谁会说起立,比如今天班长谈恋爱去了,那副班长顶上。 而且,说完起立之后,副班长也不知道谁会说老湿好。 也就是命令的发出者,只管发出一个命令,然后你们只管执行就over了.

命令模式结构

通过上面的例子,大家大致可以体会大,命令模式应该是有几个部分的。

  • setCommand: 抽象的命令类(即,绑定命令)
  • Command: 具体的命令类(即,执行的行为,提供统一的接口)
  • Invoker: 调用者(即,谁来执行这个命令)
  • Receiver: 接受者(命令作用的对象)
  • Client: 客户类(提供给客户端使用的类)

具体的联系,可以参照这张图:

图片

这是JAVA里面 命令模式的类。 其实在JS中,我们只需要了前三个就已经enough了。 这样才能体现出一门完美的,漂亮的,优美的,动态语言的特点。

talk is cheap, show u code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//事件发出者
var setCommand = function(ele,command){ //命令的绑定者
ele.onclick = function(){
command.do();
}
}
//事件的执行者

var location = (function() { //执行事件类
var ball = getEle("#ball");
var move = function(direct) {
return function() {
var style = ball.style,
dir = parseInt(style[direct]);
style[direct] = `${dir-200}px`;
}
}
var moveUp = move("top");
var moveDown = move("down");
var moveLeft = move("left");
var moveRight = move("right");
return {
moveUp,
moveDown,
moveLeft,
moveRight
}
})();
//封装命令
var MoveUp = function(exer){
this.exer = exer;
}
MoveUp.prototype.do = function(){
this.exer.moveUP();
}
var MoveDown = function(exer){
this.exer = exer;
}
MoveDown.prototype.do = function(){
this.exer.moveDown();
}
var MoveLeft = function(exer){
this.exer = exer;
}
MoveLeft.prototype.do = function(){
this.exer.moveLeft();
}
var MoveRight = function(exer){
this.exer = exer;
}
MoveRight.prototype.do = function(){
this.exer.moveRight();
}
setCommand(getEle("upBtn"), new MoveUp(location)); //给向上的button,绑定向上的执行程序
setCommand(getEle("downBtn"), new MoveDown(location)); //...
setCommand(getEle("leftBtn"), new MoveLeft(location)); //...
setCommand(getEle("rightBtn"), new MoveRight(location)); //....

可以清晰的看到,在命令模式中,触发事件(onclick)和执行程序(command.do())都是已知的。 但是这个执行的消息给谁,或者执行产生的效果是怎样的,在命令的发出者这一方都是未知的。需要注意的是,这时候的未知只限于命令的发出者而言。也就是现在命令模式将发出者和执行者给解耦开,即,可变的部分和不可变的部分分开。

上面逼逼这么多到底在说shenme…

其实一切原理都是枯燥的,实例才是王道。 来,我们来做个比较。也就是不使用命令模式,直接写上面的例子(偷个懒,只写moveUP部分).

1
2
3
4
5
6
7
var ele = getEle("#ball");
getELe(".moveUp").onclick = function(){
var style = this.style,
dir = parseInt(style["top"]);
style["top"] = `${dir-200}px`;
}
}

上面的代码同样能完成上面辣么辣么长的代码完成的效果,那为什么还要使用上面的写法呢?

艹~ 请问,你下面那段代码,能体现你的bigger吗? 能体现你是代码艺术家的feeling吗?能体现你的思维能力吗?

No No No~
我们来分析下why.

首先下面那段代码可以完成上面的功能,但是万一有一天,一个名叫产经的生物和你说

"亲爱的,你能不能在加一个button,让这个球可以斜着走,可以转个圈呢? 哈哈,我相信你一定可以的。"

呵呵,你话都没说。 想当然这个锅,你必须背。好吧,那开始做吧。(用那个渣渣代码演示一遍).

1
2
3
4
5
6
7
8
9
10
11
12
function getY(x){
var k = 1.2;
return k*x;
}
getELe("#diagnoal").onclick = function(){
var style = ele.style,
x = parseInt(style['left']),
y = parseInt(style['top']),
style['left'] = `${x-200}px`;
style["top"] = `${y-getY(200)}px`;
}
}

可以想象,最后如果产经的需求不断增多,那么你在事件处理的回调会越来越复杂,比如:

"亲爱的,你斜着走都实现了,那4个方向能不能都可以走呢?"

我想这时候,你应该会懵逼了。不要紧,我们可以使用命令模式来弥补这个缺陷,因为命令模式最大的一个扩展性就是命令者和命令的执行者分开了。而且在上面面向过程的代码中,看不出什么逻辑出来,只是知道,这个click是触发什么的。 而事件回调中的代码重用性也是非常低的。

这里使用命令模式重构一遍

//其他的还是一样,这里主要将4个方向的代码重构一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var location = (function() { //执行事件类
var ball = getEle("#ball");
var compMove = function(hori,vert) { //垂直和水平方向
var k = 1.2; //移动的斜率
var getY = function() {
return k * x;
}
return function() {
var style = ball.style,
x = parseInt(style[hori]), //水平方向上的位置
y = parseInt(style[vert]); //垂直方向上的位置
style[hori] = `${x-200}px`;
style[vert] = `${y-getY(200)}px`; //执行移动
}
};
//斜方向绑定代码
var moveLU = compMOve("left","top");
var moveRU = compMOve("right","top");
var moveLB = compMOve("left","bottom");
var moveRB = compMOve("right","bottom");
return {
moveLU
moveRU,
moveLB,
moveRB
}
})();
//封装命令
var MoveLU = function(exer) {
this.exer = exer;
}
MoveLU.prototype.do = function() {
this.exer.moveLU();
}
setCommand(getEle("leftUpBtn"), new MoveLU(location)); //给向上的button,绑定向上的执行程序

可以看出来,虽然代码多,但是至少我们将改动的地方降到最低了。

setCommand这个不变,变的只是绑定click的对象和执行者。 这样可以清楚的说明命令模式的优势到底在哪里。

当然,我们还可以做一个优化,要知道,js是一门函数至上的语言,因为函数可以像参数一样被传来传去,所以可以这样改写命令的绑定者.

1
2
3
4
5
6
var setCommand  = function(ele,fn){
ele.onclick = function(){
fn();
}
}
setCommand(getEle("leftUpBtn",()=>{location.moveLU()}))); //给向上的button,绑定向上的执行程序

这样就可以省去中间一大堆的事件修饰,从而将函数直接暴露使用。推荐这样写法,因为这个才是js的真正实力。而且上面也使用了es6的新语法,箭头函数,如果感兴趣的同学可以参考 阮老师的 ECMAscript6的讲解哦。超棒!!!

要知道一个模式的精华不是看他能怎么用,而是要看你怎么用他。

命令模式实现缓存效果

其实,缓存并不是什么高上大的东西,就是在函数里名,有一个变量来保存你的结果,而你可以遍历这个结果. 这里使用了计算一个数的阶乘,这是比较耗内存的算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

function fb(num) { //阶乘函数
if (num <= 1) {
return 1;
}
return num * fb(--num)
}
//缓存代理出场了
var cProxy = (function() {
var cache = {};
return function(num) {
if (cache[num]) {
console.log(`this is cache ${cache[num]}`);
return cache[num];
}
return cache[num] = fb(num);
}
})();
//测试
console.log(cProxy(4)); //24
cProxy(4); //"this is cache 24"

上面是我以前写代理缓存的例子。 里面有个叫cache的东西,就是来保存你的结果(放在内存中),以备下次使用。这其实就是缓存使用的基本原理。

而命令模式的缓存有个极大的用途就是一个 撤销和重做的效果(也可以说,命令模式是实现撤销功能的最好的模式).

在上面的例子中可以保留每一个节点小球的位置(简单起见,还是以最初的上下左右为基准吧)

由于代码过长,我放在fiddle里面.
撤销实例

其实,命令模式的缓存效果并不单单只是用到了命令模式,在middleCommand里,我们使用到的是代理模式,这样在执行的同时,能够将执行的结果缓存起来。
ok~

talk is cheap, show u code

1
2
3
4
5
6
7
8
9
10
11
12
13
//事件发出者
var setCommand = (function() {
var cancelBtn = getEle("#cancel"); //设置回退按钮
cancelBtn.onclick = function() {
middleCommand().undo();
}
return function(ele, middleCommand) {
ele.onclick = function() {
middleCommand.exe();
}
}

})();

这是事件发出者做的一些事情,给执行的节点绑定指定的事件。这应该没有什么需要讲解的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//封装命令
var middleCommand = (function() {
var result = [];
return (flag) => {
var exe = function() {
result.push(locat.getLoc());
locat[flag]();
}
var undo = function() {
var loc = result.pop(); //取出最近的放入的位置
if (!loc) {
alert("不能再退了~");
return;
}
locat.back(loc.left, loc.top);
}
return {
exe, undo
}
}
})();

这一部分,就是命令模式实现缓存的精华。 和我上面所列写的例子一样,这里命令者模式+代理模式,很好的实现缓存命令结果的effect. 当在执行”回退”的效果的时候,我们只需要从result数组里面按顺序取出命令结果就over了。(妈妈再也不用担心我的学习啦~ so easy)

ok~ 那大家有没有想过,为什么命令模式是实现撤销功能最好的模式呢?

In my opinion, 那你去用其他模式实现啊~
(233333)

上面纯属意淫,其实,大家回想一下,我们使用一个application的时候,就比如用PS吧, 我们从开始使用PS进行生产,接触的就是形形色色的命令(记住,这里叫命令哦),画笔,裁切,移动。。。 但在我们程序员的眼里,他们其实就是一个一个的代码块,和图形界面的结果。 对比看来,其实就和我们使用上述移动小球的例子,有着异曲同工的效果。而为什么使用命令模式的关键在于,他们就是命令,我们可以将抽象的结果,保存为命令的结果。 这也是使用命令模式最常用的一块(对不起,如果大家还是有不懂的地方的话,请google吧)

when should i use Command pattern?

Please never use it when u don’t need it.

这是实话呀~!!! 程序 永远不可能一次写的很完美, code review才是王道。
命令模式其实也有他自身的不足和天生的优势。

  • 优势

    • 完美的解决了耦合度的痛点(其实很多模式也解决了)
    • 可以设计一个命令宏,这和我下一篇说的组合模式是一样的东西
    • 当然,可以完美的实现撤销和重做的effect
  • 劣势

    • 如果,你的程序简单,请不要使用命令模式,因为他带来的结果,是庞大的集群类,类,类,类,类,累,累,累,累,累。。。
    • 任何微小的命令,都需要一个嵌入类来支持,这很蛋疼。

最后,再说一遍:

Please never use it when u don’t need it.

祝大家编程愉快~

猫友会其事

本无心插柳, 却慢慢有一定影响力,猫友会是一个邀请制的互联网优秀人才的交流平台, 以鄂籍或在鄂求学过的同学们为主。 目前在推进的一些事情:

  • 猫友会大讲坛,分享产品/技术/创业心得
  • 猫友信息平台,对接武汉最优秀的团队以及愿意回武汉的优秀人才。
  • 猫友公众号,优质原创产品/技术文章
  • 猫友读书会,阅读+思考+总结,筹备中

长按关注猫头鹰技术公众号(mtydev)

img

留言

本站总访问量

留言