Я хочу определить метод экземпляра 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 создает анонимный прокси-класс, который ссылается на этот модуль, и вставляет этот прокси в качестве прямого суперкласса класса, который вложил в него.