Javascript乱弹设计模式系列(4) - 组合模式(Composite)

前言

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

组合模式比较简单,那么开始今天的文章。

概述

它又是部分-整体的模式,元素有两种形式,一种是简单元素,一种是复杂元素,其中复杂元素是简单元素的组成,所以客户程序要操作复杂元素时,有时候它并不想了解内部包括哪些简单元素,但是又想能够保持简单元素的简易操作性,于是,就有了组合模式,它可以抽象成一个树状结构,其中简单元素为叶子结点,而复杂元素为非叶子结点。

定义

组合模式允许你将对象组合成树形结构以表示“整体/部分”的层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

类图

 

实例分析

这里我给出一个利用组合模式设计的菜单导航:

先看下效果图:

现在开始分析通过组合模式如何实现它:

1. 引用InterfaceAndClass.js文件,作为接口的构造和类的继承,这个不多说了,详细请看前面的文章;

2. 添加MenuComponent.js文件,作为Component接口或者抽象类,这里我用了接口的形式:

var MenuComponent = new Interface("MenuComponent", [["getValue"]]);

 


3. 添加MenuItem.js文件,作为Leaf简单元素,这里指菜单的叶子结点:

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

MenuItem.prototype 
= {
    getValue : 
function() {
        
//
    }
}

其中text为菜单上显示的文本,title为提示文本符,href为链接导向;并且让它继承MenuComponent接口;

 

4. 添加Menu.js文件,作为Composite复合元素,它是由一系列的Leaf简单元素组成的:

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

Menu.prototype 
= {
    getValue : 
function() {
        
//
    },
    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("索引操作数组超过上限");
        }
    }
}

它继承MenuAComponent接口,因此它和MenuItem使用同一个接口,这样可以统一简单元素和复杂元素的方法调用,add方法用来添加component类,这里的component可以是Menu,也可以是MenuItem;remove方法用来删除指定的component类;removeAt方法用过索引删除相应的component类;getValue方法取得菜单数据;

让我们先从实例上得到一个树状图:

从图上可以看出,实际上菜单1,菜单2-1,菜单2-2-1,菜单2-2-2,菜单2-3,菜单3-1,菜单4都是属于Leaf,即MenuItem类;

菜单2,菜单2-2,菜单3都属于Composite,即Menu类;

这样Menu类的GetValue方法可以通过遍历它的子结点数组,得到所有子结点菜单数据;

for(var i = 0, len = this.menuComponents.length; i < len; i++)
{
    str 
+= this.menuComponents[i].getValue();
}

其中this.menuComponents[i].getValue()可以是Menu类的getValue方法,也可以是MenuItem类的getValue方法;

这样就可以完成了所有结点的遍历。

然后给出MenuItem.js和Menu.js的完整代码如下:

MenuItem.js

 

Menu.js

 

 
5. 接着添加一个Menu操作类MenuOpr:

var MenuOpr = {
    list : 
new Array(),
    add : 
function(component) {
        
this.list.push(component);
    },
    print : 
function(container) {
        
var str = "<ul class=\"Menu\">";
        
for(var i = 0, len = this.list.length; i < len; i++) {
            str 
+= this.list[i].getValue();
        }
        document.getElementById(container).innerHTML 
= str + "</ul>";
    }
}

 


6. 最后利用组合模式编写调用代码:

var menu1 = new MenuItem("菜单1","菜单1","#");

var menu2 = new Menu("菜单2","菜单2","#");
var menu2_1 = new MenuItem("菜单2-1""菜单2-1""#");
var menu2_2 = new Menu("菜单2-2""菜单2-2""#");
var menu2_2_1 = new MenuItem("菜单2-2-1""菜单2-2-1""#");
var menu2_2_2 = new MenuItem("菜单2-2-2""菜单2-2-2""#");
var menu2_3 = new MenuItem("菜单2-3","菜单2-3","#");
menu2.add(menu2_1);
menu2.add(menu2_2);
menu2_2.add(menu2_2_1);
menu2_2.add(menu2_2_2);
menu2.add(menu2_3);

var menu3 = new Menu("菜单3","菜单3","#");
var menu3_1 = new MenuItem("菜单3-1","菜单3-1","#");
menu3.add(menu3_1);

var menu4 = new MenuItem("菜单4","菜单4","#");

MenuOpr.add(menu1);
MenuOpr.add(menu2);
MenuOpr.add(menu3);
MenuOpr.add(menu4);

MenuOpr.print(
"main_container");

main_container为一个div层的id,这里代码就不多说了,一看就看得懂的。

当然实现这个菜单还是有不少方法的,而我这肯定不是最优的,这里我只是提供一个利用组合模式的思路而已;

如果按照常规,你可以把MenuItem都整合到Menu上,即MenuItem都改为Menu,这样的代码,你是不是更喜欢点呢?

这个就留给大家自己研究吧!

最后,说下由于文中实例的菜单样式采用li:hover的方式来弹出下级菜单,由于这个方式对于IE7以下版本是无效的,所以这里引用了cssfriendly开源项目中的两个JS文件,MenuAdapter.js和AdapterUtils.js,所以把它们放进项目中引用进来,这样该菜单就兼容了IE7以下版本的浏览器。

 

附:源代码下载

 

总结

该篇文章用Javascript设计组合模式的思路,实现一个菜单实例。

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

 

参考文献:《Head First Design Pattern》

本系列文章转载时请注明出处,谢谢合作!

 相关系列文章:
