Наследование методов класса от модулей / миксинов в Ruby

95

Известно, что в Ruby методы класса наследуются:

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works

Однако меня удивляет, что он не работает с миксинами:

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

Я знаю, что метод #extend может это сделать:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

Но я пишу миксин (или, скорее, хотел бы написать), содержащий как методы экземпляра, так и методы класса:

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

Теперь я хотел бы сделать следующее:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

Я хочу, чтобы A, B наследовали методы экземпляра и класса от Commonмодуля. Но, конечно, это не работает. Итак, нет ли секретного способа заставить это наследование работать из одного модуля?

Мне кажется неэлегантным разделить это на два разных модуля: один для включения, другой для расширения. Другое возможное решение - использовать класс Commonвместо модуля. Но это всего лишь обходной путь. (Что, если есть два набора общих функций Common1и Common2нам действительно нужны миксины?) Есть ли глубокая причина, по которой наследование методов класса не работает из миксинов?

Борис Ститницкий
источник
1
С той разницей, что здесь я знаю, что это возможно - я прошу наименее уродливого способа сделать это и по причинам, по которым наивный выбор не работает.
Борис Ститницкий
1
Обладая большим опытом, я понял, что Ruby зайдет слишком далеко, догадываясь о намерениях программиста, если включение модуля также добавит методы модуля к одноэлементному классу включающего. Это потому, что "модульные методы" на самом деле не что иное, как одноэлементные методы. Модули не являются специальными для использования одноэлементных методов, они являются специальными пространствами имен, в которых определены методы и константы. Пространство имен совершенно не связано с одноэлементными методами модуля, так что наследование классов одноэлементных методов более удивительно, чем его отсутствие в модулях.
Борис Ститницкий

Ответы:

171

Распространенная идиома - использовать includedоттуда методы класса hook и inject.

module Foo
  def self.included base
    base.send :include, InstanceMethods
    base.extend ClassMethods
  end

  module InstanceMethods
    def bar1
      'bar1'
    end
  end

  module ClassMethods
    def bar2
      'bar2'
    end
  end
end

class Test
  include Foo
end

Test.new.bar1 # => "bar1"
Test.bar2 # => "bar2"
Серджио Тюленцев
источник
26
includeдобавляет методы экземпляра, extendдобавляет методы класса. Вот как это работает. Не вижу непоследовательности, только нереализованные ожидания :)
Серджио Туленцев
1
Я постепенно смиряюсь с тем фактом, что ваше предложение настолько элегантно, насколько может быть практическое решение этой проблемы. Но я был бы признателен за то, чтобы узнать причину, по которой то, что работает с классами, не работает с модулями.
Борис Ститницкий
6
@BorisStitnicky Доверяйте этому ответу. Это очень распространенная идиома в Ruby, решающая именно тот вариант использования, о котором вы спрашиваете, и именно по причинам, с которыми вы столкнулись. Это может выглядеть "неэлегантно", но это ваш лучший выбор. (Если вы делаете это часто, вы можете переместить includedопределение метода в другой модуль и включить ЭТО в свой основной модуль;)
Phrogz
2
Прочтите эту ветку, чтобы узнать «почему?» .
Phrogz
2
@werkshy: включить модуль в фиктивный класс.
Серджио Тюленцев
49

Вот полная история, объясняющая необходимые концепции метапрограммирования, необходимые для понимания того, почему включение модулей работает так же, как в Ruby.

Что происходит при включении модуля?

Включение модуля в класс добавляет модуль к предкам класса. Вы можете посмотреть на предков любого класса или модуля, вызвав его ancestorsметод:

module M
  def foo; "foo"; end
end

class C
  include M

  def bar; "bar"; end
end

C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
#       ^ look, it's right here!

Когда вы вызываете метод для экземпляра C, Ruby просматривает каждый элемент этого списка предков, чтобы найти метод экземпляра с предоставленным именем. Поскольку мы включили Mв C, Mтеперь предок C, так что, когда мы называем fooна примере C, Ruby будет найти этот метод в M:

C.new.foo
#=> "foo"

Обратите внимание, что включение не копирует в класс методы экземпляра или класса - оно просто добавляет к классу «примечание», что он также должен искать методы экземпляра во включенном модуле.

А как насчет "классовых" методов в нашем модуле?

Поскольку включение изменяет только способ отправки методов экземпляра, включение модуля в класс только делает его методы экземпляра доступными в этом классе. Методы "класса" и другие объявления в модуле не копируются автоматически в класс:

module M
  def instance_method
    "foo"
  end

  def self.class_method
    "bar"
  end
end

class C
  include M
end

M.class_method
#=> "bar"

C.new.instance_method
#=> "foo"

C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class

Как Ruby реализует методы класса?

В Ruby классы и модули - это простые объекты - они являются экземплярами класса Classи Module. Это означает, что вы можете динамически создавать новые классы, назначать их переменным и т. Д .:

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

