Javascript. Template method pattern

Шаблонный метод (Template method) - определяет "скелет" алгоритма в методе, оставляя определение реализации некоторых шагов субклассам. Субклассы могут переопределять некоторые части алгоритма без изменения его структуры.
Данный шаблон относится к паттернам поведения.
Ниже приведена общая схема данного шаблона.


Рассмотрим реализацию данного паттерна на примере приготовления таких напитков как кофе или чай. С одной стороны напитки разные, но алгоритм приготовления схож. Так как в javascript отсутствует возможность использовать абстракции (имеется в виду - синтаксические), то в общем случае реализация может выглядеть следующим образом.



/* AbstractClass */

var CaffeineBeverage = function () {
   
    this.prepareRecipe = function () {
        boilWater();
        this.brew();
        pourInCup();
        this.addCondiments();
    };
   
    // abstract brew();
    // this.brew = function () {};

    // abstract addCondiments();
    // this.addCondiments = function () {};
   
    // final boilWater();
    var boilWater = function () {
        console.log("Boiling water");
    };

    // final pourInCup();
    var pourInCup = function () {
        console.log("Pouring into cup");
    };

};

/* ConcreteClass - Tea */

var Tea = function () {

    this.brew = function () {
        console.log("Steeping the tee");
    };
   
    this.addCondiments = function () {
        console.log("Adding Lemon");
    };

};

Tea.prototype = new CaffeineBeverage();
Tea.prototype.constructor = Tea;

/* ConcreteClass - Coffee */

var Coffee = function () {

    this.brew = function () {
        console.log("Dripping Coffee through filter");
    };
   
    this.addCondiments = function () {
        console.log("Adding Sugar and Milk");
    };

};

Coffee.prototype = new CaffeineBeverage();
Coffee.prototype.constructor = Coffee;

/* test application */

var Application = function () {
   
    this.run = function () {
       
        var tea = new Tea();
        tea.prepareRecipe();
       
        var coffee = new Coffee();
        coffee.prepareRecipe();
       
    };
   
};

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


На выходе получаем:

Boiling water
Steeping the tee
Pouring into cup
Adding Lemon

Boiling water
Dripping Coffee through filter
Pouring into cup
Adding Sugar and Milk


Также можно реализовать альтернативным способом, с помощью объектов литералов.

var CaffeineBeverage = (function () {

    // final boilWater()
    var boilWater = function () {
        console.log("Boiling water");
    };
   
    // final pourInCup()
    var pourInCup = function () {
        console.log("Pouring into cup");
    };
   
    return {
   
        prepareRecipe: function () {
            boilWater();
            this.brew();
            pourInCup();
            this.addCondiments();
        }
   
    };

})();

var Tea = function () {

    this.brew = function () {
        console.log("Steeping the tee");
    };
   
    this.addCondiments = function () {
        console.log("Adding Lemon");
    };

};

Tea.prototype = CaffeineBeverage;
Tea.prototype.constructor = Tea;

var Coffee = function () {

    this.brew = function () {
        console.log("Dripping Coffee through filter");
    };
   
    this.addCondiments = function () {
        console.log("Adding Sugar and Milk");
    };

};

Coffee.prototype = CaffeineBeverage;
Coffee.prototype.constructor = Coffee;


var Application = function () {
   
    this.run = function () {
       
        var tea = new Tea();
        tea.prepareRecipe();
       
        var coffee = new Coffee();
        coffee.prepareRecipe();
       
    };
   
};

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


Но все же первый способ более привлекателен в силу того, что "классы" также объявляются с помощью функций-конструкторов. Данный паттерн подразумевает использование абстрактного класса и абстрактных методов. Javascript настолько динамичен, что дает возможность симулировать данные аспекты. С абстрактными методами немного проще, их отсутствие можно расценивать своего рода как их объявление (идиома), так как для дальнейшей работоспособности алгоритма в клиенте обязательно наличие реализации таких методов, иначе мы получим ошибку во время интерпретации приложения. Как же быть с абстрактными классами? Здесь можно воспользоваться использованием так называемых примесей (mixin), причем в роли таких примесей могут выступать "абстрактные классы". Рассмотрим данную технику на рассматриваемом паттерне.
Мы знаем, что абстрактные классы запрещено инстанцировать, тогда можно написать следующую форму "абстрактного класса":

var Shape = function () {

    if (this instanceof Shape || this === window) {
        return new Error("Can not instance object from abstract class");
    }

};


