Успадкування методів класу від модулів / комбінацій у 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
Маючи більше досвіду, я розумів, що Рубі зайшов би занадто далеко, вгадуючи намір програміста, якби включення модуля також додало модульні методи до синглтон-класу включення. Це тому, що "модульні методи" насправді є не що інше, як одиночні. Модулі не є спеціальними для того, щоб мати одиночні методи, вони спеціальні для того, щоб бути просторами імен, де визначені методи та константи. Простір імен абсолютно не пов'язаний з однотонними методами модуля, тому насправді наслідування класів одинарних методів є більш дивовижним, ніж відсутність його в модулях.
Борис Стітніцький

Відповіді:


171

Поширена ідіома - використовувати 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"

26
includeдодає методи екземпляра, extendдодає методи класу. Ось як це працює. Я не бачу суперечливості, лише незадоволені очікування :)
Серхіо Туленцев

1
Я повільно терплю з тим, що ваша пропозиція настільки ж елегантна, наскільки отримує практичне рішення цієї проблеми. Але я був би вдячний, щоб дізнатися причину, чому щось, що працює з класами, не працює з модулями.
Борис Стітнікі

6
@BorisStitnicky Довіряйте цій відповіді. Це дуже поширена ідіома в Ruby, яка вирішує саме той варіант використання, про який ви запитуєте, і саме з тих причин, з якими ви пережили. Це може виглядати "неелегантно", але це найкраще. (Якщо ви робите це часто, ви можете перенести includedвизначення методу на інший модуль і включити
ТОМ

2
Прочитайте цю тему для отримання більш глибокої інформації про "чому?" .
Фрогз

2
@werkshy: включіть модуль у фіктивний клас.
Серхіо Туленцев

47

Ось повна історія, що пояснює необхідні концепції метапрограмування, необхідні для розуміння того, чому включення модулів працює так, як це робиться в 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]

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

Як ми щойно встановили, методи класу насправді є лише методами екземпляра для класу-одиночки об'єкта класу. Чи означає це, що ми можемо просто включити модуль в клас singleton, щоб додати купу методів класу? Так!

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, що робить те саме - тобто включає модуль в клас singleton об'єкта:

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
Чудова відповідь! Нарешті зміг зрозуміти концепцію після дня боротьби. Дякую.
Санкальп

1
Я думаю, що це може бути найкраща письмова відповідь, яку я коли-небудь бачив на SO. Дякую за неймовірну ясність і за те, що я продовжую мою нестабільну Рубі. Якби я міг подарувати цей бонус 100 балів, я б!
Пітер Ніксей

7

Як згадував Серхіо в коментарях, тут корисно для хлопців, які вже є у 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

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'
Пол Доноуе
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.