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.