Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to override static class method using module in Ruby?

Tags:

ruby

module Imodule
  ???
end

class Some
  include Imodule

  def self.imethod
    puts "original"
  end
end

Some.imethod
# => "overrided"

How to create a module which will override static method?

This is an interview question for deep understanding ruby features. Don't suggest another formulation of the problem :)

like image 594
Ivan Schneider Avatar asked Feb 03 '12 12:02

Ivan Schneider


2 Answers

Ok, here's a working code. Note that you don't even have to touch target class! :)

class Klass
  def self.say
    puts 'class'
  end
end

module FooModule
  def self.included base
    base.instance_eval do
      def say
        puts "module"
      end
    end
  end
end


Klass.send(:include, FooModule)

Klass.say

Explanation

Now classic way of mixing in class methods is this (and it doesn't solve the problem, of course).

module FooModule
  def self.included base
    base.extend ClassMethods
  end

  module ClassMethods
    def bar
      puts "module"
    end
  end
end

class Klass
  include FooModule

  def self.bar
    puts 'class'
  end
end


Klass.bar #=> class

When modules are included or extended into a class, its methods are placed right above this class' methods in inheritance chain. This means that if we were to call super in that class method, it would print "module". But we don't want to touch original class definition, we want to alter it from outside.

So, can we do something?

Good for us, ruby has a concept of "open classes". This means that we can change virtually everything in the app, even some 3rd-party libraries. Every class can "opened" and new methods can be added to it, or old methods can be redefined. Let's look how it works.

class Klass
  def self.bar
    puts 'class'
  end
end

class Klass
  def self.bar
    puts 'class 2'
  end
end

Klass.bar #=> class 2

The second class definition does not overwrite previous one, it opens and alters it. In this case, it happened to define a method with the same name. This resulted in old method being overwritten by the new one. This works with any classes, even base library classes.

puts [1, 2, 3].to_s #=> [1, 2, 3]

class Array
  def to_s
    "an array: #{join ', '}"
  end
end

puts [1, 2, 3].to_s #=> an array: 1, 2, 3

Or the same code can be rewritten as

puts [1, 2, 3].to_s #=> [1, 2, 3]

Array.class_eval do
  def to_s
    "an array: #{join ', '}"
  end
end

puts [1, 2, 3].to_s #=> an array: 1, 2, 3

Applying the knowledge

Let's start with simpler things, like overriding an instance method.

class Klass
  def say
    puts 'class'
  end
end

module FooModule
  def self.included base
    base.class_eval do
      def say
        puts "module"
      end
    end
  end
end


Klass.send(:include, FooModule)

Klass.new.say #=> module

Modules have a special callback that gets called every time a module is included in a class. We can use that to call class_eval on that class and redefine a method.

Replacing a class method is done in a similar way.

class Klass
  def self.say
    puts 'class'
  end
end

module FooModule
  def self.included base
    base.instance_eval do
      def say
        puts "module"
      end
    end
  end
end


Klass.send(:include, FooModule)

Klass.say #=> module

The only difference here is that we call instance_eval instead of class_eval. This can be a very confusing part. In short, class_eval creates instance methods and instance_eval creates class methods.

This is taken from my blog post.

like image 75
Sergio Tulentsev Avatar answered Oct 27 '22 17:10

Sergio Tulentsev


What if you need to be able to call the original method that you just overrode, from within your new method?

In other words, what if you want to be able to call super from an overridden class method the same way you might call super when overriding an instance method?

Here's the solution I finally arrived upon, in case anyone else finds it useful:

class Klass
  def self.say
    puts 'original, '
  end
end

module FooModule
  def self.included base
    orig_method = base.method(:say)
    base.define_singleton_method :say do |*args|
      orig_method.call(*args)
      puts "module"
    end
  end
end


class Klass
  include FooModule
end

Klass.say  # => original, module

We have to use define_method instead of def so that we create a closure and have access to local variables (in this case, the saved version of the original method) from within the new method definition.

By the way,

base.define_singleton_method :say do

is equivalent to doing

(class << base; self; end).send :define_method, :say do

.

Special thanks to Ruby singleton methods with (class_eval, define_method) vs (instance_eval, define_method) and http://yugui.jp/articles/846 for educating me and pointing me in the right direction.

like image 40
Tyler Rick Avatar answered Oct 27 '22 16:10

Tyler Rick