спадкування рубіном проти міксин


127

У Ruby, оскільки ви можете включати декілька мікшин, але лише розширювати один клас, здається, що міксин буде кращим перед успадкуванням.

Моє запитання: якщо ви пишете код, який повинен бути розширений / включений, щоб бути корисним, чому б ви коли-небудь робили його класом? Або кажучи іншим способом, чому б ти не робив це завжди модулем?

Я можу придумати лише одну причину, чому ви хочете отримати клас, і це якщо вам потрібно інстанціювати клас. У випадку ActiveRecord :: Base, однак, ви ніколи не інстанціюєте це безпосередньо. Так чи не міг це бути модуль?

Відповіді:


176

Я щойно читав про цю тему в «Добре заземленому рубіністі» (до речі, чудова книга). Автор робить краще пояснення, ніж я б, тому я його цитую:


Жодне єдине правило або формула завжди не призводить до правильного дизайну. Але корисно мати на увазі кілька міркувань, коли ви приймаєте рішення класу проти модуля:

  • Модулі не мають примірників. Звідси випливає, що сутність або речі, як правило, найкраще моделюються в класах, а характеристики або властивості сутностей або речей найкраще інкапсульовані в модулі. Відповідно, як зазначено в розділі 4.1.1, назви класів, як правило, є іменниками, тоді як назви модулів часто є прикметниками (Стек проти Stacklike).

  • Клас може мати лише один надклас, але він може змішуватись у скільки завгодно модулів. Якщо ви використовуєте спадщину, віддавайте пріоритет створенню розумних взаємозв'язків надкласу / підкласу. Не використовуйте єдиний зв'язок класу для класу, щоб наділити клас тим, що може виявитися лише одним із декількох наборів характеристик.

Підсумовуючи ці правила на одному прикладі, ось що ви не повинні робити:

module Vehicle 
... 
class SelfPropelling 
... 
class Truck < SelfPropelling 
  include Vehicle 
... 

Швидше, ви повинні зробити це:

module SelfPropelling 
... 
class Vehicle 
  include SelfPropelling 
... 
class Truck < Vehicle 
... 

Друга версія значно моделює сутності та властивості. Вантажівка спускається з транспортного засобу (що має сенс), тоді як SelfPropelling - характеристика транспортних засобів (принаймні, усіх тих, хто нас цікавить у цій моделі світу) - характеристика, яка передається вантажівкам в силу того, що Вантажівка є нащадком, або спеціалізовану форму транспортного засобу.


1
Приклад це чітко показує - Вантажівка - це транспортний засіб - немає вантажівки, яка б не була транспортним засобом.
PL J

1
На прикладі це чітко показано - TruckIS A Vehicle- немає такого, якого не Truckбуло б Vehicle. Однак я б назвав модуль, можливо, SelfPropelable(:?) Хм SelfPropeledзвучить правильно, але це майже те саме: D. У всякому разі, я б не включив його в, Vehicleале в Truckтому, що Є ТРАНСПОРТИ, ЩО НЕ МАЮТЬ SelfPropeled. Також хорошим показником є ​​запитання - чи є інші речі, а не транспортні засоби, які є SelfPropeled? - Ну, можливо, але мені було б важче знайти. Так Vehicleможе успадковуватись від класу SelfPropelling (як клас він не підходив би SelfPropeled- оскільки це вже більше роль)
PL J

39

Я думаю, що мікіни - прекрасна ідея, але тут є ще одна проблема, яку ніхто не згадував: зіткнення в просторі імен. Поміркуйте:

module A
  HELLO = "hi"
  def sayhi
    puts HELLO
  end
end

module B
  HELLO = "you stink"
  def sayhi
    puts HELLO
  end
end

class C
  include A
  include B
end

c = C.new
c.sayhi

Хто з них виграє? У Рубі виявляється, що це останнє module B, тому що ти включив його після module A. Зараз легко уникнути цієї проблеми: переконайтесь, що все module Aіmodule B константи та методи знаходяться в малоймовірних просторах імен. Проблема полягає в тому, що компілятор взагалі не попереджає вас, коли трапляються зіткнення.

Я стверджую, що така поведінка не стосується великих команд програмістів - не слід вважати, що особа, яка реалізує, class Cзнає про кожне ім’я в області застосування. Ruby навіть дозволить вам змінити константу чи метод іншого типу . Я не впевнений, що це колись можна вважати правильною поведінкою.