Как видим, данный класс невозможно инстанцировать, далее для того, чтобы реализовывать данный класс, можно внедрить в прототип наследника данную примесь:

var Square = function () {};
Shape.call(Square.prototype);


Вот пример реализации паттерна с помощью данной техники.

/* AbstractClass as Mixin class */

var AbstractClass = function () {

    if (this instanceof AbstractClass || this === window) {
        throw new Error("");
    }
   
    this.templateMethod = function () {
        this.primitiveOperation1();
        primitiveOperation2();
    };
   
    var primitiveOperation2 = function () {
        console.log("primitiveOperation2");
    };

};

var ConcreteClass = function () {
    this.primitiveOperation1 = function () {
        console.log("primitiveOperation1");
    };
};

AbstractClass.call(ConcreteClass.prototype);

var concreteTest = new ConcreteClass();
concreteTest.templateMethod();


На выходе получаем

primitiveOperation1
primitiveOperation2


Продолжим дальше рассматривать Шаблонный метод. Стоит также рассмотреть использование так называемых "перехватчиков" (hook). Это методы, объявленные в нашем абстрактном классе, но имеющие пустую реализацию или реализацию по умолчанию. Он дает возможность
субклассу "подключаться" к алгоритму в разных точках. Впрочем, субкласс также может проигнорировать имеющийся перехватчик. Рассмотрим пример вышеприведенной реализации на основе использования одного из "методов-перехватчиков".

var CaffeineBeverage = function () {
   
    this.prepareRecipe = function () {
        boilWater();
        this.brew();
        pourInCup();
        if (this.customerWantsCondiments()) {
            this.addCondiments();
        }
    };
   
    // abstract brew();
    // this.brew = function () {};

    // abstract addCondiments();
    // this.addCondiments = function () {};
   
    // final boilWater();
    var boilWater = function () {
        console.log("Boiling water");
    };

    // final pourInCup();
    var pourInCup = function () {
        console.log("Pouring into cup");
    };
   
    // hook
    this.customerWantsCondiments = function () {
        return true;
    };

};

var Tea = function () {

    this.brew = function () {
        console.log("Steeping the tee");
    };
   
    this.addCondiments = function () {
        console.log("Adding Lemon");
    };
   
    this.customerWantsCondiments = function () {
        return getUserInput();
    };

    var getUserInput = function () {
        var msg = "Would you like limon with your tea?";
        return confirm(msg);
    };
   
};

Tea.prototype = new CaffeineBeverage();
Tea.prototype.constructor = Tea;

var Coffee = function () {

    this.brew = function () {
        console.log("Dripping Coffee through filter");
    };
   
    this.addCondiments = function () {
        console.log("Adding Sugar and Milk");
    };
   
    this.customerWantsCondiments = function () {
        return getUserInput();
    };

    var getUserInput = function () {
        var msg = "Would you like milk and sugar with your coffee?";
        return confirm(msg);
    };

};

Coffee.prototype = new CaffeineBeverage();
Coffee.prototype.constructor = Coffee;

var Application = function () {
   
    this.run = function () {
      
        var tea = new Tea();
        tea.prepareRecipe();
      
        var coffee = new Coffee();
        coffee.prepareRecipe();
      
    };
   
};

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


Как видим, мы используем данный перехватчик для уточнения у клиента о необходимости добавления одного из компонентов в напиток. Ниже приведен еще один пример использования сортировки на основе шаблонного метода.

/* Abstract Class - Sorter */

var Sorter = function () {

    // templateMethod()
    this.sort = function (array) {
        array.sort(this.compareTo);
    };

    this.compareTo = function () {};
   
};

/* Concrete Class - NumberSorter */

var NumberSorter = function () {

    this.compareTo = function (a, b) {
        return a - b;
    };

};

NumberSorter.prototype = new Sorter();
NumberSorter.prototype.constructor = NumberSorter;

/* Concrete Class - NumberStringSorter */

var NumberStringSorter = function () {

    this.compareTo = function (a, b) {
        if (a === b) {
            return 0;
        }
        if (typeof a === typeof b) {
            return a < b ? -1: 1;
        }
        return typeof a < typeof b ? -1 : 1;
    };

};

NumberStringSorter.prototype = new Sorter();
NumberStringSorter.prototype.constructor = NumberStringSorter;

/* Concrete Class - ObjectSorter */

