Fazia um tempo que eu não postava uma das bizarrices em Ruby, então vamos lá: esses dias, no grupo de usuários de Ruby, surgiu uma discussão muito boa sobre métodos privados e alto acoplamento. A idéia é mais ou menos assim: em Java (ou C++), ao definir um método público que chama um método privado, se você reimplementar aquele método privado, os métodos públicos herdados não são afetados. Um exemplo vale mais que mil palavras, então:
#include <stdio.h> class Example { public: void imprimir() { hello(); } private: void hello() { printf("Hello, world!\n"); } }; class Ex2: public Example { private: void hello() { printf("Hello, from Ex2\n"); } }; int main(void) { Ex2 e; e.imprimir(); //Essa linha imprime "Hello, world!" }
Já no código Ruby equivalente:
class Example def imprimir() hello end private def hello puts "Hello, world!" end end class Ex2 < Example private def hello puts "Hello, from Ex2" end end Ex2.new.imprimir #Imprime "Hello, from Ex2"
Ou seja, reimplementar métodos privados numa subclasse pode quebrar métodos da superclasse. Embora eu considere isso uma peculiaridade da linguagem e não como um problema de acoplamento, implementação falha, ou qualquer coisa (e até, mais pra frente, pretendo escrever um post sobre o assunto de diferenças entre linguagens de programação além da sintaxe), a discussão inteira me deu uma idéia: como implementar esse comportamento em Ruby?
Bom, primeiro um pouco de contexto: o código de C++ e de Ruby não estão equivalentes, porque o código equivalente ao de ruby em C++ é o seguinte:
#include <stdio.h> class Example { public: virtual void imprimir() { hello(); } private: virtual void hello() { printf("Hello, world!\n"); } }; class Ex2: public Example { private: virtual void hello() { printf("Hello, from Ex2\n"); } }; int main(void) { Ex2 e; e.imprimir(); //Imprime "Hello, from Ex2" }
Aí, na discussão, alguém postou o código em Javascript para fazer o comportamento igual ao de C++/Java:
var Example = function() { var hello = function() { console.log("Hello, world!"); } return { imprimir: function() { hello(); } }; };
Removi as linhas da subclasse Ex2 para ficar mais conciso, mas dá pra ver pela lista do Ruby-SP. Enfim, a idéia aqui é aproveitar que funções, em Javascript, são closures e criar uma variável local chamada “hello”, que recebe uma “function”. Embora formalmente “hello” não é um método privado e sim uma variável local, ele se comporta como um: afinal, a função “imprimir” mantém o binding atual, chama a função atribuída à variável “hello”, e para todos os efeitos essa variável/função não possui qualquer visibilidade fora deste contexto. O código equivalente em Ruby, para esse esquema é:
class Example hello = proc do puts "Hello, world!" end define_method :imprimir do hello.call end end
Mesmo esquema do Javascript, muda-se a definição de métodos para define_method para poder fazer a definição no formato de “closure” ao invés de “método”, e assim manter o binding atual. O problema dessa solução, tanto em Ruby como em Javascript é que você precisa definir o método privado antes do público. Como achei que a solução merecia um estudo interessante, resolvi montar alguns specs, que estão no meu github. Então, vamos lá: Queremos fazer a coisa dessa forma:
class Example extend PrivateMethods public_method :imprimir do hello end private_method :hello do puts "Hello, world!" end end
Primeiramente, basta implementar os métodos. A primeira idéia é usar um pseudo-alias pra “define_method”. Algo assim:
module PrivateMethods def private_method(method_name, &b) define_method method_name, &b private method_name end #mesma coisa para o public_method end
Ok, aparentemente funciona. Mas, isso nada muda de usar os padrões do Ruby-estou definindo um método privado e um público, igual eu faria em qualquer outra situação. A idéia é usar o formato igual do Javascript, aproveitando os “bindings” para tentar identificar se o método foi chamado da classe “pai” o da classe “filha”, e então aproveitar essa informação no método privado e definir qual versão do método foi chamada. Por exemplo:
module PrivateMethods def self.extended(klass) defined_methods = {} caller_class = nil #Necessário ser declarado fora, para manter o binding define_method :public_method do |method, &block| that = self define_method method do |*args, &b| caller_class = that block.call(*args, block) end end #Definição do private_method... #a idéia é usar o caller_class e o defined_methods para indicar quem deve ser chamado. define_method :private_method do |method, &block| defined_methods[self] ||= {} #Cria um novo grupo de métodos privados, dessa classe defined_methods[self][method] = block #Atribui o bloco ao método define_method method do |*args, &b| #Chama o método, dependendo do caller_class ret = defined_methods[caller_class][method].call(*args, &b) #Seta o caller_class para nulo, afinal, não estamos mais sendo chamados de nenhum método público caller_class = nil ret end private method end end end
Mas essa abordagem tem um problema grave: dentro do método definido com “private_method” ou “public_method”, o “self” aponta para a classe… logo, não é possível chamar qualquer método dentro de um método definido com “private_method” ou “public_method”, nem acessar variáveis de instância, etc… há algumas saídas para esse caso: uma delas é ao invés de usar “call” (linha 09 e 20), usar “instance_eval” ou “instance_exec”. Mas isso dá alguns problemas: no primeiro caso, é impossível repassar os parâmetros para o método. No segundo, é impossível repassar o bloco. Então, utilizarei uma abordagem diferente, mais ou menos assim:
class String #Puxo um "UnboundMethod" desta classe to_s_method = instance_method :to_s #Aqui, eu já sei quantos parâmetros o to_s recebe, mas vou deixar o exemplo genérico define_method :to_s do |*args, &b| puts "Before to_s" #Calls the original method ret = to_s_method.bind(self).call(*args, &b) puts "After to_s" ret #Mantém o retorno do método original end end
Qual a vantagem desta abordagem? Ao contrário de usar “alias_method”, eu não fico com dois métodos estranhos (tipo “old_to_s” e “to_s”), e também eu acabo podendo salvar o método a ser chamado (linha 17) como um “UnboundMethod”, ao invés de um bloco. Logo, o código fica assim:
module PrivateMethods def self.extended(klass) defined_methods = {} caller_class = nil define_method :public_method do |method, &block| that = self define_method(method, &block) bound_method = instance_method(method) define_method method do |*args, &b| caller_class = that bound_method.bind(self).call(*args, &b) end end define_method :private_method do |method, &block| define_method method, &block im = instance_method method defined_methods[self] ||= {} defined_methods[self][method] = im define_method method do |*args, &b| ret = if defined_methods[caller_class].nil? || defined_methods[caller_class][method].nil? im.bind(self).call(*args, &b) else defined_methods[caller_class][method].bind(self).call(*args, &b) end caller_class = nil ret end private method end end end
O uso dessa API é muito simples: basta usar o exemplo abaixo:
require 'private_methods' class Example include PrivateMethods public_method :say do puts word end private_method :word do 'Hello, world!' end end class Child < Example private_method :word do 'Hello, from Child' end end Child.new.say #Imprime "Hello, word!"
Como sempre: não usem, jamais, isso em produção. Embora a abordagem seja interessante, eu não tenho certeza se ela é ThreadSafe, a performance dela deve ser bem reduzida, não testei todos os casos, e além disso, sejamos sinceros, é um belo de um Hack. O ideal é tentar se entender com a forma como Ruby encara métodos públicos e privados e aprender a conviver com isso, ou usar a maneira usando Closures (logo depois do exemplo de Javascript, declarando uma variável local do tipo “proc” e definindo o método público com “define_method”) que, embora ainda assim saia do padrão da linguagem, é mais seguro do que usar este hack maluco.
1 Comment
Tweets that mention Métodos privados semelhantes a Java/C++ em Ruby | Maurício Szabo -- Topsy.com · 2010-11-26 at 10:54
[…] This post was mentioned on Twitter by Garoto que programa, Maurício Szabo. Maurício Szabo said: Mais bizarrices em Ruby… esse talvez tenha ficado mal-explicado, mas enfim…http://bit.ly/icGH3g […]
Comments are closed.