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.