Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to call a class's method, when any other method of the class is called (Ruby)

Tags:

class

ruby

In Ruby, is there a way to call a method when any other method of the class is called? For example,

class Car
    def repair
        puts "Repaired!"
    end
    def drive
        # content
    end
    def checkup
        # content
    end
end

In this example, if I call any method on an instance of Car, I should always call the repair method. How do I do this in Ruby?

NOTE: I also do want repair called in built-in methods, too, like Carinstance.class should call repair, too.

like image 272
crownusa Avatar asked Jan 16 '14 03:01

crownusa


People also ask

Can you call a class method on an instance Ruby?

In Ruby, a method provides functionality to an Object. A class method provides functionality to a class itself, while an instance method provides functionality to one instance of a class. We cannot call an instance method on the class itself, and we cannot directly call a class method on an instance.

How do you call methods from different classes?

In Java, a method can be invoked from another class based on its access modifier. For example, a method created with a public modifier can be called from inside as well as outside of a class/package. The protected method can be invoked from another class using inheritance.


2 Answers

I have assumed that you want Car#repairto be invoked after each of Car's other instance methods have returned. I see that you have added a requirement that other methods also invoke repair. I have added a few remarks at the end about extending this to include built-in instance methods.

The approach I've taken is to make use of BasicObject#method_missing:

class Car
  def repair
    puts "Repaired!"
  end

  def drive
    puts "Drive!"
  end

  def checkup
    puts "Checkup!"
  end

  def method_missing(m, *args)
    if @@ims.key?(m)
      ret = send(@@ims[m], *args)
      repair
      ret
    else
      super
    end
  end

  @@ims = instance_methods(false).each_with_object({}) do |m,h|
    next if (m == :repair || m == :method_missing)
    saved_name = "_#{m}"
    alias_method saved_name, m
    h[m] = saved_name
    remove_method(m)
  end            
end

car = Car.new

car.repair
Repaired!

car.drive
Drive!
Repaired!

car.checkup #
Checkup!
Repaired!

car.wash # => in `method_missing': undefined method `wash'...

When class Car is parsed, after all the instance methods have been constructed, the following operations are performed, which I explain with an example:

instance_methods(false) # => [:repair, :drive, :checkup, :method_missing]  

each_with_object({}) creates a hash (initially empty), referred to by the block variable h (more on this later).

next if (m == :repair || m == :method_missing)

causes :repair and :method_missing to be skipped.

When m => :drive, the following three statements effectively rename :drive to :_drive and add :drive" => "_drive" to the hash h.

each_with_object returns

@@ims = {:drive=>"_drive", :checkup=>"_checkup"}

and now

instance_methods(false) # => [:repair, :method_missing, :_drive, :_checkup]

Because there is no longer a method :drive, Car.new.drive invokes method_missing(:drive). The latter finds that @@ims has a key :drive, so it uses send to invoke :_drive, invokes :repair and returns the return value of :_drive. If method_missing is passed a method that is not a key of @@ims, super is invoked and an exception is raised.

In a now-removed edit I suggested that to include built-in instance methods, one need only change instance_methods(false) to instance_methods, but warned about possible unintended side-effects. @Kal pointed out that built-in instance methods cannot be removed, so that approach won't work. That's just as well--one should not mess with Ruby in that way. I obviously didn't test my assertion. Shame!

like image 159
Cary Swoveland Avatar answered Oct 07 '22 17:10

Cary Swoveland


class Car
  def self.default_method
      instance_methods(true).each do |meth|
        alias_method meth, :repair
      end
  end
  def initialize
    self.class.default_method
  end

  def repair
    puts "Repaired!"
  end
  def drive
    # content
  end
  def checkup
    # content
  end
end

car = Car.new

car.drive # => Repaired!
car.checkup # => Repaired!
car.class # => Repaired!

Note that redefining the built-in methods generates some warnings:

# => untitled 5:6: warning: redefining `object_id' may cause serious problems
# => untitled 5:6: warning: redefining `__send__' may cause serious problems

Edit: Oops, I posted this too quickly and didn't spot the problem. It calls repair, but not the original methods. I knew it seemed too easy! I think I'm out of my depth with this one. :-) (Note: I thought Cary's approach was really clever, and it does work for your own methods, but it looks like he hit a dead end with the built-in methods, and in any case, it alters methods in a way that you really shouldn't be attempting with built-in methods).

like image 27
Kal Avatar answered Oct 07 '22 17:10

Kal