Este é um post sobre “Duck Typing”, e finalmente eu escrevi ele (estava enrrolando para fazê-lo). Acho esse post bem importante porque eu percebi, com a experiência em Ruby, que poucas pessoas sabem lidar com o dinamismo da linguagem (e aproveitá-la ao máximo).
Então, vamos falar um pouco sobre Duck Typing. O básico, a maioria já sabe:
def imprimir(objeto) print objeto.falar end
Ou seja, na declaração do método eu não defino nenhuma regra do que aquele objeto pode receber; apenas chamo os métodos que eu o objeto passado como parâmetro deveria implementar, e confio que vai dar certo. Parece simples, e até um pouco inseguro, então tem gente que prefere fazer umas checagens antes (tipo usando objeto.is_a?(Pessoa) ou objeto.respond_to?(:falar)), mas eu acho que isso quebra a idéia de Duck Typing. Para mim, tudo se resume a uma palavra: Protocolo.
Quem já leu algo sobre protocolos na internet deve ter visto coisas sobre, digamos, o HTTP, SMTP, etc. Qual a idéia? Um protocolo, basicamente, define um conjunto de regras que devem ser seguidas caso você queira se comunicar com aquele serviço/servidor/whatever e ser entendido. Simples, fácil. Como um exemplo bem tosco, vamos tentar acessar o Google usando os seguintes comandos:
telnet google.com
**montes de informação**
GET /
Bom, você verá que, milagrosamente, o google responde para nós uma página. Agora, no lugar do “GET /”, tente escrever algo arbitrário, tipo “Hello, world”, e o que você ganha é uma mensagem de erro (que pode, ou não, ser um HTML válido).
O que isso significa? Significa, na verdade, que o Google espera um protocolo: para exibir páginas, você usa esse comando “GET”, com mais uns parâmetros, e recebe uma página. Se você usar um comando que não existe, recebe um erro. Para quem entende HTTP, isso é óbvio.
Em Ruby, assim como outras linguagens dinâmicas, a linguagem se comporta de forma parecida. Você documenta, informalmente (quando eu falo “informalmente”, eu quero dizer que o código não força aquela forma de programar. Assim como no HTTP, você pode mandar comandos que não existem, eles só não serão entendidos e podem gerar erro), como cada classe, cada método, cada parte do programa funciona, e espera que o programador tenha bom-senso suficiente para fazer as chamadas do jeito correto.
Essa é uma mudança muito mais drástica do que se espera. Por exemplo, no Rails, digamos que queremos um comportamento assim: sem usar um banco de dados, queremos fazer uma validação. Digamos, um “CAPTCHA”. Você digita aquelas letrinhas que aparecem na imagem, e se estiver errado, o campo fica vermelho, igual o Rails faz. A documentação do Rails peca aqui, mas para termos o mesmo comportamento, precisamos que o objeto (digamos, uma instancia de CaptchaValidator, talvez?) implemente os seguintes métodos: errors (que deve retornar um objeto semelhante a um hash), error_on(field) (que retorna o erro naquele campo), e errors.full_messages (que retorna as mensagens de erro em texto puro, num formato semelhante a um Array). Se você implementar esses três métodos, mais um atributo (attr_accessor) para receber a informação do captcha, definido mais um ou dois métodos (no rails 3, mais um, na classe: “model_name”, que tem que retornar um ActiveModel::Name) você pode fazer uma view assim:
<% form_for @captcha do |f| %> <%= f.error_messages %> <p><%= f.label :capcha_text %><%= f.text_field :captcha_text %></p> <p><%= f.submit 'Login' %></p> <% end %>
Sem conhecer o código, vendo “de fora”, o @label é um registro num banco de dados, é um objeto, o que ele é? Sinceramente, não importa. O que importa é que o @label atenda um protocolo, e é por isso que recomenda-se que o protocolo seja o mais simples o possível (Rails peca um pouco aqui também. Ao invés de ter que implementar o método error_on(field), o ideal seria que ele pegasse direto do errors, como “errors[field]”).
Aliás, isso é a regra de mix-in também: a idéia do mix-in (originalmente, pelo que parece) é de dar poder a um objeto usando um protocolo; por isso os modules padrão do Ruby, pelo menos a maioria, tem nomes que terminam com -able (-ável, em português) como Comparable ou Enumerable (Comparável e Enumerável). No caso do Comparable, você está dando poder a uma classe de ser comparada com outra-o protocolo diz para implementar um método (<=>(other)) que, quando for rodado, compara esse objeto com outro e retorna -1 se este objeto for maior, 0 se forem iguais, ou +1 se o outro for maior. Quando você mixar o Comparable, métodos como ==, < , <=, virão de brinde. O Enumerable, idem, você só precisa implementar o método "each". Claro, muita gente usa modules como se fossem "herança múltipla", e em alguns casos são implementações muito boas (ActiveModel::Validations sendo um ótimo exemplo), mas idealmente isso é herança múltipla, não mix-in, e isso tem que ficar claro.
Então, duck-typing de novo: a idéia de se programar nesse paradigma basicamente é assim: se você quer um objeto que se comporte como se fosse uma herança de determinada classe, tudo o que se precisa fazer é descobrir quais métodos precisam ser implementados (isso pode ser de varias formas, até mesmo criando uma classe vazia e ir acompanhando erro a erro de "undeclared method") e, então, implementá-los. Parece simples (e de fato é), porém é aonde muita gente peca, criando classes que definem um método aonde ele só lança uma exception (para que declará-lo, afinal?), esperando que ele seja herdado, ou usando mix-in como herança múltipla (tipo o que o DataMapper faz, e não, na minha opinião não é algo desejável), ou ainda herdando classes e usando "remove_method" para remover métodos que não seriam usados. Ruby é uma linguagem no qual a idéia de duck-typing é muito presente, o tempo todo. Abuse de composição, a linguagem facilita para você.
Claro, ainda há muito mais o que falar sobre Ruby e duck-typing, mas isso fica para outros posts.