本文深入解析JavaScript中的组合模式,通过餐厅菜单的实例演示如何将对象组合成树形结构。文章详细展示了抽象类、叶子节点与容器节点的实现,以及如何统一处理单个对象和组合对象,适用于类似DOM操作或控件管理的场景。
组合模式概述
介绍
组合模式(Composite)旨在将对象组织成树形结构,以此表达“部分-整体”的层级关系。该模式的核心价值在于,它允许客户端对单个对象和组合对象的使用保持一致性,无需区分处理逻辑。
这种模式常见于ASP.NET的控件机制中,Control对象可以包含子Control,支持递归地添加、删除和操作子控件。类似的场景还包括DOM机制,其中DOM节点可以包含子节点,无论是父节点还是子节点,都具备添加、删除和遍历子节点的通用功能。因此,组合模式的关键在于定义一个抽象类,它既能表示子元素,也能表示父元素,从而统一接口。
正文
为了具体说明,假设有一家餐厅提供了各类菜品,每张餐桌配有一本菜单,列出该餐厅的所有菜品,包括早餐糕点、午餐和晚餐等。每个菜单项(如咖啡)可以添加子项(如糖),而整个菜单也可以添加新的菜品。假设无论是单个菜单项还是整个菜单,都需要支持打印功能,并且可以动态添加子项。
针对这种情况,我们可以利用组合模式将这些内容表示为层次结构。接下来,我们将逐步分解实现步骤。
实现抽象类MenuComponent
第一步,实现“抽象类”函数MenuComponent。
代码如下:
var MenuComponent = function () {
};
MenuComponent.prototype.getName = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.getDescription = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.getPrice = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.isVegetarian = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.print = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.add = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.remove = function () {
throw new Error("该方法必须重写!");
};
MenuComponent.prototype.getChild = function () {
throw new Error("该方法必须重写!");
};该函数提供了两类方法:一类是获取信息的(如价格、名称等),另一类是通用操作(如打印、添加、删除、获取子菜单)。
创建叶子节点MenuItem
第二步,创建基本的菜品项(叶子节点)。
代码如下:
var MenuItem = function (sName, sDescription, bVegetarian, nPrice) {
MenuComponent.apply(this);
this.sName = sName;
this.sDescription = sDescription;
this.bVegetarian = bVegetarian;
this.nPrice = nPrice;
};
MenuItem.prototype = new MenuComponent();
MenuItem.prototype.getName = function () {
return this.sName;
};
MenuItem.prototype.getDescription = function () {
return this.sDescription;
};
MenuItem.prototype.getPrice = function () {
return this.nPrice;
};
MenuItem.prototype.isVegetarian = function () {
return this.bVegetarian;
};
MenuItem.prototype.print = function () {
console.log(this.getName() + ": " + this.getDescription() + ", " + this.getPrice() + "euros");
};
从代码可以看出,我们只重写了原型的4个获取信息的方法和print方法,没有重载其他3个操作方法(add、remove、getChild),因为基本菜品不包含添加、删除或获取子菜品的功能。
实现容器类Menu
第三步,创建容器类菜品(Menu)。
代码如下:
var Menu = function (sName, sDescription) {
MenuComponent.apply(this);
this.aMenuComponents = [];
this.sName = sName;
this.sDescription = sDescription;
this.createIterator = function () {
throw new Error("This method must be overwritten!");
};
};
Menu.prototype = new MenuComponent();
Menu.prototype.add = function (oMenuComponent) {
// 添加子菜品
this.aMenuComponents.push(oMenuComponent);
};
Menu.prototype.remove = function (oMenuComponent) {
// 删除子菜品
var aMenuItems = [];
var nMenuItem = 0;
var nLenMenuItems = this.aMenuComponents.length;
var oItem = null;
for (; nMenuItem < nLenMenuItems; ) {
oItem = this.aMenuComponents[nMenuItem];
if (oItem !== oMenuComponent) {
aMenuItems.push(oItem);
}
nMenuItem = nMenuItem + 1;
}
this.aMenuComponents = aMenuItems;
};
Menu.prototype.getChild = function (nIndex) {
//获取指定的子菜品
return this.aMenuComponents[nIndex];
};
Menu.prototype.getName = function () {
return this.sName;
};
Menu.prototype.getDescription = function () {
return this.sDescription;
};
Menu.prototype.print = function () {
// 打印当前菜品以及所有的子菜品
console.log(this.getName() + ": " + this.getDescription());
console.log("--------------------------------------------");
var nMenuComponent = 0;
var nLenMenuComponents = this.aMenuComponents.length;
var oMenuComponent = null;
for (; nMenuComponent < nLenMenuComponents; ) {
oMenuComponent = this.aMenuComponents[nMenuComponent];
oMenuComponent.print();
nMenuComponent = nMenuComponent + 1;
}
};注意上述代码,除了实现了添加、删除、获取方法外,打印print方法是首先打印当前菜品信息,然后循环遍历打印所有子菜品信息。
构建具体菜单类型
第四步,创建具体的菜单类型。
我们可以创建几个真实的菜品,比如晚餐、咖啡、糕点等等,其都是用Menu作为其原型,代码如下:
代码如下:
var DinnerMenu = function () {
Menu.apply(this);
};
DinnerMenu.prototype = new Menu();
var CafeMenu = function () {
Menu.apply(this);
};
CafeMenu.prototype = new Menu();
var PancakeHouseMenu = function () {
Menu.apply(this);
};
PancakeHouseMenu.prototype = new Menu();创建顶级菜单容器
第五步,创建最顶级的菜单容器——菜单本(MenuBook)。
代码如下:
var Mattress = function (aMenus) {
this.aMenus = aMenus;
};
Mattress.prototype.printMenu = function () {
this.aMenus.print();
};该函数接收一个菜单数组作为参数,并且只提供了printMenu方法用于打印所有的菜单内容。
组合模式调用示例
第六步,调用方式。
代码如下:
var oPanCakeHouseMenu = new Menu("Pancake House Menu", "Breakfast");
var oDinnerMenu = new Menu("Dinner Menu", "Lunch");
var oCoffeeMenu = new Menu("Cafe Menu", "Dinner");
var oAllMenus = new Menu("ALL MENUS", "All menus combined");
oAllMenus.add(oPanCakeHouseMenu);
oAllMenus.add(oDinnerMenu);
oDinnerMenu.add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce, and a slice of sourdough bread", true, 3.89));
oDinnerMenu.add(oCoffeeMenu);
oCoffeeMenu.add(new MenuItem("Express", "Coffee from machine", false, 0.99));
var oMattress = new Mattress(oAllMenus);
console.log("---------------------------------------------");
oMattress.printMenu();
console.log("---------------------------------------------");熟悉ASP.NET控件开发的同学,是不是看起来很熟悉?
总结与应用场景
总结
组合模式的使用场景非常明确:
你想表示对象的部分-整体层次结构时; 你希望用户忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象(方法) 另外该模式经常和装饰者一起使用,它们通常有一个公共的父类(也就是原型),因此装饰必须支持具有add、remove、getChild操作的 component接口。