13 de março de 2012

Design Patterns com Delphi: Visitor - Parte II

No último post, eu apresentei o conceito do Design Pattern Visitor usando para isso um diagrama UML com um exemplo prático da aplicabilidade do padrão. O exemplo consiste na representação de um Produto Acabado composto por uma lista de recursos (as matérias primas, máquinas e instruções) usados para fabricá-lo. O objetivo é permitir aplicar ao produto operações externas cujo resultado depende da aplicação da operação em cada parte que compõe a estrutura. No exemplo, há duas operações: uma que exporta a arquitetura do produto em formato XML e outra que faz a impressão dessa estrutura.

Neste post, mostro uma sugestão de implementação em Delphi para o exemplo do Visitor. Para facilitar a referência, publico novamente o diagrama que o retrata:
Diagrama UML para o padrão Visitor
O primeiro passo é definir as classes que representam o propósito do sistema, estabelecendo as regras de negócio que constituirão o cerne dele. No nosso caso, essas classes são o recurso de produção, suas heranças e o produto acabado. O quadro abaixo mostra as declarações delas, que são bastante simples :
{ Recursos para a produção }
TWRecursoProducao=class
public
_Nome: String;

Constructor Create (ANome: String);virtual;
procedure Accept (AOper: TWOperacoesPA);virtual;
end;

TWMateriaPrima=class(TWRecursoProducao)
public
_CProd: String;
_Qtde: Double;

Constructor Create (ANome: String);override;
procedure Accept (AOper: TWOperacoesPA);override;
end;

TWMaquina=class(TWRecursoProducao)
public
_Cod: String;
_Tempo: Double;
_TempoSetup: Double;

Constructor Create (ANome: String);override;
procedure Accept (AOper: TWOperacoesPA);override;
end;

TWRoteiro=class(TWRecursoProducao)
public
_Cod: String;
_Texto: String;

Constructor Create (ANome: String);override;
procedure Accept (AOper: TWOperacoesPA);override;
end;

{ Produto Acabado }
TWProdutoAcabado=class
protected
_MatPrimas : TObjectList;
_Maquinas : TObjectList;
_Roteiro : TWRoteiro;
procedure ClearListaRec (ALista: TObjectList);

public
_Nome: String;

Constructor Create (ANome: String);
Destructor Destroy; override;

procedure ClearRecursos;
procedure AddMatPrima (ARecurso: TWRecursoProducao);
procedure AddMaquina (ARecurso: TWRecursoProducao);
procedure SetRoteiro (ARoteiro: TWRoteiro);

procedure DoOperacao (AOper: TWOperacoesPA);
end;
Veja que a classe que representa o produto acabado (TWProdutoAcabado) possui membros do tipo TObjectList e também um roteiro separado. Esses membros armazenarão os recursos necessários para fabricar o produto acabado. Neste exemplo, as classes que compõem a agregação são heranças de uma mesma classe base - a TWRecursoProducao - mas o padrão Visitor não faz essa exigência. Assim, ele ainda é aplicável mesmo quando a agregação é composta por classes que não guardam relação entre si. Então, usei variáveis separadas pra cada tipo de recurso apenas para deixar clara essa possibilidade.

Repare ainda na função Accept introduzida na classe base de recursos. É ela quem define a família de classes como Visitable, determinando que os recursos de produção que elas representam poderão ser visitados por qualquer operação externa herdada de TWOperacoesPA. Na verdade, Accept simplesmente solicitará à operação que "visite" o recurso em questão, o que fará com que a operação seja aplicada ao recurso :
procedure TWMateriaPrima.Accept (AOper: TWOperacoesPA);
begin;
inherited;
AOper.Visit (Self);
end;

procedure TWMaquina.Accept (AOper: TWOperacoesPA);
begin;
inherited;
AOper.Visit (Self);
end;

procedure TWRoteiro.Accept (AOper: TWOperacoesPA);
begin;
inherited;
AOper.Visit (Self);
end;
A seguir, podemos definir a hierarquia de classes que implementarão as operações externas nos recursos de produção. Tais classes exercerão o papel de Visitors:
TWOperacoesPA=class
public
procedure InitOper(AInfo: TObject);virtual;
procedure TermOper;virtual;

procedure Visit (pRecurso: TWMateriaPrima);overload;virtual;
procedure Visit (pRecurso: TWMaquina);overload;virtual;
procedure Visit (pRecurso: TWRoteiro);overload;virtual;
end;

TWRecursoSaveToXml=class(TWOperacoesPA)
protected
_Xml : String;
public
procedure InitOper(AInfo: TObject);override;
procedure TermOper;override;

procedure Visit (pRecurso: TWMateriaPrima);overload;override;
procedure Visit (pRecurso: TWMaquina);overload;override;
procedure Visit (pRecurso: TWRoteiro);overload;override;

function GetXML : String;
end;
A operação para imprimir o produto acabado é bastante similar à de salva para XML e, por isso, eu a omiti do quadro.

