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.