Шаблонный метод (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}]
Всем успехов!