var ObjectSorter = function (name) {

    this.compareTo = (function (key) {
      
        return function (o, p) {      
            var a, b;
            if (typeof o === 'object' && typeof p === 'object' && o && p) {
                a = o[key];
                b = p[key];
                if (a === b) {
                    return 0;
                }
                if (typeof a === typeof b) {
                    return a < b ? -1 : 1;
                }
                return typeof a < typeof b ? -1 : 1;
            }
            else {
                throw {
                    name: 'Error',
                    message: 'Expected an object when sorting by ' + key
                };
            }
        };
      
    })(name);

};

ObjectSorter.prototype = new Sorter();
ObjectSorter.prototype.constructor = ObjectSorter;

/* test application */

var Application = function () {

    this.run = function () {
   
        var numberSorter = new NumberSorter();
        var objectSorterById = new ObjectSorter("id");

        var numbers = [5, 2, 8, 3, -1, 0];
        var objects = [{id:5}, {id:2}, {id:8}, {id:3}, {id:-1}, {id:0}];
      
        console.log("Before sort:");
        console.log(numbers);
        console.log(objects);
      
        numberSorter.sort(numbers);
        objectSorterById.sort(objects);
      
        console.log("After sort:");
        console.log(numbers);
        console.log(objects);
      
    };

};

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


На выходе получаем:

Before sort:
[5, 2, 8, 3, -1, 0]
[Object {id=5}, Object {id=2}, Object {id=8}, Object {id=3}, Object {id=-1}, Object {id=0}]

After sort:
[-1, 0, 2, 3, 5, 8]
[Object {id=-1}, Object {id=0}, Object {id=2}, Object {id=3}, Object {id=5}, Object {id=8}]


Под конец хотелось бы отметить, что данный паттерн и паттерн Стратегия имеют схожую цель - инкапсуляция алгоритма, причем придавая гибкость поддержки. Но все же структурно они различаются. Паттерн Стратегия - использует композицию, а Шаблонный метод - наследование; паттерн Стратегия - имеет дело с семейством алгоритмов, а Шаблонный метод - конкретно с алгоритмов, отличающимся только в определенных шагах его исполнения. Для сравнения, ниже приведен пример реализации сортировки но только с использованием паттерна Стратегия. Все вида сортировок - восходящие, если нужна поддержка и насходящей сортировки, можете попробовать реализовать ее.

/* interface SortBehavior - abstract Strategy of SortBehavior */

var SortBehavior = function () {
    this.sort = function () {};
};

/* NumberSort - concrete Strategy of SortBehavior */

var NumberSort = function () {
   
    var compareTo = function (a, b) {
        return a - b;
    };
   
    this.sort = function (array) {
        array.sort(compareTo);
    };
   
};

NumberSort.prototype = new SortBehavior();
NumberSort.prototype.constructor = NumberSort;

/* NumberStringSort - concrete Strategy of SortBehavior */

var NumberStringSort = function () {

    var compareTo = function (a, b) {
        if (a === b) {
            return 0;
        }
        if (typeof a === typeof b) {
            return a < b ? -1: 1;
        }
        return typeof a < typeof b ? -1 : 1;
    };
   
    this.sort = function (array) {
        array.sort(compareTo);
    };

};

NumberStringSort.prototype = new SortBehavior();
NumberStringSort.prototype.constructor = NumberStringSort;

/* ObjectSort - concrete Strategy of SortBehavior */

var ObjectSort = function (name) {

    var compareTo = (function (key) {
      
        return function (o, p) {      
            var a, b;
            if (typeof o === 'object' && typeof p === 'object' && o && p) {
                a = o[key];
                b = p[key];
                if (a === b) {
                    return 0;
                }
                if (typeof a === typeof b) {
                    return a < b ? -1 : 1;
                }
                return typeof a < typeof b ? -1 : 1;
            }
            else {
                throw {
                    name: 'Error',
                    message: 'Expected an object when sorting by ' + key
                };
            }
        };
      
    })(name);
   
    this.sort = function (array) {
        array.sort(compareTo);
    };

};

ObjectSort.prototype = new SortBehavior();
ObjectSort.prototype.constructor = ObjectSort;

/* Abstract Client of abstract SortBehavior Strategy */

var Sorter = function () {

    var sortBehavior;
   
    this.setSortBehavior = function (sb) {
        sortBehavior = sb;
    };

    this.performSort = function (array) {
        sortBehavior.sort(array);
    };

};

/* Concrete Client of FlyWithWings Strategy */

var NumberSorter = function () {
    this.setSortBehavior(new NumberSort());
};

