Ruby template design: how to make an extensible factory class?

Well, suppose I have a Ruby program for reading version control log files and doing something with data. (I do not know, but the situation is similar, and I enjoy these analogues). Suppose I want to support Bazaar and Git right now. Suppose the program is executed with some kind of argument indicating which version control software is used.

With this in mind, I want to create a LogFileReaderFactory, which, given the name of the version control program, will return a suitable reader for the log file (a subclass from the general one) to read the log file and spit out the canonical internal representation. Therefore, of course, I can make BazaarLogFileReader and GitLogFileReader and hard-code them in the program, but I want it to be configured in such a way that adding support for a new version control program is as simple as overflowing a new class file in a directory with Bazaar and Git readers.

So right now you can call do-something-with-log-software git and do-something-with-log software bazaar because there are magazines for those who I want so that it is possible to simply add the class and the SVNLogFileReader 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. (Of course, the files can be named with a specific pattern and globbed in the call request.)

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

+48
ruby design-patterns
Apr 14 '09 at 3:12
source share
4 answers

You do not need LogFileReaderFactory; just teach your LogFileReader class 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, a superclass can act as its own factory. Now, what about automatic registration? So, why don't we just keep the 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 you have it. Just split it into several files and claim them accordingly.

You should read Peter Norvig Design Patterns in Dynamic Languages if you are interested in such things. It demonstrates how many design patterns actually work on the limitations or shortcomings of your programming language; and with a sufficiently powerful and flexible language, you really don’t need a design template, you just implement what you want. It uses Dylan and Common Lisp for examples, but many of its aspects relate to Ruby.

You can also take a look at the Why Poignant Guide to Ruby , especially chapters 5 and 6, although only if you can deal with a surreal technical writing.

edit : now you need to perform a riff from Jörg I like to shorten the repetition, and therefore I do not repeat the name of the version control system both in class and in registration. Adding the following to my second example will allow you to write much simpler class definitions, although they are still pretty simple and straightforward.

 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 can actually name these classes by creating a permanent definition based on the name passed in for a better error message.

 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 
+93
Apr 14 '09 at 3:52
source share

It really just drives off Brian Campbell's decision. If you like it, please confirm your answer too: he did all the work.

 #!/usr/bin/env ruby class Object; def eigenclass; class << self; self end end end module LogFileReader class LogFileReaderNotFoundError < NameError; end class << self def create type (self[type] ||= const_get("#{type.to_s.capitalize}LogFileReader")).new rescue NameError => e raise LogFileReaderNotFoundError, "Bad log file type: #{type}" if e.class == NameError && e.message =~ /[^: ]LogFileReader/ raise end def []=(type, klass) @readers ||= {type => klass} def []=(type, klass) @readers[type] = klass end klass end def [](type) @readers ||= {} def [](type) @readers[type] end nil end def included klass self[klass.name[/[[:upper:]][[:lower:]]*/].downcase.to_sym] = klass if klass.is_a? Class end end end def LogFileReader type 

Here we create a global method (more similar to a procedure, actually) called LogFileReader , the name of which coincides with our LogFileReader module. This is legal in Ruby. The ambiguity is resolved as follows: the module will always be preferred, unless it explicitly calls the call, i.e. You put parentheses at the end ( Foo() ) or pass an argument ( Foo :bar ).

This is a trick that is used in several places in stdlib, as well as in Camping and other frameworks. Since things like include or extend are not really keywords, but ordinary methods that take ordinary parameters, you do not need to pass them the actual Module as an argument, you can also pass everything that is evaluated by Module . In fact, this even works for inheritance, it is perfectly legal to write class Foo < some_method_that_returns_a_class(:some, :params) .

With this trick, you can make it look like you inherit from a common class, although Ruby does not have generics. It is used, for example, in the delegation library, where you do something like class MyFoo < SimpleDelegator(Foo) , and what happens is that the SimpleDelegator method dynamically creates and returns an anonymous subclass of the SimpleDelegator class, which delegates all method calls to an instance of the Foo class.

