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



известно, что в 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


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

912   4  

4 ответов:

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

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"

вот полная история, объясняющая необходимые концепции метапрограммирования, необходимые для понимания того, почему включение модуля работает так, как это происходит в 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"

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

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

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

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

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 линия выглядит не очень красиво, поэтому Руби добавил 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 метод is для. Ruby автоматически вызывает этот метод всякий раз, когда модуль входит в другой класс (или модуль), и передает объект класса host в качестве первого аргумента:

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

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

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

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

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

Comments

    Ничего не найдено.