javascript的设计模式之六[命令模式(Command)]

概述

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

定义

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

类图


实例分析

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

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

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

其中主菜单包括:

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

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



1. 首先添加一个ICommand.js文件,其中定义一个Command接口:
程序代码 程序代码
var ICommand = new Interface("ICommand", [["execute"]]);


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

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



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设计命令模式的思路,实现一个简单的在线编辑器。

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

参考文献:《Head First Design Pattern》

《Professional Javascript Design Patterns》


[本日志由 亮亮 于 2009-06-03 01:15 PM 编辑]
文章来自: Leepy's Blogs
引用通告: 查看所有引用 | 我要引用此文章
Tags: javascript 设计模式 命令模式 command
评论: 0 | 引用: 0 | 查看次数: -
发表评论
昵 称:
密 码: 游客发言不需要密码.
内 容:
验证码: 验证码
选 项:
虽然发表评论不用注册,但是为了保护您的发言权,建议您注册帐号.