Wie erweitere ich Objekte in Ruby?

Eines der interessanteren Ruby-Konzepte nennt sich “Open Classes”. Jedes Objekt kann nach Belieben erweitert werden. Natürlich kann dies auch zu Problemen führen. Gerade wenn Code wie der folgende nicht getestet wird:

class Object
  def blank?                                       [1]
    nil? || (respond_to?(:empty?) && empty?)
  end unless method_defined?(:blank?)
end

class Numeric
  def blank?
    false
  end unless method_defined?(:blank?)
end

class NilClass
  def blank?
    true
  end unless method_defined?(:blank?)
end

class TrueClass
  def blank?
    false
  end unless method_defined?(:blank?)
end

class FalseClass
  def blank?
    true
  end unless method_defined?(:blank?)              [2]
end

class String
  def blank?
    strip.empty?
  end unless method_defined?(:blank?)
end

Abgesehen davon, dass es völlig unnötig ist, all diese Objekte mit einer blank? Methode zu erweitern und die Implementierung vom De-facto-Standard abweicht, ist sie auch noch fehlerhaft:

false.blank?  # => false

Ich hätte hier einen anderen Rückgabewert erwartet. Die Methode Method#owner verrät, dass die blank? Methode tatsächlich nicht an FalseClass, sondern an Object aufgerufen wird:

false.method(:blank?).owner  # => Object

Die Dokumentation zu method_defined? erklärt das Problem:

mod.method_defined?(symbol) => true or false
Returns true if the named method is defined by mod (or its included modules and, if mod is a class, its ancestors).
Public and protected methods are matched.

Da Object#blank? vor allen anderen Methoden definiert wird, gibt es FalseClass#blank? gar nicht.

Eine mögliche Lösung wäre, auf die De-facto-Standard Implementierung zurückzugreifen:

module MyGem
  module CoreExtensions
    module Object
      def blank?
        respond_to?(:empty?) ? empty? : !self
      end
    end
  end
end

Object.send :include, MyGem::CoreExtensions::Object

Erstens stimmt jetzt das Ergebnis:

false.blank?  # => true

zweitens ist die Änderung transparent:

Object.ancestors             # => [Object, MyGem::CoreExtensions::Object, Kernel]
false.method(:blank?).owner  # => MyGem::CoreExtensions::Object

und drittens könnte die Methode durch ein anderes Modul “überschrieben” werden.

Ps. Ruby 1.9 und Method#source_location sind sehr hilfreich um herauszufinden, wo genau eine Methode definiert wird:

require "rspec"
String.method(:should).source_location
# => ["/Users/rubiii/.rvm/gems/ruby-1.9.2-p0@rails3/gems/rspec-expectations-2.0.0.beta.19/lib/rspec/expectations/extensions/kernel.rb", 26]

Mehr? Aktuelle Artikel oder alle Artikel im Archiv.