[Objective-C] Method Missing!

2010 December 06, 02:55 h - tags: apple objective-c beginner learning

Obs: Código disponível no Github

Uma das funcionalidades mais interessantes do Ruby é sem dúvida o famoso method_missing. Graças a ele podemos enviar mensagens arbitrárias a um objeto e ainda assim fazer com que responda como queremos. Por exemplo, o seguinte código dará uma exceção:

1
2
3
4
5
6
> obj = Object.new
 => #<Object:0x0000010092ac60> 
> obj.foo
NoMethodError: undefined method `foo' for #<Object:0x0000010092ac60>
        from (irb):2
        from /Users/akitaonrails/.rvm/rubies/ruby-1.9.2-p0/bin/irb:17:in `<main>'

Agora, podemos redefinir o método method_missing do Object e veja o que acontece:

1
2
3
4
5
6
7
8
9
> def method_missing(method, *args)
>   "#{method}:#{args.size}"
> end

> obj.foo
=> "foo:0"

> obj.foo(1,2,3)
=> "foo:3"

Este foi um resumo rápido, recomendo que se ainda não estiver familiarizado com esse conceito leia meu Micro-Tutorial de Ruby – Parte II onde eu explico isso em mais detalhes.

Um exemplo que gosto de usar é a classe Builder::XmlMarkup. Diferente de plataformas que fazem ou concatenação manual de Strings (péssimo) ou manipulação burocrática de nós (DOM), em Ruby temos essa excelente classe que minimiza a quantidade de código e ao mesmo tempo gera XML bem formatado e válido. Este é um exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
require 'builder'
x = Builder::XmlMarkup.new(:target => $stdout, :indent => 1)
x.html do |h|
  h.body do |b|
    b.h1 "Hello World"
    b.p "This is a paragraph."
    b.table do |t|
      t.tr do |tr|
        tr.td "column"
      end
    end
  end
end

Esse código irá gerar diretamente este XML:

1
2
3
4
5
6
7
8
9
10
11
<html>
 <body>
  <h1>Hello World</h1>
  <p>This is a paragraph.</p>
  <table>
   <tr>
    <td>column</td>
   </tr>
  </table>
 </body>
</html>

Se nunca tinha visto isso, pare por um segundo e contemple a beleza desta API. A partir do objeto XmlMarkup enviamos mensagens como html e ele vai acumulando as tags. Usamos blocos exatamente para blocos de XML, garantindo que eles terão as tags corretas de fechamento.

Pensando nisso, resolvi tentar fazer algo semelhante em Objective-C. A funcionalidade que permite esse tipo de API no Ruby é o method_missing, algo que imaginamos que somente linguagens dinâmicas conseguem ter. Porém, ao final deste artigo, quero fazer este código funcionar:

1
2
3
4
5
6
7
8
9
10
11
12
XmlBuilder* xml = [[XmlBuilder alloc] init];
[xml htmlBlock:^(XmlBuilder* h) {
    [h bodyBlock:^(XmlBuilder* b) {
        [b h1:@"Hello World"];
        [b p:@"This is a paragraph."];
        [b tableBlock:^(XmlBuilder* t) {
            [t trBlock:^(XmlBuilder* tr) {
                [tr td:@"column"];
            }];
        }];            
    }];
}];

É bem mais verbose do que Ruby, obviamente, mas ainda assim bem mais interessante do que o jeito de manipular DOMs com métodos como createElement, appendElement, etc. De uma certa maneira dá pra ficar bastante semelhante à versão em Ruby. Mas como isso é possível?

Antes de continuar, leia meu artigo sobre Categorias e Blocos pois este artigo usará blocos.

Seletores

Recapitulando, em Ruby, quando chamamos um método de um objeto, por exemplo:

1
obj.foo("Hello World")

Na realidade podemos dizer que estamos “enviando a mensagem :foo ao objeto ‘obj’” ou seja, seria o mesmo que:

1
obj.send(:foo, "Hello World")

Em Obj-C temos algo semelhante. Quando fazemos:

1
[obj foo:@"Hello World"];

Seria o equivalente a fazer:

1
[obj performSelector:@selector(foo:) withObject:@"Hello World"];

