New bamboo web development

Bamboo blog. Our thoughts on web technology.


Micro-patterns in Ruby

almost 4 years agoby Ismael

Yup. Micro-patterns. I've always wanted to begin a blog post with some highly sophisticated buzzword so it might as well be this one (sadly, I can't say I'm the first to use it).

This is a tiny one. Let's say we have a Factory that gives me Adapters#1 to a series of hosted media services (Flickr, YouTube, Vimeo, PhotoBucket, etc.).

The user of my code provides a service prefix and a URL to a picture or photo page in a given service, and the factory returns an instance from which the user can fetch different media sizes through a standardized API.

 1 flickr_pic = ServiceParser.instance( 
 2   'flickr', 'http://www.flickr.com/photos/new_bamboo_london/2158168775/'
 3 )
 4 
 5 puts flickr_pic.thumbnail  
 6 # => http://farm3.static.flickr.com/2147/2158168775_01ae89cbfb_s.jpg
 7 
 8 yt_video = ServiceParser.instance( 
 9   'youtube', 'http://www.youtube.com/watch?v=PCunD-_mOwQ' 
10 )
11 
12 puts yt_video.thumbnail 
13 # => http://i.ytimg.com/vi/PCunD-_mOwQ/default.jpg

Simple enough. But it seems a bit redundant to have the user declare both the service name and the page URL. Since URL's are unique anyway, those should be enough for our smart factory to know what adapter to hand us, kindly.

We could have a set of regular expressions in the factory, one for each URL/service.

 1 class ServiceParser
 2   SERVICES = {
 3     FlickrAdapter   => /flickr\.com/,
 4     YouTubeAdapter  => /youtube\.com/,
 5     VimeoAdapter  => /vimeo\.com/,
 6     PhotoBucketAdapter  => /photobucket\.com/
 7   }
 8   # Factory method
 9   #
10   def self.instance( url )
11     subclass = SERVICES.find(nil) {|klass, exp| url =~ exp}
12     raise 'not implemented' if subclass.nil?
13     subclass.new url
14   end
15 end

We could. But if we did, all hell would break loose and a mob of angry Design Patterns advocates would ram our door and... reprimand us.

While they're at it, they would tell us that one of the many golden rules of Object Oriented software design is, blockquote please:

The superclass should have no knowledge of the subclasses.

But you already know that. We need to take those URL's out of the factory class and into where they belong, in each adapter class. Also, adapters should be able to register their URL's with the factory, so it knows where to look. Ruby to the rescue.

 1 # = The superclass / factory
 2 class ServiceParser
 3   # We register adapters into this class variable
 4   #
 5   @@adapters = []
 6   
 7   # class reader
 8   #
 9   def self.adapters
10     @@adapters
11   end
12 
13   # Factory method here, se below...
14   #
15 end
16 
17 # = An example subclass / adapter
18 class FlickrAdapter < ServiceParser
19   def initialize( url)
20   
21   end
22 
23   # API methods
24   #
25   def thumbnail
26     # do something clever here...
27   end
28   
29   # register ourselves with the Factory
30   #
31   ServiceParser.adapters << [self, /flickr\.com/]
32 end

The fun thing about Ruby, class definitions being executable code and all, is that you can just have subclasses add themselves (and their corresponding regex.) to the factory's list of adapters in load time.

1 ServiceParser.adapters << [self, /flickr\.com/]

Then it is a matter of modifying the factory method to search for matching URL's in the dynamically populated list of subclasses / URL expressions.

1 # Refactored factory. No pun intended.
2 #
3 def self.instance( url )
4   match = @@adapters.find(nil) {|klass, exp| url =~ exp}
5   raise 'not implemented' if match.nil?
6   match.first.new url
7 end

Finally, a little more elegance to make our adapter authors happy (and less prone to mess with the internals of our superclass!)

 1 class FlickrAdapter < ServiceParser
 2   # Initializer, API and protected methods here...
 3   #
 4   # We register our URL with a neat class method on the superclass
 5   #
 6   register_url /flickr\.com/
 7 end
 8 
 9 class ServiceParser
10   # Factory, abstract methods and cross-adapter utilities here...
11   #
12   # Class method for subclasses to register themselves.
13   #
14   @@adapters = [] #2 See note on class variables
15 
16   def self.register_url( exp )
17     @@adapters << [self, exp]
18   end
19 
20 end

#1 Two more buzzwords and just the second paragraph! And there's more...

#2 Class @@variables. These variables are not inherited but shared by all classes in the inheritance tree. That means that, whenever a subclass adds to the superclass' version of @@adapters, it'll actually have the new value in all subclasses. This is not an issue in our simple example, though.