dziedziczenie rubinów a mieszanki

127

W Rubim, ponieważ możesz dołączyć wiele mikserów, ale rozszerzyć tylko jedną klasę, wygląda na to, że mieszanki byłyby preferowane zamiast dziedziczenia.

Moje pytanie: jeśli piszesz kod, który musi zostać rozszerzony / dołączony, aby był użyteczny, dlaczego miałbyś kiedykolwiek zrobić z tego klasę? Innymi słowy, dlaczego nie zrobiłbyś z tego zawsze modułu?

Przychodzi mi do głowy tylko jeden powód, dla którego chciałbyś mieć klasę, a mianowicie, jeśli musisz utworzyć instancję klasy. Jednak w przypadku ActiveRecord :: Base nigdy nie tworzysz jej bezpośrednio. Więc czy nie powinien to być zamiast tego moduł?

Brad Cupit
źródło

Odpowiedzi:

176

Ja po prostu czytać na ten temat w The uzasadnionych Rubyist (świetna książka, nawiasem mówiąc). Autor lepiej tłumaczy, niż ja bym, więc zacytuję go:


Żadna pojedyncza reguła ani formuła nie zawsze prowadzi do właściwego projektu. Jednak podczas podejmowania decyzji dotyczących klas kontra modułów warto pamiętać o kilku kwestiach:

  • Moduły nie mają instancji. Wynika z tego, że jednostki lub rzeczy są generalnie najlepiej modelowane w klasach, a cechy lub właściwości jednostek lub rzeczy najlepiej hermetyzować w modułach. Odpowiednio, jak zauważono w sekcji 4.1.1, nazwy klas są zwykle rzeczownikami, podczas gdy nazwy modułów są często przymiotnikami (stos kontra podobny do stosu).

  • Klasa może mieć tylko jedną superklasę, ale może mieszać dowolną liczbę modułów. Jeśli używasz dziedziczenia, priorytetowo traktuj tworzenie rozsądnych relacji nadklasy / podklasy. Nie używaj jedynej relacji klasy nadklasy, aby nadać klasie coś, co może okazać się tylko jednym z kilku zestawów cech.

Podsumowując te zasady na jednym przykładzie, oto czego nie należy robić:

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

Raczej powinieneś to zrobić:

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

Druga wersja znacznie dokładniej modeluje jednostki i właściwości. Ciężarówka wywodzi się z Pojazdu (co ma sens), podczas gdy SelfPropelling to cecha pojazdów (przynajmniej tych wszystkich, na których nam zależy w tym modelu świata) - cecha, która jest przekazywana ciężarówkom z racji tego, że Truck jest potomkiem, lub wyspecjalizowana forma pojazdu.

Andy Gaskell
źródło
1
Przykład dobrze to pokazuje - Ciężarówka JEST Pojazdem - nie ma Ciężarówki, która nie byłaby Pojazdem.
PL J
1
Przykład dobrze to pokazuje - TruckJEST A Vehicle- nie ma Trucktego, co by nie było Vehicle. Jakkolwiek nazwałbym moduł może SelfPropelable(:?) Hmm SelfPropeledbrzmi dobrze, ale jest prawie taki sam: D. W każdym razie nie umieściłbym tego w, Vehicleale w Truck- ponieważ są pojazdy, których NIE MA SelfPropeled. Dobrą wskazówką jest również pytanie - czy istnieją inne rzeczy, a NIE pojazdy, które SĄ SelfPropeled? - Cóż, może, ale trudniej byłoby mnie znaleźć. Czyli Vehiclemoże dziedziczyć po klasie SelfPropelling (jako klasa nie pasowałaby jako SelfPropeled- bo to bardziej rola)
PL J
39

Myślę, że mixiny to świetny pomysł, ale jest inny problem, o którym nikt nie wspomniał: kolizje przestrzeni nazw. Rozważać:

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

Który wygrywa? W Rubim okazuje się, że to drugie module B, ponieważ umieściłeś go później module A. Teraz łatwo jest uniknąć tego problemu: upewnij się, że wszystko module Ai module B„s stałe i metody są w nieprawdopodobnych nazw. Problem polega na tym, że kompilator w ogóle nie ostrzega o kolizjach.

Twierdzę, że to zachowanie nie dotyczy dużych zespołów programistów - nie należy zakładać, że osoba wdrażająca class Cwie o każdym nazwisku w zakresie. Ruby pozwoli ci nawet zastąpić stałą lub metodę innego typu . Nie jestem pewien, czy kiedykolwiek można to uznać za prawidłowe zachowanie.

