If it walks like a duck and it quacks like a duck, then it must be a duck
We saw in a previous article that it's much better to depend on abstractions than on concrete classes. Concretions are more likely to change than abstractions, and those changes will ripple through our codebase affecting all the classes that depend on them.
We also learned that statically typed languages offer us a great construct for representing an abstraction: the interface. Most languages also provide abstract classes, that are like regular classes you can't instantiate.
How can we represent abstractions in a dynamically typed language like Ruby? The answer is Duck Typing.
Duck types are abstractions embodied by a public interface implemented across classes. If this sounds a bit cryptic, it's because the easiest way to understand duck types is through an example. Imagine we have a Rocket class, with a method called upkeep, which receives a collection of maintenance professionals to perform maintenance tasks on different parts of the rocket.
class Rocket
attr_reader :engines, :computers, :fuel_tank
# ...
def upkeep(maintainers)
maintainers.each do |maintainer|
case maintainer
when PropulsionEngineer
maintainer.verify_engines(engines)
when ComputerTechnician
maintainer.perform_computer_maintenance(computers)
when PropellantCrew
maintainer.fix_fuel_tank(fuel_tank)
end
end
end
# ...
end
class PropulsionEngineer
def verify_engines(engines)
engines.each {|engine| check_engine(engine)}
end
def check_engine(engine)
#...
end
end
class ComputerTechnician
def perform_computer_maintenance(computers)
computers.each do |computer|
verify_circuitry(computer)
update_software(computer)
end
end
def verify_circuitry(computer)
#...
end
def update_software(computer)
#...
end
end
class PropellantCrew
def fix_fuel_tank(fuel_tank)
#...
end
end
The code above works, but the solution is far from ideal. As it stands right now, Rocket depends on PropulsionEngineer, ComputerTechnician, and PropellantCrew. Any change in the signature of the methods used in upkeep will force Rocket to change. Also, the inclusion of additional maintenance steps requires us to open the Rocket class and add extra code.
It's important to realize that although the other object's classes are important, all Rocket cares about is maintenance. What these classes are lacking is a common interface Rocket can count on: a maintainer duck type.
We need to stop thinking in terms of the class an object belongs to and focus more on its behavior: more important than the type of the object are the messages it can respond to.
A Maintainer duck type
Now that we discovered we need a duck, we need to know which messages it needs to respond to. In our case, Rocket only cares that other classes perform maintenance work, so it makes sense they respond to the perform_maintenance message. We refactor and get the following code:
class Rocket
attr_reader :engines, :computers, :fuel_tank
# ...
def upkeep(maintainers)
maintainers.each do |maintainer|
maintainer.perform_maintenance(self)
end
end
# ...
end
class PropulsionEngineer
def perform_maintenance(rocket)
rocket.engines.each{|engine| check_engine(engine)}
end
#...
end
class ComputerTechnician
def perform_maintenance(rocket)
rocket.computers.each do |computer|
verify_circuitry(computer)
update_software(computer)
end
end
#...
end
class PropellantCrew
def perform_maintenance(rocket)
fix_fuel_tank(rocket.fuel_tank)
end
#...
end
By creating an abstraction for the maintainer behavior, the new upkeep method is much simpler. More important is the fact that it doesn't depend on concrete classes anymore, as long as an object implements perform_maintenance rocket will gladly work with it. It's the Dependency Inversion Principle applied on a dynamically typed language.
Finding hidden ducks in your code
There might be ducks hiding in your code: public interfaces that need to be defined, but you are not aware of yet. The most common code smell that points to the presence of a hidden duck is using a language's built-in mechanism for identifying the type of an object to determine which method should be called.
In the example we just saw, the case statement checked if the method was aPropulsionEnginner and called the right method. Case statements on the type of an object are not the only scenario you should watch out for, other common alarms for hidden ducks are:
In Ruby:
- kind_of?
- is_a?
- responds_to?
In Python:
- type/is
- isinstance
- hasattr
- callable
While these things don't always mean there is something wrong in your code, it's important to ask yourself if objects with different types perform the same action with a different name and specifics. If that is the case, you might want to define a common interface for calling that action.
The importance of documentation
Dynamically typed languages, unlike their statically typed counterparts, don't usually offer you constructs like interfaces or abstract classes for representing your abstractions. Duck types are another type of abstraction: they don't live in a file you can open and read, they are a public interface implemented across different classes.
While this approach has advantages, you can't count on the compiler for finding type errors. It's your responsibility to write docs to back these types and to write tests that serve as additional documentation. It will make it much easier for you and for other people working on the same codebase.
What to do next:
- Share this article with friends and colleagues. Thank you for helping me reach people who might find this information useful.
- You can also find an excellent explanation of this topic in chapter 5 of Practical OO Design. This and other very helpful books are in the recommended reading list.
- Send me an email with questions, comments or suggestions (it's in the About Me page). Come on, don't be shy!