设计模式-组合模式

组合模式定义

组合模式又称部分-整体模式,将对象组合成树形结构①以表示“部分整体”的层次结构。组合模式通过对象的多态性表现②使得用户对单个对象和组合对象的使用具有一致性。
①树形结构:组合模式提供了一种遍历树形结构的方案,通过调用组合对象的方法(例如下面创建文件夹的方法add),程序会递归调用组合对象下面叶节点的具有相同方法名的方法(例如下面创建文件的方法add),所以只要通过调用组合对象的方法就可以依次完成叶节点相应的方法。
②对象的多态性表现:利用对象多态性统一对待组合对象和单个对象,客户可以统一地使用组合结构中的所有对象,而不需要关心它究竟是组合对象还是单个对象。

文件夹和文件之间的关系非常适合用组合模式来描述。文件夹里面既可以包含文件,又可以包含其他文件夹,最终组成一棵树。

function Folder(name) {
    this.name=name;
    this.children=[];
    
}
Folder.prototype.add=function (child) {
    this.children.push(child);
}
Folder.prototype.show=function () {
    
    for (let i=0;i<this.children.length;i++){
        this.children[i].show();
    }
}

function File(name) {
    this.name=name;
}
File.prototype.add=function () {
    throw new Error(`文件下面不能再添加文件`);
}
File.prototype.show=function () {
    console.log('扫描文件:'+this.name);
}

在这个代码中定义了Floder组合对象和File叶对象,由于组合对象可以拥有子节点,叶对象下面就没有子节点,所以一般可以通过给叶对象也增加一个add方法,并且在调用这个方法时抛出一个异常来提醒用户。

请求在树中传递的过程

在组合模式中,请求在树中传递的过程总是遵循一种逻辑。请求从树最顶端的对象往下传递,如果当前处理请求的对象是叶对象,叶对象自身会对请求作出相应的处理;如果当前处理请求的对象是组合对象,组合对象则会遍历它的子节点,将请求继续传递给这些子节点。
总而言之,如果子节点是叶对象,叶对象自身会处理这个请求,而如果子节点还是组合对象请求会继续往下传递。叶对象下面不会再有其他子节点,一个叶对象就是树的这条枝叶的尽头,组合对象下面可能还会有子节点。
请求从上沿着树进行传递,直到树的尽头。用户只需要关心树最顶端的组合对象,用户只要请求这个组合对象,请求便会沿着树往下传递。
而且,叶节点可以被组合成更复杂的组合对象,组合对象也可以继续被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。

let folder=new Folder('视频');
let vueFolder=new Folder('Vue视频');
let reactFolder=new Folder('React视频');
let vueFile=new File('Vue从入门到精通');
let reactFile=new File('React从入门到精通');
folder.add(vueFolder);
folder.add(reactFolder);
vueFolder.add(vueFile);
reactFolder.add(reactFile);

folder.show();
vueFolder.remove();
folder.show();

例如,上面的代码创建了叶对象vueFile实例和reactFile实例,它们分别被组合在vueFolder和reactFolder两个组合对象实例中,这两个组合对象又被组合在folder组合对象实例中。用户只需要请求folder.show()方法,请求就会沿着树递归下去。

抽象类在组合模式的应用

组合模式最大的优点是可以一致地对待组合对象和基本对象,在Java中,实现组合模式的关键是组合类和叶节点类都要继承一个抽象类,这个抽象类既代表组合对象,又代表叶对象,它能保证组合对象和叶对象拥有相同名字的方法,从而对同一消息作出反馈。组合对象和叶对象的具体类型被隐藏在抽象类身后。
在JavaScript中,我们可以通过 寄生式继承 方式来继承同一个虚拟类来实现,比如让File类和Folder类都继承一个Parent虚拟类,同时可以将子类中共有的的变量在虚拟类构造函数中定义,简化子类。

var Parent = function() {
    this.name=null;
}
Parent.prototype = {
    add:function () {
        throw new Error("请重写add方法");
    },
    show:function () {
        throw new Error("请重写add方法");
    },
    remove:function () {
        throw new Error("请重写add方法");
    }
};
var Folder = function(name) {
	Parent.call(this);
    this.name=name;
    this.children=[];
    
}
//寄生式继承
inherit(Folder,Parent);
Folder.prototype.add=function (child) {
    this.children.push(child);
}
Folder.prototype.show=function () {
    
    for (let i=0;i<this.children.length;i++){
        this.children[i].show();
    }
}

var File = function(name) {
    this.name=name;
}
//寄生式继承
inherit(File,Parent);
File.prototype.add=function () {
    throw new Error(`文件下面不能再添加文件`);
}
File.prototype.show=function () {
    console.log('扫描文件:'+this.name);
}

引用父对象

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

Folder.prototype.remove=function () {
    if (!this.parent) return; // 根节点或者树外的游离节点
    for (let i=0;i<this.parent.children.length;i++){
        let current=this.parent.children[i];
        if (current === this) {
            return this.parent.children.splice(i,1);
        }
    }
}

在Folder.prototype.remove方法里首先会判断this.parent,如果this.parent为null,那么这个文件夹要么是树的根节点,要么是还没有添加到树的游离节点,这时候没有节点需要从树中移除。如果this.parent不为null,则说明该文件夹有父节点存在,此时遍历父节点中保存的子节点列表,删除需要删除的子节点。File类的实现基本一致。
完整代码如下:

function Folder(name) {
    this.name=name;
    this.children=[];
    this.parent=null;
    this.level=0;
}
Folder.prototype.add=function (child) {
    child.level=this.level+1;
    child.parent=this;
    this.children.push(child);
}
Folder.prototype.show=function () {
    console.log(' '.repeat(this.level)+'文件夹'+this.name);
    for (let i=0;i<this.children.length;i++){
        this.children[i].show();
    }
}
Folder.prototype.remove=function () {
    if (!this.parent) return; // 根节点或者树外的游离节点
    for (let i=0;i<this.parent.children.length;i++){
        let current=this.parent.children[i];
        if (current === this) {
            return this.parent.children.splice(i,1);
        }
    }
}

function File(name) {
    this.name=name;
}
File.prototype.add=function () {
    throw new Error(`文件下面不能再添加文件`);
}
File.prototype.show=function () {
    console.log(' '.repeat(this.level)+'文件'+this.name);
}

let folder=new Folder('视频');
let vueFolder=new Folder('Vue视频');
let reactFolder=new Folder('React视频');
let vueFile=new File('Vue从入门到精通');
let reactFile=new File('React从入门到精通');
folder.add(vueFolder);
folder.add(reactFolder);
vueFolder.add(vueFile);
reactFolder.add(reactFile);

folder.show();
vueFolder.remove();
folder.show();

组合模式应用场景

一般来说,组合模式适用于以下两个场景:

  1. 表示对象的部分-整体层次结构
  2. 用户希望统一对待树中的所有对象

绘制表单

小结

组合模式不是父子关系

组合模式是一种HAS-A(聚合)关系,组合对象包含一组叶对象,但叶对象并不是其子类,他们能够合作的关键是拥有相同的接口。

对叶对象操作的一致性

组合模式除了要求组合对象和叶对象有相同接口外,还要求一组叶对象的操作必须具有一致性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值