Также в Ruby у вас есть возможность определять так называемые одноэлементные методы для объектов. Эти методы добавляются как новые методы экземпляра в специальный скрытый одноэлементный класс объекта:

obj = Object.new

# define singleton method
def obj.foo
  "foo"
end

# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]

Но разве классы и модули не являются просто объектами? На самом деле они есть! Означает ли это, что у них тоже могут быть одноэлементные методы? Да! Так рождаются методы класса:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Или более распространенный способ определения метода класса - использование selfв блоке определения класса, который относится к создаваемому объекту класса:

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Как мне включить методы класса в модуль?

Как мы только что установили, методы класса на самом деле являются просто методами экземпляра в одноэлементном классе объекта класса. Означает ли это, что мы можем просто включить модуль в одноэлементный класс, чтобы добавить кучу методов класса? Да!

module M
  def new_instance_method; "hi"; end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M
  self.singleton_class.include M::ClassMethods
end

HostKlass.new_class_method
#=> "hello"

Эта self.singleton_class.include M::ClassMethodsстрока выглядит не очень красиво, поэтому добавлен Ruby Object#extend, который делает то же самое - т.е. включает модуль в одноэлементный класс объекта:

class HostKlass
  include M
  extend M::ClassMethods
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ there it is!

Перенос extendзвонка в модуль

Этот предыдущий пример не является хорошо структурированным кодом по двум причинам:

  1. Теперь нам нужно вызвать оба include и extendв HostClassопределении, чтобы правильно включить наш модуль. Это может стать очень обременительным, если вам нужно включить много похожих модулей.
  2. HostClassпрямые ссылки M::ClassMethods, что является деталью реализации модуля, Mо которой HostClassне нужно знать или заботиться.

Итак, как насчет этого: когда мы вызываем includeпервую строку, мы каким-то образом уведомляем модуль о том, что он был включен, а также передаем ему наш объект класса, чтобы он мог вызывать extendсам себя. Таким образом, задача модуля - добавлять методы класса, если он этого хочет.

Именно для этого и предназначен специальный self.includedметод . Ruby автоматически вызывает этот метод всякий раз, когда модуль включается в другой класс (или модуль), и передает объект класса хоста в качестве первого аргумента:

module M
  def new_instance_method; "hi"; end

  def self.included(base)  # `base` is `HostClass` in our case
    base.extend ClassMethods
  end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M

  def self.existing_class_method; "cool"; end
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ still there!

Конечно, добавление методов класса - не единственное, что мы можем сделать self.included. У нас есть объект класса, поэтому мы можем вызвать для него любой другой метод (класса):

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end
Матэ Солимози
источник
2
Замечательный ответ! Наконец-то смог понять концепцию после дня борьбы. Спасибо.
Sankalp
1
Я думаю, что это может быть лучший письменный ответ, который я когда-либо видел на SO. Спасибо за невероятную ясность и за то, что я не понимаю Ruby. Если бы я мог подарить этому бонусом в 100 пунктов, я бы сделал это!
Питер Никси
7

Как упоминал Серхио в комментариях, для парней, которые уже используют Rails (или не возражают, в зависимости от Active Support ), Concernздесь полезно:

require 'active_support/concern'

module Common
  extend ActiveSupport::Concern

  def instance_method
    puts "instance method here"
  end

  class_methods do
    def class_method
      puts "class method here"
    end
  end
end

class A
  include Common
end
Франклин Ю
источник
3

Вы можете съесть свой торт и съесть его, выполнив следующие действия:

module M
  def self.included(base)
    base.class_eval do # do anything you would do at class level
      def self.doit #class method
        @@fred = "Flintstone"
        "class method doit called"
      end # class method define
      def doit(str) #instance method
        @@common_var = "all instances"
        @instance_var = str
        "instance method doit called"
      end
      def get_them
        [@@common_var,@instance_var,@@fred]
      end
    end # class_eval
  end # included
end # module

class F; end
F.include M

F.doit  # >> "class method doit called"
a = F.new
b = F.new
a.doit("Yo") # "instance method doit called"
b.doit("Ho") # "instance method doit called"
a.get_them # >> ["all instances", "Yo", "Flintstone"]
b.get_them # >> ["all instances", "Ho", "Flintstone"]

Если вы намереваетесь добавить переменные экземпляра и класса, вы в конечном итоге вырветесь за волосы, поскольку столкнетесь с кучей неработающего кода, если вы не сделаете это таким образом.

Брайан Колвин
источник
Есть несколько странных вещей, которые не работают при передаче class_eval блока, например определение констант, определение вложенных классов и использование переменных класса вне методов. Для поддержки этих вещей вы можете дать class_eval heredoc (строку) вместо блока: base.class_eval << - 'END'
Пол Донохью