Atenção: esse artigo tem um vídeo complementar. Clique e assista!
Reflexão ou introspecção é a capacidade que muitas linguagens de programação modernas têm de permitir que se criem classes com o conhecimento de sua própria estrutura. Quando as classes “sabem” como são feitas, você pode obter valores de propriedades através de strings, sem necessariamente ter certeza de que a classe em questão realmente tem tal propriedade. Faremos isso usando o mecanismo de RTTI (Runtime Type Information – Informação em tempo de execução) do Delphi, que nos permite obter informações sobre a estrutura dos objetos instanciados. Mas não exploraremos apenas a RTTI, mas mostraremos como ela pode ser utilizada na flexibilização de projetos orientados a objetos.
Para que serve
Perguntando para um objeto se ele tem determinada propriedade e depois obtendo o seu valor nos permite criar em nossas classes métodos genéricos, que podem trabalhar literalmente com qualquer tipo de parâmetro.
Em que situação o tema é útil
Essa técnica pode ser muito útil quando o programador tem um objeto instanciado do tipo TForm, mas não sabe exatamente que form é e precisa consultar ou modificar o valor de determinado atributo.
Resumo do DevMan
Serão expostas algumas técnicas de RTTI para se obter informações de objetos e persistir essas informações em banco de dados. Serão mostrados desde a criação das tabelas aos métodos para inserir, alterar e excluir registros. Também será apresentado como podemos criar interfaces que o próprio usuário possa alterar, diminuindo assim os custos de manutenções simples como trocar a cor ou posição de controles no formulário.
Trabalhando com desenvolvimento de software nos deparamos com inúmeras situações em que nossos clientes nos pedem customizações. Algumas vezes estas são mudanças significativas, mas interessantes, que podemos incorporar em nossos produtos e agregar valor aos nossos projetos. Entretanto a maioria das vezes essas alterações são úteis para apenas um cliente, às vezes nos forçando a trabalhar com dois projetos em paralelo.
Essa tendência nos leva a criar sistemas bastante customizáveis, com muitos parâmetros de configuração. Estamos nos habituando a programar orientado a objetos criando classes cada vez mais genéricas para que possamos trocar livremente os objetos, unidades e componentes do nosso sistema.
Quando precisamos passar como parâmetro para um método de uma classe um objeto de uma classe qualquer já instanciado, numa variável do tipo TObject, para ser “genérico”, dentro do escopo do método precisamos saber de que tipo esse objeto é, para podermos alterar seguramente as suas propriedades e invocar suas funções e procedimentos. Há uma maneira mais elegante de tratar esses objetos genéricos do que fazer Typecasts, muitas vezes inseguros, ou uma lista interminável de desvios condicionais com muitos if's e cases.
As linguagens de programação modernas fornecem um recurso chamado Introspecção, ou Reflexão, que é a habilidade de uma classe de “conhecer” detalhes sobre sua estrutura, seus metadados, métodos e propriedades.
O Delphi provê essa habilidade na sua RTTI (Runtime Type Information - Informação em Tempo de Execução). Criada há mais de 10 anos, a RTTI tinha o objetivo inicial de facilitar o desenvolvimento da própria IDE do Delphi, para que ela pudesse cumprir com uma de suas missões que é “conhecer” as classes que estão sendo criadas ou usadas pelo desenvolvedor. Ela é amplamente utilizada na nossa ferramenta, por isso que o Object Inspector sabe de cada propriedade, valor e evento do componente que selecionamos, não importando qual componente seja.
Embora a RTTI nunca tenha sido bem documentada fora da equipe de desenvolvimento do Delphi, ela sempre pôde ser usada por programadores para facilitar algumas de suas tarefas na programação orientada a objetos.
Vamos ver como essa tecnologia pode nos ajudar a economizar bastante tempo, criar interfaces customizáveis para nossos clientes e usar a RTTI no dia-a-dia.
Conhecendo o básico
Talvez o leitor já use RTTI e não sabe. Sempre que usamos os operadores “is” para perguntar se um objeto é de um determinado tipo e “as” para fazer um Typecast seguro estamos usando na verdade RTTI.
Vamos ao código, que fala muito melhor a nossa língua. Usaremos neste exemplo o Turbo Delphi 2006, que pode ser obtido pelo site da Embarcadero. Fique à vontade para usar versões mais atuais do Delphi, apenas fique atento ao usar a versão 7 ou 2010. A versão 7 possui algumas diferenças, e o a 2010 teve a RTTI bastante modificada, assunto que já foi abordado aqui na revista e não focaremos neste artigo.
Crie uma nova aplicação Delphi VCL Forms para win32, renomeie o Form1 para frmTesteRTTI. Esta será nossa form principal e todos os nossos testes e exemplos partirão dela. Salve o projeto como ExemploRTTI.dpr e a Unit1 como uFrmTesteRTTI.pas. Inclua um Edit com o nome de edTeste e um Button com o nome de btTesteEdit.
Crie uma procedure chamada AlteraTexto, que receba 2 parâmetros, um TObject e uma string. Essa procedure será responsável por mudar o texto de nosso Edit e de qualquer outro componente que colocarmos no formulário. E depois vamos codificar o evento onClick do botão btTesteEdit para chamar esse procedimento passando como primeiro parâmetro o edTeste e como segundo parâmetro a mensagem super original “Alô Universo!”.
Neste método usaremos os operadores “is” para saber o tipo do objeto passado como parâmetro e o operador “as” para converter para o tipo que precisamos. Esse código pode ser visto na Listagem 1.
Listagem 1. Procedure AlteraTexto e botão btTesteEdit
//método público deste form: AlteraTexto
procedure TfrmTesteRTTI.AlteraTexto(obj: TObject; texto: string);
begin
if obj is TEdit then //verifica: obj é mesmo um edit?
(obj as TEdit).Text := texto; //então, sendo um edit, modificamos a propriedade Text
end;
procedure TfrmTesteRTTI.btTesteEditClick(Sender: TObject);
begin
AlteraTexto(edTeste, 'Olá Universo!');
end;
Este código não faz nada mais que usar o operador “is” para saber de que classe que é o objeto e o operador “as” para fazer typecast seguro e acessar suas propriedades. Uma das várias vantagens do Typecast seguro é que, se você tentar converter para o tipo errado, a instrução não vai passar deste ponto e causar um access violation depois da conversão, mas vai disparar uma exceção do tipo EinvalidCast com a mensagem “Invalid class typecast” no momento da conversão.
Até agora, nenhuma novidade. Esta procedure que criamos não é muito útil, mas repare que ela não necessariamente precisaria estar neste formulário. Ela poderia estar em qualquer uma das nossas classes de negócio, servindo inclusive para preencher nossos forms.
Vamos criar um outro botão, com o nome de btTesteCombo e adicionar um ComboBox com o nome cmbTeste. Nosso formulário ficará como a Figura 1.
Figura 1. Form com Edit, ComboBox e botões
Vamos fazer a mesma coisa agora para verificar o ComboBox. No código do btTesteCombo, repetimos o mesmo código do botão anterior, apenas passando o cmbTeste como primeiro parâmetro. A procedure AlteraTexto deve ser mudada para suportar o ComboBox, adicionando um if para verificar se o tipo é um TEdit ou um TComboBox, conforme a Listagem 2.
Listagem 2. Procedure AlteraTexto com ComboBox
procedure TfrmTesteRTTI.AlteraTexto(obj: TObject; texto: string);
begin
if obj is TEdit then
(obj as TEdit).Text := texto
else
if obj is TComboBox then
(obj as TComboBox).Text := texto
end;
procedure TfrmTesteRTTI.btTesteEditClick(Sender: TObject);
begin
AlteraTexto(edTeste, 'Olá Universo!');
end;
procedure TfrmTesteRTTI.btTesteComboClick(Sender: TObject);
begin
AlteraTexto(cmbTeste, 'Olá Universo!');
end;
O problema que nós criamos: além de nosso código precisar de inúmeros if's, um para cada tipo de componente, criamos a inconveniência de que a unit que tiver o nosso método AlteraTexto deve conhecer todas as classes usadas: TEdit, TComboBox e outras que possam ser adicionadas. Ela deverá ter várias units da VCL no seu uses. Isso não é bom se quisermos, no futuro, trocar todos os nossos componentes visuais por componentes da biblioteca JEDI ou da DevExpress, por exemplo (ou mesmo Web).
A unit TypInfo
A unit TypInfo contém tipos de dados e métodos especiais para se trabalhar com informações de tipos, tanto em tempo de design como em tempo de execução, em vários níveis diferentes. Foge ao escopo desse artigo detalhar ou documentar completamente o que essa unit possui, mas alguns conceitos são necessários ao que nos dispomos a fazer.
Vamos alterar a nossa procedure AlteraValor para não fazer nenhuma referência a nenhuma classe, não fazer typecast e simplesmente obter ou mudar o valor de uma propriedade pelo seu nome. Para isso, não esqueça de colocar TypInfo na seção uses do form. Vamos criar uma sessão “var” na procedure AlteraValor e criar uma variável chamada InfoPropriedades do tipo PPropInfo. Este tipo é um ponteiro para TPropInfo, que é uma estrutura de dados que armazena uma tabela de informações sobre propriedades publicadas de qualquer tipo de classe que seja derivada de TPersistent. Vamos atribuir a InfoPropriedades o resultado da função GetPropInfo, passando como parâmetros o objeto e o nome da propriedade, no nosso caso, “Text”.
Essa função GetPropInfo possui vários overloads. Isso te dá a liberdade de fazer a mesma coisa de várias formas. Experimente tentar fazer a mesma coisa, mas passando a classe do objeto ao invés de sua instância.
O que vamos fazer aqui será atribuir um valor a uma propriedade de um objeto através do nome da propriedade. Isso mesmo, o nome como uma string. Iremos verificar: “O objeto tem a propriedade X? Se tiver então atribua a propriedade X o valor Y”. Seu código ficará como a Listagem 3.
Listagem 3. Usando funções da TypInfo
procedure TfrmTesteRTTI.AlteraTexto(obj: TObject; texto: string);
var
//PPropInfo é um ponteiro para uma tabela de informações sobre propriedades
infoPropriedades: PPropInfo;
begin
//usa um objeto e o nome da propriedade para obter um ponteiro para as informações desta propriedade
infoPropriedades := GetPropInfo(obj, 'Text');
if Assigned(infoPropriedades) then
//atribui um valor a uma propriedade através de seu nome.
SetStrProp(obj, 'Text', texto);
end;
O que fizemos foi usar o método GetPropInfo(obj, ‘Text’) para obter um ponteiro para uma estrutura de dados chamada TPropInfo que contém informações sobre a propriedade. No caso pesquisamos a propriedade Text do objeto. Se o objeto tiver uma propriedade chamada “Text” o método retornará um ponteiro válido, mas se a propriedade “Text” não existir nesse objeto, então o método retornará um ponteiro nulo (nil). Por isso verificamos se infoPropriedades é um ponteiro que aponta realmente para algum lugar, usando o método Assigned(infoPropriedades). Se o resultado for verdadeiro, podemos atribuir qualquer valor à propriedade “Text” através do método SetStrProp(obj, ‘Text’, <texto a ser atribuido>).
Esse método é usado para strings, caso a propriedade seja uma string, mas existem métodos similares para trabalhar com integers, enums, variants etc.
SetStrProp(obj, ‘Text’, ‘um texto’), por exemplo, seta o valor ‘um texto’ à propriedade Text do objeto obj, se ele possuir uma propriedade Text. Por isso verificamos antes, caso contrário será disparada uma exceção.
Isso abre um leque de novas possibilidades. Podemos atribuir qualquer valor a qualquer propriedade de qualquer objeto, bastando que ele tenha a propriedade. Podemos armazenar em bancos de dados pares de Propriedade x Valor para cada componente, de cada form, que desejamos permitir que o usuário configure, sem ter que recompilar o projeto para mudar essas propriedades.
Como pudemos ver, estamos usando ponteiros. Ponteiros são uma parte fundamental da programação, e estudá-los é altamente recomendável. Muitos programadores nunca ouviram falar de ponteiros porque sempre programaram em linguagens interpretadas e/ou gerenciadas, como Java, C#, PHP, Python, Ruby, VB e assim por diante. Nessas linguagens o uso e gerenciamento de memória e ponteiros é transparente. Você pode entender ponteiros como um endereço de memória ou como uma referência para um determinado local da memória. Esse local pode ser a área da memória onde você declarou uma variável, mas também pode apontar para uma função ou vetor.
Se tudo que declaramos é armazenado em algum lugar da memória e esses lugares são “numerados”, uma variável do tipo ponteiro é uma variável que tem um valor, geralmente um inteiro de 4 bytes, que aponta para o endereço de memória de alguma coisa. No nosso caso a variável infoPropriedades vai ter o endereço de uma estrutura de dados que contém informações em tempo de execução de uma propriedade, por exemplo a propriedade Text do Edit.
Embora o uso de ponteiros possa parecer programação de baixo nível, não é tão baixo assim, pelo menos não como assembly. Digamos que é programação de nível intermediário. Estruturas de dados complexas e encadeadas usam ponteiros. E a API do Windows usa muito ponteiros, estruturas, ponteiros para estruturas e assim por diante. Você pode ver esses usos dando uma olhada nas units da VCL e da RTL. Aprende-se muito com essas units.
Não vamos nos aprofundar em ponteiros porque eles dariam assunto para muitos artigos, e uma boa seção de um livro.
Sempre que você usa um objeto, está usando na realidade um ponteiro. Pode verificar isso pelo tamanho das variáveis de um tipo objeto, que todas tem 4 bytes. O objeto propriamente dito está alocado em algum lugar da memória, que é referenciado pela variável que aponta para ele. Ponteiros também são muito úteis na implementação de estruturas de dados como pilhas, filas, listas ligadas e árvores binárias.
...