Skip to content

Instantly share code, notes, and snippets.

@wilsonehusin
Last active April 22, 2020 06:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wilsonehusin/cbcf6d98207c596837351f4dd87e539a to your computer and use it in GitHub Desktop.
Save wilsonehusin/cbcf6d98207c596837351f4dd87e539a to your computer and use it in GitHub Desktop.
Demonstration of duck-typing in Ruby

I like to imagine an ideal inheritance structure of objects to be as follows:

class Animal
  attr_reader :legs

  def name
    self.class.name
  end
end

class Dog < Animal
  def legs
    4
  end
end

class Duck < Animal
  def legs
    2
  end
end

class Fish < Animal
  def legs
    0
  end
end

This allows consumers of the classes to only care about an object being Animal. Realistic usage would be something like:

def socks_to_order(animals)
  animals.map(&:legs).inject(0, :+)
end

Now, a slippery slope maintenance of legacy code often end up like this:

class Animal
  attr_reader :legs, :type
end

class Dog < Animal
  def type
    "four_leg_one_tail"
  end

  def legs
    4
  end
end

class Duck < Animal
  def type
    "two_leg_fake_tail"
  end

  def legs
    2
  end
end

class Fish < Animal
  def type
    "no_legs_flat_tail"
  end

  def legs
    raise "lol what kind of fish has legs?"
  end
end

While the consumption looks like this:

def socks_to_order(animals)
  socks = 0
  animals.each do |animal|
    case animal.type
    when /(four|two)_leg/
      socks += animal.legs
    default
      socks
  end
end

Why is the latter bad?

The problem with the latter is that any code interacting with Animal class still needs to know the structure of the subclasses when really they should not care. This implementation leaks inheritance logic outside of the subclass tree, making additions and modifications of new subclasses extremely costly.

For example, imagine a subclass Ladybug as a subclass of Animal -- the first implementation of Animal only requires addition of:

class Ladybug < Animal
  def legs
    6
  end
end

and socks_to_order method continues to work.

On the other hand, the equivalent change for second implementation would look like this:

class Ladybug < Animal
  def type
    "six_legs_no_tail"
  end

  def legs
    6
  end
end

AND socks_to_order method needs more fix, which isn't elegant either:

 def socks_to_order(animals)
   socks = 0
   animals.each do |animal|
     case animal.type
-    when /(four|two)_leg/
+    when /(six|four|two)_leg/
       socks += animal.legs
     default
       socks
   end
 end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment