2009-08-15 12 views
113

W Rubim, ponieważ możesz dołączyć wiele mixinów, ale rozszerzyć tylko jedną klasę, wydaje się, że preferuje się mixiny zamiast dziedziczenia.Dziedziczenie rubinowe vs mixiny

Moje pytanie: jeśli piszesz kod, który musi być rozszerzony/dołączony, aby był przydatny, dlaczego miałbyś go uczynić klasą? Innymi słowy, dlaczego nie zawsze tworzyłbyś moduł?

Mogę wymyślić tylko jeden powód, dla którego chcesz mieć zajęcia, i to jest, jeśli musisz utworzyć instancję klasy. Jednak w przypadku ActiveRecord :: Base, nigdy nie tworzysz tego bezpośrednio. Więc czy nie powinien to być moduł?

Odpowiedz

156

I tylko przeczytać o tym temacie w The Well-Grounded Rubyist (wielkie książki, przy okazji). Autor radzi sobie lepiej niż ja, więc zacytuję go:


Żadna pojedyncza reguła lub wzór nie zawsze daje odpowiedni projekt. Ale jest przydatna do utrzymania kilka rozważań na uwadze, kiedy robisz klasa-versus-modułu decyzje:

  • Moduły nie mają wystąpień. Wynika z tego, że podmioty lub rzeczy są ogólnie najlepsze zamodelowane w klasach, a cechy lub właściwości podmiotów lub rzeczy są najlepiej zamknięte w modułach. Odpowiednio, jak zauważono w sekcji 4.1.1, nazwy klasy są zwykle rzeczownikami, podczas gdy nazwy modułów są często przymiotnikami (stos versus Stacklike).

  • Klasa może mieć tylko jedną nadklasę, ale może mieszać tyle modułów, ile chce. Jeśli używasz dziedziczenia , nadaj priorytet tworzeniu relację nadklasy/podklasy . Nie używaj jedynej w swoim rodzaju relacji klasy nadrzędnej do klasy , wyposażając klasę w coś, co może okazać się jednym z kilku zestawów cech.

Podsumowując te zasady w jednym przykładzie, tutaj jest to, czego nie należy robić:

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

Raczej powinien to zrobić:

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

Drugie modele wersji podmioty i właściwości znacznie bardziej starannie. Ciężarówka wywodzi się od Pojazdu (co ma sens), natomiast SelfPropelling jest cechą charakterystyczną pojazdów (a przynajmniej tych wszystkich, na których nam zależy w tym modelu świata) - cecha, która jest przekazywana ciężarówkom na mocy ciężarówki będącej potomkiem lub wyspecjalizowaną formę pojazdu, o nazwie .

+5

+1 za wyliczoną odpowiedź –

+0

Świetna odpowiedź +1 –

+0

Przykład pokazuje to zgrabnie - Truck IS A Vehicle - nie ma ciężarówki, która nie byłaby pojazdem. –

-1

W tej chwili myślę o wzorze projektu template. Po prostu nie pasuje do modułu.

9

Moje zdanie: Moduły są przeznaczone do dzielenia się zachowaniem, podczas gdy klasy służą do modelowania relacji między obiektami. Z technicznego punktu widzenia można po prostu uczynić wszystko instancją obiektu i połączyć w dowolne moduły, które chcemy uzyskać pożądany zestaw zachowań, ale byłby to kiepski, przypadkowy i raczej nieczytelny projekt.

+0

To odpowiada na pytanie w sposób bezpośredni: dziedziczenie wymusza określoną strukturę organizacyjną, która może sprawić, że twój projekt bardziej czytelny. – emery

9

Odpowiedź na twoje pytanie jest w dużej mierze kontekstualna. Destylując obserwacje pubb, wybór jest przede wszystkim prowadzony przez rozważaną domenę.

I tak, ActiveRecord powinien zostać włączony, a nie rozszerzony przez podklasę. Kolejny ORM - datamapper - precyzyjnie to osiąga!

4

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

Z tego samego powodu bardzo podoba mi się MongoId nad MongoMapper, ponieważ pozostawia deweloperowi szansę wykorzystania dziedziczenia jako sposobu modelowania czegoś znaczącego w domenie problemu.

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

38

Myślę, że mixins to świetny pomysł, ale jest tu jeszcze jeden problem, o którym nikt nie wspomniał: kolizje nazw. Rozważ:

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 z nich wygrywa? W Ruby okazuje się, że to drugie, module B, ponieważ uwzględniłeś go po module A. Teraz łatwo uniknąć tego problemu: upewnij się, że wszystkie stałe i metody module A isą w mało prawdopodobnym obszarze nazw. Problem polega na tym, że kompilator nie ostrzega w ogóle, gdy dochodzi do kolizji.

Uważam, że to zachowanie nie jest skalowalne dla dużych zespołów programistów - nie powinieneś zakładać, że osoba wdrażająca class C wie o każdym nazwisku w zakresie. Ruby pozwoli ci nawet zastąpić stałą lub metodę inny typ. Nie jestem pewien, czy można uznać za poprawne zachowanie kiedykolwiek.

+4

to zachowanie jest niedopuszczalne w większości środowisk. –

+2

To jest rozsądna uwaga. Przypomina wiele pułapek dziedziczenia w C++. –

+1

Czy są jakieś dobre środki zaradcze? Wygląda to na powód, dla którego wiele dziedzin Pythona jest lepszym rozwiązaniem (nie próbując uruchomić dopasowania do języka, tylko porównując tę ​​konkretną funkcję). – Marcin