22 de junho de 2012

Trabalhando com informações sobres tipos no Delphi em tempo de execução - parte II

A extensão do mecanismo de RTTI (Run Time Type Information) introduzida no Delphi 2010 - e que eu citei no meu último post - abre novas possibilidades de soluções para arquitetura de softwares feitos em Delphi. Isso é particularmente útil para desenvolvedores de infraestrutura de software, situação onde é comum a necessidade de se criar rotinas genéricas para certas tarefas.

Customizar classes estendendo o RTTI também permite melhorar a organização da solução já que estas classes serão mais coesas. Isto é, funcionalidades que não são centrais podem ser transferidas para outras classes associadas. É o caso, por exemplo, da exportação de objetos para XML (ou outro formato qualquer), processo conhecido como serialização.

Estender o RTTI significa fazer marcações em classes, métodos, campos ou propriedades, introduzindo características extras no elemento marcado. Como a marcação também é uma classe, podemos criar comportamentos auxiliares complexos.

O quadro abaixo mostra a declaração de uma classe simples que possui algumas marcações para permitir exportar seu conteúdo como um XML:
[TWXmlExportable('notaFiscal')]
TWNota=class
public
[TWXmlExportableNode('nro')]
Numero : longint;

[TWXmlExportableNode('serie')]
Serie : String;

[TWXmlExportableNode('observ')]
Observ: String;

[TWXmlExportableNode('vlrTotal')]
Valor: Double;

InfoNaoExport : String;
procedure GravaNota;
end;
Como podemos ver, a marcação de um elemento é feita colocando-se um par abre e fecha colchetes antes da declaração desse elemento, inserindo entre eles o nome da classe usada para marcá-lo. No exemplo, há duas classes de marcação sendo usadas - TWXmlExportable e TWXmlExportableNode, sendo que ambas aceitam um parâmetro do tipo string em seus construtores, o que também é fornecido na marcação do exemplo. Aqui, o parâmetro indica o nome da tag XML que conterá o elemento marcado. Observe que nem todos os campos foram marcados; apenas aqueles que eu quero exportar.

As classes de marcação são também chamadas de atributos e em Delphi têm obrigatoriamente que ser heranças da classe TCustomAttribute. A declaração das classes usadas no exemplo está na listagem a seguir:
TWXmlExportable = class(TCustomAttribute)
protected
FIsRoot: Boolean;
FTagName: String;
public
constructor Create (ATagName: String); virtual;

property TagName : String read FTagName;
property IsRoot: Boolean read FIsRoot;
end;

TWXmlExportableNode = class(TWXmlExportable)
public
constructor Create (ATagName: String); override;
end;

{ ... }

constructor TWXmlExportable.Create (ATagName: String);
begin
inherited Create;
FTagNAme := ATagName;
FIsRoot := true;
end;

constructor TWXmlExportableNode.Create (ATagName: String);
begin
inherited Create(ATagName);
FIsRoot := false;
end;
Introduzi nelas a propriedade IsRoot para poder diferenciar classes - que são elementos compostos por outros elementos - dos demais tipos. Assim, os campos de uma classe poderão ser aninhados em uma tag, enquanto cada campo de tipo básico é exportado em sua tag individual. Com isso, a exportação funcionará corretamente até mesmo com classes que possuam outros objetos exportáveis em sua estrutura.

Portanto, essas classes de marcação podem ser aplicadas livremente a quaisquer outros elementos no programa. A grande vantagem é que tal flexibilidade nos permite montar uma rotina genérica que seja capaz de exportar para XML qualquer objeto que contenha as marcações apropriadas. Da mesma forma que recuperamos a lista de métodos de um objeto no outro post, podemos interagir com os atributos desse objeto:
function TForm1.GetExportToXml (AObj: TObject) : TWXmlExportable;
var i : integer; lTipo: TRttiType;
begin
Result := Nil;
{_RttiCntxt é um TRttiContext instanciado em outro ponto do sistema }
lTipo := _RttiCntxt.GetType(AObj.ClassType);

{ Obtem os atributos - ou marcações - do objeto }
i := Length (lTipo.GetAttributes()) - 1;

{ Verifica se algum dos atributos é do tipo TWXmlExportable; isso significa que o objeto passado no parâmetro é "exportável" }
while (Result = Nil) And (i >= 0) do begin
if (lTipo.GetAttributes()[i] is TWXmlExportable) then
Result := (lTipo.GetAttributes()[i] As TWXmlExportable);
Dec (i);
end;
end;
A exportação para XML, então, passa a ser uma questão de levantar quais são os campos do objeto, determinar quais desses campos são "exportáveis" usando a mesma técnica acima, obter o nome da tag e o valor do campo. A função listada no quadro abaixo faz a exportação, seguindo esses passos:
function TForm1.ExportToXml (AObj: TObject) : string;
var lTipo: TRttiType;
lCampo: TRttiField;
lAttrib : TCustomAttribute;
lExpField, lExpObj : TWXmlExportable;
begin
Result := '';
lExpObj := GetExportToXml (AObj);

{ só continua se o objeto é "exportável" }
if lExpObj <> Nil then begin
lTipo := _RttiCntxt.GetType(AObj.ClassType);

{ Inicia a tag XML, obtendo o nome no atributo encontrado para o objeto }
Result := '<' + lExpObj.TagName + '>';

{ Quais são os campos nesse tipo de classe ? }
for lCampo in lTipo.GetFields() do begin
{ Analisa os atributos do campo pra ver quais são exportáveis }
for lAttrib in lCampo.GetAttributes() do
if (lAttrib is TWXmlExportable) then begin
lExpField := lAttrib As TWXmlExportable;
if (lExpField.IsRoot) then
{ campos do tipo classe são exportados recursivamente }
Result := Result + ExportToXml (lCampo.GetValue(AObj).AsObject)
else begin
{ campos de tipos simples apenas envolve com o nome da tag contido no atributo do campo }
Result := Result + '<' + lExpField.TagName + '>';
{ Obtem o valor atual do campo }
Result := Result + lCampo.GetValue(AObj).toString();
Result := Result + '</' + lExpField.TagName + '>';
end;
end;
end;

{ Encerra a tag desse objeto }
Result := Result + '</' + lExpObj.TagName + '>';
end;
end;
As informações sobre cada campo de uma classe são mantidas em instâncias de TRttiField. Além dos eventuais atributos associados ao campo, podemos descobrir com ela o nome do campo, seu tipo e sua visibilidade (público, protegido ou privado). Ela também fornece meios para recuperarmos ou modificarmos o valor do campo, conforme podemos ver no quadro anterior.

Agora, para exportar uma instância da classe de Nota precisamos apenas passá-la para a função acima. Na verdade, a função é genérica o suficiente pra ser capaz de exportar para XML instâncias de qualquer classe anotada com os atributos apresentados aqui.

Do jeito que esse recurso funciona, a classe de Nota apresentada no início do post pode focar na implementação das tarefas inerentes ao negócio, deixando a tarefa acessória de exportar os dados para outros pontos do sistema, favorecendo a coesão das classes na solução.

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.