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.

1 de junho de 2012

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

Em muitas situações num programa é conveniente saber o tipo de dado exato com o qual uma rotina está trabalhando, principalmente quando envolve objetos complexos. Dependendo do cenário, é preciso tomar uma decisão baseada no tipo e acessar propriedades e métodos do tipo específico.

Imagine, por exemplo, que você queira garantir que os textos nas telas do seu sistema sejam exibidos usando uma fonte predeterminada. Ao invés de ajustar o fonte manualmente em cada componente da tela, podemos percorrer a lista de componentes e automatizar o ajuste:
procedure TForm1.FormCreate(Sender: TObject);
var i : integer;
lLabel : TLabel;
begin
for i := 0 to ComponentCount - 1 do
if (Components[i] is TLabel) then begin
lLabel := Components[i] as TLabel;
lLabel.Font.Name := 'Calibri';
lLabel.Font.Color := clBlue;
lLabel.Font.Size := 10;
end;
end;
O código acima percorre a lista de componentes da tela e usa a palavra chave IS para saber se o componente atual é do tipo TLabel (ou uma herança dessa classe). Em caso positivo, é feito um cast usando o AS e, então, as propriedades relacionadas ao fonte podem ser configuradas à vontade.

O recurso de uma linguagem de programação que permite obter informações sobre tipos de dados em tempo de execução é chamado de RTTI (Run Time Type Information). Em alguns ambientes, isso também é chamado de Reflexion.

Todos os objetos em Delphi conseguem reportar informações básicas sobre si mesmos, como o nome da classe a que pertencem, de qual classe ela herda, se implementa um determinado método, entre outros.

No entanto, a versão 2010 do Delphi passou por uma grande reformulação no tratamento de RTTI, aumentando significativamente sua capacidade. Agora você também pode obter informações sobre tipos atômicos, além de poder levantar os métodos e propriedades de classes, mesmo aqueles que sejam protegidos ou privados. Também é possível personalizar o RTTI, disponibilizando outras informações e funcionalidades a classes Delphi.

O novo mecanismo é baseado no conceito de "contexto", que nada mais é que o escopo constituido pelo programa principal, as bibliotecas (DLLs) e pacotes agregados. Toda aplicação Delphi passa a ter um único contexto RTTI onde são lançadas as informações sobre os tipos de dados em uso. Com isso, o primeiro passo para se obter informações sobre tipos é declarar e instanciar um contexto.
var lContexto: TRttiContext;
lType: TRttiType;
lMetodo : TRttiMethod;
lParm : TRttiParameter;
begin
lContexto := TRttiContext.Create;
lType := lContexto.GetType(TypeInfo (TForm1));

for lMetodo in lType.GetMethods() do begin
ListBox1.AddItem(lMetodo.Name, lMetodo);
for lParm in lMetodo.GetParameters () do
ListBox1.AddItem(' ' + lParm.ToString (), lParm);
end;

lContexto.Free;
end;
Após instanciar o contexto, esse trecho de código obtém informações sobre o Form atual em dois passos. No primeiro, a função TypeInfo retorna um ponteiro para as informações básicas. Então, o ponteiro é usado num segundo passo para obter as informações extendidas que foram introduzidas na nova versão do RTTI e que são representadas pela classe TRttiType.

Em seguida, o código percorre a lista de métodos do Form1 - incluindo toda a hierarquia de classes a qual ele pertence - e adiciona seus nomes num list box para exibição. Para cada método também é recuperada a lista dos parâmetros que devem ser passados quando ele for chamado em seu programa.

Embora não esteja representado no exemplo, o mesmo tipo de levantamento pode ser feito com as propriedades, ainda que elas não estejam declaradas como published.

A dúvida mais frequente sobre esse tipo de recurso é: em que situações isso pode ser aplicado ? Além do cenário citado no início do post, outras situações podem ser mais facilmente resolvidas com RTTI, tais como a clonagem de objetos (incluindo o estado completo dele), serialização de objetos (transformá-los em um XML ou enviá-los para a impressora, por exemplo), criação de uma infraestutura para aceitar plugins em uma aplicação, chamada dinâmica de métodos, etc.

O próprio IDE do Delphi usa aspectos do RTTI para poder trabalhar com as classes e Forms que você desenvolve, fornecendo informações, por exemplo, para o Code Insight ou serializando a estrutura do Form em um arquivo DFM.

No próximo post, mostro o recurso que permite incrementar as informações de runtime em classes, métodos e propriedades.

Mais Informações
Working with RTTI