Po co dziedziczyć klasę, a nie dodawać właściwości?

39

Znalazłem drzewo dziedziczenia w naszej (raczej dużej) bazie kodu, które wygląda mniej więcej tak:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

Z tego, co mogłem zebrać, jest to przede wszystkim używane do wiązania rzeczy na froncie.

Dla mnie ma to sens, ponieważ nadaje konkretną nazwę klasie, zamiast polegać na ogólnym NamedEntity. Z drugiej strony istnieje wiele takich klas, które po prostu nie mają żadnych dodatkowych właściwości.

Czy są jakieś wady tego podejścia?

robotron
źródło
25
Do góry nie wspomniano: Można odróżnić metody, które są istotne tylko dla OrderDateInfos, od tych, które są istotne dla innych NamedEntitys
Caleth
8
Z tego samego powodu, dla którego zdarzyło Ci się mieć, powiedzmy, Identifierklasę nie mającą nic wspólnego z NamedEntitywymaganiem zarówno właściwości, jak Idi Namewłasności, nie użyłbyś NamedEntityzamiast tego. Kontekst i właściwe użycie klasy to coś więcej niż tylko jej właściwości i metody.
Neil,

Odpowiedzi:

15

Dla mnie ma to sens, ponieważ nadaje konkretną nazwę klasie, zamiast polegać na ogólnej nazwie NamedEntity. Z drugiej strony istnieje wiele takich klas, które po prostu nie mają żadnych dodatkowych właściwości.

Czy są jakieś wady tego podejścia?

Podejście nie jest złe, ale dostępne są lepsze rozwiązania. Krótko mówiąc, interfejs byłby znacznie lepszym rozwiązaniem. Głównym powodem, dla którego interfejsy i dziedziczenie są różne, jest to, że można dziedziczyć tylko z jednej klasy, ale można zaimplementować wiele interfejsów .

Weźmy na przykład pod uwagę, że nazwano byty i poddano inspekcji. Masz kilka podmiotów:

Onenie jest podmiotem kontrolowanym ani podmiotem nazwanym. To proste:

public class One 
{ }

Twojest jednostką nazwaną, ale nie kontrolowaną. Zasadniczo masz to teraz:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Threejest zarówno wpisem nazwanym, jak i kontrolowanym. Tutaj napotykasz problem. Możesz utworzyć AuditedEntityklasę podstawową, ale nie możesz Threedziedziczyć zarówno, jak AuditedEntity i NamedEntity :

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

Możesz jednak pomyśleć o obejściu, AuditedEntityodziedzicząc po NamedEntity. Jest to sprytny hack, aby upewnić się, że każda klasa musi dziedziczyć (bezpośrednio) od jednej innej klasy.

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