We use a similar trick here: we are going to dynamically create a Module , which, when mixed with a class, will automatically register this class with the LogFileReader registry.

  LogFileReader.const_set type.to_s.capitalize, Module.new { 

There is a lot going on in this line. Start over: Module.new creates a new anonymous module. The block passed to it becomes the body of the module — it is basically the same as using the keyword Module .

Now, on const_set . This is a constant setting method. Thus, this is the same as saying FOO = :bar , except that we can pass the name of the constant as a parameter, instead of knowing it in advance. Since we call the method in the LogFileReader module, a constant will be defined inside this namespace, IOW will be called LogFileReader::Something .

So what is a constant called? Well, the type argument is passed to the method, uppercase. So, when I go to :cvs , the resulting constant will be LogFileParser::Cvs .

And why are we setting a constant? To our newly created anonymous module, which is now no longer anonymous!

All this is actually just a long way to say module LogFileReader::Cvs , except that we did not know the "Cvs" part in advance and therefore could not write like that.

  eigenclass.send :define_method, :included do |klass| 

This is the body of our module. Here we use define_method to dynamically define a method called included . And we actually do not define the method on the module itself, but on the eigenclass module (using the small helper method that we defined above), which means that the method will not become an instance method, but rather a “static” method (in terms of Java / .NET )

included is actually a special hook method that is called by the Ruby environment every time a module is included in a class and the class is passed as an argument. So, our newly created module now has a hook method that will inform it whenever it turns on somewhere.

  LogFileReader[type] = klass 

And this is what our hook method does: it registers a class that is passed to the hook method in the LogFileReader registry. And the key that it registers is in the type argument of the LogFileReader method described above, which, thanks to the closure magic, is actually available inside the included method.

  end include LogFileReader 

And last but not least, we include the LogFileReader module in an anonymous module. [Note: I forgot this line in the original example.]

  } end class GitLogFileReader def display puts "I'm a git log file reader!" end end class BzrFrobnicator include LogFileReader def display puts "A bzr log file reader..." end end LogFileReader.create(:git).display LogFileReader.create(:bzr).display class NameThatDoesntFitThePattern include LogFileReader(:darcs) def display puts "Darcs reader, lazily evaluating your pure functions." end end LogFileReader.create(:darcs).display puts 'Here you can see, how the LogFileReader::Darcs module ended up in the inheritance chain:' p LogFileReader.create(:darcs).class.ancestors puts 'Here you can see, how all the lookups ended up getting cached in the registry:' p LogFileReader.send :instance_variable_get, :@readers puts 'And this is what happens, when you try instantiating a non-existent reader:' LogFileReader.create(:gobbledigook) 

This new extended version allows three different ways to define LogFileReader s:

  • All classes whose name matches the <Name>LogFileReader will be automatically found and registered as LogFileReader for :name (see: GitLogFileReader ),
  • All classes that are mixed in the LogFileReader module and whose name matches the <Name>Whatever template will be registered for the handler :name (see BzrFrobnicator ) and
  • All classes that are mixed in the LogFileReader(:name) module will be registered for the :name handler, regardless of their name (see NameThatDoesntFitThePattern ).

Please note that this is just a very far-fetched demonstration. This, for example, is definitely not thread safe. It can also lead to a memory leak. Use with caution!

+18
Apr 14 '09 at 10:40
source share

Another little suggestion for Brian Cumbell -

In fact, you can automatically register subclasses with an inherited callback. I.e.

 class LogFileReader cattr_accessor :subclasses; self.subclasses = {} def self.inherited(klass) # turns SvnLogFileReader in to :svn key = klass.to_s.gsub(Regexp.new(Regexp.new(self.to_s)),'').underscore.to_sym # self in this context is always LogFileReader self.subclasses[key] = klass end def self.create(type) return self.subclasses[type.to_sym].new if self.subclasses[type.to_sym] raise "No such type #{type}" end end 

Now we have

 class SvnLogFileReader < LogFileReader def display # do stuff here end end 

No need to register it

+10
Sep 24 '11 at 19:40
source share

This should work too, without having to register class names

 class LogFileReader def self.create(name) classified_name = name.to_s.split('_').collect!{ |w| w.capitalize }.join Object.const_get(classified_name).new end end class GitLogFileReader < LogFileReader def display puts "I'm a git log file reader!" end end 

and now

 LogFileReader.create(:git_log_file_reader).display 
+7
Nov 10 '11 at 18:47
source share



All Articles