A diferença é que em Ruby temos Symbols e em Obj-C temos Selectors, que é semelhante, uma forma de não desperdiçar espaço com Strings. Porém, selectores são mais do que apenas métodos: eles representam o nome do método e seus atributos que em Obj-C são nomeados. Por exemplo, um método foo, com dois argumentos, poderia ser assim:

1
- (void) foo:(NSString*)bar value:(NSString*)xyz;

Esse método seria enviado ao objeto assim:

1
[obj foo:@"Hello" value:@"World"];

E seu seletor seria assim:

1
SEL t = @selector(foo:value:);

Se o método tivesse apenas o primeiro argumento (que não tem nome), o seletor seria assim: foo: – note o “:” no final. Se não tiver nenhum argumento, não tem nenhum “:”. Isso é importante não confundir. Alguns erros podem passar em branco pela falta desse “:” na hora de montar o seletor.

Agora, quando se envia uma mensagem a um objeto e o método correspondente não está implementado, o Obj-C fará duas coisas: primeiro vai chamar o método methodSignatureForSelector:, que devolve um objeto NSMethodSignature. Em seguida vai criar um objeto NSInvocation a partir disso e passará como argumento ao método forwardInvocation:. Este último é quem mais ou menos faz o papel do method_missing de Ruby.

Na realidade, em Obj-C essa técnica é chamada de Message Forwarding. O objetivo na realidade é criar objetos que são Proxy para outros objetos. Nesse caso o framework facilita as coisas para passar mensagens que realmente existem no objeto de destino.

O pseudo-código seria mais ou menos assim:

1
2
3
4
5
6
7
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [anotherObject methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anotherObject performSelector:[anInvocation selector]];
}

Os métodos são chamados nessa sequência mesmo. No primeiro ele consegue a assinatura do método no objeto destino com o método methodSignatureForSelector. A assinatura é um array que representa o tipo do valor de retorno e os tipos dos argumentos. Essa assinatura é usada em conjunto com o seletor para criar o NSInvocation. Daí no segundo método o seletor é enviado ao objeto de destino, sendo assim executado.

Se precisar criar Proxies ou Adapters, esse é o padrão para fazê-lo.

Proxy Dinâmico

O problema é que o padrão anterior pressupõe que o objeto de destino possui os métodos definidos a partir dos quais podemos extrair suas assinaturas. Porém, no nosso caso queremos um objeto que aceite qualquer tipo de mensagem representando tags arbitrários de um XML.

Para começar, quero definir uma classe chamada XmlBuilder e as variáveis internas que vão acumular o XML e controlar o nível de indentação:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import <Foundation/Foundation.h>

@interface XmlBuilder : NSObject
{
    NSMutableString* buffer;
    int indentationLevel;
}
@property (retain) NSMutableString* buffer;
@property (assign) int indentationLevel;
@end

@implementation XmlBuilder
@synthesize buffer, indentationLevel;
- (id) init {
    self = [super init];
    if (self) {
        self.buffer = [[NSMutableString alloc] init];
        self.indentationLevel = 0;
    }
    return self;
}

- (void) dealloc {
    [buffer release];
    [super dealloc];
}
...
@end

Até aqui nada de mais, apenas definição de interface, implementação, propriedades, construtor e destrutor. Coisa padrão.

Agora a coisa começa a esquentar:

1
2
3
4
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // não importa o retorno porque não vamos usar essa assinatura
    return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}

Esta primeira versão de implementação não precisa se preocupar com a assinatura. Vamos usar uma convenção para extrair os argumentos e não precisamos de assinaturas, então apenas devolvemos uma genérica qualquer.

Para começar, quero entender mensagens mais ou menos desse tipo:

1
2
3
4
5
6
/*
 Convenção para métodos dinâmicos:
 
 - (id) entity:(NSString*)value;
 - (id) entityBlock:(id (^)(id))block;
 */

Ou seja, mensagens assim:

1
2
[xml p:@"Hello World"];
[xml tableBlock:^(XmlBuilder* t) { ... }];

Agora, vamos definir o método principal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // #1
    NSString* method = NSStringFromSelector([anInvocation selector]);

    // #2
    BOOL hasBlock = [method hasSuffix:@"Block:"];
    method = [method stringByReplacingOccurrencesOfString:@"Block" 
                                               withString:@""];
    method = [method stringByReplacingOccurrencesOfString:@":" 
                                               withString:@""];
    
    // #3
    int tabsLength = self.indentationLevel * 2;
    NSMutableString* tabs = [NSMutableString stringWithCapacity:tabsLength];
    int i;
    for ( i = 0 ; i < tabsLength; i ++ ) {
        [tabs appendString:@" "];
    }
    ...

