Javascript. Adapter pattern

Паттерн Адаптер (Adapter pattern) преобразует интерфейс класса к другому интерфейсу, на который рассчитан клиент. Адаптер обеспечивает совместную работу классов, невозможную в обычных условиях из-за несовместимости интерфейсов. Поэтому для использования клиента с несовместимым интерфейсом, необходимо создать адаптер, который выполняет преобразование.Таким образом клиент отделяется от реализованного интерфейса; и если ожидаются изменения интерфейса со временем, то адаптер способен инкапсулировать эти изменения, чтобы клиента не приходилось изменять каждый раз, когда ему потребуется работать с новым интерфейсом. Данный паттерн относится к структурным паттернам.


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


Рассмотрим пример. Допустим мы имеем дело с резиновой уткой MallardDuck с интерфейсом

interface Duck {
  public void quack(); 
  public void fly();


и дикой индюшкой WildTurkey с интерфейсом

interface Turkey {
  public void gooble();
  public void fly();
}

Допустим, что нам необходимо работать с дикой индюшкой так, словно мы работаем с уткой. Для этого создадим необходимый адаптер TurkeyAdapter, который сослужит нам полезную работу:

/* Target class */

var MallardDuck = function () {

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

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

/* Adaptee class */

var WildTurkey = function () {

this.gooble = function () {
console.log("Gobble gooble");
};

this.fly = function () {
console.log("I`m flying a short distance");
};
};

/* Adapter class: interface WildTurkey to interface MallardDuck */

var TurkeyAdapter = function (turkey) {

var turkey = turkey;

this.quack = function () {
turkey.gooble();
};

this.fly = function () {
for (var i = 0; i < 5; i++) {
turkey.fly()
}
};
};

/* testing... */

var duck = new MallardDuck();
var turkey = new WildTurkey();
var turkeyAdapter = new TurkeyAdapter(turkey);
turkeyAdapter.quack();
turkeyAdapter.fly(); 

На выходе получаем ожидаемый результат

Gobble gooble
I`m flying a short distance
I`m flying a short distance
I`m flying a short distance
I`m flying a short distance
I`m flying a short distance

Всем удачи!

Javascript. Command pattern

Паттерн Команда (Command pattern) инкапсулирует запрос в виде объекта, делая возможной параметризацию клиентских объектов с другими запросами, организацию очереди или регистрацию запросов, а также поддержку отмены операций. Данный паттерн относится к поведенческим паттернам.
Объект команды инкапсулирует запрос посредством привязки набора операций к конкретному получателю.  Для этого информация об операции и получателе "упаковывается" в объекте с методами execute() и undo(). При вызове метод execute() выполняет операцию с данным получателем (Receivers). Метод undo() выполняет обратную операцию (отмену). Внешние объекты (Invokers) не знают, какие именно операции выполняются, и с каким получателем; они только знают, что при вызове методов execute() или undo() их запросы будут выполнены.
На практике нередко встречаются "умные" объекты команд, которые реализуют запрос самостоятельно вместо его делегирования получателю. Команды также могут использоваться для реализации систем регистрации команд и поддержки транзакций.


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

/* Receiver */

var Calculator = function () {

    var result = 0;

    // add operation
    this.add = function (value) {
        result += value;
    };
   
    // subtract operation
    this.subtract = function (value) {
        result -= value;
    };
   
    // multiply operation
    this.multiply = function (value) {
        result *= value;
    };
   
    // divide operation
    this.divide = function (value) {
        if (value === 0) {
            throw new Error("Divide by zero is forbidden");
        }
        result /= value;
    };
   
    // set common result
    this.setResult = function (value) {
        result = value;
    };
   
    // get common result
    this.getResult = function () {
        return result;
    };

};

/* interface Command */

var Command = function () {
    this.execute = function () {};
    this.undo = function () {};
};

/* ConcreteCommand - AddCommand */

var AddCommand = function (calculator, value) {
   
    var calculator = calculator,
        value = value;
   
    this.execute = function () {
        calculator.add(value);
    };
   
    this.undo = function () {
        calculator.subtract(value);
    };
};

AddCommand.prototype = new Command();
AddCommand.prototype.constructor = AddCommand;

/* ConcreteCommand - SubtractCommand */

var SubtractCommand = function (calculator, value) {

    var calculator = calculator,
        value = value;
   
    this.execute = function () {
        calculator.subtract(value);
    };
   
    this.undo = function () {
        calculator.add(value);
    };

};

SubtractCommand.prototype = new Command();
SubtractCommand.prototype.constructor = SubtractCommand;

/* ConcreteCommand - MultiplyCommand */

var MultiplyCommand = function (calculator, value) {
   
    var calculator = calculator,
        value = value,
        undoResult;
   
    this.execute = function () {
        result = calculator.getResult();
        calculator.multiply(value);
    };
   
    this.undo = function () {
        if (value === 0) {
          
        }
        calculator.divide(value);
    };
};

MultiplyCommand.prototype = new Command();
MultiplyCommand.prototype.constructor = MultiplyCommand;

/* ConcreteCommand - DivideCommand */

var DivideCommand = function (calculator, value) {

    var calculator = calculator,
        value = value;
   
    this.execute = function () {
        calculator.divide(value);
    };
   
    this.undo = function () {
        calculator.multiply(value);
    };

};

DivideCommand.prototype = new Command();
DivideCommand.prototype.constructor = DivideCommand;

/* Invoker */

var User = function () {

    var calculator = new Calculator();
        commands = [],
        command = null;
   
    this.add = function (value) {
        command = new AddCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.subtract = function (value) {
        command = new SubtractCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.multiply = function (value) {
        command = new MultiplyCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.divide = function (value) {
        command = new DivideCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.undo = function () {
        if (commands.length === 0) {
            throw new Error("Command stack is empty");
        }
        command = commands.pop();
        command.undo();
    };
   
    this.getCount = function () {
        return calculator.getResult();
    };
   
};


// testing
var accountant = new User();
accountant.add(5);
accountant.subtract(4);
console.log(accountant.getCount()); // 1


Данная реализация имеет один существенный недостаток: предположим, что одной из операций будет accountant.multiply(0), и в случае, если мы захотим откатить результат, то команда MultiplyCommand попытает сделать запрос undo(), но как известно на ноль делить нельзя и на выходе получим неприятное сообщение "Divide by zero is forbidden". То есть результат не будет откатан к первоначальному виду, одно из решений может быть таким:

/* Receiver */

var Calculator = function () {

    var result = 0;

    // add operation
    this.add = function (value) {
        result += value;
    };
   
    // subtract operation
    this.subtract = function (value) {
        result -= value;
    };
   
    // multiply operation
    this.multiply = function (value) {
        result *= value;
    };
   
    // divide operation
    this.divide = function (value) {
        if (value === 0) {
            throw new Error("Divide by zero is forbidden");
        }
        result /= value;
    };
   
    // set common result
    this.setResult = function (value) {
        result = value;
    };
   
    // get common result
    this.getResult = function () {
        return result;
    };

};

/* interface Command */

var Command = function () {
    this.execute = function () {};
    this.undo = function () {};
};

/* ConcreteCommand - AddCommand */

var AddCommand = function (calculator, value) {
   
    var calculator = calculator,
        value = value,
        undoResult = calculator.getResult();
   
    this.execute = function () {
        calculator.add(value);
    };
   
    this.undo = function () {
        calculator.setResult(undoResult);
    };
   
};

AddCommand.prototype = new Command();
AddCommand.prototype.constructor = AddCommand;

/* ConcreteCommand - SubtractCommand */

var SubtractCommand = function (calculator, value) {

    var calculator = calculator,
        value = value,
        undoResult = calculator.getResult();
   
    this.execute = function () {
        calculator.subtract(value);
    };
   
    this.undo = function () {
        calculator.setResult(undoResult);
    };

};

SubtractCommand.prototype = new Command();
SubtractCommand.prototype.constructor = SubtractCommand;

/* ConcreteCommand - MultiplyCommand */

var MultiplyCommand = function (calculator, value) {
   
    var calculator = calculator,
        value = value,
        undoResult = calculator.getResult();
   
    this.execute = function () {
        calculator.multiply(value);
    };
   
    this.undo = function () {
        calculator.setResult(undoResult);
    };
   
};

MultiplyCommand.prototype = new Command();
MultiplyCommand.prototype.constructor = MultiplyCommand;

/* ConcreteCommand - DivideCommand */

var DivideCommand = function (calculator, value) {

    var calculator = calculator,
        value = value,
        undoResult = calculator.getResult();;
   
    this.execute = function () {
        calculator.divide(value);
    };
   
    this.undo = function () {
        calculator.setResult(undoResult);
    };

};

DivideCommand.prototype = new Command();
DivideCommand.prototype.constructor = DivideCommand;

/* Invoker */

var User = function () {

    var calculator = new Calculator();
        commands = [],
        command = null;
   
    this.add = function (value) {
        command = new AddCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.subtract = function (value) {
        command = new SubtractCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.multiply = function (value) {
        command = new MultiplyCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.divide = function (value) {
        command = new DivideCommand(calculator, value);
        commands.push(command);
        command.execute();
    };
   
    this.undo = function () {
        if (commands.length === 0) {
            throw new Error("Command stack is empty");
        }
        command = commands.pop();
        command.undo();
    };
   
    this.getCount = function () {
        return calculator.getResult();
    };
   
};

// testing
var accountant = new User();
accountant.add(5);
accountant.add(10);
accountant.add(1);
accountant.multiply(0);
accountant.undo();
console.log(accountant.getCount()); // 16


Как видим, все отрабатывает без ошибок. Вот еще один простой пример:

/* interface Command */

var Command = function () {
    this.execute = function () {};
};

/* Receiver - Light */

var Light = function () {
    this.on = function () {
        console.log("The light is on");
    };
    this.off = function () {
        console.log("The light is off");
    };
};

/* Concrete Command - LightOnCommand */

var LightOnCommand = function (light) {
    var light = light;
    this.execute = function () {
        light.on();
    };
};

LightOnCommand.prototype = new Command();
LightOnCommand.prototype.constructor = LightOnCommand;

/* Concrete Command - LightOffCommand */

var LightOffCommand = function (light) {
    var light = light;
    this.execute = function () {
        light.off();
    };
};

LightOffCommand.prototype = new Command();
LightOffCommand.prototype.constructor = LightOffCommand;

/* Invoker - SimpleSwitcher */

var SimpleSwitcher = function () {
  
    var onComand,
        offCommand;
  
    this.setCommand = function (onCmd, offCmd) {
        onCommand = onCmd;
        offCommand = offCmd;
    };
  
    this.onButton = function () {
        onCommand.execute();
    };
  
    this.offButton = function () {
        offCommand.execute();
    };
  
};

/* test application */

var Application = function () {

    this.run = function () {
  
        // light switcher
        var switcher = new SimpleSwitcher();
      
        //
        var light = new Light();
      
        var lightOnCommand = new LightOnCommand(light);
        var lightOffCommand = new LightOffCommand(light);
      
        switcher.setCommand(lightOnCommand, lightOffCommand);
      
        switcher.onButton();
        switcher.offButton();
  
    };

};

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


На выходе:

The light is on
The light is off


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

Javascript. Singleton pattern

Паттерн Одиночка (Singleton) гарантирует, что класс имеет только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру. Данный паттерн определяет альтернативный способ создания объектов - в данном случае уникальных объектов, то есть гарантируется существование в приложении не более одного экземпляра данного класса. Данный паттерн относится к порождающим шаблонам.


Самый простой способ создать такой уникальный объект с глобальной точкой доступа на javascript - создать объект с помощью литералов.

var unique = (function (global) {

        // create global unique singleton object
return global.unique = {};

})(window);

Вот иная реализация (согласно вышеприведенной диаграмме класса Sigleton):

var Singleton = (function () {

// static variable
var uniqueInstance = null;

function Singleton() {
throw new Error("Forbidden to instantiate the Singleton class. Use Singleton.getInstance method");
}

// private constructor
function ctor() {

var id = id || 0;

this.getId = function () {};

this.setId = function (_id) {
id = _id || id;
};
}

// for checking by instanceof Singleton 
ctor.prototype = Singleton.prototype;

Singleton.getInstance = function () {
if (uniqueInstance === null) {
uniqueInstance = new ctor();
}
return uniqueInstance;
};

return Singleton;
})();

// var object1 = new Singleton(); // Forbidden to instantiate the Singleton class. Use Singleton.getInstance method

var object1 = Singleton.getInstance();
var object2 = Singleton.getInstance();

console.log(object1 instanceof Singleton); // true
console.log(object1 === object2); // true

Или вот еще один пример реализации данного паттерна:

var Module = function () {
    
    if (typeof Module.instance === "object") {
        return Module.instance;
    }
    
    var days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
        today = new Date(),
        week = days[today.getDay()],
        day = today.getDate(),
        day = day < 10 ? '0' + day : day,
        month = today.getMonth() + 1,
        month = month < 10 ? '0' + month : month,
        year = today.getFullYear();
        hours = today.getHours(),
        minutes = today.getMinutes(),
        initDate = week + ', ' + day + '.' + month + '.' + year,
        initFullDate = week + ', ' + day + '.' + month + '.' + year + ', ' + hours + ':' + minutes;
        
    this.getInitDate = function () {
        return initDate;
    };
    
    this.getInitFullDate = function () {
        return initFullDate;
    };
    
    Module.instance = this;
    
};

var module1 = new Module();
var module2 = new Module();

console.log(module1 === module2); // true
console.log(module1.getInitFullDate()); // Sun, 18.05.2014, 22:44
console.log(module2.getInitFullDate()); // Sun, 18.05.2014, 22:44

Module.prototype.isInit = true;

console.log(module1.isInit); // true
console.log(module2.isInit); // true

Javascript. Factory Method pattern

Паттерн Фабричный Метод (Factory Method) определяет интерфейс создания объекта, но позволяет субклассам выбрать класс создаваемого экземпляра. Таким образом, Фабричный метод делегирует операцию создания экземпляра субклассам. Относится к порождающим шаблонам проектирования.



Паттерн Фабричный Метод, как все остальные разновидности фабрик, предоставляет интерфейс к методу создания объектов, также называемому "фабричным методом". Остальные методы, реализуемые в абстрактном классе Creator, работают с продуктами, созданными фабричным методом. Только субклассы фактически реализуют фабричный метод и создают продукты.
Рассмотрим паттерн на примере заказа пиццы с использованием фабричного метода.

/* Product - Pizza */

var Pizza = function () {

    this.prepare = function () {
        console.log("Preparing " + this.name);
        console.log("Tossing dough...");
        console.log("Adding sauce...");
        console.log("Adding toppings: ");
        for (var i = 0; i < this.toppings.length; i++) {
            console.log("  " + this.toppings[i]);
        }
    };
   
    this.bake = function () {
        console.log("Bake for 25 minutes at 350");
    };
   
    this.cut = function () {
        console.log("Cutting the pizza into diagonal slices");
    };
   
    this.box = function () {
        console.log("Place pizza in official PizzaStore box");
    };
   
    this.getName = function () {
        return this.name;
    };

};

/* ConcreteProduct - NYStyleCheesePizza */

var NYStyleCheesePizza = function () {
    this.name = "NY Style Sauce and Cheese Pizza";
    this.dough = "Thin Crust Dough";
    this.sauce = "Marinara Sauce";
    this.toppings = ["Grated Reggiano Cheese"];
};

NYStyleCheesePizza.prototype = new Pizza();
NYStyleCheesePizza.prototype.constructor = NYStyleCheesePizza;

/* ConcreteProduct - ChicagoStyleCheesePizza */

var ChicagoStyleCheesePizza = function () {
    this.name = "Chicago Style Deep Dish Cheese Pizza";
    this.dough = "Extra Thick Crust Dough";
    this.sauce = "Plum Tomato Sauce";
    this.toppings = ["Shredded Mozzarella Cheese"];
    this.cut = function () {
        console.log("Cutting the pizza into square slices");
    };
};

ChicagoStyleCheesePizza.prototype = new Pizza();
ChicagoStyleCheesePizza.prototype.constructor = ChicagoStyleCheesePizza;

/* Creator - PizzaStore */

var PizzaStore = function () {

    this.orderPizza = function () {
        var pizza = this.createPizza();
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();
        return pizza
    };

    // abstract createPizza();

};

/* ConcreteCreator - NYPizzaStore */

var NYPizzaStore = function () {
    // factory method
    this.createPizza = function () {
        return new NYStyleCheesePizza();
    };
};

NYPizzaStore.prototype = new PizzaStore();
NYPizzaStore.prototype.constructor = NYPizzaStore;

/* ConcreteCreator - NYPizzaStore */

var ChicagoPizzaStore = function () {
    // factory method
    this.createPizza = function () {
        return new ChicagoStyleCheesePizza();
    };
};

ChicagoPizzaStore.prototype = new PizzaStore();
ChicagoPizzaStore.prototype.constructor = ChicagoPizzaStore;

/* test application */

var Application = function () {
    this.run = function () {
       
        var nyStore = new NYPizzaStore();
        var chicagoStore = new ChicagoPizzaStore();
       
        var pizza = nyStore.orderPizza();
        console.log("Ethan ordered a " + pizza.getName());
       
        pizza = chicagoStore.orderPizza();
        console.log("Joel ordered a " + pizza.getName());
       
    };
};

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


На выходе:

Preparing NY Style Sauce and Cheese Pizza
Tossing dough...
Adding sauce...
Adding toppings:
  Grated Reggiano Cheese
Bake for 25 minutes at 350
Cutting the pizza into diagonal slices
Place pizza in official PizzaStore box
Ethan ordered a NY Style Sauce and Cheese Pizza

Preparing Chicago Style Deep Dish Cheese Pizza
Tossing dough...
Adding sauce...
Adding toppings:
  Shredded Mozzarella Cheese
Bake for 25 minutes at 350
Cutting the pizza into square slices
Place pizza in official PizzaStore box
Joel ordered a Chicago Style Deep Dish Cheese Pizza

Javascript. Decorator pattern

Паттерн Декоратор - динамически наделяет объект новыми возможностями, и является гибкой альтернативой субклассированию в области расширения функциональности.
Данный паттерн относится к структурным паттернам. Для динамического добавления нового поведения используется композиция и делегирование. Компонент может декорироваться любым количеством декораторов.






Ниже рассмотрены примеры реализации данного паттерна. Первый пример показывает применение декораторов в роли дополнительных компонентов для того или иного вида кофе (см.рис. ниже).



/* Component - Beverage */

var Beverage = function () {

    this.description = "Unknown Beverage";
   
    this.getDescription = function () {
        return this.description;
    };
   
    // abstract cost();
    this.cost = function () {
        throw new Error("Must implement the abstract method - cost()");
    };

};

/* Decorator - CondimentDecorator */

var CondimentDecorator = function () {

    // abstract getDescription();
    this.getDescription = function () {
        throw new Error("Must implement the abstract method - getDescription()");
    };

};

CondimentDecorator.prototype = new Beverage();
CondimentDecorator.prototype.constructor = CondimentDecorator;

/* ConcreteComponent - Expresso */

var Expresso = function () {

    this.description = "Expresso";
   
    this.cost = function () {
        return 1.99;
    };

};

Expresso.prototype = new Beverage();
Expresso.prototype.constructor = Expresso;

/* ConcreteComponent - HouseBlend */

var HouseBlend = function () {

    this.description = "House Blend Coffee";
   
    this.cost = function () {
        return 0.89;
    };

};

HouseBlend.prototype = new Beverage();
HouseBlend.prototype.constructor = HouseBlend;

/* ConcreteComponent - DarkRoast */

var DarkRoast = function () {

    this.description = "Dark Roast Coffee";
   
    this.cost = function () {
        return 0.99;
    };

};

DarkRoast.prototype = new Beverage();
DarkRoast.prototype.constructor = DarkRoast;

/* ConcreteComponent - Decaf */

var Decaf = function () {

    this.description = "Decaf Coffee";
   
    this.cost = function () {
        return 1.05;
    };

};

Decaf.prototype = new Beverage();
Decaf.prototype.constructor = Decaf;

/* ConcreteDecorator - Mocha */

var Mocha = function (begerage) {

    var begerage = begerage;
   
    this.getDescription = function () {
        return begerage.getDescription() + ", Mocha";
    };
   
    this.cost = function () {
        return 0.20 + begerage.cost();
    };

};

Mocha.prototype = new CondimentDecorator();
Mocha.prototype.constructor = Mocha;

/* ConcreteDecorator - Milk */

var Milk = function (begerage) {

    var begerage = begerage;
   
    this.getDescription = function () {
        return begerage.getDescription() + ", Milk";
    };
   
    this.cost = function () {
        return 0.10 + begerage.cost();
    };

};

Milk.prototype = new CondimentDecorator();
Milk.prototype.constructor = Milk;

/* ConcreteDecorator - Soy */

var Soy = function (begerage) {

    var begerage = begerage;
   
    this.getDescription = function () {
        return begerage.getDescription() + ", Soy";
    };
   
    this.cost = function () {
        return 0.15 + begerage.cost();
    };

};

Soy.prototype = new CondimentDecorator();
Soy.prototype.constructor = Soy;

/* ConcreteDecorator - Whip */

var Whip = function (begerage) {

    var begerage = begerage;
   
    this.getDescription = function () {
        return begerage.getDescription() + ", Whip";
    };
   
    this.cost = function () {
        return 0.10 + begerage.cost();
    };

};

Whip.prototype = new CondimentDecorator();
Whip.prototype.constructor = Whip;

/* test application - StarbuzzCoffee */

var StarbuzzCoffee = function () {

    var beverage = new Expresso();
   
    console.log(beverage.getDescription() + " $" + beverage.cost());
   
    var beverage2 = new DarkRoast();
    beverage2 = new Mocha(beverage2);
    beverage2 = new Mocha(beverage2);
    beverage2 = new Whip(beverage2);
   
    console.log(beverage2.getDescription() + " $" + beverage2.cost());
   
    var beverage3 = new HouseBlend();
    beverage3 = new Soy(beverage3);
    beverage3 = new Mocha(beverage3);
    beverage3 = new Whip(beverage3);
   
    console.log(beverage3.getDescription() + " $" + beverage3.cost());

};

StarbuzzCoffee();


На выходе:

Expresso $1.99
Dark Roast Coffee, Mocha, Mocha, Whip $1.49
House Blend Coffee, Soy, Mocha, Whip $1.34


Или вот другой пример на основе списков декораторов:

var Sale = function (price) {
    this.price = (price > 0) || 100;
    this.decorators_list = [];
};

Sale.decorators = {};

Sale.decorators.fedtax = {
    getPrice: function (price) {
        return price + price * 5 / 100;
    }
};

Sale.decorators.quebec = {
    getPrice: function (price) {
        return price + price * 7.5 / 100;
    }
};

Sale.decorators.money = {
    getPrice: function (price) {
        return "$" + price.toFixed(2);
    }
};

Sale.decorators.cdn = {
    getPrice: function (price) {
        return "CDN$ " + price.toFixed(2);
    }
};

Sale.prototype.decorate = function (decorator) {
    this.decorators_list.push(decorator);
};

Sale.prototype.getPrice = function () {
    var price = this.price,
        i,
        max = this.decorators_list.length,
        name;
    for (i = 0; i < max; i += 1) {
        name = this.decorators_list[i];
        price = Sale.decorators[name].getPrice(price);
    }
    return price;
};

var Application = function () {
    this.run = function () {
   
        var sale = new Sale(100);
        sale.decorate('fedtax');
        sale.decorate('quebec');
        sale.decorate('money');
        console.log(sale.getPrice()); // $113
   
    };
};

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

Javascript. Observer pattern

Паттерн Наблюдатель (Observer) - определяет отношение "один-ко-многим" между объектами таким образом, что при изменении состояния одного объекта происходит автоматическое оповещение и обновление всех зависимых объектов.
При использовании паттерна возможен как запрос, так и активная доставка данных от субъекта (запрос считается более "правильным"). Данный паттерн относится к поведенческим паттернам.


Рассмотрим примеры реализации данного паттерна.

1. Модель активной доставки данных

/* interface Observer */

var Observer = function () {
    this.update = function () {};
};

/* interface Observable */

var Observable = function () {
    this.registerObserver = function () {};
    this.removeObserver = function () {};
    this.notifyObservers = function () {};
};

/* ConcreteObservable - WeatherData */

var WeatherData = function () {
   
    var observers = [],
        temperature,
        humidity,
        pressure;
   
    this.registerObserver = function (o) {
        observers.push(o);
    };
   
    this.removeObserver = function (o) {
        var index = observers.indexOf(o);
        if (index >= 0) {
            observers.splice(index, 1);
        }
    };
   
    this.notifyObservers = function () {
        for (var observer in observers) {
            observers[observer].update(temperature, humidity, pressure);
        }
    };
   
    this.setMeasurements = function (_temperature, _humidity, _pressure) {
        temperature = _temperature;
        humidity = _humidity;
        pressure = _pressure;
        this.notifyObservers();
    };
   
};

WeatherData.prototype = new Observable();
WeatherData.prototype.constructor = WeatherData;

/* ConcreteObserver - CurrentConditionsDisplay */

var CurrentConditionsDisplay = function (weatherData) {
   
    var temperature,
        humidity,
        weatherData = weatherData;
   
    weatherData.registerObserver(this);
   
    this.update = function (_temperature, _humidity, _pressure) {
        temperature = _temperature;
        humidity = _humidity;
        this.display();
    };
   
    this.display = function () {
        console.log("Current values: " + temperature + " degrees Celsius and " + humidity + "% humidity");
    };
   
};

CurrentConditionsDisplay.prototype = new Observer();
CurrentConditionsDisplay.prototype.constructor = CurrentConditionsDisplay;

/* test application - WeatherStation */

var WeatherStation = function () {

    var weatherData = new WeatherData();
   
    var currentDisplay = new CurrentConditionsDisplay(weatherData);
   
    weatherData.setMeasurements(29, 65, 30.4);
    weatherData.setMeasurements(39, 70, 29.4);
    weatherData.setMeasurements(42, 72, 31.4);
   
}

WeatherStation();


На выходе:

Current values: 29 degrees Celsius and 65% humidity
Current values: 39 degrees Celsius and 70% humidity
Current values: 42 degrees Celsius and 72% humidity


2. Модель запроса данных

/* interface Observer */

var Observer = function () {
    this.update = function () {};
};

/* interface Observable */

var Observable = function () {
    this.registerObserver = function () {};
    this.removeObserver = function () {};
    this.notifyObservers = function () {};
};

/* ConcreteObservable - WeatherData */

var WeatherData = function () {
   
    var observers = [],
        changed = false,
        temperature,
        humidity,
        pressure;
   
    this.registerObserver = function (o) {
        observers.push(o);
    };
   
    this.removeObserver = function (o) {
        var index = observers.indexOf(o);
        if (index >= 0) {
            observers.splice(index, 1);
        }
    };
   
    this.notifyObservers = function () {
        if (changed) {
            for (var observer in observers) {
                observers[observer].update(this);
            }
            changed = false;
        }
    };
   
    this.setMeasurements = function (_temperature, _humidity, _pressure) {
        temperature = _temperature;
        humidity = _humidity;
        pressure = _pressure;
        this.measurementsChanged();
    };
   
    this.measurementsChanged = function () {
        this.setChanged();
        this.notifyObservers();
    };
   
    this.setChanged = function () {
        changed = true;
    };
   
    this.getTemperature = function () {
        return temperature;
    };
   
    this.getHumidity = function () {
        return humidity;
    };
   
    this.getPressure = function () {
        return pressure;
    };
   
};

WeatherData.prototype = new Observable();
WeatherData.prototype.constructor = WeatherData;

/* ConcreteObserver - CurrentConditionsDisplay */

var CurrentConditionsDisplay = function (_weatherData) {
   
    var temperature,
        humidity,
        weatherData = weatherData;

    this.update = function (obs) {
        if (obs instanceof WeatherData) {
            var weatherData = obs;
            temperature = weatherData.getTemperature();
            humidity = weatherData.getHumidity();
            this.display();
        }
    };
   
    this.display = function () {
        console.log("Current values: " + temperature + " degrees Celsius and " + humidity + "% humidity");
    };
   
    (function (widget) {
        weatherData = _weatherData;
        weatherData.registerObserver(widget);
    })(this);
   
};

CurrentConditionsDisplay.prototype = new Observer();
CurrentConditionsDisplay.prototype.constructor = CurrentConditionsDisplay;

/* test application - WeatherStation */

var WeatherStation = function () {

    var weatherData = new WeatherData();
   
    var currentDisplay = new CurrentConditionsDisplay(weatherData);
   
    weatherData.setMeasurements(29, 65, 30.4);
    weatherData.setMeasurements(39, 70, 29.4);
    weatherData.setMeasurements(42, 72, 31.4);
   
}

WeatherStation();


На выходе получаем аналогичный результат:

Current values: 29 degrees Celsius and 65% humidity
Current values: 39 degrees Celsius and 70% humidity
Current values: 42 degrees Celsius and 72% humidit
y

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}]


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