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.

12 de fevereiro de 2011

Design Patterns com Delphi: Memento – Parte I

Num sistema computacional, são muitas as situações que podem exigir que tenhamos controle sobre o estado atual de um objeto para poder revertê-lo caso alguma alteração feita no estado desse objeto não deva ser levada adiante. O comando "desfazer" (undo), presente em muitos programas, é o exemplo mais comum, mas esse tipo de necessidade também pode ter outras aplicações: o tratamento de rollback (uma das características mais marcantes em sistemas de gerenciamento de banco de dados), em funções de preview (onde alterações podem ser aplicadas temporariamente para que se avalie o impacto que elas terão caso sejam efetivadas) e para trazer de volta o estado original de objetos quando uma situação de erro é encontrada durante uma atualização.

O Design Pattern comportamental Memento foi concebido justamente para ser aplicado nessas situações. A implementação proposta por ele respeita o encapsulamento do objeto envolvido enquanto proporciona-lhe a capacidade de salvar e exportar seu estado num determinado instante e de retornar a qualquer um dos estados salvos no momento em que isso for necessário.

A imagem abaixo é um diagrama UML retratando a relação típica entre as classes participantes da solução para o Memento. Aqui, a ideia é construir uma tela para realizar uma operação de crédito que terá que ser desfeita caso o limite de crédito seja atingido. Nesta condição, um estado previamente salvo pela rotina será restaurado.
Diagrama UML para o padrão Memento

O GoF classifica os participantes nesta solução com a seguinte nomenclatura:
Originator: É a classe capaz de criar um instantâneo do estado de suas variáveis internas, exportando-o de uma forma que permita a restauração desse estado a qualquer momento. Por consequência, a classe também deve ter uma função que aceite como parâmetro um estado previamente salvo para poder fazer com as variáveis internas da instância da classe assumam os valores contidos nesse parâmetro. No diagrama acima, a classe com essas características é a TWCliente. Repare que as variáveis internas da classe TWCliente são mantidas como privadas (private), sendo acessíveis exclusivamente através dos métodos que realizam as operações dela; isto garante o encapsulamento e o baixo acoplamento em relação à classe que utilizará o recurso de salvar/restaurar o estado dos objetos.

Memento: Armazena todas as informações que forem necessárias para que o estado de uma classe do tipo Originator possa ser restaurado com sucesso. No entanto, a relação das informações que devem ser salvas é determinada pela própria classe Originator. Normalmente, a classe Memento inclui mecanismos para barrar a edição de seu conteúdo, preservando as informações que estão salvas nela para garantir que o estado restaurado é rigorosamente o mesmo que foi salvo. Este papel cabe à classe TWClienteStatus no nosso exemplo.

Caretaker: É a classe que utiliza o recurso de salvar/restaurar de um Originator, determinando o melhor momento para que cada etapa aconteça. Ela, portanto, se relaciona com o Originator, salvando-lhe o estado, realizando operações do Originator através da chamada dos métodos que forem convenientes e restaurando-lhe o estado, se e quando isto for necessário. Para isso, o Caretaker sabe como armazenar uma (ou mais) instância(s) de estados do Originator, mas não tem acesso ao conteúdo salvo nem pode modificá-lo. No exemplo, quem detem esse papel é TWTelaCredito.

Há diferenças em relação ao padrão Command, abordado neste outro post. Enquanto no Command definimos uma interface através da qual ações serão executadas, no Memento a interface é substituída por um mecanismo apenas para salvar o estado atual do objeto e para restaurá-lo quando for necessário. Por causa do uso da interface, polimorfismo é parte importante da solução no Command mas irrelevante na adoção do Memento. É claro que podemos planejar a ação inerente a um Command de um modo que ela também possa ser desfeita. Quando isso ocorre, é comum que ambos os padrões sejam postos para trabalhar em conjunto na mesma solução.

Uma curiosidade: memento é um termo que em latim significa "lembra-te". Em português é pouco usado, mas é o nome que se dá a uma caderneta onde se anota coisas que devem ser lembradas.

No próximo post, eu proponho uma implementação em Delphi para o exemplo usado aqui.

Mais Informações
Posts sobre Design Patterns

9 de fevereiro de 2011

Personalizando a edição de propriedades em componentes Delphi - parte II

