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


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