javascript设计模式 之 7组合模式

111 阅读7分钟

1 组合模式的定义

组合模式:用小的子对象来构成更大的对象,而这些小的对象本身也许是由更小的孙对象构成的。
我理解的例如:一个人,是由肌肉+肥肉+骨头+器官构成。而肌肉,肥肉等又是又化学物质构成,化学物质里面还分为很多细小的钙铁锌硒等元素组成....。由小到大最终组成了一个人。

2 宏命令例子

宏命令是一组命令的集合,通过执行宏命令的方式,可以一次执行一批命令。家里有一个万能遥控器,每天回家的时候,只要按一个特别的按钮,它就会帮我们关上房间门,顺便打开电视,开始煮饭。

var closeDoorCommand = {
    execute: function() {
        console.log('close door');
    }
};
var openTVCommand = {
    execute: function() {
        console.log('open TV');
    };
};
var cookCommand = {
    execute: function() {
        console.log('cook');
    }
};

var MacroCommand = function() {
    var cache = [];
    return {
        add: function(command) {
            cache.push(command);
        },
        execute: function() {
            cache.map(function(command) {
                command.execute()
            });
        }
    };
}

var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openTVCommand );
macroCommand.add( cookCommand );
macroCommand.execute();

通过观察这段代码,我们很容易发现,宏命令中包含了一组子命令。看起来像是在执行macroCommand的execute方法,表现的像一个命令,但是实际只是一组命令的‘代理’,并非正真的代理,虽然结构相似,但是macroCommand只负责传递请求给叶子对象,不低不在于控制对叶子对象的访问。

3 组合模式传递

  • 组合模式将对象组合成为树形结构,以表示部分-整体的层次结构。通过调用组合对象的execute方法,程序便会递归调用组合对象下面的叶对象的execute方法。
  • 组合对象与叶对象有对象多态性,客户端可以忽略组合对象与单个对象的不同。在组合模式中,客户将统一使用组合对象结构中的所有对象,而不需要关系它究竟是组合对象还是单个对象。 实际开发中,我们往组合对象中添加一个命令时,并不会关心这个命令时宏命令还是普通名,只要它是一个命令并拥有execute方法,那么就能被添加到组合对象(例如上面的万能遥控器)中。当宏命令和子命令接收到执行请求时,它们就会做自己认为正确的事。这些差异隐藏在客户背后。对于客户来说,只看到了组合对象(万能遥控器)

组合模式的树形结构,让它的调用遵循一种自上而下的请求顺序规则。例如上面的宏命令对象调用后,就会将它下面的子命令一个一个调用。如果子命令中还有子命令,也会继续往下传递,直到不再有其他子节点。

4 更强大的宏命令

  • 基本对象可以被组合成为复杂的组合对象,组合对象又可以被组合,这样不断地递归下去,这棵树可以支持任意多的复杂度。而想让这棵树运行起来很简单,只需要执行execute方法就可以了。
  • 组合模式的透明性让客户不用顾忌树中组合对象与叶子对象的区别,但是实际叶子对象不能再添加子节点。因此为了保证一致性,可以为叶子节点也添加add方法并抛出不能添加的提示信息。

    上面的万能遥控器只有一层子节点。现在我们来实现一个多层节点的遥控器。
// 定义一个组合命令的功能函数
var MachroCommand = function() {
    var cache = [];
    return {
        add: function(command) {
            cache.push(command);
        },
        execute: function() {
            cache.map(function(command) {
                command.execute()
            });
        }
    };
};

var closeDoorCommand = {
    execute: function() {
        console.log('close door');
    },
    add: function() {
        throw Error('can\'t add child node');
    }
};
var openTVCommand = {
    execute: function() {
        console.log('open TV');
    },
     add: function() {
        throw Error('can\'t add child node');
    }
};

var openAirConditionCommand = {
    execute: function() {
        console.log('open 冰箱');
    },
     add: function() {
        throw Error('can\'t add child node');
    }
};

var openComputerCommand = {
    execute: function() {
        console.log('open computer');
    },
     add: function() {
        throw Error('can\'t add child node');
    }
};

// 组合命令(打开家电)
var openApplianceCommand = MachroCommand();
openApplianceCommand.add(openTVCommand);
openApplianceCommand.add(openAirConditionCommand);

// 组合命令(万能遥控器)
var machroCommand = MachroCommand();
machroCommand.add(openApplianceCommand);
machroCommand.add(openComputerCommand);
machroCommand.add(closeDoorCommand);

// 调用万能遥控器
machroCommand.execute();

