javaScript设计模式——组合模式(十)

设计模式:有助于提高代码的复用性和可维护性

组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。 除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性

宏命令:对象包含了一组具体的字命令对象,不管是宏命令对象,还是字命令对象,都有一个execute方法负责执行命令。现在看一下这段安装在万能遥控器上的宏命令代码

// 新建一个关门的命令
var closeDoorCommand = {
    execute: function(){
        console.log( '关门' );
    }
};
// 新建一个开电脑的命令
var openPcCommand = {
    execute: function(){
        console.log( '开电脑' );
    }
};
// 登陆QQ的命令
var openQQCommand = {
    execute: function(){
        console.log( '登录QQ' );
    }
};

// 创建一个宏命令
var MacroCommand = function(){
    return {
        // 宏命令的子命令列表
        commandsList: [],
        // 添加命令到子命令列表
        add: function( command ){
            this.commandsList.push( command );
        },
        // 依次执行子命令列表里面的命令
        execute: function(){
            for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                command.execute();
            }
        }
    }
};

var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();

通过观察这段代码,我们很容易发现,宏命令中包含了一组字命令,他们组成了一个树形结构,这里是一棵结构非常简单的树
在这里插入图片描述
其中,marcoCommand被称为组合对象,closeDoorCommand,openPcCommand、openQQCommand都是叶对象,在marcoCommand的execute方法里,并不执行真正的操作,而是遍历它所包含的叶对象,把真正的execute请求委托给这些叶对象

请求在树中传递的过程

在组合模式中,请求在树中传递的过程总是遵循一种逻辑
以宏命令为例,请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象(普通的子命令),叶对象自身会对请求作出相应对处理,如果当前处理请求对对象是组合对象(宏命令),组合模式则会遍历它属下对子节点,将请求继续传递给这些子节点
在这里插入图片描述

更强大的宏命令

目前的万能遥控器,包含了关门、开电脑、登录QQ这3个命令。现在我们需要一个“超级万能遥控器”,可以控制家里所有的电器,这个遥控器拥有以下功能:

1、打开空调

2、打开电视和音响

3、关门、开电脑、登录QQ

首先在节点中放置一个按钮button来表示这个超级万能遥控器,超级万能遥控器上安装了一个宏命令,当执行这个宏命令时,会依次遍历执行它所包含的子命令,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="button">按我</button>
    <script>
    var MacroCommand = function(){
        return {
            commandsList: [],
            add: function( command ){
                this.commandsList.push( command );
            },
            execute: function(){
                for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
                    command.execute();
                }
            }
         }
    };
    var openAcCommand = {
        execute: function(){
            console.log( '打开空调' );
        }
    };

    // 家里的电视和音响是连接在一起的,所以可以用一个宏命令来组合打开电视和打开音响的命令

    var openTvCommand = {
        execute: function(){
            console.log( '打开电视' );
        }
    };

    var openSoundCommand = {
        execute: function(){
            console.log( '打开音响' );
        }
    };

    var macroCommand1 = MacroCommand();
    macroCommand1.add( openTvCommand );
    macroCommand1.add( openSoundCommand );

// 关门、打开电脑和打登录QQ的命令

    var closeDoorCommand = {
        execute: function(){
              console.log( '关门' );
        }
    };

    var openPcCommand = {
        execute: function(){
            console.log( '开电脑' );
        }
    };

    var openQQCommand = {
        execute: function(){
            console.log( '登录QQ' );
        }
    };

    var macroCommand2 = MacroCommand();
    macroCommand2.add( closeDoorCommand );
    macroCommand2.add( openPcCommand );
    macroCommand2.add( openQQCommand );

    //现在把所有的命令组合成一个“超级命令”

    var macroCommand = MacroCommand();
    macroCommand.add( openAcCommand );
    macroCommand.add( macroCommand1 );
    macroCommand.add( macroCommand2 );

    //最后给遥控器绑定“超级命令”

    var setCommand = (function( command ){
        document.getElementById( 'button' ).onclick = function(){
            command.execute();
        }
    })( macroCommand );

    </script>
</html>

执行结果如下
在这里插入图片描述

从这个例子中可以看到,基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。在树最终被构造完成之后,让整颗树最终运转起来的步骤非常简单,只需要调用最上层对象的execute方法。每当对最上层的对象进行一次请求时,实际上是在对整个树进行深度优先的搜索,而创建组合对象的程序员并不关心这些内在的细节,往这棵树里面添加一些新的节点对象是非常容易的事情

扫描文件夹

文件夹和文件之间的关系,非常适合用组合模式来描述。文件夹里既可以包含文件,又可以 包含其他文件夹,最终可能组合成一棵树
当使用用杀毒软件扫描该文件夹时,往往不会关心里面有多少文件和子文件夹,组合模式使得我们只需要操作最外层的文件夹进行扫描。

现在我们来编写代码,首先分别定义好文件夹Folder和文件File这两个类。见如下代码:

/* Folder */ 
class Folder{
    constructor(name){
        this.name = name;
        this.files = [];
    }
    add(file){
        this.files.push(file );
    }
    scan(){
        console.log( '开始扫描文件夹: ' + this.name );
        for ( var i = 0; i<this.files.length; i++){
            this.files[i].scan();
        } 
    }
}
/*File*/
class File{
    constructor(name){
        this.name = name;
    }
    add(){
        throw new Error( '文件下面不能再添加文件' );
    }
    scan(){
        console.log( '开始扫描文件: ' + this.name );
    }
}

/*创建一些文件夹和文件对象, 并且让它们组合成一棵树,这棵树就是我们 F 盘里的 现有文件目录结构*/
var folder = new Folder( '学习资料' ); 
var folder1 = new Folder( 'JavaScript' ); 
var folder2 = new Folder ( 'jQuery' );