Veja que a classe TWOperacoesPA, que é base para todas as operações, possui 3 métodos Visit sobrecarregados (overload). Cada um deles trata um tipo diferente de recurso, o que, na prática, nos permite adequar o comportamento da operação. Isso quer dizer que a operação será realizada de um modo coerente com o tipo do recurso.

Um outro detalhe nessa classe é a função InitOper. Ela é desenhada para realizar procedimentos iniciais da operação, aceitando um parâmetro genérico do tipo TObject com informações que façam sentido para a operação. Por exemplo, pode representar uma impressora para a operação de Imprimir ou um objeto DOM para a exportação em formato XML.

O trecho de código abaixo mostra as funções de inicialização e encerramento da operação de exportação para XML. Também retrata a versão da função Visit sobrecarregada para os tipos de recurso "Matéria Prima" e "Máquina":
procedure TWRecursoSaveToXml.InitOper(AInfo: TObject);
var lProd: TWProdutoAcabado;
begin
inherited;

{Abre a tag raiz para o XML }
lProd := AInfo As TWProdutoAcabado;
_XML := '<?xml version="1.0" encoding="ISO-8859-1"?>' + #13#10;
_XML := _XML + '<produtoAcabado name="' + lProd._Nome + '">' + #13#10;
end;

procedure TWRecursoSaveToXml.TermOper;
begin
{ Fecha a tag XML raiz }
_XML := _XML + '</produtoAcabado>';
inherited;
end;

procedure TWRecursoSaveToXml.Visit (pRecurso: TWMateriaPrima);
begin
inherited;

{ Versão específica dessa operação p/ o recurso "Matéria Prima" }
_XML := _XML + ' <materiaPrima>' + #13#10;
_XML := _XML + ' <codigo>' + pRecurso._CProd + '</codigo>' + #13#10;
_XML := _XML + ' <nome>' + pRecurso._Nome + '</nome>' + #13#10;
_XML := _XML + ' <qtde>' + FormatFloat ('#,##0.00', pRecurso._Qtde) + '</qtde>' + #13#10;
_XML := _XML + ' </materiaPrima>' + #13#10;
end;

procedure TWRecursoSaveToXml.Visit (pRecurso: TWMaquina);
begin
inherited;
{ Versão específica dessa operação p/ o recurso "Máquina" }
_XML := _XML + ' <maquina>' + #13#10;
_XML := _XML + ' <codigo>' + pRecurso._Cod + '</codigo>' + #13#10;
_XML := _XML + ' <nome>' + pRecurso._Nome + '</nome>' + #13#10;
_XML := _XML + ' <setup>' + FormatFloat ('#,##0.00', pRecurso._TempoSetup) + '</setup>' + #13#10;
_XML := _XML + ' <tempo>' + FormatFloat ('#,##0.00', pRecurso._Tempo) + '</tempo>' + #13#10;
_XML := _XML + ' </maquina>' + #13#10;
end;
Por fim, podemos implementar a função que aplica a operação ao produto acabado. Por definição, isso implica aplicar a mesma operação sobre os recursos que compõe esse produto acabado. Por isso, teremos que passar por todos os recursos, chamando a função Accept para determinar como a operação deve ser executada em cada um deles:
procedure TWProdutoAcabado.DoOperacao (AOper: TWOperacoesPA);
var i : integer;
lMatPrima: TWMateriaPrima;
lMaquina : TWMaquina;
begin
AOper.InitOper (Self);

{ Aplica a operação a cada matéria prima da lista }
for i := 0 to _MatPrimas.Count - 1 do
begin
lMatPrima := _MatPrimas.Items[i] As TWMateriaPrima;
lMatPrima.Accept (AOper);
end;

{ Aplica a operação a cada máquina da lista }
for i := 0 to _Maquinas.Count - 1 do
begin
lMaquina := _Maquinas.Items[i] As TWMaquina;
lMaquina.Accept (AOper);
end;

{ Aplica a operação no roteiro, se houver um }
if (_Roteiro <> Nil) then
_Roteiro.Accept (AOper);

AOper.TermOper;
end;

{ ... }

var lOperacao : TWRecursoSaveToXml;
lProd : TWProdutoAcabado;
lXml : String;
begin
lProd := TWProdutoAcabado.Create ('Computador');

lOperacao := TWRecursoSaveToXml.Create;

lProd.DoOperacao(lOperacao);

{ Publica o resultado da operação }
lXml := lOperacao.GetXML ();
{ ... }
end;

A parte inferior do quadro demonstra como se dá a aplicação de uma operação.

O projeto com esse exemplo pode ser salvo a partir desse link. Ele foi criado em Delphi 2005 mas deve ser possível compilá-lo em outras versões do IDE sem problemas.

Um comentário :

PedroJr disse...

Muito boa sua série sobre Design Patterns estou aprendendo muito e só tenho a agradecer.

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.