Javascript乱弹设计模式系列(5) - 命令模式(Command)


前言

博客园谈设计模式的文章很多,我也受益匪浅,包括TerryLee吕震宇等等的.NET设计模式系列文章,强烈推荐。对于我,擅长于前台代码的开发,对于设计模式也有一定的了解,于是我想结合Javascript来设计前台方面的“设计模式”,以对后台“设计模式”做个补充。开始这个系列我也诚惶诚恐,怕自己写得不好,不过我也想做个尝试,一来希望能给一些人有些帮助吧,二来从写文章中锻炼下自己,三来通过写文章对自己增加自信;如果写得不好,欢迎拍砖,我会虚心向博客园高手牛人们学习请教;如果觉得写得还可以,谢谢大家的支持了:)

今天开始介绍命令模式,并且将利用命令模式设计一个简单的在线编辑器

概述

在各种各样的行为实现中,行为请求者与行为实现者紧密耦合,当每增加一个行为实现的时候,行为请求者必须增加一个对行为的处理,这样就需要大量改动请求者的操作,显然这样不利于维护和扩展。为了让行为请求者和行为实现者解耦,可以将行为封装为一个命令对象,但需要处理行为时,只要请求者知道命令对象,它本身不需要知道命令对象都做些什么,命令对象负责执行 接收者 的真正实现,这样就达到二者之间松耦合的目的。

定义

命令模式是将请求封装成对象,这可以让你使用不同请求、队列,或者日志请求来参数化其他对象。命令模式也可以支持撤销操作。

类图

实例分析

现在开始利用命令模式来应用到一个在线编辑器的场景中,并且详细分析一下:

这里先给大家看下效果图(兼容多浏览器):

这里我引用了我的第4篇-组合模式的菜单例子进行改进;将命令模式和组合模式相结合的方式来阐述例子。

其中主菜单包括:

  • 文件(子菜单:新建、导出、退出)
  • 编辑(子菜单:剪切、复制、粘贴、删除)
  • 格式(子菜单:字体、字号、加粗、斜体、下划线、位置、编号、字体颜色)
  • 插入(子菜单:插入链接、插入图片),操作(撤销、重做、切换HTML)
  • 自定义格式(子菜单:格式1)
  • 帮助(子菜单:关于作者)

编辑器的这些功能实际上很常用。最后点击“得到HTML值”的按钮,可以得到HTML的内容,这里就可以按照您的需要来存储编辑器内容;文章的最后我将附上源代码。

1. 首先添加一个ICommand.js文件,其中定义一个Command接口:

varICommand=newInterface("ICommand",[["execute"]]);

其中execute作为Command执行的接口方法;

关于接口的定义,可以参考我的第0篇-面向对象基础以及接口和继承类的实现的实现。

2. 添加一个ConcreteCommand.js文件,作为继承ICommand接口的所有具体实现类:

因为子菜单中的每个按钮都可作为一个具体实现类,那么我以“加粗”按钮为例,就可以得到:

//加粗
functiononBoldCommand(){
Interface.registerImplements(
this,ICommand);
}
onBoldCommand.prototype.execute
=function(){
vareditor=window.frames["HtmlEditor"];
editor.document.execCommand(
"Bold",false,false);
editor.focus();
};

其中Interface.registerImplements(this, ICommand);说明它继承于ICommand接口,而execute方法作为基于接口方法的具体实现,这里实现了文档加粗功能;

3. 现在要创建“主菜单”和“子菜单”,Menu类和MenuItem类,注意这里“子菜单”也可能是Menu类,比如“格式”主菜单下的“位置”下面还包括下级菜单“居左对齐”,“居中对齐”,“居右对齐”等等,所以作为“叶子”结点的菜单就以MenuItem类来实现


Menu类的实现如下:

functionMenu(text,title,href){
this.menuComponents=newArray();
this.text=text;
this.title=title;
this.href=href;
Interface.registerImplements(
this,MenuComponent);
}

Menu.prototype
={
getElement:
function(){
if(this.menuComponents.length==0)
{
thrownewError(this.text+"菜单下没有子菜单");
}
varliElement=document.createElement("li");
liElement.className
="Menu-WithChildren";
liElement.title
=this.title;
varanchor=document.createElement("a");
anchor.className
="Menu-Link";
anchor.href
=this.href;
liElement.appendChild(anchor);
anchor.innerHTML
=this.text;
varulElement=document.createElement("ul");
liElement.appendChild(ulElement);
for(vari=0,len=this.menuComponents.length;i<len;i++)
{
ulElement.appendChild(
this.menuComponents[i].getElement());
}
returnliElement;
},
add:
function(component){
this.menuComponents.push(component);
},
remove:
function(component){
for(vari=0,len=this.menuComponents.length;i<len;i++)
{
if(this.menuComponents[i]==component)
{
this.menuComponents.splice(i,1);
break;
}
}
},
removeAt:
function(index){
if(this.menuComponents.length<=index)
{
this.menuComponents.splice(index,1);
}
else
{
thrownewError("索引操作数组超过上限");
}
}
}

