Reflexão (programação)

Em ciência da computação, reflexão computacional (ou somente reflexão) é a capacidade de um programa observar ou até mesmo modificar sua estrutura ou comportamento. Tipicamente, o termo se refere à reflexão dinâmica (em tempo de execução), embora muitas linguagens suportem reflexão estática (em tempo de compilação). A reflexão é mais comum em linguagens alto nível, como SmallTalk e menos comum em linguagens de mais baixo nível como o C.

Geralmente, reflexão é a atividade numa computação que permite que um objeto obtenha informações sobre sua estrutura e sobre os motivos de sua computação. O paradigma de programação por trás dessa capacidade é chamado programação reflexiva.

Quando o código fonte de um programa é compilado, a informação sobre sua estrutura normalmente se perde quando o código de baixo nível (tipicamente em assembly) é produzido. Já num sistema que suporta reflexão, essa informação é preservada como metadados, anexados ao código gerado.

Paradigma reflexivo editar

A programação reflexiva, também chamada de programação orientada à reflexão, é usada para escrever programas no paradigma reflexivo. Este, é usado como uma extensão para o paradigma da orientação a objeto, para adicionar auto-otimização e aumentar a flexibilidade de um aplicativo. Nesse paradigma a computação não é trabalhada somente durante a compilação do programa, mas também durante sua execução. Outras abordagens imperativas, tais como os paradigmas da programação procedural ou orientada a objeto, especificam que há uma sequência pré estabelecida de operações (sejam elas funções ou chamadas de métodos), que modificam qualquer dado a elas submetido. Por outro lado, o paradigma reflexivo diz que as operações não são definidas em tempo de compilação, e sim, que seu fluxo será decidido dinamicamente, baseado nos dados ao qual terá que trabalhar, e nas operações que devem ser realizadas. O programa só conterá mecanismo de como identificar esses dados e como decidir que operações ele deve realizar.

Qualquer computação pode ser classificada entre atômica, em que a operação completa num passo simples e lógico (como na adição de dois números), ou composta, definida como uma sequência de múltiplas operações atômicas. Um grupo de instruções composto, na linguagem procedural ou orientada a objeto, perde sua estrutura quando compilado. Mas o paradigma reflexivo introduz o conceito de metainformação, que mantém o conhecimento sobre essa estrutura. Essa metainformação armazena dados como o nome dos métodos, nome da classe, nome das superclasses ou até mesmo sobre a suposta funcionalidade do código. O último é obtido mantendo informações sobre as trocas de estados que o código causa nos dados que o percorrem. Assim, quando um dado (ou objeto) é encontrado, ele pode ser refletido para encontrar quais operações ele suporta e, aquela que causa a transição de estado desejada pode ser decidida em tempo de execução, sem a necessidade de ser especificada previamente no código.

Usos da reflexão editar

A reflexão pode ser utilizada para auto-otimização ou auto-modificação de um programa. Um sub-componente reflexivo de um programa monitorará a execução e poderá otimizar-se ou modificar-se de acordo com a função que o programa está resolvendo. Isso pode ser feito modificando a própria área de memória do programa, onde o código está armazenado.

A reflexão pode ser também utilizada para adaptar um determinado sistema dinamicamente à diferentes situações. Considere, por exemplo, uma aplicação que use uma classe X para comunicar-se com algum serviço. Agora, suponha que essa aplicação precise comunicar-se com um serviço diferente, usando a classe Y, que tem nomes de métodos diferentes. Sem reflexão, a aplicação teria de ser modificada e recompilada. Mas, se a reflexão for usada, isso pode ser evitado. A aplicação poderia conhecer os métodos da classe X e essa classe lhe diria que método era usado para que propósito. Assim, quando um novo serviço for usado, via classe Y, a aplicação também procuraria pelos métodos necessários e os utilizaria. Nenhuma modificação no código seria necessária. Nem mesmo o nome da nova classe deveria ser recompilado com a aplicação, uma vez que ele poderia estar armazenado em algum arquivo de configuração, ser verificado e ter a sua classe carregada em tempo de execução. Esse mecanismo é conhecido comumente como plugin.

No modelagem orientada a objeto, a reflexão é uma parte natural do idioma diário. Quando verbos (métodos) são chamados, várias variáveis como verb (nome do verbo sendo evocado) e this (objeto no qual o verbo é chamado) são populadas para determinar o contexto da chamada. A segurança tipicamente é gerenciada acessando a pilha de execução através de programação. Como callers() retorna uma lista de métodos que a partir do qual método atual foi eventualmente evocado, realizar testes em callers()[1] (o comando evocado pelo próprio usuário) permite ao verbo proteger a si mesmo contra uso não-autorizado.

Implementação editar

Uma linguagem que suporta reflexão fornece uma série de recursos em tempo de execução que, caso contrário, seria muito obscuro ou impossível de serem desenvolvidas em uma linguagem de baixo nível. Muitos desses recursos são a capacidade de:

  • Descobrir e modificar construções do código fonte (como blocos de código, classes, métodos, protocolos, etc.) usando objetos em tempo de execução;
  • Converter uma cadeia de caracteres que seja igual ao nome simbólico de uma classe ou um função numa referência ou evocação dessa classe ou função;
  • Avaliar uma cadeia de caracteres como se eles fossem o código fonte, em tempo de execução.

