O padrão Template Method, uma técnica de design baseada em herança, permite definir a estrutura de um algoritmo em uma classe pai, delegando a subclasses a implementação de etapas específicas. Isso promove a reutilização de código e a extensibilidade, ao mesmo tempo que mantém um controle centralizado sobre o fluxo geral do algoritmo.
Exemplo Clássico: Café e Chá
Um exemplo comum para ilustrar o Template Method envolve a preparação de bebidas. Considere a preparação de café:
var CoffeeMaker = function () {};
CoffeeMaker.prototype.boilWater = function() {
console.log('Fervendo a água');
};
CoffeeMaker.prototype.brewCoffee = function() {
console.log('Preparando café com água fervente');
};
CoffeeMaker.prototype.pourInCup = function() {
console.log('Despejando café na xícara');
};
CoffeeMaker.prototype.addSugarAndMilk = function() {
console.log('Adicionando açúcar e leite');
};
CoffeeMaker.prototype.makeDrink = function() {
this.boilWater();
this.brewCoffee();
this.pourInCup();
this.addSugarAndMilk();
};
var myCoffee = new CoffeeMaker();
myCoffee.makeDrink();
Agora, vamos pensar na preparação de chá. Os passos são semelhantes:
- Ferver a água
- Infusão das folhas de chá com água fervente
- Despejar o chá na xícara
- Adicionar limão
Observamos que há semelhanças fundamentais. Podemos abstrair os passos comuns para uma classe base 'Beverage' (Bebida):
var Beverage = function() {};
Beverage.prototype.boilWater = function() {
console.log('Fervendo a água');
};
Beverage.prototype.brew = function() {}; // Método a ser implementado pelas subclasses
Beverage.prototype.pourInCup = function() {}; // Método a ser implementado pelas subclasses
Beverage.prototype.addCondiments = function() {}; // Método a ser implementado pelas subclasses
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
Agora, criamos as classes concretas 'Coffee' e 'Tea' que herdam de 'Beverage':
var Coffee = function() {};
Coffee.prototype = new Beverage(); // Estabelecendo a herança
Coffee.prototype.brew = function() {
console.log('Preparando café com água fervente');
};
Coffee.prototype.pourInCup = function() {
console.log('Despejando café na xícara');
};
Coffee.prototype.addCondiments = function() {
console.log('Adicionando açúcar e leite');
};
var myCoffee = new Coffee();
myCoffee.init(); // Executa o método init da classe pai (Beverage)
var Tea = function() {};
Tea.prototype = new Beverage(); // Estabelecendo a herança
Tea.prototype.brew = function() {
console.log('Infusão das folhas de chá com água fervente');
};
Tea.prototype.pourInCup = function() {
console.log('Despejando chá na xícara');
};
Tea.prototype.addCondiments = function() {
console.log('Adicionando limão');
};
var myTea = new Tea();
myTea.init(); // Executa o método init da classe pai (Beverage)
O Papel do Método Template
Em Beverage.prototype.init, temos o método template. Ele define o esqueleto do algoritmo, chamando métodos que podem (e devem) ser sobrescritos pelas subclasses. No JavaScript, diferentemente de linguagens com verificação estática, não há garantia de que as subclasses sobrescreverão os métodos abstratos. Para mitigar isso, podemos:
- Utilizar "duck typing" para simular verificações de interface.
- Fazer com que os métodos abstratos em
Beveragelancem um erro se não forem implementados, indicando claramente a falha em tempo de execução.
Exemplo lançando erro:
Beverage.prototype.brew = function() {
throw new Error('Subclasse deve sobrescrever o método brew');
};
Introduzindo Hooks (Ganchos)
Às vezes, uma sucblasse pode precisar variar um passo específico do algoritmo. O padrão Template Method pode acomodar isso com "hooks" (ganchos). Um hook é um método na classe pai com uma implementação padrão (ou nenhuma), que as subclasses podem optar por sobrescrever para alterar o comportamento.
Vamos modificar o exemplo para permitir que o cliente decida se quer condimentos:
var Beverage = function() {};
Beverage.prototype.boilWater = function() {
console.log('Fervendo a água');
};
Beverage.prototype.brew = function() {
throw new Error('Subclasse deve sobrescrever o método brew');
};
Beverage.prototype.pourInCup = function() {
throw new Error('Subclasse deve sobrescrever o método pourInCup');
};
Beverage.prototype.addCondiments = function() {
throw new Error('Subclasse deve sobrescrever o método addCondiments');
};
// Hook: decide se condimentos serão adicionados
Beverage.prototype.wantsCondiments = function() {
return true; // Implementação padrão: sempre adiciona
};
Beverage.prototype.init = function() {
this.boilWater();
this.brew();
this.pourInCup();
if (this.wantsCondiments()) { // Utiliza o hook
this.addCondiments();
}
};
var Coffee = function() {};
Coffee.prototype = new Beverage();
Coffee.prototype.brew = function() {
console.log('Preparando café com água fervente');
};
Coffee.prototype.pourInCup = function() {
console.log('Despejando café na xícara');
};
Coffee.prototype.addCondiments = function() {
console.log('Adicionando açúcar e leite');
};
// Subclasse sobrescreve o hook para perguntar ao usuário
Coffee.prototype.wantsCondiments = function() {
return window.confirm('Deseja adicionar açúcar e leite?');
};
var myCoffee = new Coffee();
myCoffee.init();
O Princípio Hollywood
O uso de hooks no Template Method exemplifica o Princípio Hollywood: "Não nos chame, nós chamaremos você". Em vez de o componente filho iniciar a execução, a classe pai (alta camada) gerencia o fluxo e chama os métodos apropriados do componente filho (baixa camada) quendo necessário.
Esse princípio é visto em padrões como Observer (Publish/Subscribe) e em callbacks, onde um componente de ordem superior decide quando e como utilizar um componente de ordem inferior.
Implementação Alternativa em JavaScript (Sem Herança Prototípica Explícita)
Em JavaScript, podemos alcançar um resultado similar ao Template Method utilizando funções de ordem superior e composição, em vez de depender estritamente da herança prototípica:
var createBeverageMaker = function(steps) {
var boilWater = function() {
console.log('Fervendo a água');
};
var brew = steps.brew || function() {
throw new Error('A função "brew" é obrigatória.');
};
var pourInCup = steps.pourInCup || function() {
throw new Error('A função "pourInCup" é obrigatória.');
};
var addCondiments = steps.addCondiments || function() {
// Opcional, pode não fazer nada se não for definido
};
var Maker = function() {};
Maker.prototype.init = function() {
boilWater();
brew();
pourInCup();
addCondiments();
};
return Maker;
};
var CoffeeMaker = createBeverageMaker({
brew: function() {
console.log('Preparando café com água fervente');
},
pourInCup: function() {
console.log('Despejando café na xícara');
},
addCondiments: function() {
console.log('Adicionando açúcar e leite');
}
});
var myCoffee = new CoffeeMaker();
myCoffee.init();
Nesta abordagem, a função createBeverageMaker atua como uma fábrica que retorna construtores. Os passos específicos são passados como argumentos, e o método init da classe retornada define o algoritmo, similar ao método template.
O Template Method é eficaz para encapsular variações em um algoritmo, definindo um framework em uma classe pai e permitindo que as subclasses preencham os detalhes. Embora a herança prototípica seja uma forma comum de implementá-lo, funções de ordem superior e composição oferecem alternativas flexíveis em JavaScript.