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

前言

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

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

概述

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

定义

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

类图

Javascript乱弹设计模式系列(5) - 命令模式(Command)_第1张图片

 

实例分析

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

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

Javascript乱弹设计模式系列(5) - 命令模式(Command)_第2张图片

 

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

其中主菜单包括:

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

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

 

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

var  ICommand  =   new  Interface( " ICommand " , [[ " execute " ]]);

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

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

 

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

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

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

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

 

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

 
Menu类的实现如下:

function  Menu(text, title, href) {
    
this .menuComponents  =   new  Array();
    
this .text  =  text;
    
this .title  =  title;
    
this .href  =  href;
    Interface.registerImplements(
this , MenuComponent);


Menu.prototype 
=  {
    getElement : 
function () {
        
if ( this .menuComponents.length  ==   0 )
        {
            
throw   new  Error( this .text  +   " 菜单下没有子菜单 " );
        }
        
var  liElement  =  document.createElement( " li " );
        liElement.className 
=   " Menu-WithChildren " ;
        liElement.title 
=   this .title;
        
var  anchor  =  document.createElement( " a " );
        anchor.className 
=   " Menu-Link " ;
        anchor.href 
=   this .href;
        liElement.appendChild(anchor);
        anchor.innerHTML 
=   this .text;
        
var  ulElement  =  document.createElement( " ul " );
        liElement.appendChild(ulElement);
        
for ( var  i  =   0 , len  =   this .menuComponents.length; i  <  len; i ++ )
        {
            ulElement.appendChild(
this .menuComponents[i].getElement());
        }
        
return  liElement;
    },
    add : 
function (component) {
        
this .menuComponents.push(component);
    },
    remove : 
function (component) {
        
for ( var  i  =   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
        {
            
throw   new  Error( " 索引操作数组超过上限 " );
        }
    }
}

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


MenuItem的实现如下:

function  MenuItem(text, title, href, command) {
    
this .text  =  text;
    
this .title  =  title;
    
this .href  =  href;
    
this .command  =  command;
    Interface.registerImplements(
this , MenuComponent);


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

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

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

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

 

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

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

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

 

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

var  file_menu  =   new  Menu( " 文件 " " 文件 " " # " );
file_menu.add(
new  MenuItem( " 新建 " " 新建 " " # " new  onNewCommand()));
file_menu.add(
new  MenuItem( " 导出 " " 导出 " " # " new  onExportCommand()));
file_menu.add(
new  MenuItem( " 退出 " " 退出 " " # " new  onExitCommand()));
var  edit_menu  =   new  Menu( " 编辑 " " 编辑 " " # " );
edit_menu.add(
new  MenuItem( " 剪切 " " 剪切 " " # " new  onCutCommand()));
edit_menu.add(
new  MenuItem( " 复制 " " 复制 " " # " new  onCopyCommand()));
edit_menu.add(
new  MenuItem( " 粘贴 " " 粘贴 " " # " new  onPasteCommand()));
edit_menu.add(
new  MenuItem( " 删除 " " 删除 " " # " new  onDeleteCommand())); 

var  format_menu  =   new  Menu( " 格式 " " 格式 " " # " );
format_menu.add(
new  MenuItem( " 字体 " " 字体 " " # " new  onFontFaceCommand()));
format_menu.add(
new  MenuItem( " 字号 " " 字号 " " # " new  onFontSizeCommand()));
format_menu.add(
new  MenuItem( " 加粗 " " 加粗 " " # " new  onBoldCommand()));
format_menu.add(
new  MenuItem( " 斜体 " " 斜体 " " # " new  onItalicCommand()));
format_menu.add(
new  MenuItem( " 下划线 " " 下划线 " " # " new  onUnderlineCommand()));
var  format_menu_1  =   new  Menu( " 位置 " " 位置 " " # " );
format_menu_1.add(
new  MenuItem( " 居左对齐 " " 居左对齐 " " # " new  onLeftCommand()));
format_menu_1.add(
new  MenuItem( " 居中对齐 " " 居中对齐 " " # " new  onCenterCommand()));
format_menu_1.add(
new  MenuItem( " 居右对齐 " " 居右对齐 " " # " new  onRightCommand()));
format_menu_1.add(
new  MenuItem( " 减少缩进 " " 减少缩进 " " # " new  onOutdentCommand()));
format_menu_1.add(
new  MenuItem( " 增加缩进 " " 增加缩进 " " # " new  onIndentCommand()));
format_menu.add(format_menu_1);
var  format_menu_2  =   new  Menu( " 编号 " " 编号 " " # " );
format_menu_2.add(
new  MenuItem( " 数字编号 " " 数字编号 " " # " new  onOrderedCommand()));
format_menu_2.add(
new  MenuItem( " 项目编号 " " 项目编号 " " # " new  onUnorderedCommand()));
format_menu.add(format_menu_2);
var  format_menu_3  =   new  Menu( " 字体颜色 " " 字体颜色 " " # " );
format_menu_3.add(
new  MenuItem( " 前景颜色 " " 前景颜色 " " # " new  onForeColorCommand()));
format_menu_3.add(
new  MenuItem( " 背景颜色 " " 背景颜色 " " # " new  onBackColorCommand()));
format_menu.add(format_menu_3); 

var  insert_menu  =   new  Menu( " 插入 " " 插入 " " # " );
insert_menu.add(
new  MenuItem( " 插入链接 " " 插入链接 " " # " new  onLinkCommand()));
insert_menu.add(
new  MenuItem( " 插入图片 " " 插入图片 " " # " new  onImageCommand())); 

var  opr_menu  =   new  Menu( " 操作 " " 操作 " " # " );
opr_menu.add(
new  MenuItem( " 撤销 " " 撤销 " " # " new  onUndoCommand()));
opr_menu.add(
new  MenuItem( " 重做 " " 重做 " " # " new  onRedoCommand()));
opr_menu.add(
new  MenuItem( " 切换HTML " " 切换HTML " " # " new  onToHtmlCommand())); 

var  custom_menu  =   new  Menu( " 自定义格式 " " 自定义格式 " " # " );
custom_menu.add(
new  MenuItem( " 格式1 " " 加粗+斜体+下划线 " " # " new  onMacro1Command( new  onBoldCommand(),  new  onItalicCommand(),  new  onUnderlineCommand()))); 

var  help_menu  =   new  Menu( " 帮助 " " 帮助 " " # " );
help_menu.add(
new  MenuItem( " 关于作者 " " 关于作者 " " # " new  onAuthorCommand())); 

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类实现如下:

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

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

 

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

function  addEvent(target, event_type, handler) {
    
if  (target.addEventListener) 
        target.addEventListener(event_type, handler, 
false );
    
else   if  (target.attachEvent)
        target.attachEvent(
" on "   +  event_type, handler);
    
else
        target[
" on "   +  event_type]  =  handler;
}
// 弹出DIV层
function  showDiver(str, width, height) {
    
var  iWidth  =  width;
    
var  iHeight  =  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层
function  closeDiver() {
    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) - 面向对象基础以及接口和继承类的实现

你可能感兴趣的:(JavaScript)