JavaScript设计模式:组合模式(Composite)详解与实现

作者:闭包旅人

时间:2026-10-29

浏览:619

本文深入解析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接口。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章