NumberSorter.prototype = new Sorter();
NumberSorter.prototype.constructor = NumberSorter;

/* Concrete Client of FlyWithWings Strategy */

var NumberStringSorter = function () {
    this.setSortBehavior(new NumberStringSort());
};

NumberStringSorter.prototype = new Sorter();
NumberStringSorter.prototype.constructor = NumberStringSorter;

/* Concrete Client of FlyWithWings Strategy */

var ObjectSorter = function (name) {
    this.setSortBehavior(new ObjectSort(name));
};

ObjectSorter.prototype = new Sorter();
ObjectSorter.prototype.constructor = ObjectSorter;

/* test application */

var Application = function () {

    this.run = function () {
   
        var numberSorter = new NumberSorter();
        var objectSorterById = new ObjectSorter("id");

        var numbers = [5, 2, 8, 3, -1, 0];
        var objects = [{id:5}, {id:2}, {id:8}, {id:3}, {id:-1}, {id:0}];
      
        console.log("Before sort:");
        console.log(numbers);
        console.log(objects);
      
        numberSorter.performSort(numbers);
        objectSorterById.performSort(objects);
      
        console.log("After sort:");
        console.log(numbers);
        console.log(objects);
      
    };

};

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


На выходе

Before sort:
[5, 2, 8, 3, -1, 0]
[Object {id=5}, Object {id=2}, Object {id=8}, Object {id=3}, Object {id=-1}, Object {id=0}]

After sort:
[-1, 0, 2, 3, 5, 8]
[Object {id=-1}, Object {id=0}, Object {id=2}, Object {id=3}, Object {id=5}, Object {id=8}]


Всем успехов!  

Javascript. Strategy pattern

Как известно, такой паттерн проектирования как Стратегия (Strategy) определяет семейство алгоритмов, инкапсулирует каждый из них и обеспечивает их взаимозаменяемость. Он позволяет модифицировать алгоритмы независимо от их использования на стороне клиента. Данный паттерн относится к паттернам поведения. Пример использования паттерна взят из книги - Э.Фримена, К.Сьерра, Б.Бейтс - Паттерны проектирования - с адаптацией под javascript. С идентичной реализацией на php можно ознакомиться по ссылке - PHP5. Композиция на примере паттерна Стратегия.

/* interface FlyBehavior - abstract Strategy of FlyBehavior */

var FlyBehavior = function () {
this.fly = function () {};
};

/* FlyWithWings - concrete Strategy of FlyBehavior */

var FlyWithWings = function () {
this.fly = function () {
console.log("I`m flying");
};
};

FlyWithWings.prototype = new FlyBehavior();
FlyWithWings.prototype.constructor = FlyWithWings;

/* FlyNoWay - concrete Strategy of FlyBehavior */

var FlyNoWay = function () {
this.fly = function () {
console.log("I can`t fly");
}
};

FlyNoWay.prototype = new FlyBehavior();
FlyNoWay.prototype.constructor = FlyNoWay;

/* FlyRocketPowered - concrete Strategy of FlyBehavior */

var FlyRocketPowered = function () {
this.fly = function () {
console.log("I`m flying with a rocket");
};
};

FlyRocketPowered.prototype = new FlyBehavior();
FlyRocketPowered.prototype.constructor = FlyRocketPowered;

/* interface QuackBehavior - abstract Strategy of QuackBehavior */

var QuackBehavior = function () {
this.quack = function () {};
};

/* Quack - concrete Strategy of QuackBehavior */

var Quack = function () {
this.quack = function () {
console.log("Quack");
}
};

Quack.prototype = new QuackBehavior();
Quack.prototype.constructor = Quack;

/* Squeak - concrete Strategy of QuackBehavior */

var Squeak = function () {
this.quack = function () {
console.log("Squeak");
}
};

Squeak.prototype = new QuackBehavior();
Squeak.prototype.constructor = Squeak;

/* MuteQuack - concrete Strategy of QuackBehavior */

var MuteQuack = function () {
this.quack = function () {
console.log("<< Silence >>");
}
};

MuteQuack.prototype = new QuackBehavior();
MuteQuack.prototype.constructor = MuteQuack;

/* Abstract Client of abstract FlyBehavior Strategy and abstract  QuackBehavior Strategy */

var Duck = function () {

var flyBehavior;
var quackBehavior;
// abstract method
this.display = function () {};
this.setFlyBehavior = function (fb) {
flyBehavior = fb;
};
this.setQuackBehavior = function (qb) {
quackBehavior = qb;
};
this.performFly = function () {
flyBehavior.fly();
};
this.performQuack = function () {
quackBehavior.quack();
};
this.swim = function () {
console.log("All ducks float, even decoys!");
};

};

