6 de outubro de 2011

Design Patterns com Delphi: Strategy - Parte II

No último post, eu apresentei o conceito e a aplicabilidade do Design Pattern comportamental Strategy. Aqui, vou mostrar como implementar em Delphi o exemplo prático utilizado naquele post, relacionado a pagamento de títulos. Para facilitar o entendimento, reproduzo abaixo o diagrama UML com a sugestão de modelagem proposta:
Diagrama UML para o padrão Strategy
A implementação das relações expostas no diagrama não é especialmente complexa. Ela envolve apenas conceitos comuns da programação orientada a objetos, como herança e polimorfismo.

O primeiro passo é declarar uma classe abstrata (ou interface, se preferir), definindo nela as linhas mestras que deverão ser respeitadas por todos as implementações reais do algoritmo em questão. Isto é, essa classe introduz funções que determinam a estratégia para execução da tarefa, impondo-a a todas as classes que queiram atuar como um algoritmo alternativo para a tarefa - daí o nome do padrão ser Strategy:
TWTitulo = class;

TWPagamento=class
public
function efetuaPgto (ATitulo: TWTitulo) : integer;virtual;abstract;
end;
Nesse exemplo, há uma única função abstrata que obrigatoriamente terá que existir em cada nova implementação do algoritmo. Essa função aceita como parâmetro uma instância da classe Context da solução, papel encarnado aqui pelo título a pagar (TWTitulo). Com isso, qualquer algoritmo que adote a estratégia definida tem acesso às informações relevantes do título, bem como a suas operações. Permitir essa comunicação é imprescindível para que a tarefa (o pagamento do título) possa ser concretizada.

Agora que temos a interface estabelecida, podemos adicionar os diferentes algoritmos previstos no diagrama, criando cada um deles como uma herança simples que forneça código para a função abstrata da interface:
TWContaBancaria = class;

TWPgtoBoleto=class(TWPagamento)
{ ... }
public
function efetuaPgto (ATitulo: TWTitulo) : integer;override;
end;

TWPgtoDebitoAuto=class(TWPagamento)
{ ... }
_Conta : TWContaBancaria;
public
constructor Create (AConta: TWContaBancaria);
function efetuaPgto (ATitulo: TWTitulo) : integer;override;
end;

TWPgtoInternet=class(TWPagamento)
{ ... }
public
function efetuaPgto (ATitulo: TWTitulo) : integer;override;
end;

implementation

{ ... }
constructor TWPgtoDebitoAuto.Create (AConta: TWContaBancaria);
begin
_Conta := AConta;
{ ... }
end;

function TWPgtoDebitoAuto.efetuaPgto (ATitulo: TWTitulo) : integer;
begin
_Conta.Conecta;
_Conta.DebitaValor(ATitulo.ObtemValor);
ATitulo.RegistraPagto;
{ ... }
end;
A implementação da TWPgtoDebitoAuto no quadro acima mostra que podem ser necessárias outras informações para que a tarefa seja executada num determinado algorítmo. A função que executa a tarefa, no entanto, foi fixada na classe base (a interface) e não deve ser alterada já que isso implicaria que todas as outras heranças teriam que respeitar os novos parâmetros, o que nem sempre é desejável. Então, uma solução é passar as informações extras no construtor da própria classe.

Esse detalhe tem que ser levado em conta pela Factory responsável pela criação de instâncias da classe de pagamento. De acordo com o conceito de Factory, devemos criar uma função que centraliza a instanciação de uma família de classes:
TWTipoPagto = (tpBoleto, tpDebitoAuto, tpInternet);

function CriaNovoPagto (ATipo: TWTipoPagto; AContaDebito : TWContaBancaria) : TWPagamento;
begin
Result := Nil;

case (ATipo) of
tpBoleto:
Result := TWPgtoBoleto.Create;
tpDebitoAuto:
Result := TWPgtoDebitoAuto.Create (AContaDebito);
tpInternet:
Result := TWPgtoInternet.Create;
end;
end;
O último aspecto que falta abordar é com relação às características da classe de contexto TWTitulo. Além de propriedades inerentes a títulos - como seu valor, data de vencimento, código de barras, identificação do beneficiário, etc. - essa classe terá que manter uma instância da classe base de estratégia (o pagamento) para poder trocar mensagens com ela.
TWTitulo=class
protected
_Id : String;
_DataVencto : TDateTime;
_Valor : Double;
_Pagto : TWPagamento;
_FoiPago : Boolean;
procedure RegistraPagto;
{ ... }
public
function ObtemValor : Double;
function Pagar (AMeioPgto: TWTipoPagto): integer;
{ ... }
end;

{ ... }

function TWTitulo.Pagar (AMeioPgto: TWTipoPagto): integer;
begin
if (_Pagto = Nil) then
_Pagto := CriaNovoPagto (AMeioPgto, _Sessao.ObtemContaBancaria);

_Pagto.efetuaPgto (Self);
end;
Com isso, temos a classe de contexto armazenando internamente uma instância do pagamento (a estratégia) e controlando seu ciclo de vida. A instância fica disponível para que o contexto possa utilizá-la sempre que for necessário executar a tarefa embutida nela.

Note que a solução fica aberta para receber facilmente novos algoritmos, preservando o fraco acoplamento entre as classes já previstas e implementadas. Assim, poucos pontos têm que ser alterados para se introduzir uma nova forma de realizar a tarefa: basta codificar a nova classe e prever a criação de suas instâncias na função factory.

5 comentários :

Blogueiro disse...

Bom Dia. Estou tentando usar este conceito em um projeto meu. Surgiu uma dúvida. Criei a interface de pagamento. Nela preciso da referência do Título pra passar ele nos parâmetros. No objeto Título tenho que ter a referência da interface. Isso está causando um problema de referência circular pois estão em units diferentes. Como posso resolver isso?

Blogueiro disse...

Agora que descobri que na interface não precisa fazer o uses do Titulo.

TWTitulo = class;

TWPagamento=class
public
function efetuaPgto (ATitulo: TWTitulo) : integer;virtual;abstract;
end;

Blogueiro disse...

Bom Dia. Ainda não consegui sair do problema em questão. Me diz uma coisa por favor, a classe Context tem que ser declarada dentro da abstrata(interface) ? Pois se faço da forma acima não consigo acessar os métodos e atributos do TWTitulo pois ele fica de um tipo diferente de como ele realmente é, pois no meu caso fiz ele em outra unit como objeto Título mesmo. Como posso resolver este problema de referência circular? Se puder me ajudar ficarei muito grato.

Luís Gustavo Fabbro disse...

Na forma como é sugerido no post, as classes estão numa mesma unit. Na sua estruturação, remova da declaração do TWTitulo a instância de TWPagamento (variável _Pagto) e restrinja seu uso ao método de pagamento (função Pagar do título). Assim, você pode colocar o uses para a unit do TWPagamento na área implementation da unit do TWTitulo, eliminando a referência cíclica entre essas classes.

Blogueiro disse...

Ok, muito obrigado pelas dicas. Este seu blog é uma enorme contribuição para a comunidade e uma excelente fonte de conhecimento. Obrigado por compartilhar este conhecimento.

Postar um comentário

OBS: Os comentários enviados a este Blog são submetidos a moderação. Por isso, eles serão publicados somente após aprovação.

Observação: somente um membro deste blog pode postar um comentário.