Menu继承于MenuComponent接口(var MenuComponent = new Interface("MenuComponent", [["getElement"]]);),并且在上一篇组合模式讲过,它作为Composite复合元素。


MenuItem的实现如下:

functionMenuItem(text,title,href,command){
this.text=text;
this.title=title;
this.href=href;
this.command=command;
Interface.registerImplements(
this,MenuComponent);
}

MenuItem.prototype
={
getElement:
function(){
varliElement=document.createElement("li");
liElement.className
="Menu-Leaf";
liElement.title
=this.title;
varanchor=document.createElement("a");
anchor.href
=this.href;
liElement.appendChild(anchor);
anchor.innerHTML
=this.text;
varcommand=this.command;
addEvent(anchor,
"click",function(){
command.execute();
});
returnliElement;
}
}

其中参数command是作为传递进来的“命令对象”,比如前面的onBoldCommand的“加粗”命令对象,并且通过方法getElement()实现菜单按钮的点击触发达到命令对象的触发请求,而MenuItem不需要知道命令对象的具体实现;

addEvent(anchor, "click", function(){
command.execute();
});

通过这句代码来添加点击事件从而执行命令对象的实现;

4. 既然有“主菜单”,“子菜单”,那么就需要有个“导航条”来“挂接”它们了,这里我添加了一个MenuBar对象,它作为一个初始化菜单显示和所有命令按钮行为实现方法的“容器”,代码如下:

varMenuBar={
list:
newArray(),
add:
function(component){
this.list.push(component);
},
show:
function(container){
varulElement=document.createElement("ul");
ulElement.className
="Menu";
for(vari=0,len=this.list.length;i<len;i++){
ulElement.appendChild(
this.list[i].getElement());
}
document.getElementById(container).appendChild(ulElement);
}
}

通过show方法进行初始化菜单显示和所有命令按钮行为的实现方法;

5. 现在利用命令模式来进行在线编辑器的实现,新建HTML页面,在window.onload方法中实现:

varfile_menu=newMenu("文件","文件","#");
file_menu.add(
newMenuItem("新建","新建","#",newonNewCommand()));
file_menu.add(
newMenuItem("导出","导出","#",newonExportCommand()));
file_menu.add(
newMenuItem("退出","退出","#",newonExitCommand()));
varedit_menu=newMenu("编辑","编辑","#");
edit_menu.add(
newMenuItem("剪切","剪切","#",newonCutCommand()));
edit_menu.add(
newMenuItem("复制","复制","#",newonCopyCommand()));
edit_menu.add(
newMenuItem("粘贴","粘贴","#",newonPasteCommand()));
edit_menu.add(
newMenuItem("删除","删除","#",newonDeleteCommand()));

varformat_menu=newMenu("格式","格式","#");
format_menu.add(
newMenuItem("字体","字体","#",newonFontFaceCommand()));
format_menu.add(
newMenuItem("字号","字号","#",newonFontSizeCommand()));
format_menu.add(
newMenuItem("加粗","加粗","#",newonBoldCommand()));
format_menu.add(
newMenuItem("斜体","斜体","#",newonItalicCommand()));
format_menu.add(
newMenuItem("下划线","下划线","#",newonUnderlineCommand()));
varformat_menu_1=newMenu("位置","位置","#");
format_menu_1.add(
newMenuItem("居左对齐","居左对齐","#",newonLeftCommand()));
format_menu_1.add(
newMenuItem("居中对齐","居中对齐","#",newonCenterCommand()));
format_menu_1.add(
newMenuItem("居右对齐","居右对齐","#",newonRightCommand()));
format_menu_1.add(
newMenuItem("减少缩进","减少缩进","#",newonOutdentCommand()));
format_menu_1.add(
newMenuItem("增加缩进","增加缩进","#",newonIndentCommand()));
format_menu.add(format_menu_1);
varformat_menu_2=newMenu("编号","编号","#");
format_menu_2.add(
newMenuItem("数字编号","数字编号","#",newonOrderedCommand()));
format_menu_2.add(
newMenuItem("项目编号","项目编号","#",newonUnorderedCommand()));
format_menu.add(format_menu_2);
varformat_menu_3=newMenu("字体颜色","字体颜色","#");
format_menu_3.add(
newMenuItem("前景颜色","前景颜色","#",newonForeColorCommand()));
format_menu_3.add(
newMenuItem("背景颜色","背景颜色","#",newonBackColorCommand()));
format_menu.add(format_menu_3);

