Javascript. Composite pattern

Компоновщик (англ. Composite pattern) — структурный шаблон проектирования, относится к структурным паттернам, объединяет объекты в древовидную структуру для представления иерархии от частного к целому. Компоновщик позволяет клиентам обращаться к отдельным объектам и к группам объектов одинаково


var _hasProp = {}.hasOwnProperty,
_extends = function(child, parent) { 
for (var key in parent) { 
if (_hasProp.call(parent, key)) { 
child[key] = parent[key]; 
}
function ctor() { 
this.constructor = child; 
ctor.prototype = parent.prototype; 
child.prototype = new ctor(); 
child.__super__ = parent.prototype; 
return child; 
};

// Component
var Component = (function () {

function Component(name) {
this.name = name || 'undefined';
}
Component.prototype.add = function () {};
Component.prototype.remove = function () {};
Component.prototype.display = function () {};
return Component;

})();

// Composite
var Composite = (function (_super) {

_extends(Composite, _super);
function Composite() {
this.children = [];
}
Composite.prototype.add = function (component) {
if (component instanceof Component) {
this.children.push(component);
}
};
Composite.prototype.remove = function (component) {
if (component instanceof Component) {
var index = this.children.indexOf(component);
if (~index) {
this.children.splice(index, 1);
}
}
};
Composite.prototype.display = function () {
console.log(this.children);
};
return Composite;

})(Component);

// Leaf
var Leaf = (function (_super) {

_extends(Leaf, _super);

function Leaf() {
return Leaf.__super__.constructor.apply(this, arguments);
}

Leaf.prototype.display = function() {
console.log("Cannot add to a leaf");
};
Leaf.prototype.display = function() {
console.log("Cannot remove from a leaf");
};
Leaf.prototype.display = function() {
console.log(this.name);
};
return Leaf;

})(Component);

var root = new Composite('root');

root.add(new Leaf('Leaf A'));
root.add(new Leaf('Leaf B'));

var comp = new Composite('Composite X');

comp.add(new Leaf('Leaf XA'));
comp.add(new Leaf('Leaf XB'));
comp.add(new Leaf('Leaf XC'));
comp.add(new Leaf('Leaf XD'));

root.add(comp);
root.add(new Leaf('Leaf C'));

var leaf = new Leaf('Leaf D');

root.add(leaf);

root.remove(comp);
root.remove(leaf);

root.display();

Вот еще один пример реализации данного паттерна. Стоит только оговориться: паттерн Компоновщик ниже реализован с использованием паттерна Итератор (см. здесь).

Array.prototype.iterator = function () {
    var self = this, position = 0, undefined;
    return {
        next:function () {
            return self[position++];
        },
        hasNext: function () {
            if (position >= self.length) {
                return false;
            } 
            else {
                return true;
            }
        },
        remove: function () {
            if (position <= 0) {
                throw new Error("You can`t an item until you`ve done at least one next()");
            }
            if (self[position - 1] !== undefined) {
                for (var i = position - 1; i < (self.length - 1); i++) {
                    self[i] = self[i + 1];
                }
                self[self.length - 1] = undefined;
            }
        }
    };
};

/* abstract class MenuComponent */

var MenuComponent = function () {

    this.add = function (menuComponent) {
        throw new Error("UnsupportedOperationException");
    };
    
    this.remove = function (menuComponent) {
        throw new Error("UnsupportedOperationException");
    };
    
    this.getChild = function (i) {
        throw new Error("UnsupportedOperationException");
    };
    
    this.getName = function () {
        throw new Error("UnsupportedOperationException");
    };
    
    this.getDescription = function () {
        throw new Error("UnsupportedOperationException");
    };
    
    this.getPrice = function () {
        throw new Error("UnsupportedOperationException");
    };
    
    this.isVegetarian = function () {
        throw new Error("UnsupportedOperationException");
    };
    
    this.print = function () {
        throw new Error("UnsupportedOperationException");
    };

}

/* Menu Leaf */

var MenuItem = function (name, description, vegetarian, price) {

    var name = name,
        description = description,
        vegetarian = vegetarian,
        price = price;
    
    this.getName = function () {
        return name;
    };
    
    this.getDescription = function () {
        return description;
    };
    
    this.getPrice = function () {
        return price;
    };
    
    this.isVegetarian = function () {
        return vegetarian;
    };
    
    this.print = function () {
        var item =  " " + this.getName() + 
                    (this.isVegetarian() ? "(v)" : "") + 
                    ", " + this.getPrice() + 
                    "   -- " + this.getDescription();
        console.log(item);
    };

};

MenuItem.prototype = new MenuComponent();
MenuItem.prototype.constructor = MenuItem;

/* Menu Composite */

var Menu = function (name, description) {

    var menuComponents = [],
        name = name,
        description = description;

    this.add = function (menuComponent) {
        menuComponents.push(menuComponent);
    };
    
    this.remove = function (menuComponent) {
        var index = menuComponents.indexOf(menuComponent);
        if (index >= 0) {
            menuComponents.splice(index, 1);
        };
    };
    
    this.getChild = function (i) {
        return menuComponents[i];
    };
    
    this.getName = function () {
        return name;
    };
    
    this.getDescription = function () {
        return description;
    };
    
    this.print = function () {
        var item =  this.getName() + ", " +    this.getDescription();
        console.log(item);
        console.log("----------------------");
        
        var iterator = menuComponents.iterator();
        while (iterator.hasNext()) {
            var menuComponent = iterator.next();
            menuComponent.print();
        }
    };

};

Menu.prototype = new MenuComponent();
Menu.prototype.constructor = Menu;

/* Client */

var Waitress = function (allMenus) {

    var allMenus = allMenus;
    
    this.printMenu = function () {
        allMenus.print();
    };

};

/* test application */

var Application = function () {
    this.run = function () {
    
        var pancakeHouseMenu = new Menu("PANCAKE HOUSE MENU", "Breakfast"), 
            dinerMenu = new Menu("DINER MENU", "Lunch"), 
            cafeMenu = new Menu("CAFE MENU", "Dinner"),
            dessertMenu = new Menu("DESSERT MENU", "Dessert of course!");
            
        var allMenus = new Menu("ALL MENUS", "All menus combined");
        
        allMenus.add(pancakeHouseMenu);
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);
        
        pancakeHouseMenu.add(new MenuItem("K&B`s Pancake Breakfast", "Pancakes with scrambled eggs, and toast", true, 2.99));
        pancakeHouseMenu.add(new MenuItem("Regular Pancake Breakfast", "Pancakes with fried eggs, sausage", false, 2.99));
        pancakeHouseMenu.add(new MenuItem("Blueberry Pancakes", "Pancakes made with fresh blueberries", true, 3.49));
        pancakeHouseMenu.add(new MenuItem("Waffles", "Waffles, with your choice of blueberries or strawberries", true, 3.59));
    
        dinerMenu.add(new MenuItem("Vegetarian BLT", "(Fakin) Bacon with lettuce & tomato on whle wheat", true, 2.99));
        dinerMenu.add(new MenuItem("BLT", "Bacon with lettuce & tomato on whle wheat", false, 2.99));
        dinerMenu.add(new MenuItem("Soup of the day", "Soup of the day, with a side of potato salad", false, 3.29));
        dinerMenu.add(new MenuItem("Hotdog", "A hot dog, with saurkraut, relish, onions, topped with cheese", false, 3.05));
        dinerMenu.add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce, and a slice of sourdough bread", true, 3.89));
        
        dinerMenu.add(dessertMenu);        
        dessertMenu.add(new MenuItem("Apple Pie", "Apple pie with a flakey crust, topped with vanilla icecream", true, 1.59));
        
        cafeMenu.add(new MenuItem("Veggie Burger and Air Fries", "Veggie burger on a whole wheat bun, lettuce, tomato, and fries", true, 3.99));
        cafeMenu.add(new MenuItem("Soup of the day", "A cup of the soup of the day, with a side salad", false, 3.69));
        cafeMenu.add(new MenuItem("Burrito", "A large burrito, with whole pinto beans, salsa, guacamole", true, 4.29));
        
        var waitress = new Waitress(allMenus);
        
        waitress.printMenu();
    
    };
};

var application = new Application();
application.run();

На выходе имеем:

ALL MENUS, All menus combined
----------------------

PANCAKE HOUSE MENU, Breakfast
----------------------
K&B`s Pancake Breakfast(v), 2.99   -- Pancakes with scrambled eggs, and toast
Regular Pancake Breakfast, 2.99   -- Pancakes with fried eggs, sausage
Blueberry Pancakes(v), 3.49   -- Pancakes made with fresh blueberries
Waffles(v), 3.59   -- Waffles, with your choice of blueberries or strawberries

DINER MENU, Lunch
----------------------
Vegetarian BLT(v), 2.99   -- (Fakin) Bacon with lettuce & tomato on whle wheat
BLT, 2.99   -- Bacon with lettuce & tomato on whle wheat
Soup of the day, 3.29   -- Soup of the day, with a side of potato salad
Hotdog, 3.05   -- A hot dog, with saurkraut, relish, onions, topped with cheese
Pasta(v), 3.89   -- Spaghetti with Marinara Sauce, and a slice of sourdough bread

DESSERT MENU, Dessert of course!
----------------------
Apple Pie(v), 1.59   -- Apple pie with a flakey crust, topped with vanilla icecream

CAFE MENU, Dinner
----------------------
Veggie Burger and Air Fries(v), 3.99   -- Veggie burger on a whole wheat bun, lettuce, tomato, and fries
Soup of the day, 3.69   -- A cup of the soup of the day, with a side salad
Burrito(v), 4.29   -- A large burrito, with whole pinto beans, salsa, guacamole