Meet Duck Typer: your new duck typing friend

The Ruby language leans on duck typing rather than formal interfaces. “If it walks like a duck and quacks like a duck, it’s a duck.”

bodies = [poem, essay, case_study, user_manual].map(&:body)

These objects come from different classes but all respond to body, so you can treat them uniformly. That’s informal polymorphism: no type annotations, no ceremony.

Duck typing is great, but what I miss is enforcement. If one class implements a method and another forgets to, I want something to complain.

The interface is a living document

In Ruby, interfaces are implicit. They live in the public methods of your classes. Private methods are implementation details and not part of the interface.

class MessagePresenter
  def title; end
  def url(format:); end
end

class CommentPresenter
  def title; end
  def url(format:); end
end

Just by looking at these two classes, we can see an implicit interface: title and url(format:).

If CommentPresenter also had a body method, title and url(format:) would still form a partial interface, one we might call Linkable.

Now the question is: how do we enforce that these classes adhere to this implicit interface?

Enforcing an interface

How do we enforce that future changes don’t break this contract? Tests can help:

test "presenter interface" do
  message = Message.new(title: "Dummy", path: "/dummy")
  comment = Comment.new(title: "Dummy", path: "/dummy")

  [message, comment].each do |object|
    assert_respond_to object, :title
    assert_respond_to object, :url
  end
end

It works, but it feels weird to instantiate objects just to ask what methods they have. We can skip instances and check the class directly:

def assert_instance_responds_to(klass, method, arity: 0)
  # asserts method is public and arity matches
end

test "presenter interface" do
  [MessagePresenter, CommentPresenter].each do |klass|
    assert_instance_responds_to klass, :title, arity: 0
    assert_instance_responds_to klass, :url, arity: 1
  end
end

Nicer, and arity gives us more confidence than just checking the method name. Still, arity alone misses keyword names, optional vs. required params, and won’t catch added or removed methods automatically.

The duck_typer gem

To solve this optimally (keeping the spirit of duck typing while still enforcing interfaces), I wrote a tiny gem called duck_typer. It’s a minimal set of helpers extracted from patterns I’ve used to test duck-typed interfaces across several projects. You don’t need to explicitly define an interface. What matters is comparing the objects’ public interfaces. If they match, the interface is implemented correctly; if there are differences, the tooling surfaces them.

After installing, you can assert that a list of classes share compatible interfaces:

test "presenter interface" do
  assert_interfaces_match [MessagePresenter, CommentPresenter]
end

You can add as many classes as you want. The failure messages are very explicit. If you add a body method to only one presenter:

Expected MessagePresenter and CommentPresenter to implement compatible
interfaces, but the following method signatures differ:

MessagePresenter: body()
CommentPresenter: body not defined

It also catches signature drift. Remove a keyword argument in one class:

MessagePresenter: url()
CommentPresenter: url(format:)

Add a positional argument:

MessagePresenter: title(truncate)
CommentPresenter: title()

Yep, everything you’d expect to break does break.

To check a partial interface, pass methods:. You can also name it, which shows up in the failure message:

assert_interfaces_match [MessagePresenter, CommentPresenter],
  name: "Linkable",
  methods: %i[title url]
Expected MessagePresenter and CommentPresenter to implement compatible
"Linkable" interfaces, but the following method signatures differ:

And if you want to check class methods instead of instance methods, pass type: :class_methods:

assert_interfaces_match [MessagePresenter, CommentPresenter],
  type: :class_methods

If you’re on RSpec, the same options are available via have_matching_interfaces or it_behaves_like "an interface", [...].

The README has full setup instructions and all available options.

Bonus feature

Suppose you create a new class that implements the interface. You still need to remember to add it to the list of classes under test.

If your classes share a namespace, you can pass it directly and duck_typer will find them for you:

assert_interfaces_match namespace: Presenters::Linkable

If your codebase already gathers related classes in one place, you can pass that directly instead:

assert_interfaces_match Presenter::LINKABLE_CLASSES, name: "Linkable"

Either way, if someone adds a PostPresenter that forgets to implement url(format:), you’ll get a failure without needing to update the test list.

The same options work in RSpec via have_matching_interfaces or it_behaves_like "an interface".

Limitations

There’s one fundamental limitation: you can’t check types of method arguments. This is Ruby; that’s the tradeoff. And honestly, types aren’t really part of a Ruby interface anyway.

Conclusion

By treating public methods as an implicit interface and enforcing that interface through tests, we get the best of both worlds: the freedom of Ruby with the safety net of interface checks. That’s what duck_typer aims to provide: a tiny tool that keeps your classes honest without annotations or explicit interface definitions.

If you’ve ever caught yourself writing ad-hoc tests to enforce interfaces, as was my case, duck_typer might be for you.

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.