varinsert_menu=newMenu("插入","插入","#");
insert_menu.add(
newMenuItem("插入链接","插入链接","#",newonLinkCommand()));
insert_menu.add(
newMenuItem("插入图片","插入图片","#",newonImageCommand()));

varopr_menu=newMenu("操作","操作","#");
opr_menu.add(
newMenuItem("撤销","撤销","#",newonUndoCommand()));
opr_menu.add(
newMenuItem("重做","重做","#",newonRedoCommand()));
opr_menu.add(
newMenuItem("切换HTML","切换HTML","#",newonToHtmlCommand()));

varcustom_menu=newMenu("自定义格式","自定义格式","#");
custom_menu.add(
newMenuItem("格式1","加粗+斜体+下划线","#",newonMacro1Command(newonBoldCommand(),newonItalicCommand(),newonUnderlineCommand())));

varhelp_menu=newMenu("帮助","帮助","#");
help_menu.add(
newMenuItem("关于作者","关于作者","#",newonAuthorCommand()));

MenuBar.add(file_menu);
MenuBar.add(edit_menu);
MenuBar.add(format_menu);
MenuBar.add(insert_menu);
MenuBar.add(opr_menu);
MenuBar.add(custom_menu);
MenuBar.add(help_menu);

MenuBar.show(
"main_container");

各个命令对象作为命令参数传递给对应的子菜单项对象中,其中我们发现有个onMacro1Command的命令对象,它里面包含一系列的其他单命令对象,这个菜单按钮属于“自定义格式”。顾名思义,这里执行一个新的命令,它包括一连串的单命令,“加粗+斜体+下划线”,onMacro1Command类实现如下:

functiononMacro1Command(){
this.commands=newArray();
for(vari=0,len=arguments.length;i<len;i++)
{
this.commands.push(arguments[i]);
}
Interface.registerImplements(
this,ICommand);
}
onMacro1Command.prototype.execute
=function(){
for(vari=0,len=this.commands.length;i<len;i++)
{
this.commands[i].execute();
}
};

这里通过一个commands数组存储这一系列的命令对象,当onMacro1Command对象执行execute方法时,就一次性地执行数组中的所有命令对象;

6. 还有其他一些JS函数介绍下:

functionaddEvent(target,event_type,handler){
if(target.addEventListener)
target.addEventListener(event_type,handler,
false);
elseif(target.attachEvent)
target.attachEvent(
"on"+event_type,handler);
else
target[
"on"+event_type]=handler;
}
//弹出DIV层
functionshowDiver(str,width,height){
variWidth=width;
variHeight=height;
document.getElementById(
"diver").style.width=iWidth+"px";
document.getElementById(
"diver").style.height=iHeight+"px";
document.getElementById(
"diver").style.left=(document.body.clientWidth-iWidth)/2+"px";
document.getElementById("diver").style.top=(document.body.clientHeight-iHeight)/2+"px";
document.getElementById("diver").style.display="inline";
document.getElementById(
"divMore").innerHTML=str;
}
//关闭DIV层
functioncloseDiver(){
document.getElementById(
"diver").style.display="none";
document.getElementById(
"divMore").innerHTML="";
}

其中addEvent方法为了兼容各个浏览器的绑定事件的实现。

7. 至于ConcreteCommand各种命令类的实现,请下载源代码自己查看研究吧,这里不进行讲述了。


附:源代码下载

总结

该篇文章用Javascript设计命令模式的思路,实现一个简单的在线编辑器。

本篇到此为止,谢谢大家阅读!

最后祝:大家在新的一年里,工作顺利,事业进步,牛年牛运!Fighting!!!

参考文献:《Head First Design Pattern》

《Professional Javascript Design Patterns》

本系列文章转载时请注明出处,谢谢合作!

相关系列文章:
Javascript乱弹设计模式系列(6) - 单件模式(Singleton)
Javascript乱弹设计模式系列(5) - 命令模式(Command)
Javascript乱弹设计模式系列(4) - 组合模式(Composite)
Javascript乱弹设计模式系列(3) - 装饰者模式(Decorator)
Javascript乱弹设计模式系列(2) - 抽象工厂以及工厂方法模式(Factory)
Javascript乱弹设计模式系列(1) - 观察者模式(Observer)
Javascript乱弹设计模式系列(0) - 面向对象基础以及接口和继承类的实现

优质内容筛选与推荐>>
1、SOA (service oriented architecture)
2、RabbitMQ-Windows单机集群搭建
3、站外seo详解!
4、mysql 批量更新和批量插入
5、算法模型定义介绍


长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号





    联系我们

    欢迎来到TinyMind。

    关于TinyMind的内容或商务合作、网站建议,举报不良信息等均可联系我们。

    TinyMind客服邮箱:support@tinymind.net.cn