No último post, eu falei um pouco sobre as preocupações que devemos ter na criação de componentes no Delphi quanto à necessidade de separação do comportamento deles em tempo de execução daqueles comportamentos que são exclusivos do tempo de design – portanto, oferecidos apenas aos programadores.Citei também que a edição de propriedades no Object Inspector é um comportamento de tempo de execução que pode ser customizado pelo programador do componente, bastando criar uma herança da classe TBasePropertyEditor que implemente a interface IProperty.

Dando sequência ao assunto, detalho aqui alguns métodos dessa interface e mostro como criar o editor para uma propriedade que nos permita montar filtros para tipos de arquivos, como o exibido abaixo:
Editor de filtros de tipos de arquivos
Os principais métodos introduzidos pela interface IProperty são listados abaixo:
Edit: Este método é chamado quando se clica o botão de edição apresentado junto à propriedade no Object Inspector. No nosso exemplo, ele criará e exibirá a tela para o programador entrar com os filtros de arquivos desejados.
GetAttributes: Retorna as características da propriedade de forma que o Object Inspector possa determinar quais tipos de controle apresentar ao programador, como botões de edição, caixa com lista para seleção, etc. . Por exemplo, como a propriedade de filtro apresenta uma tela para edição, o Object Inspector deve ser preparado para exibir um botão que dê acesso à tela.
GetEditLimit: Retorna a quantidade máxima de caracteres que o programador pode informar como valor para a propriedade. O nosso exemplo não fará restrições nesse aspecto.
GetValue e SetValue: Respectivamente, obtém o valor atual da propriedade e ajusta um novo valor para a propriedade em tempo de design. Ambas são chamadas automaticamente pelo Object Inspector, desde que a edição da propriedade não se dê através de um diálogo. Como esse é o caso do nosso exemplo, teremos que chamá-las manualmente dentro do método Edit.
GetEditValue: Obtém um texto formatado para apresentar como valor da propriedade em tempo de design. Diferentemente de GetValue e SetValue, essa função não levanta uma exceção caso a propriedade não tenha valor. No nosso exemplo, essa função retornará o mesmo texto que GetValue, já que a propriedade que estamos tratando é do tipo texto.
Para implementar o exemplo com a tela de filtros de arquivos, temos que declarar duas classes. A primeira representa a tela de filtro em si enquanto a segunda tratará os requisitos para intermediar a comunicação com o Object Inspector, isto é, será uma herança de TBasePropertyEditor e implementará a interface IProperty.
{ A tela para edição dos filtros }
TWFilterEditor = class(TForm)
Bevel1: TBevel;
OKButton: TButton;
CancelButton: TButton;
HelpButton: TButton;
procedure FormCreate(Sender: TObject);
procedure HelpBtnClick(Sender: TObject);
private
procedure SetFilter(Value: string);
function GetFilter: string;
end;

{ Classe que gerenciará a comunicação com o Object Inspector }
TWFilterProperty = class(TStringProperty)
public
procedure Edit; override;
function GetAttributes: TPropertyAttributes; override;
end;
Veja que ao criar a classe para comunicação com o Object Inspector eu herdei diretamente da classe da VCL TStringProperty pois ela já implementa grande parte do que precisamos, incluindo os métodos requeridos pela interface IProperty. Aqui, teremos que sobrepor apenas os métodos Edit e GetAttributes para introduzir a parte do comportamento que será diferente para nós:
procedure TWFilterProperty.Edit;
var FilterEditor: TWFilterEditor;
begin
{ Cria e apresenta a tela de filtros }
FilterEditor := TWFilterEditor.Create(Application);
try
FilterEditor.SetFilter(GetValue);
FilterEditor.ShowModal;

{ A edição foi feita com sucesso, ajusta o novo valor para a propriedade }
if FilterEditor.ModalResult = mrOK then
SetValue(FilterEditor.GetFilter);
finally
FilterEditor.Free;
end;
end;

function TWFilterProperty.GetAttributes: TPropertyAttributes;
begin
Result := [paDialog, paMultiSelect];
end;
O método Edit cria uma instância da classe de tela e obtém manualmente o valor da propriedade através do GetValue antes de exibi-la. Ao encerrar a tela após uma edição com sucesso, o valor é novamente atribuído à propriedade, através de uma chamada ao SetValue.