Vamos ver até a metade do método:

  1. Primeiro extraímos o nome do seletor em forma de String para conseguirmos manipulá-lo
  2. Nesta primeira versão, por convenção, assumimos que se seletor terminar com “Block:” significa que receberemos um bloco como argumento. Em seguida limpamos isso no nome do seletor para ficar só o nome da tag. Por exemplo: “htmlBlock:” ficaria “html” apenas. Eu faço isso porque ainda não aprendi a identificar o tipo do argumento recebido, pois se eu puder identificar que recebi um bloco, não preciso da convenção (fica como um TODO).
  3. Aqui geramos uma String com espaços em branco representando o nível de identação atual do XML, para que o resultado final esteja bonito.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    ...
    if (hasBlock) {
        // #4
        id (^block)(id);
        [anInvocation getArgument:&block atIndex:2];            

        // #5
        [buffer appendFormat:@"%@<%@>\n", tabs, method];
        self.indentationLevel = self.indentationLevel + 1;
        block(self);
        self.indentationLevel = self.indentationLevel - 1;
        [buffer appendFormat:@"%@</%@>\n", tabs, method];
    } else {
        // #6
        NSString* value;
        [anInvocation getArgument:&value atIndex:2];
        [buffer appendFormat:@"%@<%@>\n%@%@%@\n%@</%@>\n", 
          tabs, method, tabs, @"  ", value, tabs, method];
    }    
}

Continuando a partir da metade do código:

  1. Se a convenção disser que existe um bloco, assumimos que ele foi passado como argumento no 3o ítem do array do NSInvocation. Por convenção o primeiro elemento desse array (0) é o self e o segundo (1) o método, portanto o 3o. elemento (2) é o equivalente ao primeiro argumento.
  2. Agora vem a sequência para criar a tag que abre o bloco, depois aumentamos o nível de identação para o bloco; daí chamamos o bloco passado passando nós mesmos como parâmetro; o bloco é executado e quando retorna podemos diminuir o nível de identação; finalmente criamos a tag que fecha o bloco.
  3. Caso não tenha sido passado um bloco então é uma tag simples com um valor simples dentro. Agora no 3o. elemento do array, em vez de esperar um bloco podemos esperar um String. Finalmente basta criar a tag com o valor String dentro e acertar a identação de acordo.

Fazendo isso, podemos agora fazer chamadas assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    XmlBuilder* xml = [[XmlBuilder alloc] init];
    [xml htmlBlock:^(XmlBuilder* h) {
        [h bodyBlock:^(XmlBuilder* b) {
            [b h1:@"Hello World"];
            [b p:@"This is a paragraph."];
            [b tableBlock:^(XmlBuilder* t) {
                [t trBlock:^(XmlBuilder* tr) {
                    [tr td:@"column"];
                }];
            }];            
        }];
    }];
    NSLog(@"%@", [xml buffer]);
    [pool drain];
    return 0;
}

Impressionantemente, se abrirmos o Console (Shift+Command+R) ao executar teremos exatamente esta saída do comando NSLog:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
  <body>
    <h1>
      Hello World
    </h1>
    <p>
      This is a paragraph.
    </p>
    <table>
      <tr>
        <td>
          column
        </td>
      </tr>
    </table>
  </body>
</html>

Que é o que queríamos e é semelhante ao gerado pelo equivalente Builder::XmlMarkup do Ruby. Claro, esta é uma versão hiper-simplificada, mas que pode ser o esqueleto para uma versão completa. O próximo passo é fazer a classe aceitar atributos de tags. E outra coisa é ver se é possível não precisar da convenção de “tableBlock:” para identificar se o argumento passado é um bloco e checar se é possível verificar o tipo do argumento passado em tempo de execução.

De qualquer forma, com isso podemos ver o quanto o Obj-C pode ser dinâmico e flexível mesmo em se tratando de uma linguagem de baixo nível como é C. Com um pouco de criatividade podemos criar bibliotecas que lembram em muito o que usamos em Ruby.

Comments

comentários deste blog disponibilizados por Disqus