To nadal działa. Ale to, co tutaj zrobiłeś, stwierdza, że każda kontrolowana jednostka jest z natury również jednostką nazwaną . Co prowadzi mnie do mojego ostatniego przykładu. Fourjest kontrolowaną jednostką, ale nie jednostką nazwaną. Ale nie możesz pozwolić na Fourdziedziczenie, AuditedEntitytak jak NamedEntityrobiłbyś to z powodu dziedziczenia między AuditedEntity andNamedEntity`.

Korzystając z dziedziczenia, nie ma sposobu na zrobienie obu Threei Fourdziałanie, chyba że zaczniesz duplikować klasy (co otwiera zupełnie nowy zestaw problemów).

Za pomocą interfejsów można to łatwo osiągnąć:

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

Jedyną niewielką wadą jest to, że nadal musisz wdrożyć interfejs. Ale zyskujesz wszystkie korzyści z posiadania wspólnego typu wielokrotnego użytku, bez żadnych wad, które pojawiają się, gdy potrzebujesz odmian wielu wspólnych typów dla danej jednostki.

Ale twój polimorfizm pozostaje nienaruszony:

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

Z drugiej strony istnieje wiele takich klas, które po prostu nie mają żadnych dodatkowych właściwości.

Jest to odmiana wzorca interfejsu znacznika , w której implementuje się pusty interfejs wyłącznie po to, aby móc użyć typu interfejsu do sprawdzenia, czy dana klasa jest „oznaczona” tym interfejsem.
Używasz klas odziedziczonych zamiast zaimplementowanych interfejsów, ale cel jest taki sam, więc zamierzam nazywać to „klasą oznaczoną”.

Na pierwszy rzut oka nie ma nic złego w interfejsach / klasach znaczników. Są poprawne pod względem składniowym i technicznym i nie ma nieodłącznych wad ich używania, pod warunkiem, że marker jest ogólnie prawdziwy (w czasie kompilacji) i nie jest warunkowy .

Właśnie tak należy rozróżniać różne wyjątki, nawet jeśli wyjątki te nie mają żadnych dodatkowych właściwości / metod w porównaniu z metodą podstawową.

Nie ma w tym nic złego, ale radziłbym to robić ostrożnie, upewniając się, że nie tylko próbujesz ukryć istniejący błąd architektoniczny źle zaprojektowanym polimorfizmem.

Flater
źródło
4
„możesz dziedziczyć tylko z jednej klasy, ale możesz zaimplementować wiele interfejsów”. Nie dotyczy to jednak każdego języka. Niektóre języki umożliwiają dziedziczenie wielu klas.
Kenneth K.,
jak powiedział Kenneth, dwa dość popularne języki mają możliwość wielokrotnego dziedziczenia, C ++ i Python. klasa threew twoim przykładzie pułapek nieużywania interfejsów jest w rzeczywistości poprawna na przykład w C ++, w rzeczywistości działa to poprawnie dla wszystkich czterech przykładów. W rzeczywistości wydaje się, że jest to ograniczenie języka C # jako języka, a nie przestroga polegająca na niestosowaniu dziedziczenia, gdy powinieneś używać interfejsów.
kiedy
63

To jest coś, czego używam, aby zapobiec wykorzystaniu polimorfizmu.

Załóżmy, że masz 15 różnych klas, które mają NamedEntityjako klasę podstawową gdzieś w swoim łańcuchu dziedziczenia, i piszesz nową metodę, która ma zastosowanie tylko do OrderDateInfo.

Możesz „po prostu” napisać podpis jako

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

I miejcie nadzieję i módlcie się, aby nikt nie nadużywał systemu czcionek, aby FooBazNamedEntitytam wprowadzić.

Lub „mógłbyś” po prostu napisać void MyMethod(OrderDateInfo foo). Teraz jest to wymuszone przez kompilator. Prosty, elegancki i nie polega na tym, że ludzie nie popełniają błędów.

Ponadto, jak wskazał @candied_orange , wyjątki są tego doskonałym przykładem. Bardzo rzadko (i mam na myśli bardzo, bardzo rzadko) czy kiedykolwiek chcesz złapać wszystko catch (Exception e). Bardziej prawdopodobne chcesz złapać SqlExceptionlub FileNotFoundExceptionlub niestandardową wyjątku dla danej aplikacji. Klasy te często nie dostarczają więcej danych ani funkcji niż Exceptionklasa podstawowa , ale pozwalają odróżnić to, co reprezentują, bez konieczności ich sprawdzania i sprawdzania pola typu lub wyszukiwania określonego tekstu.

Ogólnie rzecz biorąc, sztuczka jest taka, aby system typów pozwalał na użycie węższego zestawu typów niż gdybyś używał klasy bazowej. To znaczy, możesz zdefiniować wszystkie swoje zmienne i argumenty jako posiadające typ Object, ale to tylko utrudniłoby twoją pracę, prawda?

Becuzz
źródło
4
Przepraszam za moją nowość, ale jestem ciekawy, dlaczego nie byłoby lepszym podejściem do uczynienia OrderDateInfo Mają obiekt NamedEntity jako właściwość?
Adam B,
9
@AdamB To prawidłowy wybór projektu. To, czy będzie lepiej, czy nie, zależy między innymi od tego, do czego będzie on wykorzystywany. W przypadku wyjątku nie mogę utworzyć nowej klasy z właściwością wyjątku, ponieważ wtedy język (C # dla mnie) nie pozwala mi go wyrzucić. Mogą istnieć inne względy projektowe, które wykluczałyby użycie kompozycji zamiast dziedziczenia. Wiele razy kompozycja jest lepsza. To zależy tylko od twojego systemu.
Becuzz, 12.12.18
17
@AdamB Z tego samego powodu, kiedy dziedziczysz klasę podstawową i dodajesz metody lub właściwości, których brakuje w bazie, nie tworzysz nowej klasy i nie umieszczasz jej jako właściwości. Istnieje różnica między CZŁOWIEKIEM, KTÓRYM jest ssak, a POSIADANIEM ssaka.
Logarr,
8
@Logarr to prawda, że ​​istnieje różnica między mną, a ssakiem. Ale jeśli pozostanę wierny mojemu wewnętrznemu ssakowi, nigdy nie poznasz różnicy. Możesz się zastanawiać, dlaczego mogę podarować tak wiele różnych rodzajów mleka kaprysowi.
candied_orange
5
@AdamB Możesz to zrobić, co nazywa się kompozycją nad dziedziczeniem. Ale zmieniłbyś nazwę NamedEntitytego, np. Na EntityName, aby kompozycja miała sens, gdy została opisana.
Sebastian Redl,
28

To jest moje ulubione zastosowanie dziedziczenia. Używam go głównie do wyjątków, które mogłyby używać lepszych, bardziej szczegółowych nazw

Zwykła kwestia dziedziczenia prowadząca do długich łańcuchów i powodująca problem jo-jo nie ma tutaj zastosowania, ponieważ nic nie motywuje cię do tworzenia łańcucha.

candied_orange
źródło
1
Hmm, dobra uwaga na wyjątki. Chciałem powiedzieć, że to było okropne. Ale przekonałeś mnie inaczej, dzięki doskonałemu przypadkowi użycia.
David Arno,
Jo-jo Ho i butelka rumu. To rzadkie, że orlop jest yar. Często przecieka z braku właściwego dębu między deskami. Do szafki Davey Jonesa z lubulami lądowymi, które ją zbudowały. TŁUMACZENIE: Rzadko zdarza się, że łańcuch dziedziczenia jest tak dobry, że klasę podstawową można abstrakcyjnie / skutecznie zignorować.
radarbob,
Myślę, że warto zwrócić uwagę, że nowa klasa dziedziczy z innego ma dodać nową właściwość - nazwę nowej klasy. Właśnie tego używasz do tworzenia wyjątków o lepszych nazwach.
Logan Pickup
1

IMHO projekt klasy jest zły. Powinno być.

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoMA A Namejest relacją bardziej naturalną i tworzy dwie łatwe do zrozumienia klasy, które nie sprowokowałyby pierwotnego pytania.

Każda metoda, która przyjęta NamedEntityjako parametr powinien być zainteresowany tylko w Idi Namewłaściwości, więc wszelkie takie metody powinny zostać zmienione, aby zaakceptować EntityNamezamiast.

Jedynym technicznym powodem, dla którego zaakceptowałem oryginalny projekt, jest wiązanie własności, o czym wspomniał PO. Środowisko bzdur nie byłoby w stanie poradzić sobie z dodatkową właściwością i wiązać się z nią object.Name.Id. Ale jeśli twoje wiążące ramy nie są w stanie sobie z tym poradzić, masz jeszcze więcej długów technologicznych do dodania do listy.

Zgadzam się z odpowiedzią @ Flater, ale z wieloma interfejsami zawierającymi właściwości kończy się pisanie dużej ilości kodu, nawet z pięknymi automatycznymi właściwościami C #. Wyobraź sobie, że robisz to w Javie!

Batwad
źródło
1

Klasy ujawniają zachowanie, a struktury danych ujawniają dane.

Widzę słowa kluczowe klasy, ale nie widzę żadnego zachowania. Oznacza to, że zacznę oglądać tę klasę jako strukturę danych. W tym duchu sformułuję twoje pytanie jako

Dlaczego wspólna struktura danych najwyższego poziomu?

Możesz więc użyć typu danych najwyższego poziomu. Pozwala to na wykorzystanie systemu typów w celu zapewnienia polityki dla dużych zestawów różnych struktur danych poprzez zapewnienie, że wszystkie właściwości są dostępne.

Dlaczego struktura danych obejmuje najwyższy poziom, ale nic nie dodaje?

Możesz więc użyć typu danych niższego poziomu. Pozwala to na umieszczanie podpowiedzi w systemie pisania w celu wyrażenia celu zmiennej

Top level data structure - Named
   property: name;

Bottom level data structure - Person

W powyższej hierarchii wygodnie jest określić, że Osoba jest nazwana, aby ludzie mogli uzyskać i zmienić jej imię. Chociaż może być uzasadnione dodanie dodatkowych właściwości do Personstruktury danych, problem, który rozwiązujemy, nie wymaga doskonałego modelowania Osoby, dlatego zaniedbaliśmy dodanie wspólnych właściwości, takich jak ageitp.

Jest to więc wykorzystanie systemu pisania, aby wyrazić zamiar tego Nazwanego elementu w sposób, który nie psuje się wraz z aktualizacjami (jak dokumentacja) i można go z łatwością rozszerzyć w późniejszym terminie (jeśli okaże się, że naprawdę potrzebujesz agepola później ).

Edwin Buck
źródło