Nem só de troca de mensagens vivem os objetos
Sunday, May 25th, 2008Percebi que boa parte das dúvidas quanto ao meu post sobre como objetos não possuem atributos se deve ao fato das pessoas não terem geralmente um conhecimento real sobre o que é troca de mensagens.
Perfeitamente compreensível. Na maior parte dos livros e faculdades as pessoas aprendem que Orientação a Objetos é sobre como utilizar classes e sobre como as funções são chamadas de métodos. Por algum motivo esquecido nas areias do tempo decidiu-se que chamar o método em uma classe era passar uma mensagem e por isso algumas pessoas notoriamente pedantes usam este termo ao invés de dizer apenas “chama a função”.
Bem, os conceitos no parágrafo acima estão errados. Orientação a Objetos não é sobre classes e sim sobre…er… objetos. Você pode ter OO sem ter classes, como JavaScript e Io e pode ter também OO sem mensagens.
Troca de mensagens é um conceito utilizado em diversas áreas, não apenas Orientação a Objetos. Você pode ter um Sistema Operacional baseado neste conceito -como o MINIX por exemplo- ou criar uma arquitetura de computação distribuída como SOAP.
O que distingue a passagem de mensagens é o fato de que o recipiente da mensagem, seja um objeto, um serviço ou um processo, é quem decide o que é feito em decorrência de sua invocação.
Para tentar içar um pouco mais claro eu criei um meta-modelo bem bobinho em Java. Este representa um sistema Orientado a Objetos com classes e passagem de mensagens. O código abaixo mostra como declarar uma Classe calculadora e enviar uma mensagem dizendo para que esta multiplique números.
BlocoDeCodigo bloco = new BlocoDeCodigoImpl<Integer, Integer, Integer>(){
public Integer executar(Integer a, Integer b){
return a * b;
}
};
Classe classeCalculadora = novaClasse("Calculadora");
classeCalculadora.declaraMensagem("multiplique", bloco);
Instancia calculadora = instanciar("Calculadora");
assertEquals(8, calculadora.enviaMensagem("multiplique", 2, 4));
Note os passos realizados. Primeiro criamos um bloco de código, uma função. Depois dizemos ao sistema que existe uma classe chamada calculadora. Logo apos registramos o fato de que calculadora responde a uma mensagem executando o bloco que havíamos declarado.
Em termos de semântica, este código é mais ou menos equivalente a este:
public class Calculadora{
public Integer multiplicar(Integer a, Integer b){
return a * b;
}
}
Depois nós instanciamos a classe e passamos uma mensagem para ela, o que seria equivalente a:
Calculadora calc = new Calculadora(); calc.multiplicar(2,4);
As classes relevantes:
public class Ambiente {
static Map<String, Classe> classesDeclaradas = new HashMap<String, Classe>();
static Classe novaClasse(String nomeDaClasse) {
ClasseImpl classe = new ClasseImpl(nomeDaClasse);
classesDeclaradas.put(nomeDaClasse, classe);
return classe;
}
static Instancia instanciar(String nomeDaClasse) {
return new Instancia(classesDeclaradas.get(nomeDaClasse));
}
}
public class ClasseImpl implements Classe {
private final String nome;
private Map<String, BlocoDeCodigo> mensagens = new HashMap<String, BlocoDeCodigo>();
public ClasseImpl(String nome) {
this.nome = nome;
}
public void declaraMensagem(String nomeDaMensagem, BlocoDeCodigo blocoASerExecutado) {
mensagens.put(nomeDaMensagem, blocoASerExecutado);
}
public String nome() {
return nome;
}
public boolean respondeA(String nomeDaMensagem) {
return mensagens.containsKey(nomeDaMensagem);
}
public BlocoDeCodigo codigoParaMensagem(String nomeDaMensagem) {
return mensagens.get(nomeDaMensagem);
}
}
public class Instancia {
private final Classe minhaClasse;
public Instancia(Classe classe) {
this.minhaClasse = classe;
}
public Object enviaMensagem(String mensagem, Object... args) {
Object primeiro = args.length > 0 ? args[0] : null;
Object segundo = args.length > 1 ? args[1] : null;
return minhaClasse.codigoParaMensagem(mensagem).executar(primeiro,
segundo);
}
public Classe classe() {
return minhaClasse;
}
}
Esse meta-modelo é baseado em troca de mensagens. A classe Calculadora não recebe código a ser executado ela apenas recebe o nome de uma mensagem e parâmetros. Imagine que eu registre o mesmo bloco de código para várias mensagens, ou que eu use recursos de AOP e intercepte a execução do bloco. Nada disso é relevante para quem invoca a mensagem, ele apenas a envia e o que acontece em decorrência disso é responsabilidade do receptor.
Como quase todas as linguagens atuam desta forma pode ser difícil entender o conceito já que nunca se viu nada diferente. Vamos então implementar outro meta-modelo que não usa troca de mensagens mas sim uma outra forma chamada Data-Directed.
Nesta forma de invocar operações em objetos –que, como a anterior não é específica de OO- quem decide qual função será aplicada é o ambiente de execução, o runtime. Quando você invoca uma operação o ambiente vai procurar dentre os métodos registrados qual é o aplicável para aquele objeto e vai executar o método nele. Common Lisp utiliza este recurso de maneira tão poderosa em suas Generic Functions que praticamente elimina a necessidade de coisas como proxies e AOP.
Nosso meta-modelo para Data-Directed é executado dessa forma:
BlocoDeCodigo bloco = new BlocoDeCodigoImpl<Integer, Integer, Integer>(){
public Integer executar(Instancia instancia, Integer a, Integer b) {
return a * b;
}
};
Classe classeCalculadora = novaClasse("Calculadora");
registrarMetodo("multiplique", classeCalculadora, bloco);
Instancia calculadora = instanciar("Calculadora");
assertEquals(8, executarMetodo("multiplique", calculadora, 2, 4));
Repare que agora o bloco de código recebe como seu primeiro argumento uma referência para a instancia a qual se aplica (se você já usou java.lang.Method sabe que isso não é incomum quando se desce ao nível de implementação de linguagem). Caso nosso exemplo fosse minimamente usável seria desta forma que o bloco obteria acesso ao objeto em si.
Logo depois criamos a classe como antes mas ao invés de registrar uma mensagem na classe nós registramos um método no ambiente, dizendo que o método se aplica àquela classe. A invocação em si é bem parecida com a anterior.
Na implementação a única classe mais interessante é o Ambiente, que agora é bem mais esperto:
public class Ambiente {
static Map<String, Classe> classesDeclaradas = new HashMap<String, Classe>();
static Map<String, Map<Classe, BlocoDeCodigo>> metodos = new HashMap<String, Map<Classe, BlocoDeCodigo>>();
static Classe novaClasse(String nomeDaClasse) {
ClasseImpl classe = new ClasseImpl(nomeDaClasse);
classesDeclaradas.put(nomeDaClasse, classe);
return classe;
}
static Instancia instanciar(String nomeDaClasse) {
return new Instancia(classesDeclaradas.get(nomeDaClasse));
}
static void registrarMetodo(String nomeDoMetodo, Classe tipoEmQueSeAplica,
BlocoDeCodigo bloco) {
metodo(nomeDoMetodo).put(tipoEmQueSeAplica, bloco);
}
static Object executarMetodo(String nomeDoMetodo, Instancia instancia,
Object... args) {
Map<Classe, BlocoDeCodigo> tiposAceitaveis = metodo(nomeDoMetodo);
if (!tiposAceitaveis.containsKey(instancia.classe()))
throw new RuntimeException("Metodo inexistente");
BlocoDeCodigo bloco = tiposAceitaveis.get(instancia.classe());
Object primeiro = args.length > 0 ? args[0] : null;
Object segundo = args.length > 1 ? args[1] : null;
return bloco.executar(instancia, primeiro, segundo);
}
private static Map<Classe, BlocoDeCodigo> metodo(String nomeDoMetodo) {
if (!metodos.containsKey(nomeDoMetodo))
metodos.put(nomeDoMetodo, new HashMap<Classe, BlocoDeCodigo>());
return metodos.get(nomeDoMetodo);
}
}
Agora não apenas registra as classes mas também os métodos que são aplicáveis à cada classe e faz a invocação dos métodos em si.
Estes exemplos são bem educacionais, sem muita aplicação pratica, mas como já vimos em posts passados o fato de se usar message-passing ou Data-Directed ou properties, ou componentes ou qualquer outra coisa interfere no modo como devemos projetar nosso software. Existe uma vasta literatura sobre este tema mas ainda assim é uma das coisas mais desconhecidas pelo programador profissional.


