16 de fevereiro de 2011

Design Patterns com Delphi: Memento – Parte II

Neste post, eu apresento uma proposta de implementação em Delphi para o Design Pattern comportamental Memento, introduzido no meu último post. Lá, o padrão foi conceituado com a ajuda de um exemplo, cujo diagrama UML eu reproduzo abaixo:
Diagrama UML para o padrão Memento

Embora não seja especialmente complexa, a implementação em Delphi se baseia num detalhe da linguagem que é poucas vezes citado, relacionado com as regras de visibilidade dos membros de uma classe. Como no C++, o Delphi possui basicamente 3 níveis de visibilidade: private (onde os membros só podem ser manipulados pela classe que os declarou); protected (onde os membros são diretamente acessíveis tanto pela classe que os declarou quanto por qualquer outra que seja uma herança da classe original); e public (onde os membros assim declarados são universalmente acessíveis, isto é, qualquer parte do programa tem acesso a eles).

Entretanto, em classes declaradas numa mesma unit há flexibilização nas regras de visibilidade, fazendo com que os membros protegidos de uma classe sejam acessíveis pelas outras, enquanto os membros privados passam a ser visíveis para as heranças. Ou seja, agem mais ou menos como as classes associadas com a palavra chave friend em C++. Você ainda pode forçar que uma unit siga as regras de visibilidade válidas nos demais contextos; para isso, acrescente a palavra reservada strict ao nome da regra tradicional. Assim, temos na prática duas novas regras, strict private e strict protected.

Isto posto, a solução para o Memento em Delphi fica bastante simples. O relacionamento entre as classes TWCliente e TWClienteStatus exige que ao menos uma delas enxergue propriedades não-públicas da outra, pois o estado da TWCliente deverá ser armazenado internamente em TWClienteStatus. Nenhuma dessas informações, no entanto, poderá estar disponível fora desse contexto, sob pena de violar o encapsulamento. Então, as classes citadas devem ser declaradas na mesma unit para garantir o tipo de acesso necessário:
TWClienteStatus = class;

TWCliente = class
strict private
_CreditoEmUso: double;
_LimiteCredito: double;

public
Constructor Create;

function RealizaOpCredito (Valor: Double) : boolean;
function RealizaPagto (Valor: Double) : boolean;
function SalvaStatus : TWClienteStatus;
procedure RestauraStatus (Estado: TWClienteStatus);
end;

TWClienteStatus = class
protected
_Dados: TMemoryStream;

procedure AdicInfoDbl (Valor: Double);
procedure AdicInfoStr (Valor: String);

procedure InitGetInfo;
function ExtraiInfoDbl : Double;
function ExtraiInfoStr : String;

public
Constructor Create;
Destructor Destroy;override;
end;

Veja que as variáveis internas da TWCliente são estritamente privadas. Com isso, nem mesmo o TWClienteStatus tem acesso ao estado delas. Por outro lado, o stream _Dados e as funções para alimentá-lo foram incluídas na área protegida de TWClienteStatus, o que dá acesso total para que uma instância de TWCliente armazene aí seu estado. Isso também garante que ninguém fora dessa unit poderá ler, modificar ou gravar qualquer coisa diretamente numa instância do TWClienteStatus, preservando a integridade do estado que ela carrega.

Em TWClienteStatus, há métodos para adicionar valores de diversos tipos de dado ao stream interno:
procedure TWClienteStatus.AdicInfoDbl (Valor: Double);
begin
_Dados.Write(Valor, sizeof(Double));
end;

procedure TWClienteStatus.AdicInfoStr (Valor: String);
var lTamanho, i : integer;
lChr : char;
begin
lTamanho := Length (Valor);
{ Grava o tamanho do texto }
_Dados.Write(lTamanho, sizeof(integer));
{ Grava o texto em si }
for i := 1 to lTamanho do
begin
lChr := Valor[i];
_Dados.Write(lChr, 1);
end;
end;

O armazenamento de dados do tipo string é feito em duas etapas; primeiro, o tamanho do texto é inserido no stream para depois acrescentarmos o texto em si. No TWClienteStatus, os métodos para recuperar cada tipo de valor armazenado seguem essa mesma regra. Isso torna obrigatório fazer na recuperação de dados chamadas rigorosamente equivalentes às feitas durante o salvamento, isto é, a ordem de tipos gravados deve ser a mesma ordem nos tipos recuperados. Os métodos associados à recuperação são os seguintes:
procedure TWClienteStatus.InitGetInfo;
begin
{ Volta o ponteiro do stream para o início dos dados }
_Dados.Position := 0;
end;

function TWClienteStatus.ExtraiInfoDbl : Double;
begin
_Dados.Read(Result, sizeof(Double));
end;

function TWClienteStatus.ExtraiInfoStr : String;
var lTamanho, i : integer;
lChr : char;
begin
Result := '';
{ Obtém o tamanho do texto }
_Dados.Read(lTamanho, sizeof(Integer));

for i := 1 to lTamanho do
begin
_Dados.Read(lChr, 1);
Result := Result + lChr;
end;
end;

A função InitGetInfo reproduzida acima deve preceder a recuperação do estado pois ela faz com que a posição atual do stream aponte novamente o início dos dados que foram salvos. Assim, as funções para salvamento e recuperação do estado do Cliente podem ser escritas como segue:
function TWCliente.SalvaStatus : TWClienteStatus;
begin
Result := TWClienteStatus.Create;

{ Usa as funções protegidas de TWClienteStatus, o que só é permitido porque ambas as classes estão na mesma Unit.}
Result.AdicInfoStr(_Nome);
Result.AdicInfoDbl (_CreditoEmUso);
Result.AdicInfoDbl (_LimiteCredito);
end;

procedure TWCliente.RestauraStatus (Estado: TWClienteStatus);
begin
Estado.InitGetInfo;
_Nome := Estado.ExtraiInfoStr;
_CreditoEmUso := Estado.ExtraiInfoDbl;
_LimiteCredito := Estado.ExtraiInfoDbl;
end;

Neste tipo de solução, algo importante a se planejar é de quem será a responsabilidade por restituir a memória alocada pela função SalvaStatus. No exemplo, ela pode ser encarada como um construtor da classe que mantém estados (TWClienteStatus); portanto, o candidato natural a responsável é quem fez a chamada à função. No nosso caso, deve ser a própria classe de tela, que exerce o papel de Caretaker do Memento:
procedure TWTelaCredito.FormDestroy(Sender: TObject);
begin
FreeAndNil (_Estado);
FreeAndNil (_Cliente);
end;

procedure TWTelaCredito.btnOpCreditoClick(Sender: TObject);
var lValor : Double;
begin
{ Devolve os recursos usados até aqui antes de fazer nova alocação. }
FreeAndNil (_Estado);

try
lValor := StrToFloat (Trim (edNovo.Text));
{ Salva o estado inicial do Cliente }
_Estado := _Cliente.SalvaStatus;

{ Se houve erro na operação, volta ao estado inicial }
if (not _Cliente.RealizaOpCredito(lValor) ) then
_Cliente.RestauraStatus(_Estado);
ExibeCredito;
finally
end;
end;

Embora no nosso exemplo apenas o saldo de crédito seja modificado pela operação aplicada, todos os membros internos do Cliente são salvos, permitindo restaurar o estado anterior a quaisquer operações, não importando quais membros tenham sido alterados.

O projeto do exemplo, construído em Delphi 2010, pode ser baixado neste 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.