One interface, multiple implementations is one of these design concepts I like most. It’s basically polymorphism in its pure form!
Coding a little bit a while ago, I realized how DSLs could reinforce this idea. My example here is a simple implementation for queue consumers, as a simple Ruby internal DSL:
class GitRepositoryCloner < Consumer queue "RepositoriesToBeCloned" exclusive true handle do |message| # git clone repository! # I'm pretty sure github has something alike end end
And to enqueue a message, we could do:
GitRepositoryCloner.publish("rails")
GitRepositoryCloner
is a simple consumer of the queue named RepositoriesToBeCloned
. One of the times I did something similar to this, I needed support for exclusive consumers. Then, my choices were ActiveMQ as the messaging middleware together with the Stomp protocol.
Using them as an example, let’s take a look on a possible implementation for the Consumer
class, using the stomp gem:
module ActiveMQ class Consumer def self.queue(name) @queue_name = name end def self.exclusive(bool) @exclusive = bool end def self.handle(&blk) @callback = blk end def self.listen broker = Stomp::Client.new(Config[:broker]) broker.subscribe(@queue_name, :'activemq.exclusive' => @exclusive) do |message| @callback.call(message) broker.acknowledge(message) end end def self.publish(message) broker = Stomp::Client.new(Config[:broker]) broker.publish(@queue_name, message, :persistent => true) end end end Consumer = ActiveMQ::Consumer
The last line is where we choose the ActiveMQ::Consumer as the default implementation.
The beautiful aspect of this little internal DSL, composed only of three methods (queue
, exclusive
and handle
), is that it defines an interface. Here, I have seen a common misconception from many developers coming from Java, C# and similar languages which have the interface
construct. An interface in Object Orientation is composed of all accessible methods of an object. In other words, the interface is the object’s face to the rest of the world. We are not necessarily talking about a Java or C# interface
construct.
In this sense, these three methods (queue
, exclusive
and handle
) are the interface of the Consumer
internal DSL (or class object, as you wish).
Let’s say for some reason, we would like to switch our messaging infrastructure to something else, like Resque, which Github uses and is awesome. Resque’s documentation says that things are a little bit different for Resque consumers. They must define a @queue
class attribute and must have a perform
class method.
As we would do with regular Java/C# interfaces, let’s make another implementation respecting the previous contract:
module Resque class Consumer def self.queue(name) @queue = name end def self.exclusive(bool) self.extend(Resque::Plugins::LockTimeout) if bool end def self.handle(&blk) self.send(:define_method, :perform, &@blk) end def self.listen raise "Not ready yet" unless self.respond_to?(:perform) end def self.publish(message) Resque.enqueue(self, message) end end end
There you can see how the implementations differ. The exclusive consumer feature is provided by the LockTimeout plugin. In this case, instead of passing the activemq.exclusive
parameter to the connection, we must use the Resque::Plugins::LockTimeout
module, as the documentation says. Another key difference is in the message handling process. Instead of passing a handler block to the subscribe
method, Resque consumers are required to define a perform
method, which we are dynamically creating with some metaprogramming: Class#define_method(name)
.
Finally, here is how we switch our messaging backend to Resque, without any changes to the consumer classes (GitRepositoryCloner
in this example):
Consumer = Resque::Consumer
That’s it: one interface, two (multiple) implementations.
I think I saw something like that somewhere. 😛
😛