Percebi 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.