RSS: Articles| Comments| Trackbacks
 

Final methods in ruby (prevent method override)

Posted by haakon, Fri, 06 Oct 2006 14:10:00 GMT

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
end
This then allowed me to write a child class that did the real work:
class MyJob < Cronjob
  def run_job
    # real work goes here
  end
end 
And then call:
job = MyJob.new
job.run

All 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 
This is bad! The author thinks they are taking full advantage of the base class, but they are not. In reality they are overriding the run method, and Ruby does not complain a bit. This is where if I were in Java I could use the final keyword to say that a method should not be overriden by any child classes. What I wanted was to be able to write:
class Cronjob
  final :run

  def run
  end
end
So, how can we make this work? Here is my solution that does the job.
class 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 
Now if someone tries to override the method in a child class they get an exception:
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
Trackbacks

Use the following link to trackback from your own site:
http://www.thesorensens.org/articles/trackback/16

  1. Final methods in ruby (prevent method override) "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 overri...
Comments

Leave a response

Comments