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 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 (
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#
In this sense, these three methods (
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:
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.