Javascript乱弹设计模式系列(6) - 单件模式(Singleton)
Javascript乱弹设计模式系列(5) - 命令模式(Command)
Javascript乱弹设计模式系列(4) - 组合模式(Composite)
Javascript乱弹设计模式系列(3) - 装饰者模式(Decorator)
Javascript乱弹设计模式系列(2) - 抽象工厂以及工厂方法模式(Factory)
Javascript乱弹设计模式系列(1) - 观察者模式(Observer)
Javascript乱弹设计模式系列(0) - 面向对象基础以及接口和继承类的实现

 

作者:Leepy
 
邮箱:sunleepy(AT)gmail.com
 
    
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。
你可以通过快速通道评论:
posted @ 2009-01-15 22:23 Leepy 阅读(1766) 评论(14) 编辑 收藏

 回复 引用   
#1楼 2009-01-15 22:35 winter-cn[未注册用户]
唉 组合模式可不是树型结构这么简单的
Menu和MenuItem属于父子关系 不是组合关系

似是而非的模式太多了 现在到网上搜设计模式很难找到真正体现GoF思想的解释了

 回复 引用 查看   
#2楼[楼主] 2009-01-15 22:54 Leepy      
@winter-cn
我也在想这个例子是不是不太适合,这里我是理解为由多个MenuItem的简单元素组合而成一个Menu的复杂元素,而多个Menu元素还可以组合成更复杂的元素,所以Menu就作为一个Composite类;如果现实上,MenuItem那只是简单元素中的一种,就像三角形也只是简单元素中的一种,方形也是,它们都有边,那就组合了多边形的对象,这样就可以保证单一对象和组合对象使用上的一致

 回复 引用   
#3楼 2009-01-15 23:08 winter-cn[未注册用户]
@Leepy
我最近恰巧在做Menu和MenuItem开发
是这样的 组合模式和树型结构最大的不同就是组合体对象不应该有其它任何的修饰,比如你可以考虑,几个MenuItem摆在一起是不是就是一个Menu了呢?
答案显然是否定的,Menu还应该有自己的容器和边框、背景等等。还有一点就是组合模式是可以再组合的(就像修饰器模式可以再修饰一样),就是Menu里面是否可以放任何MenuComponent 呢?里面是否可以放Menu呢?

关于这个模式我举过一个例子:一个人是生物,但是一个公司就不是生物了
一个几个圆形是一个几何图形,几个圆形仍然是几何图形。

楼主好样的 ,比那些只说不写的 人 强万倍
 回复 引用 查看   
#5楼[楼主] 2009-01-15 23:24 Leepy      
@winter-cn
你的意思是说Menu的组合体对象不能包含任何的修饰吗?嗯,组合模式是可以再组合的,因为它们共继承于同一个接口或者抽象类。
那个例子明白你的意思。不单单只是MenuItem简单的累加

@Leepy
说修饰可能不太贴切 就是说组合体是不应该有实体的 它应该就是组成它的对象的集合

我不知道你是否了解Silverlight
SL控件的Template是组合模式 Content就不是

 回复 引用 查看   
#7楼[楼主] 2009-01-15 23:44 Leepy      
@winter-cn,我也不能总是不登录是吧
Silverlight 是说控件模版是吧,我记得Template绑定的是控件外形,如矩形,圆形等等,Content绑定的是样式

@Leepy
我想表达的区别就是
把模板置空了 整个控件都没有了
把Content置空了 仍然有个外框

所以说 Control和Template Child之间是组合关系
而和Content之间是包含关系

在Silverlight中Control实际上是一个组合模式 而公共接口是FrameworkElement

 回复 引用 查看   
#9楼[楼主] 2009-01-16 00:06 Leepy      
@winter-cn,我也不能总是不登录是吧
嗯,明白了!Template Child可以有多种多样的外框类型,但是Content修饰类不会影响Composite和Leaf之间的关系

@Leepy
呃 我的意思是 其实组合模式跟从属关系是2条不同的线

组合模式还是挺爽的:P 等你遇到需要的场景的时候就会自然想起来了

建议你做做Command模式 可以跟Composite模式一起使用的 你就可以比较深入理解组合模式的优点了

 回复 引用 查看   
#11楼[楼主] 2009-01-16 01:15 Leepy      
@winter-cn,我也不能总是不登录是吧
兄弟还没睡啊!谢谢你的建议了:)

 回复 引用 查看   
#12楼 2009-01-16 09:53 ted      
个人理解Composite Pattern的意图是Client可以无差别的使用Container节点以及Leaf节点,当然Composite Pattern是有一个树形关系在里面.

Menu和MenuItem是父子关系,但是注意到Menu下面只能有MenuItem, 而MenuItem下面也只能有子MenuItem, Client在无差别的使用方面还有很多制约.

个人意见.

 回复 引用 查看   
#13楼 2009-01-16 09:55 ted      
个人理解Composite Pattern的意图是Client可以无差别的使用Container节点以及Leaf节点,当然Composite Pattern是有一个树形关系在里面.

Menu和MenuItem是父子关系,但是注意到Menu下面只能有MenuItem, 而MenuItem下面也只能有子MenuItem, Client在无差别的使用方面还有很多制约.

个人意见.

 回复 引用 查看   
#14楼[楼主] 2009-01-16 10:14 Leepy      
组合模式是可以再组合的,比如Menu下不仅仅可以有MenuItem,还可以是Menu,而MenuItem只是做为简单元素;而在client的使用上,他们不管简单复杂与否,他们只需要知道统一的方法,就是你说的无差别的使用上;而在我这个例子上的确有制约,比如在Menu和MenuItem的调用上在这个例子中菜单的显示会有问题
发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1375390 oQrGMUhySJ0=