No GetAttributes, retornamos um valor que indica ao Object Inspector que essa propriedade será editada através de uma caixa de diálogo (nossa tela de filtro) e que é permitido ao programador selecionar vários componentes ao mesmo tempo e editar a propriedade em todos eles (paMultiSelect) simultaneamente. Para finalizar, precisamos notificar a VCL sobre qual(is) propriedade(s) deverá(ão) usar nosso editor personalizado:
procedure Register;
begin
RegisterComponents('ABC71', [TWAttachFileBtn]);
RegisterPropertyEditor (TypeInfo(string), TWAttachFileBtn, 'TiposPermitidos', TWFilterProperty);
end;
Observe que o registro do editor para a propriedade ocorre no mesmo local em que o próprio componente é registrado na paleta de componentes do Delphi, isto é, uma função isolada chamada Register criada na própria Unit. Na função acima, nosso editor é associado à propriedade 'TiposPermitidos' de um componente chamado TWAttachFileBtn.

7 de fevereiro de 2011

Personalizando a edição de propriedades em componentes Delphi - parte I

Já comentei em outros posts sobre a criação de componentes em Delphi que programadores que se aventuram nessa seara devem ter em mente preocupações com dois públicos alvos distintos. Em primeiro lugar, definimos a aparência do componente e qual o tipo de interação esperado dele por um usuário final dos programas em que esse componente estará inserido. Isto é, devemos nos preocupar com a forma com que o componente aparecerá para seus usuários e como ele deve se comportar quando utilizado num sistema real.

Por outro lado, é importante também facilitar a vida do usuário mais imediato de um componente: o próprio programador que o usará em design time para montar seus programas. Em Delphi e C++ Builder, essa facilidade se traduz na exposição de propriedades e eventos no Object Inspector, de uma maneira que revela um pouco do funcionamento do componente ao mesmo tempo em que reduz a codificação necessária para que o programador ponha tal componente para funcionar.

Conforme explicato no post Criando Componentes em Delphi - parte II, a forma mais básica de publicar uma propriedade (ou evento) no Object Inspector é declará-la na área pública da classe do componente, através da palavra chave published. Neste caso, a validação do valor informado pelo programador no inspector é feita apenas com base no tipo de dado da propriedade. Isso também determina o modo com que o valor da propriedade será automaticamente anexado ao arquivo DFM do Form onde o componente foi inserido. Observação: um DFM contém os componentes adicionados em tempo de design, descrevendo o estado inicial de suas propriedades em Foms do Delphi.

Mas, nem sempre o básico é suficiente. Imagine, por exemplo, que você está criando um componente no qual uma das propriedades publicadas aceita uma lista com tipos de arquivos permitidos, nos moldes do filtro existente no TOpenDialog. Se tal propriedade for publicada como um texto simples, o programador poderá informar aí qualquer valor, mesmo um que não seja compatível com o formato de lista que definirmos.

Para essas situações, o Delphi permite que o criador de componentes registre editores diferentes do padrão para propriedades onde isso for necessário, evitando (ou até mesmo impedindo) que o programador tenha que informar diretamente o valor desejado em tempo de design. O registro é feito através da função RegisterPropertyEditor, que vincula uma propriedade do componente à classe que deve ser usada para editar seus valores durante o projeto. Essa classe deve ser uma herança de TBasePropertyEditor e deve implementar a interface IProperty. A boa notícia é que para os tipos básicos do Delphi já há classes prontas que satisfazem ambas as exigências, de modo que para montar nosso editor de filtros de arquivos nos bastará derivar uma classe a partir da TStringProperty, que é a classe nativa para edição do valor de propriedades do tipo string (texto) no Object Inspector. A ideia é abrir para o programador uma tela onde ele poderá facilmente estipular em tempo de design a lista de tipos de arquivos considerados pelo componente. A tela apresentada ao programador é reproduzida abaixo:
Editor de filtros de tipos de arquivos
É claro que essa interação é exclusiva para o programador, de modo que a tela para escolha de filtros não é apresentada em nenhum momento ao usuário final da aplicação. Esta é a razão para o Delphi diferenciar os tipos de pacotes: aqueles configurados como Design Time Only devem conter apenas as classes necessárias para o programador fazer seu trabalho, o que as dispensa de serem distribuídas junto com as aplicações que usarem o componente, reduzindo o tamanho dessas aplicações.

A implementação do comportamento inerente ao componente deve permanecer em pacotes do tipo Run Time Only, imprescindíveis para o funcionamento tanto do executável final quanto das interações em tempo de design.

No próximo post, apresento os métodos que merecem atenção quando criamos nossas próprias classes de editores e também mostro a construção de uma propriedade com as características do filtro de arquivos citada acima.