Dan Barowy
źródło
2
To mądre słowo przestrogi. Przypomina pułapki wielokrotnego dziedziczenia w C ++.
Chris Tonkinson,
1
Czy jest jakieś dobre złagodzenie tego? Wygląda to na powód, dla którego dziedziczenie wielokrotne w Pythonie jest lepszym rozwiązaniem (nie próbuje rozpocząć dopasowywania p * ssingu języka; wystarczy porównać tę konkretną funkcję).
Marcin
1
@bazz To świetnie i wszystko, ale kompozycja w większości języków jest uciążliwa. Jest to również szczególnie istotne w językach pisanych kaczką. Nie gwarantuje również, że nie dostaniesz dziwnych stanów.
Marcin
Wiem, stary post, ale wciąż okazuje się w wyszukiwaniach. Odpowiedź jest częściowo niepoprawna - wyświetla C#sayhiwyniki B::HELLOnie dlatego, że Ruby miesza stałe, ale dlatego, że ruby ​​rozwiązuje stałe z bliższej do dalszej odległości - więc HELLOodwołanie w programie Bzawsze rozwiązywałoby do B::HELLO. Dzieje się tak nawet wtedy, gdy klasa C również zdefiniowała swoją własną C::HELLO.
Laas,
13

Moja opinia: moduły służą do udostępniania zachowań, a klasy do modelowania relacji między obiektami. Technicznie rzecz biorąc, możesz po prostu uczynić wszystko instancją Object i wmieszać dowolne moduły, które chcesz uzyskać, aby uzyskać pożądany zestaw zachowań, ale byłby to kiepski, przypadkowy i raczej nieczytelny projekt.

Głaskanie pod brodę
źródło
2
Odpowiada to bezpośrednio na pytanie: dziedziczenie wymusza określoną strukturę organizacyjną, która może uczynić projekt bardziej czytelnym.
emery
10

Odpowiedź na twoje pytanie jest w dużej mierze kontekstowa. Wychodząc z obserwacji pubba, wybór jest napędzany przede wszystkim przez rozważaną dziedzinę.

I tak, ActiveRecord powinien zostać uwzględniony, a nie rozszerzony o podklasę. Kolejny ORM - datamapper - dokładnie to osiąga!

nareshb
źródło
4

Bardzo podoba mi się odpowiedź Andy'ego Gaskella - chciałem tylko dodać, że tak, ActiveRecord nie powinien używać dziedziczenia, ale raczej zawierać moduł dodający zachowanie (głównie trwałość) do modelu / klasy. ActiveRecord po prostu używa złego paradygmatu.

Z tego samego powodu bardzo podoba mi się MongoId zamiast MongoMapper, ponieważ pozostawia on programistom możliwość wykorzystania dziedziczenia jako sposobu na modelowanie czegoś znaczącego w domenie problemowej.

To smutne, że prawie nikt w społeczności Railsów nie używa „dziedziczenia Ruby” w sposób, w jaki powinien być używany - do definiowania hierarchii klas, a nie tylko do dodawania zachowania.

Tilo
źródło
1

Najlepszym sposobem, w jaki rozumiem, że mixiny są wirtualne klasy. Miksery to „klasy wirtualne”, które zostały wprowadzone do łańcucha przodków klasy lub modułu.

Kiedy używamy „włączania” i przekazujemy mu moduł, dodaje on moduł do łańcucha nadrzędnego bezpośrednio przed klasą, z której dziedziczimy:

class Parent
end 

module M
end

class Child < Parent
  include M
end

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

Każdy obiekt w Rubim ma również klasę pojedynczą. Metody dodane do tej klasy pojedynczej mogą być wywoływane bezpośrednio w obiekcie, a więc działają jako metody „klasowe”. Kiedy używamy "rozszerzania" na obiekcie i przekazujemy obiektowi moduł, dodajemy metody modułu do pojedynczej klasy obiektu:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

Możemy uzyskać dostęp do klasy singleton za pomocą metody singleton_class:

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

Ruby udostępnia kilka punktów zaczepienia dla modułów, gdy są one mieszane w klasy / moduły. includedjest metodą przechwytującą dostarczaną przez Rubiego, która jest wywoływana za każdym razem, gdy włączysz moduł do jakiegoś modułu lub klasy. Podobnie jak w zestawie, jest powiązany extendedhak do rozszerzenia. Zostanie wywołany, gdy moduł zostanie rozszerzony o inny moduł lub klasę.

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

Tworzy to interesujący wzorzec, którego mogą użyć programiści:

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

Jak widać, ten pojedynczy moduł dodaje metody instancji, metody „klasy” i działa bezpośrednio na klasę docelową (w tym przypadku wywołanie metody a_class_method ()).

ActiveSupport :: Concern hermetyzuje ten wzorzec. Oto ten sam moduł przepisany do korzystania z 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
Donato
źródło
-1

W tej chwili myślę o templatewzorcu projektowym. Po prostu nie byłoby dobrze z modułem.

Geo
źródło