BrainsToBytes

Duck typing and the importance of good public interfaces

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!
Author image
Budapest, Hungary
Hey there, I'm Juan. A programmer currently living in Budapest. I believe in well-engineered solutions, clean code and sharing knowledge. Thanks for reading, I hope you find my articles useful!