/* Concrete Client of FlyWithWings Strategy and Quack Strategy */

var MallardDuck = function () {

this.setFlyBehavior(new FlyWithWings());
this.setQuackBehavior(new Quack());
this.display = function () {
console.log("I`m a real Mallard duck");
};

};

MallardDuck.prototype = new Duck();
MallardDuck.prototype.constructor = MallardDuck;

/* Concrete Client of FlyNoWay Strategy and Quack Strategy */

var ModelDuck = function () {

this.setFlyBehavior(new FlyNoWay());
this.setQuackBehavior(new Quack());

this.display = function () {
console.log("I`m a model duck");
};
};

ModelDuck.prototype = new Duck();
ModelDuck.prototype.constructor = ModelDuck;

/* test application */

var Application = function () {

this.run = function () {
var mallard = new MallardDuck();
mallard.performQuack();
mallard.performFly();
var model = new ModelDuck();
model.performQuack();
model.performFly();
                // dynamic set new Concrete Strategy FlyRocketPowered 
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
};

};

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

На выходе получаем следующие сообщения клиентов 

Quack
I`m flying

Quack
I can`t fly
I`m flying with a rocket

Ниже представлена схема реализации паттерна Стратегия (Strategy)

Javascript. Memento pattern

Memento (хранитель) - поведенческий шаблон проектирования, позволяющий, не нарушая инкапсуляцию, зафиксировать и сохранить внутреннее состояния объекта так, чтобы позднее восстановить его в это состояние.

Клиент запрашивает Memento (хранителя) у исходного объекта, когда ему необходимо сохранить состояние исходного объекта (установить контрольную точку). Исходный объект инициализирует Memento своим текущим состоянием. Клиент является "посыльным" за Memento, но только исходный объект может сохранять и извлекать информацию из Memento (Memento является "непрозрачным" для клиентов и других объектов). Если клиенту в дальнейшем нужно "откатить" состояние исходного объекта, он передает Memento обратно в исходный объект для его восстановления.
Паттерн проектирования Memento определяет трех различных участников:

Originator (хозяин) - объект, умеющий создавать хранителя, а также знающий, как восстановить свое внутреннее состояние из хранителя.

Caretaker (смотритель) - объект, который знает, почему и когда хозяин должен сохранять и восстанавливать себя.

Memento (хранитель) - "ящик на замке", который пишется и читается хозяином и за которым присматривает смотритель.


Ниже представлен пример реализации данного паттерна.

// "Хранитель"
var Memento = function (state) {
var state = state || 'undefined';
this.getState = function () {
return state;
};
};

// "Опекун"
var Caretaker = function () {

var memento;
this.getMemento = function () {
return memento;
};
this.setMemento = function (_memento) {
if (_memento instanceof Memento) {
memento = _memento;
}
};

};

// "Создатель"
var Originator = function () {

var state;
this.setState = function (_state) {
state = _state || "undefined";
};
this.getState = function () {
return state;
};
this.saveState = function () {
return new Memento(state);
};
this.restoreState = function (memento) {
if (memento instanceof Memento) {
state = memento.getState();
}
};

};

var Application = function () {
this.run = function () {
var originator = new Originator();
var caretaker = new Caretaker();
originator.setState('on');
console.log('State is ' + originator.getState());
caretaker.setMemento(originator.saveState());
originator.setState('off');
console.log('State is ' + originator.getState());
originator.restoreState(caretaker.getMemento());
console.log('State is ' + originator.getState());
};
};

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

На выходе приложения получаем следующие сообщения:

State is on
State is off
State is on

Javascript. Strict mode


Как известно, в спецификации ES5 определены так называемые Strict Mode правила, которые включают в себя ограничения, учитываемые во время «компиляции» и интерпретации (выполнения скрипта). Причем правила эти декларативного характера, то есть должны соблюдаться разработчиками, но не всегда. В настоящее время имеется возможность включать режим Strict Mode непосредственно в скрипт, тем самым оговаривая необходимость жесткого соблюдения правил. Для включения данного режима достаточно прописать

"use strict";

в заголовке кода и режим включен. Стоит также отметить, что данный режим в полной мере  поддерживается не всеми браузерами (есть тесты, в которых можно оценить степень поддержки режима в своем рабочем окружении). 

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