पाठ 13 / 25

Duck Typing, Open Classes and Refinements

Use duck typing and understand monkey patching and refinements.

Behaviour over types

Ruby relies on duck typing: what matters is whether an object responds to the methods you call, not its class. A method that calls io.write(text) works with a file, a StringIO or a network socket. Check capabilities with respond_to?(:write) when needed, rather than is_a?. Ruby classes are open: you can reopen any class, even core ones like String or Integer, and add or change methods. This monkey patching is powerful, and Rails' ActiveSupport adds conveniences such as 3.days.ago this way, but global patches to core classes can conflict between libraries and surprise other developers. Refinements (refine String do ... end inside a module, activated with using) scope such changes to a file or module, making them much safer. Prefer adding methods to your own classes, wrapping objects in your own types, or helper modules; reach for refinements only when extending a core or third-party class really makes the calling code clearer.

Duck typing

Any object that responds to the needed method works, whatever its class.

Three different shaped objects each with the same small speaker icon, all feeding into one receiving box.
Figure 5.1 — Different classes satisfying the same interface by behaviour.

Duck typing and a scoped refinement

Any writable object works; the String extension is visible only where refined.

require "stringio"

def export_csv(rows, io)
  rows.each { |r| io.write(r.join(",") + "\n") }   # anything with #write works
  io
end

buffer = export_csv([["o1", 1200], ["o2", 300]], StringIO.new)
puts buffer.string
File.open("orders.csv", "w") { |f| export_csv([["o1", 1200]], f) }

module RupeeFormatting
  refine Integer do
    def to_rupees = format("Rs %.2f", self / 100.0)
  end
end

module Receipts
  using RupeeFormatting                     # refinement active only inside this module

  def self.line(item, paise) = "#{item}: #{paise.to_rupees}"
end

puts Receipts.line("Notebook", 12_000)    # Notebook: Rs 120.00
# 12_000.to_rupees                         # NoMethodError here: refinement not active

Patch core classes only with care

A global patch to String or Hash affects every gem in the process. If two libraries patch the same method differently, the result depends on load order. Refinements or your own wrapper classes avoid this.

त्वरित जाँच: What does duck typing mean in Ruby?

  • Code depends on what methods an object responds to, not on its class
  • Objects must inherit from Duck
  • All variables must declare types
  • Only classes can be passed to methods
Answer

Code depends on what methods an object responds to, not on its class — If it responds to the right methods, it can be used.