var file1 = new File( 'JavaScript 设计模式与开发实践' );
var file2 = new File( '精通 jQuery' );
var file3 = new File('重构与模式' );

folder1.add( file1 );  
folder2.add( file2 );

folder.add( folder1 ); 
folder.add( folder2 ); 
folder.add( file3 );

现在的需求是把移动硬盘里的文件和文件夹都复制到这棵树中,假设我们已经得到了这些文件对象

var folder3 = new Folder( 'Nodejs' );
var file4 = new File( '深入浅出Node.js' );
folder3.add( file4 );

var file5 = new File( 'JavaScript语言精髓与编程实践' );

接下来就是把这些文件都添加到原有的树中:

folder.add( folder3 );
folder.add( file5 );

过这个例子,我们再次看到客户是如何同等对待组合对象和叶对象。在添加一批文件的操作过程中,客户不用分辨它们到底是文件还是文件夹。新增加的文件和文件夹能够很容易地添加到原来的树结构中,和树里已有的对象一起工作。

我们改变了树的结构,增加了新的数据,却不用修改任何一句原有的代码,这是符合开放-封闭原则的。

运用了组合模式之后,扫描整个文件夹的操作也是轻而易举的,我们只需要操作树的最顶端对象:

folder.scan()

执行结果如下:
在这里插入图片描述

一些值得注意的地方

在使用组合模式的时候,还有以下几个值得我们注意的地方。

1.组合模式不是父子关系
组合模式的树型结构容易让人误以为组合对象和叶对象是父子关系,这是不正确的。

2.对叶对象操作的一致性
组合模式除了要求组合对象和叶对象拥有相同的接口之外,还有一个必要条件,就是对一组叶对象的操作必须具有一致性。

引用父对象

在之前提到的例子中,组合对象保存了它下面的子节点的引用,这是组合模式的特点,此时树结构是从上至下的。但有时候我们需要在子节点上保持对父节点的引用,比如在组合模式中使用职责链时,有可能需要让请求从子节点往父节点上冒泡传递。还有当我们删除某个文件的时候,实际上是从这个文件所在的上层文件夹中删除该文件的。

现在来改写扫描文件夹的代码,使得在扫描整个文件夹之前,我们可以先移除某一个具体的文件。

首先改写Folder类和File类,在这两个类的构造函数中,增加this.parent属性,并且在调用add方法的时候,正确设置文件或者文件夹的父节点:

var Folder = function( name ){
    this.name = name;
    this.parent = null;    // 增加this.parent属性
    this.files = [];
};

Folder.prototype.add = function( file ){
    file.parent = this;    // 设置父对象
    this.files.push( file );
};

Folder.prototype.scan = function(){
    console.log( '开始扫描文件夹: ' + this.name );
    for ( var i = 0, file, files = this.files; file = files[ i++ ]; ){
        file.scan();
    }
};

接下来增加Folder.prototype.remove方法,表示移除该文件夹:

Folder.prototype.remove = function(){
    if ( !this.parent ){    // 根节点或者树外的游离节点
        return;
    }
    for ( var files = this.parent.files, l = files.length - 1; l >=0; l-- ){
        var file = files[ l ];
        if ( file === this ){
            files.splice( l, 1 );
        }
    }
};

在 File.prototype.remove方法里,首先会判断this.parent,如果this.parent为null,那么这个文件夹要么是树的根节点,要么是还没有添加到树的游离节点,这时候没有节点需要从树中移除,我们暂且让remove方法直接return,表示不做任何操作。

如果this.parent不为null,则说明该文件夹有父节点存在,此时遍历父节点中保存的子节点列表,删除想要删除的子节点。

File类的实现基本一致:

var File = function( name ){
    this.name = name;
    this.parent = null;
};

File.prototype.add = function(){
    throw new Error( '不能添加在文件下面' );
};

File.prototype.scan = function(){
    console.log( '开始扫描文件: ' + this.name );
};

File.prototype.remove = function(){
    if ( !this.parent ){    // 根节点或者树外的游离节点
        return;
    }
    for ( var files = this.parent.files, l = files.length - 1; l >=0; l-- ){
        var file = files[ l ];
        if ( file === this ){
            files.splice( l, 1 );
        }
    }
};

下面测试一下我们的移除文件功能:

var folder = new Folder( '学习资料' );
var folder1 = new Folder( 'JavaScript' );
var file1 = new Folder ( '深入浅出Node.js' );

folder1.add( new File( 'JavaScript设计模式与开发实践' ) );
folder.add( folder1 );
folder.add( file1 );

folder1.remove();    // 移除文件夹
folder.scan();

执行结果如图
在这里插入图片描述

何时使用组合模式

组合模式如果运用得当,可以大大简化客户的代码。一般来说,组合模式适用于以下这两种情况。

表示对象的部分-整体层次结构。组合模式可以方便地构造一棵树来表示对象的部分-整体结构。特别是我们在开发期间不确定这棵树到底存在多少层次的时候。在树的构造最终完成之后,只需要通过请求树的最顶层对象,便能对整棵树做统一的操作。在组合模式中增加和删除树的节点非常方便,并且符合开放-封闭原则。

客户希望统一对待树中的所有对象。组合模式使客户可以忽略组合对象和叶对象的区别,客户在面对这棵树的时候,不用关心当前正在处理的对象是组合对象还是叶对象,也就不用写一堆if、else语句来分别处理它们。组合对象和叶对象会各自做自己正确的事情,这是组合模式最重要的能力。

常用的12种设计模式
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值