Ruby e o duck typing


Leia em 2 minutos

No Ruby, nós não declaramos o tipo de objetos, nem o tipo do retorno de métodos. Embora isso possa parecer algo muito ruim para quem está acostumado com linguagens como Java, linguagens dinamicamente tipadas como o Ruby são muito flexíveis, produtivas e, acredite, seguras. Na maioria das vezes, o medo de não poder contar com o compilador para fazer verificações de tipos não tem fundamento.

Desenvolvedores Ruby estão mais acostumados em definir objetos pelo que eles podem fazer, do que por seu tipo. Esta técnica é chamada de duck typing.

Se anda como um pato e faz barulho como um pato, então deve ser um pato. E o interpretador ficará feliz em fazer com que o objeto seja tratado como um pato. Na prática, isso significa que em vez de fazer verificações de tipo de um objeto, você deve se preocupar se este objeto é capaz de executar o método que você precisa.

Pegue como exemplo strings, arquivos e arrays. As classes Array, File e String implementam o método de instância <<, que quase sempre significa append. Você pode se aproveitar desta interface para criar, por exemplo, uma classe de log que não se importa com o tipo de objeto que irá armazenar esses logs.

class SimpleLogger
  def initialize(io)
    @io = io
  end

  def log(message)
    @io << "#{Time.now} - #{message}\n"
  end
end

A classe SimpleLogger consegue enviar os logs para arrays, strings, arquivos e, se quiser, para qualquer outro objeto que implemente o método <<.

O Ruby realmente abraça o duck typing por toda a linguagem. Diversos protocolos exigem que o objeto apenas implemente um método to_<protocol>. Muitas operações que envolvem arrays, por exemplo, exigem que o objeto do lado direito da expressão apenas implemente o método to_ary.

class Numbers
  def to_ary
    [4, 5, 6]
  end
end

[1, 2, 3] + Numbers.new
#=> [1, 2, 3, 4, 5, 6]

A classe Hash, por exemplo, permite que você una dois objetos, desde que o método to_hash seja implementado.

class Configuration
  def to_hash
    {root: "/etc"}
  end
end

config = Configuration.new

{name: "Custom config"}.merge(config)
#=> {:name=>"Custom config", :root=>"/etc"}

Não é preciso dizer que este tipo de protocolo por convenção permite criar códigos muito mais flexíveis, com extrema facilidade.

Isso não significa que saber o tipo de objetos não seja útil. Veja, por exemplo, as operações matemáticas. Qualquer objeto pode ser convertido em números. Para isso, basta implementar o método coerce, que recebe o objeto que está solicitando a coerção. O exemplo abaixo mostra como criar uma classe cuja instância pode ser convertida em números inteiros e flutuantes, mas não em instâncias da classe BigDecimal.

class NumberOne
  def coerce(object)
    case object
    when Integer
      [object, 1]
    when Float
      [object, 1.0]
    else
      raise TypeError, "#{self.inspect} can't be coerced into #{object.class}"
    end
  end
end

puts 1 + NumberOne.new
#=> 2

puts 1.0 + NumberOne.new
#=> 2.0

require "bigdecimal"
puts BigDecimal.new("1.0") + NumberOne.new
#=> TypeError: FakeNumber can't be coerced into BigDecimal

O duck typing vai além de simples regras; é um estilo de programação. Antes de exigir tipos de objetos, pergunte-se se isso é realmente necessário. Às vezes, o tipo do objeto é muito importante, mas muitas vezes isso simplesmente não importa.

NOTA: Este artigo foi tirado do e-book "Conhecendo o Ruby" que estou escrevendo. Se inscreva na newsletter do HOWTO e saiba quando ele for lançado.