In a reversal of fortune, I recently found myself wishing Ruby was more like Java. Java has the ideas of abstract base classes and final methods (methods which should not be overridden in child classes). Such ideas don’t really exist in Ruby.
My problem was this. I had a base class Cronjob which represented some job which was going to run under cron. This class managed stuff like setting up logging, db connections, etc. I then wanted other jobs to be able to extend Cronjob and take advantage of the base class:
class Cronjob
def initialize
# do useful stuff here, setting up db connections, logging, etc.
end
def run # method which no child class should override
start = Time.now
puts "starting at #{start}"
run_job # method which child class should override
stop = Time.now
puts "finished at #{stop}, took #{stop-start} seconds"
end
endclass MyJob < Cronjob
def run_job
# real work goes here
end
end job = MyJob.new
job.runAll well and good. If people use the base class correctly, they get some nice bits of functionality. However, I eventually noticed that someone had written this class:
class TheirJob < Cronjob
def run
# real work goes here
end
end class Cronjob
final :run
def run
end
endclass Object
@@final_methods = {}
class < < self
def prevent_override?(method_name)
@@final_methods.each do |class_name, final_methods|
ancestors = self.ancestors
ancestors.shift # remove myself from the list
if ancestors.include?(class_name) and
final_methods.include?(method_name)
raise "Child class '#{self}' should not override parent class method '#{class_name}.#{method_name}'."
end
end
end
def method_added(method_name)
prevent_override?(method_name)
end
def final(*names)
@@final_methods[self] = names
end
end
end in `prevent_override?': Child class 'TheirJob' should not
override parent class method 'Cronjob.run'.(RuntimeError)How does it work? The magic is possible because Ruby has a method called method_added. This gets called when a method is added to a class. So, when a source code file is being processed, if a method is defined with “def foo”, after the method has been added to the class this method_added method gets fired with “foo” as the argument. We can then implement the method with our desired behavior. In my case, I just wanted to blow up with an exception which is easy enough to do.
Ruby also has a method to get an object’s “ancestors”.>> true.class.ancestors
=> [TrueClass, Object, Kernel]
>> [].class.ancestors
=> [Array, Enumerable, Object, Kernel]So, the logic becomes simple. The final method just stores a hash of class => [methods which you cannot override]. Then, on method_added we do a check to see if the method being added is in this hash, and Bob’s your uncle!
So, while it was mildly surprising to find Ruby missing a language feature that I wanted, the language is powerful enough that you can “add to” the language! I’m also half expecting people to weigh in with suggestions of a better way to do this. I would be pleased to hear better variations. This solution definitely doesn’t make it impossible to override the method in a child class; a determined person could get around it. But it does solve my problem of someone inadvertently overriding the method.
Update: Now available via gems, thanks to Dr. Nic’s newgem magic:
gem install finalizer
Posted by haakon,