5 组合模式扫描文件夹

当我们将文件夹从A处复制到B处, 只需要使用Ctrl + C, Ctrl + v 命令就可以了。而这里来的复制和粘贴就是组合命令,文件的层级关系是:

  • 文件夹(组合对象)可包含文件夹(组合对象)和文件(子节点)
  • 文件不能包含文件或文件夹
// 文件
Var File = function(name) {
    return {
        add: function() {
            console.log('can not add child file');
        },
        execute: function() {
            console.log('复制文件: ' + name);
        },
        scan: function() {
            console.log('扫描文件: ' + name);
        }
    }
};

// 文件夹
var Folder =function(name) {
    var cache = [];
    return {
        add: function(file) {
            cache.push(file);
        },
        execute: function() {
            console.log('复制文件夹: ' + name);
        },
        scan: function() {
            console.log('扫描文件夹: ' + name);
        }
    }
};

// 创建文件夹和文件对象,让他们组成一棵树
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 );

此时在我们的目录中已经有了一颗完整的树了,那么我希望将folder文件夹复制到另一个位置,会有扫描文件,和复制文件的过程:

// 将键盘按下CTRL + C的时候,注册事件执行
foler.scan();
// 将键盘按下CTRL + V的时候,注册事件执行
folder.execute();

当我们希望添加新的节点的时候,我们可以直接增加新增件节点到folder中,我们改变了树的结构,增加了新的数据,却不用修改任何一句原有的代码,这是符合开放-封闭原则的。

var folder3 = new Folder( 'Nodejs' );
var file4 = new File( '深入浅出 Node.js' );
folder3.add( file4 );
var file5 = new File( 'JavaScript 语言精髓与编程实践' );
// 添加到folder结构树中,修改了整体结构
folder.add( folder3 );
folder.add( file5 );

6 组合模式小结

  • 组合模式不是父子关系(继承)
    组合模式是一种HAS-A(聚合)关系,不是IS-A。组合对象包含一组叶对象,但是叶对象并不是Composite(组合)的子类。组合对象最终会将请求委托给所有的叶子对象,它们能够合作关键按是拥有相同的接口。
  • 对叶对象的操作一致
    组合对象与叶对象拥有相同的接口外,对同一组叶子对象的操作必须是一致的。例如上面的我要扫描文件,不能在组合对象中出现调用复制文件的接口。
  • 双向映射关系
    例如公司给员工发放福利卡。公司给各个部分发放,部分给员工发放。组合模式就很适合,但是如果员工A是一个架构师,既属于A部门也属于B部门,那么很有可能被发放两次。就不适合了。
  • 用职责链模式提高组合模式性能
    组合模式中,如果树结构比较复杂,节点较多,那么遍历过程中性能就不好了。我们可以借助职责链:手动设置链条,将组合对象与子节点之间性能职责链。让请求从组合象到子对象,或从子对象到父对象都能传递。
  • 组合模式缺点:因为组合的内容都是代码中已存在的,会让系统中的对象看起来和其他的对象差不多,只有在运行的时候才能显示出来。如果组合模式创建太多对象,可能让系统负担不起。
    利用职责链思想在子节点保持对父节点的引用。可以从自己子节点往父节点冒泡。
var File = function(name) {
    this.parent = undefined;
	this.name = name;
};
File.prototype.add = function(file) {
    console.log('can not add child file');
}
File.prototype.remove = function() {
    // 根节点或则游离无组织的节点
   if (!this.parent) {
       return;
   }
	var self = this;
   this.parent.cache = this.parent.cache.filter(function(f) {
       return f != self;
   });

}

// 文件夹
var Folder = function(name) {
    this.parent = undefined;
    this.cache = [];
	this.name = name;
};
Folder.prototype.add = function(file) {
    file.parent = this; // 设置父对象
    this.cache.push(file);
}

Folder.prototype.remove = function() {
    // 根节点或则游离无组织的节点
   if (!this.parent) {
       return;
   }
      console.log(' 移除文件夹' + this.name,    this.parent.cache );
   var self = this;
   this.parent.cache = this.parent.cache.filter(function(f) {
       return f != self;
   });
   console.log(' 移除文件夹' + this.name,    this.parent.cache );
}

// 移除文件夹
var folder = new Folder( '学习资料' );
var folder1 = new Folder( 'JavaScript' );
var file1 = new File ( '深入浅出 Node.js' );
folder1.add( new File( 'JavaScript 设计模式与开发实践' ) );
folder.add( folder1 );
folder.add( file1 );
folder1.remove(); //移除文件夹