Type-Checking Interface in Ruby
One of my favorite parts in Sandi Metz’s excellent Practical Object Oriented Design in Ruby is when she describes how to enforce lightweight interfaces so that multiple objects can play a role reliably. And I thought: how I can I enforce this in a much-more heavy-handed and annoying way?
This question also ties into two ridiculous tweets of mine:
{.aligncenter .size-full .wp-image-2366 .content width=”592” height=”145”}
{.aligncenter .size-full .wp-image-2367 .content width=”592” height=”123”}
Metz’s approach is to enforce each role (aka duck type) with a default implementation in a superclass (ch 6, p127-129). The superclass has a method raising NotImplementError
for each method required by the interface. I’ve been leaning away from inheritance lately, so I’m going to work through this with a shared module. Here’s an example:
module Notifier def notify message raise NotImplementedError end
def acknowledged? raise NotImplemented Error end end
class TextMessageSender include Notifier
def notify message SMSGateway.send(...) end
# ... end
class EmailSender include Notifier
def notify message Email.new(...).deliver! end
# ... end
class ShoppingCart def checkout notifier # billing code, etc. notifier.notify “Thanks for buying our stuff...” end end
The code that sends a message can have either a TextMessageSender
or EmailSender
injected without having to care which it’s using. If another kind of Notifier
comes along and fails to implement the interface, it will raise clear error messages.
This pattern is a sweet spot between an unspecified anything-goes approach and compiler-enforced interfaces.
As a coding exercise, I tried to push this design towards strictness. If you squint hard enough, this is a run-time type check.
But nothing in this pattern requires that the object playing a rule includes the module (or inherits from the superclass, in that style). After some tinkering I came up with a method to enforce
the type check:
class Class def enforce requirements caller_binding = binding.of_caller(1) params = method(caller_binding.eval(‘method’).parameters params.each do |type, name| next unless type == :req # formating for width of blog here... if requirements.has_key? name unless caller_binding.local_variable_get(name).is_a? Kernel::const_get(requirements[name]) raise ArgumentError, “#{name} didn’t include #{requirements[name]}” end end end end end
# for example: class ShoppingCart def checkout notifier enforce notifier: Notifier # billing code, etc. notifier.notify “Thanks for buying our stuff...” end end
The enforce
method looks up the binding of the method it was
called from (using the delightfully hack and not-at-all production-safe
binding_of_caller gem, the magic behind the insanely useful
better_errors Rails debugging tool). From there it users method to look up the
method object to check if each of the method’s required
parameters includes the appropriate module.
So enforce
is a runtime assertion that the object playing a role
includes the module that enforces the interface.
Then I decided to push a little farther, and model interfaces explicitly:
require ‘set’
# here’s the Interface model:
class Interface attr_reader :messages def initialize messages @messages = Set.new(messages) end
def knows? message @messages.include(message) end end
# and a handy shortcut for defining one:
class Kernel def interface name, messages iface = Interface.new messages Kernel::const_set name, iface end end
# so somewhere in the code an interface can be cleanly defined with: interface :Notifier, [:notify, :acknowledged?]
# and then we’ll set up objects to be checked for interfaces: class Object def provides iface @_interfaces ||= [] @_interfaces << iface unless defined? :provides? define_method :provides? do |iface| @_interfaces.include? iface
# Yes, this is wasteful to check on every method call using ‘enforce’, # but I didn’t want to put the ‘provides’ after the class definition, # and I suppose it’s required if your object does something really # dynamic and tricky. If this were real, I might remove this check from # here and add a separate ‘dynamically_provides’ class method with this check. unresponded = iface.messages.select { |m| !arg.respond_to?(m) } raise ArgumentError, “Arg #{name} claims to provide #{iface} but doesn’t respond_to? #{unresponded.join(‘, ‘)}” unless unresponded.empty? end end end end
# this is used as: class TextMessageSender provides Notifier def notify ; ... ; end def acknowledged? ; ... ; end end
# and enforce changes its check: class Class def enforce requirements caller_binding = binding.of_caller(1) params = method(caller_binding.eval(‘method’).parameters params.each do |type, name| next unless type == :req if requirements.has_key? name raise ArgumentError, “Arg #{name} does not provide #{iface}” unless arg.respond_to?(:provides?) and arg.provides? iface end end end end
# so, finally, the interface is enforced the same way: class ShoppingCart # ... def checkout notifier enforce notifier: Notifier # billing code, etc. notifier.notify “Thanks for buying our stuff...” end end
It’s certainly not static, compile-time type-checking of arguments, but it’s more thorough than voluntary inclusion of a module. When I started I seriously doubted it would be possible to implement, but Ruby’s metaprogramming (extended by binding_of_caller) is powerful.
And if it wasn’t clear, this is not production-ready code and should never ever be used on a real project. I only wrote this because I wanted to experiment with introspection in Ruby while thinking about interfaces.