Avoid the Kingdom of Nouns with Procs
January 30, 2012 📬 Get My Weekly Newsletter ☞
Hopefully you’ve read Steve Yegge’s excellent kingdom of nouns essay, in which he bemoans a pattern that exists in a lot of Java-base systems. The tell-tale sign is a class named ThingDoer
with a single method doThing()
. Systems like this don’t arise simply because Java is the way it is, but when you follow SOLID principles (particularly the single responsibility and dependency inversion principles), your code ends up with lots of small classes that do one thing only.
In Java, you are basically stuck with this, but in Ruby (or any OO language that supports closures/blocks/functions), we can fight this by using Procs instead of classes.
SOLIDifying some code
First, let’s take some code that needs refactoring and see what it looks like with classes. We’ll look at a very simple base class for handling events in a system based on Resque. Our base class allows us to do two things that a generic Resque event can’t: retry events later, and queue arbitrary events. Let’s have a look at the code1:
class Event::Base
def self.perform(params)
self.new.perform(params)
end
def perform(params)
raise "subclass must implement"
end
protected
def self.queue_event(klass,options)
Resque.enque(klass,options.merge({ :queued_at => Time.now }))
end
def self.requeue_later(params)
new_params = { :attempt_number => 0 }.merge(params)
new_params[:attempt_number] += 1
raise "Requeued too many times" if new_params[:attempt_number] > 5
sleep(new_params[:attempt_number])
queue_event(class,new_params)
end
end
We might use this like so:
class RenameEvent < Event::base
def perform(params)
if person = Person.find_by_id(params[:person_id]).nil?
requeue_later(params)
else
person.name = params[:name]
person.save!
end
end
end
Our base class is doing too much. It’s OK for it to provide the queuing and re-queuing functionality, but it shouldn’t be implemented there. Further, there’s aspects of how the functionality is implemented that we might want to be able to change in our subclasses. This is the perfect application for dependency inversion.
In our naive approach, we’ll make one class for each function we have, namely:
- A class to queue events onto Resque, adding in the
queued_at
timestamp - A class to orchestrate requeuing events, failing after a certain number of attempts
- A class to sleep and perform the actual requeuing
- Our base class to provide access to these features
Let’s have a look:
class Queuer
def queue(klass,options)
Resque.enque(klass,options.merge({ :queued_at => Time.now }))
end
end
class Requeuer
def initialize(requeue_strategy,max_attempts=5)
@requeue_strategy = requeue_strategy
@max_attempts = max_attempts
end
def requeue(klass,options)
new_params = { :attempt_number => 0 }.merge(params)
new_params[:attempt_number] += 1
raise "Requeued too many times" if new_params[:attempt_number] > @max_attempts
@requeue_strategy.requeue(klass,new_params[:attempt_number],options)
end
end
class RequeueStrategy
def initialize(queuer)
@queuer = queuer
end
def requeue(klass,attempt_number,options)
sleep(attempt_number)
@queuer.queue(klass,options)
end
end
Whew! Now, to use all this, our base class becomes:
class Event::Base
def initialize(requeuer=nil)
@requeuer = requeuer || Requeuer.new(RequeueStrategy.new(Queuer.new))
end
def self.perform(params)
self.new.perform(params)
end
def perform(params)
raise "subclass must implement"
end
protected
def requeue_later(params)
@requeuer.requeue(self.class,params)
end
end
Our base class is a lot cleaner, and we can now test it more easily without mocks making things difficult.
But, we’re firmly in the Kingdom of Nouns, e.g. queuer.queue()
. We’d like to keep our code nicely designed, but get rid of the superfluous naming and structure around the tiny bits of code we have. Let’s use Procs.
Procs instead of classes
The easiest class to convert to a Proc
is going to be Queuer
, since it has no real dependencies and is just a wrapper around a very simple line of code:
class Event::Base
QueueEvent = lambda { |klass,params|
Resque.enque(klass,options.merge({ :queued_at => Time.now }))
}
def initialize(requeuer=nil)
@requeuer = requeuer || Requeuer.new(RequeueStrategy.new(QueueEvent))
end
def self.perform(params)
self.new.perform(params)
end
def perform(params)
raise "subclass must implement"
end
protected
def requeue_later(params)
@requeuer.requeue(self.class,params)
end
end
RequeueStrategy
now becomes:
class RequeueStrategy
def initialize(queue_event)
@queue_event = queue_event
end
def requeue(klass,attempt_number,options)
sleep(attempt_number)
@queue_event.call(klass,options)
end
end
Notice that we’re using the name queue_event
instead of queuer
. A Proc isn’t, conceptually, a thing, but an action that we’re passing around, so we name it as such.
Of course, RequeueStrategy
itself isn’t much code; can we convert that? The tricky part is that RequeueStrategy
requires the ability to queue events and thus needs a Queuer
. We pass this in the constructor, which a Proc
doesn’t really have (at least conceptually). Instead, we’ll pass the queueing code in as a parameter to our newly re-made SleepThenRequeue
Proc
, which is now part of our base class.
class Event::Base
SleepThenRequeue = lambda { |queue_event,klass,attempt_num,options|
sleep(attempt_number)
queue_event.call(klass,options)
}
QueueEvent = lambda { |klass,params|
Resque.enque(klass,options.merge({ :queued_at => Time.now }))
}
def initialize(requeuer=nil)
@requeuer = requeuer || Requeuer.new(QueueEvent,SleepThenRequeue)
end
def self.perform(params)
self.new.perform(params)
end
def perform(params)
raise "subclass must implement"
end
protected
def requeue_later(params)
@requeuer.requeue(self.class,params)
end
end
We now need to update Requeuer
to hold onto the QueueEvent
Proc
so that it can pass it to the SleepThenRequeue
Proc
:
class Requeuer
def initialize(queue_event,requeue_event,max_attempts=5)
@queue_event = queue_event
@requeue_event = requeue_event
@max_attempts = max_attempts
end
def requeue(klass,options)
new_params = { :attempt_number => 0 }.merge(params)
new_params[:attempt_number] += 1
raise "Requeued too many times" if new_params[:attempt_number] > @max_attempts
@requeue_event.call(@queue_event,klass,new_params[:attempt_number],options)
end
end
Now, our system has all the flexbility, testability, and comprehensibility that we get from applying SOLID principles, but we don’t have any of the baggage and boilerplate of making actual classes that are mere wrappers for simple functionality.
Taking Advantage
Let’s see how this works be implementing a second requeuing strategy. Suppose a subclass wants to have retried events go onto a different queue, instead of sleeping and re-queuing. To enable this, we first make our base class a bit more configurable by introducing the method self.requeue_strategy
, which returns a Proc
. The base class’ implementation will simply return SleepThenRequeue
.
class Event::Base
QueueEvent = lambda { |klass,params|
Resque.enque(klass,options.merge({ :queued_at => Time.now }))
}
SleepThenRequeue = lambda { |queue_event,klass,attempt_num,options|
sleep(attempt_number)
queue_event.call(klass,options)
}
def initialize(requeuer=nil)
@requeuer = requeuer || Requeuer.new(QueueEvent,self.class.requeue_strategy)
end
def self.perform(params)
self.new.perform(params)
end
def perform(params)
raise "subclass must implement"
end
protected
def self.requeue_strategy
SleepThenRequeue
end
def requeue_later(params)
@requeuer.requeue(self.class,params)
end
end
Now, our subclass can return something else, but it won’t have to make an entire class to do so:
class SomeEvent < Event::Base
protected
def self.requeue_strategy
lambda { |queue_event,klass,attempt_num,options|
queue_event.call(klass,options.merge(:queue => 'scheduled',
:for => Time.now + attempt_num.minutes)
}
end
Of course, we’re not constrained by Procs; after all a Proc
is just a structural type for an object that reponds to call
. If
we needed some really complex requeuing, we could make a class:
class ComplexRequeueingStrategy
def call(queue_event,klass,attempt_num,options)
# Do whatever
end
end
This results in a much more flexible system that keeps ceremony, boilerplate, and noise to a minimum; the majority of our code is the “business logic” or “necessary complexity”.
Conclusions
Of course, we can take this too far. Suppose we made Requeuer
into a Proc
. It would start getting cumbersome, since it has so many dependent objects to manage; a class is actually helpful here2.
Just because Ruby is object-oriented doesn’t mean that every bit of functionality has to live inside a method of a class. A Proc
is tailor-made to hold functionality and pass it around, so don’t be afraid to use it if the situation warrants.