2
Це мудре слово обережності. Нагадує про декілька підводних каменів спадкування в C ++.
Кріс Тонкінсон

1
Чи є для цього хороші пом’якшення? Це виглядає як причина, чому багатократне успадкування Python є чудовим рішенням (не намагаючись запустити відповідність мови p * ssing; просто порівнюючи цю специфічну особливість).
Марцін

1
@bazz Це чудово і все, але композиція в більшості мов громіздка. Це також в основному актуально для мов, набраних качками. Це також не гарантує, що ви не отримаєте дивних станів.
Марцін

Старий пост, я знаю, але все ж виявляється у пошуках. Відповідь частково невірна - C#sayhiвиводиться B::HELLOне тому, що Рубі змішує константи, а тому, що рубін розбирає константи від ближчих до віддалених - так на що HELLOпосилається Bзавжди B::HELLO. Це справедливо, навіть якщо клас C визначив, що він C::HELLOтеж є власним .
Лаас

13

Моя думка: Модулі призначені для обміну поведінкою, а класи - для моделювання відносин між об'єктами. Ви технічно можете просто зробити все екземпляром Object і змішати в будь-яких модулях, які ви хочете отримати бажаний набір поведінки, але це було б поганим, випадковим і досить нечитабельним дизайном.


2
Це відповідає на питання прямо: успадкування застосовує певну організаційну структуру, яка може зробити ваш проект більш читабельним.
наждак

10

Відповідь на ваше запитання багато в чому контекстуальна. Вибираючи спостереження pubb, вибір в першу чергу визначається доменом, що розглядається.

І так, ActiveRecord повинен був бути включений, а не розширений підкласом. Інший ORM - datamapper - саме цього досягає!


4

Мені дуже подобається відповідь Енді Гаскелла - просто хотілося додати, що так, ActiveRecord не повинен використовувати успадкування, а скоріше включати модуль для додавання поведінки (переважно стійкості) до моделі / класу. ActiveRecord просто використовує неправильну парадигму.

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

Прикро, що майже ніхто в спільноті Rails не використовує "спадкування Ruby" так, як це слід використовувати - для визначення ієрархій класів, а не лише для додання поведінки.


1

Я найкраще розумію міксин - це віртуальні класи. Міксини - це "віртуальні класи", які були введені в ланцюжок предків класу або модуля.

Коли ми використовуємо "включати" і передаємо йому модуль, він додає модуль до ланцюга предків прямо перед класом, який ми успадковуємо:

class Parent
end 

module M
end

class Child < Parent
  include M
end

Child.ancestors
 => [Child, M, Parent, Object ...

Кожен об’єкт у Ruby також має однотонний клас. Методи, додані до цього класу синглтон, можна безпосередньо викликати на об'єкт, і тому вони діють як "клас" методи. Коли ми використовуємо "розширити" на об'єкт і передаємо об'єкт модулю, ми додаємо методи модуля до однотонного класу об'єкта:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

Ми можемо отримати доступ до класу синглтон методом singleton_class:

Test.singleton_class.ancestors
 => [#<Class:Test>, M, #<Class:Object>, ...

Ruby забезпечує деякі гачки для модулів, коли вони змішуються в класи / модулі. includedце метод гака, що надається Ruby, який викликає кожен раз, коли ви включаєте модуль у якийсь модуль чи клас. Як і в комплекті, є пов'язаний extendedгачок для розширення. Він буде викликаний, коли модуль розширений іншим модулем або класом.

module M
  def self.included(target)
    puts "included into #{target}"
  end

  def self.extended(target)
    puts "extended into #{target}"
  end
end

class MyClass
  include M
end

class MyClass2
  extend M
end

Це створює цікавий зразок, який розробники могли використовувати:

module M
  def self.included(target)
    target.send(:include, InstanceMethods)
    target.extend ClassMethods
    target.class_eval do
      a_class_method
    end
  end

  module InstanceMethods
    def an_instance_method
    end
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

class MyClass
  include M
  # a_class_method called
end

Як бачимо, цей єдиний модуль додає методи екземпляра, "class" методи та діє безпосередньо на цільовий клас (в цьому випадку викликає a_class_method ()).

ActiveSupport :: Концерн інкапсулює цей шаблон. Ось такий же модуль переписаний для використання ActiveSupport :: Concern:

module M
  extend ActiveSupport::Concern

  included do
    a_class_method
  end

  def an_instance_method
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

-1

Зараз я замислююся над templateсхемою дизайну. З модулем просто не буде добре.

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