Saudações, programadores!
Estou certo de que, em algum momento (ou vários deles), você já trabalhou com herança de classes no desenvolvimento de software. Trata-se de um recurso valiosÃssimo da Orientação a Objetos que contribui para uma arquitetura de fácil manutenção através do reaproveitamento de código. O padrão de projeto Template Method está intimamente associado a este conceito. Confira!
Introdução
Nós, programadores, utilizamos herança com bastante frequência, principalmente quando uma regra de negócio é decomposta em várias regras especÃficas, mas parte dela é mantida em comum, compartilhada. Quando isso ocorre, geralmente criamos uma classe generalizada (ou popularmente chamada de classe base, classe pai ou superbase) e derivamos novas classes a partir dela, dando origem à s classes especializadas (também conhecidas como subclasses ou classes filhas). Optamos por essa abordagem pelo motivo de que algumas classes possuem comportamentos similares e, ao invés de copiar e colar o código, o reaproveitamos.
Um caso clássico que podemos citar sobre herança envolve a recuperação de dados de uma tabela. Considere, por exemplo, em um software contábil, um método que traz os dados de documentos executando os seguintes passos:
1 2 3 4 5 6 |
function ConsultarDados: TObjectList<T>; begin ConectarBancoDeDados; result := ExecutarConsulta; DesconectarBancoDeDados; end; |
Para recuperar dados de outra tabela, como a de impostos, terÃamos a mesma sequência de passos, com exceção de que a segunda linha retornaria os dados de impostos ao invés de documentos.
Observe, então, que temos dois comportamentos em comum: conectar e desconectar o banco de dados (primeira e terceira linhas). O método ExecutarConsulta
, na segunda linha, poderia ser implementado somente pelas subclasses. Cada uma executaria um tipo de consulta, enquanto o controle da conexão seria feito na classe base, já que é idêntico para qualquer consulta.
Pois bem, neste exemplo, o método ConsultarDados
(que executa os três passos) pode ser considerado um Template Method! Recebe essa definição porque consiste em um método que estabelece uma sequência de passos, porém, alguns deles são “delegados” para as subclasses em tempo de execução. Em outras palavras, as subclasses ganham a liberdade de alterar o comportamento de alguns trechos do método, contanto que a sequência de passos sempre permaneça a mesma.
Para concluir o exemplo, o método ExecutarConsulta
seria declarado como abstrato na classe base e implementado nas subclasses, utilizando as palavras reservadas virtual
, abstract
e override
do Delphi. Para os desenvolvedores que já estão acostumados a trabalhar com herança, esse conceito fica bem fácil de compreender.
Embora o padrão de projeto proponha uma arquitetura de classes com base em heranças, a operação principal ocorre em um único método, que compreende uma sequência prevista de passos. É ali que tudo ocorre.
O Template Method traz uma breve semelhança com o Strategy, mas há duas diferenças importantes que os distinguem:
- Com o Strategy, implementamos algoritmos que geram resultados semelhantes. Já com o Template Method, os resultados são sempre diferentes;
- No Template Method, há uma classe base que executa ações compartilhadas por todas as subclasses, diferente do Strategy, no qual trabalha-se com Interfaces e cada classe possui um algoritmo especÃfico.
Exemplo de codificação do Template Method
Dito isso, avançaremos para um exemplo prático bem interessante. Criaremos uma aplicação para consultar repositórios e usuários do GitHub através de uma API REST disponibilizada pelo próprio serviço. Conforme a documentação da API, as rotas são:
- Repositórios: https://api.github.com/search/repositories?q={busca}
- Usuários: https://api.github.com/search/users?q={busca}
No artigo anterior, utilizamos o TIdHTTP
para receber o retorno de uma requisição GET. Dessa vez, faremos diferente. O Delphi fornece um conjunto de componentes exclusivo para esse padrão de comunicação, encontrados na paleta REST Client. Utilizaremos os componentes TRESTClient
para configurar a URL base, TRESTRequest
para enviar a requisição e TRESTResponse
para receber a resposta em JSON.
O procedimento consiste em quatro passos: inicializar os objetos, enviar a requisição, processar o retorno e liberar os objetos da memória. Essa sequência de etapas será executada em um método chamado ConsultarDadosGitHub
que, como já podemos induzir, é o nosso Template Method.
1 2 3 4 5 6 7 |
function TAbstractClass.ConsultarDadosGitHub: olevariant; begin InicializarObjetos; EnviarRequisicao; result := ProcessarRetorno; // este método será abstrato LiberarObjetos; end; |
De todos os Design Patterns já apresentados até o momento, acredito que o Template Method é o que possui o menor número de elementos. Precisamos codificar apenas a Abstract Class, que declara os métodos abstratos, e as Concrete Classes, que implementam os métodos abstratos.
Abstract Class
Iniciaremos, então, pela Abstract Class. Nela, teremos a declaração do método ConsultarDadosGitHub
e a maior parte dos passos já implementada. A inicialização de objetos, envio da requisição e liberação dos objetos da memória serão codificados aqui.
O destaque da Abstract Class está na existência de métodos abstratos que fazem parte da sequência de passos. No código a seguir, essa caracterÃstica é dada ao método ProcessarRetorno
. Cada Concrete Class será responsável por implementar este método para processar o JSON de diferentes formas.
Para facilitar a compreensão, procurei adicionar comentários na maioria das linhas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
uses System.JSON, REST.Client, IPPeerClient; type TAbstractClass = class private // Objetos necessário para a comunicação com a API FRESTClient: TRESTClient; FRESTRequest: TRESTRequest; FRESTResponse: TRESTResponse; // Métodos internos da classe procedure InicializarObjetos; procedure EnviarRequisicao; procedure LiberarObjetos; protected // Variável para armazenar o parâmetro de consulta que será enviado na URL FParametros: string; // Objeto para receber o JSON de retorno FJSON: TJSONObject; // Método que será implementado nas subclasses function ProcessarRetorno: olevariant; virtual; abstract; public // Template Method function ConsultarDadosGitHub: olevariant; end; implementation uses System.SysUtils, REST.Types; { TAbstractClass } procedure TAbstractClass.EnviarRequisicao; begin // Configura os parâmetros da requisição FRESTRequest.Resource := FParametros; // Executa a requisição FRESTRequest.Execute; // Recebe o retorno em JSON e o atribui ao objeto FJSON FJSON.Parse(TEncoding.ASCII.GetBytes(FRESTResponse.JSONValue.ToString), 0); end; procedure TAbstractClass.InicializarObjetos; begin // Cria o objeto da classe TRESTClient FRESTClient := TRESTClient.Create('https://api.github.com/search/'); // Cria o objeto da classe TRESTResponse FRESTResponse := TRESTResponse.Create(nil); // Cria e configura o objeto da classe TRESTRequest FRESTRequest := TRESTRequest.Create(nil); FRESTRequest.Client := FRESTClient; FRESTRequest.Response := FRESTResponse; FRESTRequest.Method := rmGET; // Cria o objeto para manipular o JSON FJSON := TJSONObject.Create; end; procedure TAbstractClass.LiberarObjetos; begin // Libera os objetos da memória FRESTRequest.Free; FRESTResponse.Free; FRESTClient.Free; end; function TAbstractClass.ConsultarDadosGitHub: olevariant; begin // Este é o Template Method // O comportamento do método "ProcessarRetorno" será definido em tempo de execução InicializarObjetos; EnviarRequisicao; result := ProcessarRetorno; LiberarObjetos; end; |
Concrete Class
Em seguida, codificaremos a Concrete Class referente à consulta de repositórios, encarregada de implementar o método abstrato ProcessarRetorno
para converter o JSON em registros de um DataSet. Além disso, no construtor, atribuÃmos o resource (parâmetro de busca) à variável FParametro
, que será consumida pelo método EnviarRequisicao
da Abstract Class.
Aproveitando o ensejo, observem a simplicidade na leitura do array JSON utilizando as classes do namespace System.JSON do Delphi.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
uses Pattern.AbstractClass; type TConcreteClassRepositories = class(TAbstractClass) protected function ProcessarRetorno: olevariant; override; public constructor Create(const Parametro: string); end; implementation uses System.SysUtils, System.JSON, Data.DB, Datasnap.DBClient; { TConcreteClassRepositories } constructor TConcreteClassRepositories.Create(const Parametro: string); begin // Configura os parâmetros referente à consulta de repositórios FParametros := 'repositories?q=%s' + Parametro; end; function TConcreteClassRepositories.ProcessarRetorno: olevariant; var DataSetRetorno: TClientDataSet; JSONValue: TJSONValue; JSONObject: TJSONObject; begin // Cria um DataSet para tabular os dados consultados DataSetRetorno := TClientDataSet.Create(nil); try // Define as colunas DataSetRetorno.FieldDefs.Add('ID', ftInteger); DataSetRetorno.FieldDefs.Add('Nome', ftString, 40); DataSetRetorno.FieldDefs.Add('Linguagem', ftString, 15); DataSetRetorno.FieldDefs.Add('Observadores', ftInteger); DataSetRetorno.CreateDataSet; // Percorre o JSON, lendo os valores das chaves for JSONValue in FJSON.GetValue('items') as TJSONArray do begin JSONObject := JSONValue as TJSONObject; DataSetRetorno.AppendRecord([ JSONObject.GetValue('id').Value, JSONObject.GetValue('full_name').Value, JSONObject.GetValue('language').Value, JSONObject.GetValue('watchers').Value ]); end; // Devolve os dados tabulados result := DataSetRetorno.Data; finally DataSetRetorno.Free; end; end; |
A Concrete Class para consulta de usuários segue o mesmo padrão. Implementa o método abstrato ProcessarRetorno
e define o resource no construtor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
uses Pattern.AbstractClass; type TConcreteClassUsers = class(TAbstractClass) protected function ProcessarRetorno: olevariant; override; public constructor Create(const Parametro: string); end; implementation uses System.SysUtils, System.JSON, Data.DB, Datasnap.DBClient; { TConcreteClassUsers } constructor TConcreteClassUsers.Create(const Parametro: string); begin // Configura os parâmetros referente à consulta de usuários FParametros := 'users?q=%s' + Parametro; end; function TConcreteClassUsers.ProcessarRetorno: olevariant; var DataSetRetorno: TClientDataSet; JSONValue: TJSONValue; JSONObject: TJSONObject; begin // Cria um DataSet para tabular os dados consultados DataSetRetorno := TClientDataSet.Create(nil); try // Define as colunas DataSetRetorno.FieldDefs.Add('ID', ftInteger); DataSetRetorno.FieldDefs.Add('Login', ftString, 25); DataSetRetorno.FieldDefs.Add('URL', ftString, 40); DataSetRetorno.FieldDefs.Add('Score', ftFloat); DataSetRetorno.CreateDataSet; // Percorre o JSON, lendo os valores das chaves for JSONValue in FJSON.GetValue('items') as TJSONArray do begin JSONObject := JSONValue as TJSONObject; DataSetRetorno.AppendRecord([ JSONObject.GetValue('id').Value, JSONObject.GetValue('login').Value, JSONObject.GetValue('html_url').Value, JSONObject.GetValue('score').Value ]); end; // Devolve os dados tabulados result := DataSetRetorno.Data; finally DataSetRetorno.Free; end; end; |
Para finalizar, elaborei um formulário bem simples para atuar como Client:
Em ação!
No botão “Consultar”, basta chamar o Template Method (ConsultarDadosGitHub
) da Concrete Class desejada. Como este exemplo é didático, não adicionei tratamento de exceções, mas eles são importantes!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var ConcreteClass: TAbstractClass; begin ConcreteClass := nil; // Obtém a instância de uma Concrete Class de acordo com o tipo selecionado case RadioGroupTipo.ItemIndex of 0: ConcreteClass := TConcreteClassRepositories.Create(EditConsulta.Text); 1: ConcreteClass := TConcreteClassUsers.Create(EditConsulta.Text); end; try // Executa o Template Method ClientDataSet.Data := ConcreteClass.ConsultarDadosGitHub; finally ConcreteClass.Free; end; end; |
Feito, pessoal!
Conclusão
Ao analisar este exemplo, a proposta do Template Method pode nos remeter à uma única necessidade: conectar e desconectar de um serviço ou um banco de dados. No entanto, estes são apenas cenários tradicionais. O Template Method é adequado para qualquer situação em que partes de um algoritmo devem ser definidos em tempo de execução como, por exemplo, a manipulação de um arquivo:
1 2 3 |
procedure AbrirArquivo; procedure EscreverDados; virtual; abstract; // implementado pelas subclasses procedure FecharArquivo; |
É importante destacar também que os métodos abstratos não devem necessariamente aparecer no meio do algoritmo, como no código anterior. Não existe essa uma regra. Os métodos abstratos podem surgir em qualquer posição, como intercalados:
1 2 3 4 |
ConfigurarFormatoExportacao; virtual; abstract; ExportarDados; ConfigurarEmail; virtual; abstract; EnviarEmailComArquivo; |
O essencial, na verdade, é a existência um método que execute uma sequência de passos, mas alguns deles são definidos em tempo de execução por classes derivadas. Aos poucos, você notará que a implementação do Template Method é tão comum quanto parece. 🙂
O projeto de exemplo deste artigo está disponÃvel para download no link abaixo. Neste projeto, adicionei mais alguns parâmetros na rota de busca para trazer 100 resultados, já que, por padrão, a API retorna somente 30.
Grande abraço, pessoal!
Até breve.
Grande André!
Mais um artigo cinco estrelas sobre design patterns. Excelente! Não por acaso vc é um dos Mestres do Código na DB1. Parabéns e sucesso! 😀
Abração!
Muito obrigado pela consideração, Cleo!
Agradeço também por acompanhar cada artigo da série! 🙂
Grande abraço!
É isso André, parabéns pelo post e idem ao que Cleo disse.
Então pelo que entendi seria algo assim:
Quando precisamos refazer as mesmas tarefas em uma determinada ordem, ou seja, reutilizar código, sem perder o controle (ordem) dos nossos algoritmos (métodos).
Olá, João. Isso mesmo!
Com o Template Method, é possÃvel definir uma ordem de execução de passos (métodos), no qual um ou mais destes passos são delegados para as classes filhas. O objetivo é evitar a duplicação de código!
Abraço!