Esses recursos podem ser implementados de maneiras diferentes. Linguagens de programação interpretadas, como o Ruby e o PHP são totalmente adaptadas para reflexão, já que seu código fonte nunca se perde no processo de tradução para o código de máquina – o interpretador possui o código fonte legível disponível. Linguagens compiladas usam o seu ambiente de execução para fornecer informação sobre o código fonte. Um executável compilado em Objective-C, por exemplo, grava o nome de todos os métodos num bloco executável, fornecendo uma tabela que faz a correspondência desses nomes com os métodos (ou seletores dos métodos) compilados no programa. Uma linguagem compilada que suporte a criação em tempo de execução das funções como a Common Lisp, o ambiente de execução terá de incluir um compilador.

Problemas da reflexão editar

Embora sejam grandes os benefícios da reflexão, sobretudo num ambiente onde a flexibilidade seja muito necessária, o uso desse recurso também traz seus problemas. Entre eles está a maior complexidade do código. Como será visto nos exemplos, o código usando reflexão é mais complexo e, portanto, sujeito a erros. Também há a necessidade de um bom controle de versões: pequenas peças de código podem ser compiladas e disponibilizadas separadamente, dessa forma, um bom controle de versões faz-se necessário para garantir a compatibilidade de todo o sistema. Outro problema é a maior susceptibilidade a erros, não só devido a complexidade mas também à ausência de verificação sintática e semântica em tempo de compilação. Por fim, há uma redução do desempenho geral da aplicação, especialmente percebido em sistemas com diversas classes refletidas.

Exemplos editar

O exemplo a seguir foi construído em Java usando o pacote java.lang.reflect.[1] Considere os dois fragmentos de código:

 // Sem reflexão
 Foo foo = new Foo();
 foo.hello();
 
 // Com reflexão
 Class cl = Class.forName("Foo");
 Method method = cl.getMethod("hello", null);
 method.invoke(cl.newInstance(), null);

Ambos os fragmentos criam uma instância da classe Foo e chamam o seu método hello(). A diferença é que, no primeiro trecho, os nomes da classe e do método serão compilados e, após isso, não será possível utilizar uma classe com outro nome. No segundo fragmento, tanto o nome da classe quanto do método podem ser facilmente transformados em variáveis e, portanto, variar em tempo de execução. O lado negativo é que a segunda versão é mais difícil de ler e não tem qualquer tipo de verificação de sintaxe e semântica em tempo de compilação. Por exemplo, se a classe Foo não existir, um erro será exibido durante a compilação para o primeiro exemplo e, no segundo caso, o erro só ocorrerá em tempo de execução.

Abaixo segue um exemplo equivalente em Perl:

 # sem reflexão
 Foo->new->hello;
 
 # com reflexão
 my $class = "Foo";
 my $method = "hello";
 $class->new->$method;

O exemplo seguinte, escrito em C#, demonstra o uso de recursos avançados de reflexão. O programa lê o nome de um assembly como sua entrada em linha de comando. O assembly pode ser uma biblioteca de classes. O assembly é carregado e a reflexão é utilizada para descobrir seus métodos. Para cada método a reflexão é usada para descobrir se ele foi recentemente modificado ou não. Se foi recentemente modificado, e se não requer nenhum parâmetro o nome e o tipo de retorno de método é exibido.

