This is really just riffing off Brian Campbell's solution. If you like this, please upvote his 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 like a procedure, actually) called LogFileReader
, which is the same name as our module LogFileReader
. This is legal in Ruby. The ambiguity is resolved like this: the module will always be preferred, except when it's obviously a method call, i.e. you either put parentheses at the end (Foo()
) or pass an argument (Foo :bar
).
This is a trick that is used in a few places in the stdlib, and also in Camping and other frameworks. Because things like include
or extend
aren't actually keywords, but ordinary methods that take ordinary parameters, you don't have to pass them an actual Module
as an argument, you can also pass anything that evaluates to a 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 are inheriting from a generic class, even though Ruby doesn't have generics. It's 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 it is mixed into a class, will automatically register that class with the LogFileReader
registry.
LogFileReader.const_set type.to_s.capitalize, Module.new {
There's a lot going on in just this line. Let's start from the right: Module.new
creates a new anonymous module. The block passed to it, becomes the body of the module – it's basically the same as using the module
keyword.
Now, on to const_set
. It's a method for setting a constant. So, it's the same as saying FOO = :bar
, except that we can pass in the name of the constant as a parameter, instead of having to know it in advance. Since we are calling the method on the LogFileReader
module, the constant will be defined inside that namespace, IOW it will be named LogFileReader::Something
.
So, what is the name of the constant? Well, it's the type
argument passed into the method, capitalized. So, when I pass in :cvs
, the resulting constant will be LogFileParser::Cvs
.
And what do we set the constant to? To our newly created anonymous module, which is now no longer anonymous!
All of this is really just a longwinded way of saying module LogFileReader::Cvs
, except that we didn't know the "Cvs" part in advance, and thus couldn't have written it that way.
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 don't actually define the method on the module itself, but on the module's eigenclass (via a small helper method that we defined above), which means that the method will not become an instance method, but rather a "static" method (in Java/.NET terms).
included
is actually a special hook method, that gets called by the Ruby runtime, everytime a module gets included into a class, and the class gets passed in as an argument. So, our newly created module now has a hook method that will inform it whenever it gets included somewhere.
LogFileReader[type] = klass
And this is what our hook method does: it registers the class that gets passed into the hook method into the LogFileReader
registry. And the key that it registers it under, is the type
argument from the LogFileReader
method way above, which, thanks to the magic of closures, is actually accessible inside the included
method.
end
include LogFileReader
And last but not least, we include the LogFileReader
module in the 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 expanded version allows three different ways of defining LogFileReader
s:
- All classes whose name matches the pattern
<Name>LogFileReader
will automatically be found and registered as a LogFileReader
for :name
(see: GitLogFileReader
),
- All classes that mix in the
LogFileReader
module and whose name matches the pattern <Name>Whatever
will be registered for the :name
handler (see: BzrFrobnicator
) and
- All classes that mix 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 contrived demonstration. It is, for example, definitely not thread-safe. It might also leak memory. Use with caution!