Sitting Ducks
A common refactoring in object-oriented programming is to take a complex method or set of methods and extract a new class from them. These classes — often referred to as “service objects” or “operations” — allow you to break a complex operation into small well-named components as private methods. And because classes can hold state in instance variables, you no longer need to pass state around through the use of method arguments.
These classes are usually named NounVerber, e.g. record.clean becomes RecordCleaner.new(record).perform.
Since the instance method, perform, here doesn’t take an argument, it’s common practice to take a shortcut by defining a class method by the same name that delegates its arguments to the initialiser and then calls perform on the new instance.
class RecordCleaner
def self.perform(...)
new(...).perform
end
def initialize(record)
@record = record
end
def perform
# do the thing
end
end
Now RecordCleaner.perform(record) is the same as RecordCleaner.new(record).perform.
You’ll see this pattern everywhere: they’re Jobs, Operations, Services, Queries or Filters; they’re performable, runnable, executable, callable, dispatchable, enactable or enforceable. And besides these abstract verbs, there are infinite permutations of this pattern using specific, concrete verbs, like RecordCleaner.clean(record).
At the end of the day, they’re all the same: a class that does one thing. But we miss some powerful polymorphism by making them not quite the same. What we have here are sitting ducks: infinite permutations of almost-duck-types just begging to be ducks.
If it walks like a duck and it quacks like a duck, then it must be a duck.
Duck typing is recognising that an object is of a given type (e.g. a Duck) because it has all the methods and properties of that type (e.g. it walks and quacks).
All these objects have one public method that does-the-thing. So we should be able to pass these objects to any method that accepts an argument for a-thing-to-be-done. Ruby has a duck type for this: callable.
Callable objects are objects that respond to call. Ruby has several built-in callable objects like Methods and Procs.
Here’s my point: if we just consistently use the method call on all these sitting ducks, they’ll all fit the callable duck type and we can go on using them interchangeably wherever a callable is expected. There’s nothing to stop us from keeping the cute custom verbs as alias methods too.
One more thing…
…and this is where it gets a bit confusing. Ruby has another duck type which I’ll call to-procable. To-procable objects must respond to the method to_proc, returning a Proc object (which is callable). All Ruby’s built-in callable objects, as far as I can tell, are also to-procable. And the nice thing about to-procable objects is they can be coerced into blocks with the ampersand prefix operator.
When calling a method that takes a Block, such as Enumerable#map, you can put an &
before any to-procable object and it’ll be coerced into a Block. Symbols, for example, respond to to_proc, returning their namesake method, which is why you can coerce a Symbol into a Block like this:
["foo", "bar"].map(&:upcase) # returns ["FOO", "BAR"]
Methods are also to-procable, meaning the call methods on callable objects can be coerced into Blocks. This means callables, too, are sitting ducks. All callables should be to-procable or at least block-coercible.
Ruby should either:
fall back to coercing blocks from method(:call).to_proc when to_proc is not defined on an object to be coerced; or
provide a default implementation for to_proc on Objects. The only catch with this is you wouldn’t want every object claiming to respond to to_proc if it doesn’t even respond to call.
There’s a discussion about this on the Ruby issue tracking system. For now, you can start making almost-callable objects actually-callable in the hope that one day they’ll be block-coercible too.
You could also define an explicit Callable module to be extended or included into your callable objects, providing a definition of to_proc that delegates to the call method.
module Callable
def call
raise NoMethodError
end
def to_proc
method(:call).to_proc
end
end
Alternatively, you could monkey patch Object to provide a default implementation for to_proc when call is defined.
class Object
def method_missing(name, ...)
if name == :to_proc && respond_to?(:call)
send def self.to_proc
method(:call).to_proc
end
else
super
end
end
def respond_to_missing?(name, ...)
(name == :to_proc && respond_to?(:call, ...)) || super
end
end
I hope that soon, Ruby will be able to coerce any callable to a Block automatically.