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?
object-oriented-design
inheritance
robotron
źródło
źródło
OrderDateInfo
s, od tych, które są istotne dla innychNamedEntity
sIdentifier
klasę nie mającą nic wspólnego zNamedEntity
wymaganiem zarówno właściwości, jakId
iName
własności, nie użyłbyśNamedEntity
zamiast tego. Kontekst i właściwe użycie klasy to coś więcej niż tylko jej właściwości i metody.Odpowiedzi:
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:
One
nie jest podmiotem kontrolowanym ani podmiotem nazwanym. To proste:Two
jest jednostką nazwaną, ale nie kontrolowaną. Zasadniczo masz to teraz:Three
jest zarówno wpisem nazwanym, jak i kontrolowanym. Tutaj napotykasz problem. Możesz utworzyćAuditedEntity
klasę podstawową, ale nie możeszThree
dziedziczyć zarówno, jakAuditedEntity
iNamedEntity
:Możesz jednak pomyśleć o obejściu,
AuditedEntity
odziedzicząc poNamedEntity
. Jest to sprytny hack, aby upewnić się, że każda klasa musi dziedziczyć (bezpośrednio) od jednej innej klasy.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.
Four
jest kontrolowaną jednostką, ale nie jednostką nazwaną. Ale nie możesz pozwolić naFour
dziedziczenie,AuditedEntity
tak jakNamedEntity
robiłbyś to z powodu dziedziczenia między AuditedEntityand
NamedEntity`.Korzystając z dziedziczenia, nie ma sposobu na zrobienie obu
Three
iFour
działanie, chyba że zaczniesz duplikować klasy (co otwiera zupełnie nowy zestaw problemów).Za pomocą interfejsów można to łatwo osiągnąć:
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:
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.
źródło
three
w 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.To jest coś, czego używam, aby zapobiec wykorzystaniu polimorfizmu.
Załóżmy, że masz 15 różnych klas, które mają
NamedEntity
jako klasę podstawową gdzieś w swoim łańcuchu dziedziczenia, i piszesz nową metodę, która ma zastosowanie tylko doOrderDateInfo
.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
FooBazNamedEntity
tam 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ćSqlException
lubFileNotFoundException
lub niestandardową wyjątku dla danej aplikacji. Klasy te często nie dostarczają więcej danych ani funkcji niżException
klasa 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?źródło
NamedEntity
tego, np. NaEntityName
, aby kompozycja miała sens, gdy została opisana.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.
źródło
IMHO projekt klasy jest zły. Powinno być.
OrderDateInfo
MA AName
jest relacją bardziej naturalną i tworzy dwie łatwe do zrozumienia klasy, które nie sprowokowałyby pierwotnego pytania.Każda metoda, która przyjęta
NamedEntity
jako parametr powinien być zainteresowany tylko wId
iName
właściwości, więc wszelkie takie metody powinny zostać zmienione, aby zaakceptowaćEntityName
zamiast.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!
źródło
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
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.
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
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
Person
struktury danych, problem, który rozwiązujemy, nie wymaga doskonałego modelowania Osoby, dlatego zaniedbaliśmy dodanie wspólnych właściwości, takich jakage
itp.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
age
pola później ).źródło