29 de março de 2010

Design Patterns com Delphi : Comando - parte II

Uma vez tendo modelado as relações entre as classes envolvidas, implementar uma solução com o Design Pattern Comando (introduzido no post anterior) é tarefa relativamente simples. Acaba se tornando uma questão de prestar atenção em que classe deve instanciar outras, quais tem relação de herança e quais terão internamente instâncias de outros.
Para falar sobre a implementação em si, vou usar o exemplo do diagrama abaixo, também apresentado no último post:
Diagrama UML para o padrão Command
Neste exemplo, as classes herdadas de TWImportableObj são capazes de carregar para dentro do sistema os dados gravados por terceiros em tabelas específicas para esse fim, aplicando-lhes as regras de negócio pertinentes para que se tornem dados válidos no sistema. Essas operações podem ser comandadas a partir de pontos distintos do sistema - através de uma solicitação do usuário ou como processos agendados, por exemplo.

Para implementar tais classes é o bastante ter uma relação de herança entre elas. Assim, a classe base TWImportableObj estabelece o comportamento padrão para cargas através da definição de métodos e funções, de modo que poderá ser usada mais adiante como repositório para quaisquer de suas heranças. É recomendável, então, que a criação de instâncias dessas classes seja feita por um pattern como o Factory Method, garantindo a flexibilização e robustez na criação.
TWImportableObj = class
protected
_DtDe, _DtAte : TDateTime;
public
procedure ConfigImportacao(ADtDe, ADtAte : TDateTime);
procedure ImportaRegistros;virtual;abstract;
end;

TWApontamentos = class(TWImportableObj)
public
procedure ImportaRegistros;override;
end;

TWClientes = class(TWImportableObj)
public
procedure ImportaRegistros;override;
end;

{ Factory Method }
TWTipoImport = (tiApontamentos, tiClientes);
function GetImportable (ATipo: TWTipoImport;
ADtDe, ADtAte: TDateTime): TWImportableObj;

implementation

function GetImportable (ATipo: TWTipoImport;
ADtDe, ADtAte: TDateTime): TWImportableObj;
begin
case ATipo of
tiApontamentos: Result := TWApontamentos.Create;
tiClientes: Result := TWClientes.Create;
end;

Result.ConfigImportacao (ADtDe, ADtAte);
end;

procedure TWApontamentos.ImportaRegistros;
begin
{ Ações específicas para carregar apontamentos de produção }
end;

procedure TWClientes.ImportaRegistros;
begin
{ Ações específicas para carregar dados de clientes }
end;

A seguir, a teoria diz que temos que ter uma interface para definir o comportamento esperado para os tipos de comandos. No entanto, como há uma série de considerações a levar em conta quando usando interfaces no Delphi, prefiro implementar essa parte usando uma classe abstrata já que assim o efeito desejado é obtido. Isto é, teremos funções e métodos que devem ser obrigatoriamente implementados nas heranças:
TWComandoBase = class
public
procedure Execute;virtual;abstract;
procedure Undo;virtual;abstract;
function IsUndoable : boolean;virtual;abstract;
end;

TWComandoCarga = class(TWComandoBase)
protected
_Obj : TWImportableObj;
public
Constructor Create (AObj: TWImportableObj);
procedure Execute;override;
procedure Undo;override;
function IsUndoable : boolean;override;
end;

implementation

Constructor TWComandoCarga.Create (AObj: TWImportableObj);
begin
{ Configura o "Receiver" para esse comando: }
_Obj := AObj;
end;

procedure TWComandoCarga.Execute;
begin
{ Repassa a ação para o "Receiver" : }
_Obj.ImportaRegistros;
end;

procedure TWComandoCarga.Undo;
begin
{ Neste exemplo, o Undo não está implementado. Para implementá-lo, o Receiver teria que salvar informações suficientes para permitir que as operações realizadas sejam desfeitas }
end;

function TWComandoCarga.IsUndoable : boolean;
begin
Result := False;
end;
Aqui vale chamar a atenção para o fato da classe TWComandoCarga possuir o membro _Obj do tipo TWImportableObj. Conforme foi exposto no post anterior sobre o pattern Comando, o papel de um ConcreteCommand - como o TWComandoCarga - é fornecer uma ligação entre o Comando e o Receiver (no caso, qualquer herança do TWImportableObj). O membro _Obj é quem estabele a ligação nesse exemplo, sendo que a requisição para execução é repassada para ele através do TWComandoCarga. É importante ressaltar também que essa característica faz com que o TWComandoCarga em si sirva apenas para executar comandos de carga.

A próxima etapa é criar o Invoker, que nada mais é do que uma lista com os comandos que devem ser executados em sequência - embora para o exemplo fosse suficiente uma única ocorrência de Comando.
TWComandos = class
protected
_Lista: TList;
public
procedure Add (ACmd: TWComandoBase);
procedure Clear;
procedure ExecuteAll;
end;

{ ... }

procedure TWComandos.ExecuteAll;
var i : integer;
lCmd : TWComandoBase;
begin
for i := 0 to _Lista.Count - 1 do
begin
lCmd := (TWComandoBase)Lista.Item[i];
lCmd.Execute;
end;
end;
Obviamente, o código acima está simplificado para facilitar a compreensão da teoria por trás do pattern. O correto seria embutir nele tratamento dos erros que poderão acontecer durante uma execução qualquer. Também não estão representados o construtor nem o destrutor da classe.

Para finalizar, a classe Client junta todas as peças e as põe para funcionar. O que vai abaixo seria a resposta a um evento produzido pelo usuário (clique num botão ou seleção de um item de menu) requisitando que uma carga de Apontamentos de produção seja executada:
var lCarga: TWImportableObj;
lCmd: TWComandoBase;
begin
lCarga := GetImportable (tiApontamentos, _DtDe, _DtAte);
lCmd := TWComandoCarga.Create (lCarga);

_Comandos.Add (lCmd);
_Comandos.ExecuteAll;
{ ... }
end;
Neste exemplo, o comando criado é local na função mas isto não é uma obrigação. Pelo contrário. O ideal é criar o comando globalmente numa classe e adicioná-lo localmente ao Invoker (_Comandos) associado ao cenário. Assim, o mesmo comando pode ser utilizado para executar a carga a partir de um menu, de um clique de botão, agendado, num executor de macros ou qualquer outra opção que se queira dar ao usuário. Lembre-se apenas que o Receiver (o TWImportableObj) é externo ao comando e deve haver meios de se passar a ele as configurações de execução desejadas pelo usuário.

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.