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

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

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

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

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

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

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, и нам действительно нужно иметь mixins?) Есть ли какая-то серьезная причина, почему наследование класса методов не работает из mixins?

Ответ 1

Общая идиома - использовать included hook и вводить методы класса оттуда.

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"

Ответ 2

Вот полная история, объясняющая необходимые концепции метапрограммирования, необходимые для понимания того, почему включение модуля работает так же, как в 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 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 у вас есть возможность определить так называемые методы singleton для объектов. Эти методы добавляются как новые методы экземпляра в специальный скрытый singleton class объекта:

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 в модуль

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

  • Теперь нам нужно вызвать как include, так и extend в определении HostClass, чтобы правильно включить наш модуль. Это может стать очень громоздким, если вы должны включить множество подобных модулей.
  • 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

Ответ 3

Как сказал Серджио в комментариях, для парней, которые уже находятся в 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

Ответ 4

Вы можете получить свой торт и съесть его, сделав это:

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"]

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