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:
- Primeiro extraímos o nome do seletor em forma de String para conseguirmos manipulá-lo
- 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).
- 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:
- 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.
- 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.
- 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.