Para descobrir se um método foi recentemente modificado ou não, o desenvolvedor precisa usar um atributo personalizado para marcá-lo como modificado e o assembly dessa classe é marcado como contendo tal atributo. A presença desse atributo denota que o método foi modificado e a ausência, que o método é antigo. Um atributo é um metadado denotando a estrutura do programa, aqui um método. Um atributo, por si só, é implementado como uma classe. O programa carrega o assembly dinamicamente em tempo de execução e verifica se este suporta o atributo sobre a modificação do método. Se for suportado o nome de todos os métodos é retornado. Para cada método, uma lista de todos os seus argumentos é requisitada. Se a lista estiver vazia, ou seja, se o método não tiver argumentos, o método é impresso juntamente com o seu tipo de retorno e um comentário que o desenvolvedor pode ter adicionado juntamente ao atributo que define o método recente, para deixa-lo mais informativo.

 using System;
 using System.Collections.Generic;
 using System.Text;
 using System.Reflection;
 using Recent;
 
 namespace Reflect
 {
     class Program
     {
         private Assembly a;
         Program(String assemblyName)
         {
             a = Assembly.Load(new AssemblyName(assemblyName));
             //Certifica-se de que o assembly suporta o atributo DateCreated
             Attribute c=Attribute.GetCustomAttribute(a, typeof(SupportsRecentlyModifiedAttribute));
             if (c == null)
             {
                  //A obtencao do "SupportsRecentlyModified" falhou.
                  //Isso significa que o assembly provavelmente nao o suporta.
                  throw new Exception("O assembly " + assemblyName + " não suporta o atributo requerido.");
             }
             Console.WriteLine("Carregado: " + a.FullName);
         }
 
         public void FindNewMethodsWithNoArgs()
         {
             //Localiza todos os tipos definidos no assembly
             Type[] t = a.GetTypes();
             foreach (Type type in t)
             {
                 //Se o tipo nao e uma classe, ele e pulado o próximo e verificado
                 if (!type.IsClass)
                     break;
                 Console.WriteLine("Classe: " + type.FullName);
                 MethodInfo[] methods = type.GetMethods();
                 foreach (MethodInfo method in methods)
                 {
                     object[] a=method.GetCustomAttributes(typeof(RecentlyModifiedAttribute), true);
                     //Se o atributo nao for encontrado, o metodo e velho.
                     if (a.Length != 0)
                     {
                         //Caso contrario, apenas um atributo sera recebido pois "RecentlyModified" nao
                         //suporta o uso concomitante com outros atributos
                         Console.Write("\tNovo método: " + method.Name);
                         //Uma classe representando "RecentlyModifiedAttribute" e instanciada
                         //a partir do atributo e é usada para obter o comentário que o 
                         //desenvolver colocou nela.
                         if (method.GetParameters.Length > 0)
                             break;
                         Console.WriteLine("\t" + (a[0] as RecentlyModifiedAttribute).comment);
                         Console.WriteLine("\t\tTipo de retorno: " + method.ReturnType.Name);
                     }
                 }
             }
         }
 
         static void Main(string[] args)
         {
             try
             {
                 Program reflector = new Program("UseAttributes");//Console.ReadLine());
                 reflector.FindNewMethodsWithNoArgs();
             }
             catch (Exception e)
             {
                 Console.Error.WriteLine(e.Message);
             }
         }
     }
 }

A implementação dos atributos personalizados é exibida a seguir:

 using System;
 using System.Collections.Generic;
 using System.Text;
 
 namespace Recent
 {
     //Certifica-se de que o atributo e aplicado apenas a metodos
     //e que nao pode ser usado com outros atributos
     [AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
     public class RecentlyModifiedAttribute : Attribute
     {
         //O atributo deve ser usado em metodos sem argumentos 
         //(como em [RecentlyModified])
         //ou com comentários (como em [RecentlyModified(comment="<someComment>")])
 
         private String Comment = "Esse método foi recentemente modificado";
         public RecentlyModifiedAttribute()
         {
             //Esse é um construtor vazio que manipula a instanciacao do atributo. 
             //Ele é vazio pois nenhum argumento é necessário para o uso do atributo. 
         }
 
         //Opcionalmente o argumento chamado "comment" deve ser suportado. Entao uma propriedade "comment" é definida 
         //espeficicando como manipular o comentário. Ele deve ser usado como um argumento nomeado quando o 
         //atributo estiver sendo usado. Isso irá dizer ao compilador que propriedade lida com o argumento. 
         public String comment
         {
             get
             {
                 return Comment;
             }
             set
             {
                 Comment = comment;
             }
         }
     }
 
     [AttributeUsage(AttributeTargets.Assembly, AllowMultiple=false)]
     public class SupportsRecentlyModifiedAttribute : Attribute
     {
         //O atributo deve ser usado em classes sem argumentos
         //como em [SupportsRecentlyModified]
         public SupportsRecentlyModifiedAttribute()
         {
             //O construtor está vazio pois nenhum argumento é necessário para o uso do atributo. 
         }
     }
 }

E finalmente, o uso dos atributos personalizados, definidos acima, numa classe construída aqui.

 using System;
 using System.Collections.Generic;
 using System.Text;
 using Recent;
 
 //Usa o atributo "SupportsRecentlyModified" para especificar que é suportado pelo assembly.
 [assembly: SupportsRecentlyModified]
 
 namespace Reflect
 {
     class UseAttributes
     {
         private Object info;
 
         public UseAttributes()
         {
             info = (object) "Ola mundo!";
         }
 
         public void OldMethodWithNoArgs()
         {
             Console.WriteLine("Essa é um método antigo sem argumentos.");
         }
 
         //Usa o atributo "RecentlyModified" para especificar que o método foi modificado 
         [RecentlyModified]
         public void NewMethodWithNoArgs()
         {
             Console.WriteLine("Esse é o método modificado recentemente com argumentos.");
         }
 
         public void OldMethodWithOneArg(object something)
         {
             info = something;
             Console.WriteLine("Esse é o método antigo com um argumento.");
         }
 
         [RecentlyModified]
         public void NewMethodWithOneArg(object something)
         {
             info = something;
             Console.WriteLine("Esse é o novo método com um argumento.");
         } 
 
     }
 }

Referências

  1. «java.lang.reflect (Java 2 Platform SE 5.0)». download.oracle.com. Consultado em 1 de Maio de 2011 

Ligações externas editar