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!