Переопределение метода другим, определенным в модуле

Я хочу определить метод экземпляра Date#next, который возвращает следующий день. Поэтому я создал модуль DateExtension, например:

module DateExtension
  def next(symb=:day)
    dt = DateTime.now
    {:day   => Date.new(dt.year, dt.month, dt.day + 1),
     :week  => Date.new(dt.year, dt.month, dt.day + 7),
     :month => Date.new(dt.year, dt.month + 1, dt.day),
     :year  => Date.new(dt.year + 1, dt.month, dt.day)}[symb]
  end
end

Используя его:

class Date
  include DateExtension
end

Вызов метода d.next(:week) заставляет Ruby вызывать ошибку ArgumentError: wrong number of arguments (1 for 0). Как я могу переопределить метод next по умолчанию из класса Date с объявленным в модуле DateExtension?

Ответ 1

В Ruby 2.0 и более поздних версиях вы можете использовать Module#prepend:

class Date
  prepend DateExtension
end

Оригинальный ответ для более старых версий Ruby приведен ниже.


Проблема с include (как показано в на следующей диаграмме) заключается в том, что методы класса не могут быть переопределены модулями, включенными в это класс (решения следуют диаграмме): Ruby Method Lookup Flow

Решение

  • Подкласс Дата только для этого метода:

    irb(main):001:0> require 'date'; module Foo; def next(a=:hi); a; end; end
    #=> nil
    irb(main):002:0> class MyDate < Date; include Foo; end
    #=> MyDate
    irb(main):003:0> MyDate.today.next(:world)
    #=> :world
    
  • Расширьте только нужные вам экземпляры с помощью собственного метода:

    irb(main):001:0> require 'date'; module Foo; def next(a=:hi); a; end; end
    #=> nil
    irb(main):002:0> d = Date.today; d.extend(Foo); d.next(:world)
    #=> :world
    
  • При включении вашего модуля выполняйте грубый взлом и попадаете внутрь класса и уничтожаете старый "следующий", чтобы ваш вызов вызывался:

    irb(main):001:0> require 'date'
    #=> true
    irb(main):002:0> module Foo
    irb(main):003:1>   def self.included(klass)
    irb(main):004:2>     klass.class_eval do
    irb(main):005:3*       remove_method :next
    irb(main):006:3>     end
    irb(main):007:2>   end
    irb(main):008:1>   def next(a=:hi); a; end
    irb(main):009:1> end
    #=> nil
    irb(main):010:0> class Date; include Foo; end
    #=> Date
    irb(main):011:0> Date.today.next(:world)
    #=> :world
    

    Этот метод гораздо более инвазивен, чем просто модуль, но единственный способ (из тех методов, которые были показаны до сих пор) сделать так, чтобы новые экземпляры Date, возвращаемые системными методами, автоматически использовали методы из вашего собственного модуля.

  • Но если вы собираетесь это сделать, вы можете также полностью пропустить модуль и просто перейти прямо к земле monkeypatch:

    irb(main):001:0> require 'date'
    #=> true
    irb(main):002:0> class Date
    irb(main):003:1>   alias_method :_real_next, :next
    irb(main):004:1>   def next(a=:hi); a; end
    irb(main):005:1> end
    #=> nil
    irb(main):006:0> Date.today.next(:world)
    #=> :world
    
  • Если вам действительно нужна эта функциональность в вашей собственной среде, обратите внимание, что библиотека Prepend by banisterfiend может дать вам возможность чтобы вызвать поиск в модуле перед классом, в который он был смешан.

Ответ 2

Метод next для Date определяется в классе Date, а методы, определенные в классе, имеют приоритет над теми, которые определены в включенном модуле. Итак, когда вы это сделаете:

class Date
  include DateExtension
end

Вы используете версию next, но next, определенная в Date, по-прежнему имеет приоритет. Вы должны будете поместить свой next вправо в Date:

class Date
  def next(symb=:day)
    dt = DateTime.now
      {:day   => Date.new(dt.year, dt.month, dt.day + 1),
       :week  => Date.new(dt.year, dt.month, dt.day + 7),
       :month => Date.new(dt.year, dt.month + 1, dt.day),
       :year  => Date.new(dt.year + 1, dt.month, dt.day)}[symb]
    end
end

В главе "Программирование Ruby" на Классы и объекты:

Когда класс включает модуль, методы экземпляра модуля становятся доступными как методы экземпляра класса. Это почти так, как если бы модуль стал суперклассом класса, который его использует. Неудивительно, что о том, как это работает. Когда вы включаете модуль, Ruby создает анонимный прокси-класс, который ссылается на этот модуль, и вставляет этот прокси в качестве прямого суперкласса класса, который вложил в него.