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.