7 de julho de 2011

Design Patterns com Delphi: State - Parte II

No último post, apresentei o conceito do Design Pattern comportamental State. Como exemplo de situação onde o padrão é aplicável, criei um controle simples de conta corrente no qual a conta tem comportamentos diferentes, reagindo ao nível do saldo que ela atualmente contém.

Para ficar mais fácil visualizar, reproduzo abaixo o diagrama de classes que representa uma solução para a situação do exemplo.
Diagrama UML para o padrão State

O modo como uma conta tem que se comportar é ditado por uma classe abstrata simples, chamada de TWEstadoConta no diagrama. Essa classe é apenas uma interface que introduz as funções e propriedades esperadas para a operação básica da conta em si. Os estados reais podem, então, ser implementados como heranças dessa interface, providenciando a diferenciação necessária.
TWEstadoConta = class
protected
_Saldo : Double;
_PorcRendim : Double;
_LimInf, _LimSup : Double;
_Conta : TWConta; { ... }

public
{ ... }
procedure Depositar (AValor : Double);virtual;abstract;
procedure Sacar (AValor : Double);virtual;abstract;
procedure AplicarRendimento;virtual;abstract;
end;

TWContaComum = class(TWEstadoConta)
protected
_TaxaServico : Double;
Constructor Create (AConta : TWConta);override;
public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;


TWContaDiferenciada = class(TWContaComum)
protected
_TaxaJuros : Double;
Constructor Create (AConta : TWConta);override;

public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;

TWContaOuro = class(TWEstadoConta)
protected
Constructor Create (AConta : TWConta);override;

public
procedure Depositar (AValor : Double);override;
procedure Sacar (AValor : Double);override;
procedure AplicarRendimento;override;
end;

As operações para depósito, saque e a que aplica rendimentos ao saldo da conta são abstratas na interface mas implementadas pela classe de cada estado possível. Veja, por exemplo, as diferenças da operação de saque:
procedure TWContaComum.Sacar (AValor : Double);
var lValorReal : Double;
begin
{ Conta comum, aplica a taxa de serviço }
lValorReal := AValor + _TaxaServico;
_Saldo := _Saldo - lValorReal;
end;

procedure TWContaDiferenciada.Sacar (AValor : Double);
var lJuros, lValorReal : Double;
begin
{ Na Conta diferenciada, aplica taxa de serviço, critica se extrapolar o limite de crédito e aplica uma taxa de juros se a conta ficar negativa }
lValorReal := AValor + _TaxaServico;
if (_Saldo - lValorReal) < _LimInf then
raise Exception.Create('Saque não pode ser efetivado. Conta sem saldo.');

lJuros := 0.0;
if (_Saldo - lValorReal) < 0.0 then
lJuros := (lValorReal - _Saldo) * _TaxaJuros / 100.0;

_Saldo := _Saldo - lValorReal - lJuros;
end;

procedure TWContaOuro.Sacar (AValor : Double);
begin
{ Conta Ouro, não se preocupa com nada - apenas saca. }
_Saldo := _Saldo - AValor;
end;

Os construtores das classes que representam estados têm que alimentar os valores das propriedades internas, caracterizando os respectivos estados. É isso que permitirá à classe da conta decidir qual estado deve estar ativo num determinado momento:
Constructor TWContaComum.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 1.0; { Rendimento padrão : 1%}
_TaxaServico := 5.00; { Saques são cobrados }
{ Valores de saldo que delimitam um estado }
_LimInf := 100.00;
_LimSup := 999.99;
end;

Constructor TWContaDiferenciada.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 0.0; { Sem rendimento }
_TaxaJuros := 1.00; { Taxa de juros sobre saques qdo a conta fica sem saldo }
_TaxaServico := 10.00; { Saques são cobrados }

{ Valores de saldo que delimitam um estado }
_LimInf := -100.00;
_LimSup := 99.99;
end;

Constructor TWContaOuro.Create (AConta : TWConta);
begin
inherited;
_PorcRendim := 2.00; { Rendimento Ouro : 2% }
{ Valores de saldo que delimitam um estado }
_LimInf := 1000.00;
_LimSup := 99999.99;
end;

A classe da Conta, então, deve guardar uma referência do estado atual para poder realizar as operações solicitadas. Portanto, ela age como uma ponte, transferindo para a classe de estado a responsabilidade de executar efetivamente a operação.
TWConta = class
private
_Estado : TWEstadoConta;
{ ... }
public
{ ... }
procedure Depositar (AValor : Double);
procedure Sacar (AValor : Double);
procedure AplicarRendimento;
{ ... }
function ObtemSaldo : double;
function ObtemEstado : TWEstadoConta;
end;
Para que a implementação desse padrão funcione, é preciso trocar dinamicamente a instância de classe que representa o estado atual. E quando é necessário efetuar essa troca da instância de estado ? O bom senso diz que isso deve acontecer após qualquer operação que possa afetar o estado monitorado. No nosso caso, isso significa atuar em todas as operações que modifiquem o saldo da conta.
procedure TWConta.VerificaStatus;
var lNovoEstado : TWEstadoConta;
begin
{ ... }
{ Testa as variações de estado }
lNovoEstado := Nil;
if (_Estado._Saldo < _Estado._LimInf) then
begin
{ Rebaixa de COMUM para DIFERENCIADA }
if (_Estado.InheritsFrom (TWContaComum)) then
lNovoEstado := TWContaDiferenciada.Create (Self);

{ Rebaixa de OURO para COMUM }
if (_Estado.InheritsFrom (TWContaOuro)) then
lNovoEstado := TWContaComum.Create (Self);
end;

if (_Estado._Saldo > _Estado._LimSup) then
begin
{ Eleva de COMUM para OURO }
if (_Estado.InheritsFrom (TWContaComum)) then
lNovoEstado := TWContaOuro.Create (Self);

{ Rebaixa de OURO para COMUM }
if (_Estado.InheritsFrom (TWContaOuro)) then
lNovoEstado := TWContaComum.Create (Self);
end;

{ Efetiva a troca de estado }
if (lNovoEstado <> Nil) then
SetEstado (lNovoEstado);
end;

procedure TWConta.Depositar (AValor : Double);
begin
_Estado.Depositar(Avalor);
VerificaStatus ();
end;

procedure TWConta.Sacar (AValor : Double); begin
_Estado.Sacar (AValor);
VerificaStatus ();
end;

procedure TWConta.AplicarRendimento;
begin
_Estado.AplicarRendimento ();
VerificaStatus ();
end;

Pelo quadro acima, vemos que quem faz a troca é a própria conta pois as classes de estado não conhecem o fluxo de mudança, isto é, nenhuma delas sabe qual é o estado seguinte para o qual a conta pode ser lançada. Isso facilita a inclusão de novos estados já que apenas um ponto precisa ser modificado para levá-lo em consideração.

A recuperação de uma instância da classe de estado pode ser modelada para usar o Design Pattern criacional Factory ou ainda o Singleton.

Usar uma Factory como fizemos em VerificaStatus pode acarretar problemas de desempenho se as trocas de estado ocorrerem com muita frequência. O mesmo é válido se for grande o volume de dados que caracterizam os estados. Neste caso, optar por um Singleton é recomendável, sem esquecer de que ele deve se restringir ao escopo da classe. Isto é, cada instancia da classe deve ter seu Singleton particular; senão, o estado de uma instância poderá se confundir com o de outras que porventura existam ao mesmo tempo.

O download do projeto Delphi com esse exemplo pode ser feito a partir desse link.

Nenhum comentário :

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.