Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby design pattern: How to make an extensible factory class?

Ok, suppose I have Ruby program to read version control log files and do something with the data. (I don't, but the situation is analogous, and I have fun with these analogies). Let's suppose right now I want to support Bazaar and Git. Let's suppose the program will be executed with some kind of argument indicating which version control software is being used.

Given this, I want to make a LogFileReaderFactory which given the name of a version control program will return an appropriate log file reader (subclassed from a generic) to read the log file and spit out a canonical internal representation. So, of course, I can make BazaarLogFileReader and GitLogFileReader and hard-code them into the program, but I want it to be set up in such a way that adding support for a new version control program is as simple as plopping a new class file in the directory with the Bazaar and Git readers.

So, right now you can call "do-something-with-the-log --software git" and "do-something-with-the-log --software bazaar" because there are log readers for those. What I want is for it to be possible to simply add a SVNLogFileReader class and file to the same directory and automatically be able to call "do-something-with-the-log --software svn" without ANY changes to the rest of the program. (The files can of course be named with a specific pattern and globbed in the require call.)

I know this can be done in Ruby... I just don't how I should do it... or if I should do it at all.

like image 799
Instance Hunter Avatar asked Apr 14 '09 03:04

Instance Hunter


1 Answers

You don't need a LogFileReaderFactory; just teach your LogFileReader class how to instantiate its subclasses:

class LogFileReader   def self.create type     case type      when :git       GitLogFileReader.new     when :bzr       BzrLogFileReader.new     else       raise "Bad log file type: #{type}"     end   end end  class GitLogFileReader < LogFileReader   def display     puts "I'm a git log file reader!"   end end  class BzrLogFileReader < LogFileReader   def display     puts "A bzr log file reader..."   end end 

As you can see, the superclass can act as its own factory. Now, how about automatic registration? Well, why don't we just keep a hash of our registered subclasses, and register each one when we define them:

class LogFileReader   @@subclasses = { }   def self.create type     c = @@subclasses[type]     if c       c.new     else       raise "Bad log file type: #{type}"     end   end   def self.register_reader name     @@subclasses[name] = self   end end  class GitLogFileReader < LogFileReader   def display     puts "I'm a git log file reader!"   end   register_reader :git end  class BzrLogFileReader < LogFileReader   def display     puts "A bzr log file reader..."   end   register_reader :bzr end  LogFileReader.create(:git).display LogFileReader.create(:bzr).display  class SvnLogFileReader < LogFileReader   def display     puts "Subersion reader, at your service."   end   register_reader :svn end  LogFileReader.create(:svn).display 

And there you have it. Just split that up into a few files, and require them appropriately.

You should read Peter Norvig's Design Patterns in Dynamic Languages if you're interested in this sort of thing. He demonstrates how many design patterns are actually working around restrictions or inadequacies in your programming language; and with a sufficiently powerful and flexible language, you don't really need a design pattern, you just implement what you want to do. He uses Dylan and Common Lisp for examples, but many of his points are relevant to Ruby as well.

You might also want to take a look at Why's Poignant Guide to Ruby, particularly chapters 5 and 6, though only if you can deal with surrealist technical writing.

edit: Riffing of off Jörg's answer now; I do like reducing repetition, and so not repeating the name of the version control system in both the class and the registration. Adding the following to my second example will allow you to write much simpler class definitions while still being pretty simple and easy to understand.

def log_file_reader name, superclass=LogFileReader, &block   Class.new(superclass, &block).register_reader(name) end  log_file_reader :git do   def display     puts "I'm a git log file reader!"   end end  log_file_reader :bzr do   def display     puts "A bzr log file reader..."   end end 

Of course, in production code, you may want to actually name those classes, by generating a constant definition based on the name passed in, for better error messages.

def log_file_reader name, superclass=LogFileReader, &block   c = Class.new(superclass, &block)   c.register_reader(name)   Object.const_set("#{name.to_s.capitalize}LogFileReader", c) end 
like image 137
Brian Campbell Avatar answered Sep 30 '22 01:09

Brian Campbell