W moim ostatnim projekcie zdefiniowałem klasę z następującym nagłówkiem:
public class Node extends ArrayList<Node> {
...
}
Jednak po rozmowie z moim profesorem CS stwierdził, że klasa będzie zarówno „okropna dla pamięci”, jak i „zła praktyka”. Nie uważam, że pierwsze jest szczególnie prawdziwe, a drugie subiektywne.
Moje uzasadnienie tego użycia jest takie, że wpadłem na pomysł obiektu, który musiał zostać zdefiniowany jako coś, co może mieć dowolną głębię, w którym zachowanie instancji można zdefiniować albo za pomocą niestandardowej implementacji, albo poprzez zachowanie kilku podobnych obiektów wchodzących w interakcje . Pozwoliłoby to na abstrakcję obiektów, których fizyczna implementacja składałaby się z wielu współdziałających podskładników
Z drugiej strony widzę, jak może to być zła praktyka. Pomysł zdefiniowania czegoś jako listy własnej nie jest prosty ani fizyczny.
Czy istnieje jakiś ważny powód, dla którego nie powinienem tego używać w kodzie, biorąc pod uwagę moje użycie?
¹ Z przyjemnością to wyjaśnię, chętnie to zrobię; Próbuję tylko zachować zwięzłość tego pytania.
źródło
ArrayList<Node>
, w porównaniu do implementacjiList<Node>
?Odpowiedzi:
Szczerze mówiąc, nie widzę tutaj potrzeby dziedziczenia. To nie ma sensu;
Node
jestArrayList
zNode
?Jeśli jest to tylko rekurencyjna struktura danych, wystarczy napisać coś takiego:
Co ma sens; węzeł ma listę węzłów potomnych lub potomnych.
źródło
Item
jest iItem
działa niezależnie od list.Node
toArrayList
zNode
jakNode
zawiera 0 do wieluNode
obiektów. Ich funkcja jest zasadniczo taka sama, ale zamiast być listą podobnych obiektów, ma listę podobnych obiektów. Pierwotny projekt napisanej klasy polegał na przekonaniu, że każdyNode
może być wyrażony jako zbiór napisówNode
, co robią obie implementacje, ale ten z pewnością jest mniej mylący. +1Children
tylko raz lub dwa, i zwykle lepiej jest napisać go w takich przypadkach ze względu na przejrzystość kodu.Argument „okropny dla pamięci” jest całkowicie błędny, ale jest to obiektywnie „zła praktyka”. Dziedzicząc po klasie, nie dziedziczysz tylko pól i metod, którymi jesteś zainteresowany. Zamiast tego dziedziczysz wszystko . Każda metoda, którą deklaruje, nawet jeśli nie jest dla Ciebie przydatna. A co najważniejsze, dziedziczysz także wszystkie umowy i gwarancje, które zapewnia klasa.
Skrót SOLID zapewnia pewną heurystykę dla dobrego projektowania obiektowego. Tutaj ja nterface Segregacja Zasada (ISP) i L iskov Zmiana Pricinple (LSP) mają coś do powiedzenia.
ISP mówi nam, aby nasze interfejsy były jak najmniejsze. Ale dziedzicząc po
ArrayList
, masz wiele metod. Jest to sensowneget()
,remove()
,set()
(wymienić) alboadd()
(wkładka) węzeł dziecko w określonym indeksie? Czy ma to sensensureCapacity()
w przypadku bazowej listy? Co to oznacza dlasort()
węzła? Czy użytkownicy Twojej klasy naprawdę powinni to zrobićsubList()
? Ponieważ nie możesz ukryć metod, których nie chcesz, jedynym rozwiązaniem jest posiadanie ArrayList jako zmiennej składowej i przekazywanie wszystkich metod, których faktycznie potrzebujesz:Jeśli naprawdę chcesz wszystkich metod, które widzisz w dokumentacji, możemy przejść do LSP. LSP mówi nam, że musimy być w stanie korzystać z podklasy wszędzie tam, gdzie oczekiwana jest klasa nadrzędna. Jeśli funkcja przyjmuje
ArrayList
parametr jako, aNode
zamiast tego przekazujemy nasze , nic nie powinno się zmienić.Zgodność podklas zaczyna się od prostych rzeczy, takich jak podpisy typów. Podczas przesłonięcia metody nie można zaostrzyć typów parametrów, ponieważ może to wykluczać zastosowania, które były legalne dla klasy nadrzędnej. Ale to jest coś, co kompilator sprawdza dla nas w Javie.
Ale LSP działa znacznie głębiej: musimy zachować zgodność ze wszystkim, co obiecuje dokumentacja wszystkich klas nadrzędnych i interfejsów. W swojej odpowiedzi Lynn znalazł jeden taki przypadek, w którym
List
interfejs (który odziedziczyłeś za pośrednictwemArrayList
) gwarantuje, jak powinny działać metodyequals()
ihashCode()
. NahashCode()
jesteś nawet biorąc pod uwagę konkretny algorytm, który musi być realizowany dokładnie. Załóżmy, że napisałeśNode
:Wymaga to, że
value
nie mogą przyczyniać sięhashCode()
i nie mogą wpływaćequals()
.List
Interfejs - które obiecują cześć przez dziedziczenie z nim - wymaganew Node(0).equals(new Node(123))
, aby mogło być prawdziwe.Ponieważ dziedziczenie po klasach zbyt łatwo przypadkowo złamać obietnicę złożoną przez klasę nadrzędną, a ponieważ zwykle ujawnia więcej metod, niż zamierzałeś, ogólnie sugeruje się, że wolisz kompozycję niż dziedziczenie . Jeśli musisz coś odziedziczyć, zaleca się dziedziczenie tylko interfejsów. Jeśli chcesz ponownie wykorzystać zachowanie określonej klasy, możesz zachować ją jako osobny obiekt w zmiennej instancji, w ten sposób wszystkie obietnice i wymagania nie zostaną narzucone.
Czasami nasz język naturalny sugeruje związek spadkowy: samochód to pojazd. Motocykl to pojazd. Czy powinienem definiować klasy
Car
iMotorcycle
dziedziczyć je poVehicle
klasie? Projektowanie obiektowe nie polega na odzwierciedlaniu realnego świata dokładnie w naszym kodzie. W naszym kodzie źródłowym nie możemy z łatwością zakodować bogatej taksonomii realnego świata.Jednym z takich przykładów jest problem modelowania pracownik-szef. Mamy wiele
Person
liter, każda z nazwą i adresem. AnEmployee
jest aPerson
i maBoss
. ABoss
jest takżePerson
. Czy powinienem więc stworzyćPerson
klasę dziedziczoną przezBoss
iEmployee
? Teraz mam problem: szef jest także pracownikiem i ma innego przełożonego. Wygląda na to, żeBoss
powinien się rozszerzyćEmployee
. Ale toCompanyOwner
jestBoss
ale nie jestEmployee
? Jakikolwiek wykres dziedziczenia jakoś tu się załamie.OOP nie dotyczy hierarchii, dziedziczenia i ponownego wykorzystywania istniejących klas, chodzi o uogólnienie zachowania . OOP polega na tym, że „mam mnóstwo obiektów i chcę, aby zrobiły określoną rzecz - i nie obchodzi mnie to, w jaki sposób”. Po to są interfejsy . Jeśli zaimplementujesz
Iterable
interfejs dla siebie,Node
ponieważ chcesz, aby był iterowalny, to w porządku. Jeśli implementujeszCollection
interfejs, ponieważ chcesz dodawać / usuwać węzły potomne itp., To w porządku. Ale dziedziczenie od innej klasy, ponieważ zdarza się, że daje ci wszystko, co nie jest, a przynajmniej nie, chyba że dokładnie przemyślisz to, jak opisano powyżej.źródło
Samo przedłużanie pojemnika jest zwykle uważane za złą praktykę. Naprawdę nie ma powodu, aby rozszerzać pojemnik zamiast go mieć. Poszerzenie pojemnika sprawia, że jest to bardzo dziwne.
źródło
Oprócz tego, co zostało powiedziane, istnieje nieco specyficzny dla Javy powód, aby unikać tego rodzaju struktur.
Kontrakt
equals
metody dla list wymaga, aby listę uznać za równą innemu obiektowiŹródło: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)
W szczególności oznacza to, że posiadanie klasy zaprojektowanej jako lista sama w sobie może sprawić, że porównania równości będą kosztowne (i obliczenia skrótu również, jeśli listy są zmienne), a jeśli klasa ma pewne pola instancji, należy je zignorować w porównaniu równości .
źródło
Odnośnie pamięci:
Powiedziałbym, że to kwestia perfekcjonizmu. Domyślny konstruktor
ArrayList
wygląda następująco:Źródło . Ten konstruktor jest także używany w Oracle-JDK.
Teraz zastanów się nad stworzeniem pojedynczo połączonej listy z twoim kodem. Właśnie z powodzeniem zwiększyłeś zużycie pamięci 10-krotnie (a ściślej nawet nieco wyżej). W przypadku drzew może to być również bardzo złe, bez żadnych specjalnych wymagań dotyczących struktury drzewa.
LinkedList
Alternatywnie użycie innej klasy powinno rozwiązać ten problem.Szczerze mówiąc, w większości przypadków jest to po prostu perfekcjonizm, nawet jeśli zignorujemy ilość dostępnej pamięci. A
LinkedList
nieco spowolniłby kod jako alternatywa, więc jest to kompromis między wydajnością a zużyciem pamięci. Jednak moim osobistym zdaniem w tej sprawie byłoby nie marnowanie tak dużej ilości pamięci w sposób, który można by tak łatwo obejść jak ten.EDYCJA: wyjaśnienie dotyczące komentarzy (dokładniej @amon). Ta część odpowiedzi nie dotyczy kwestii dziedziczenia. Porównanie wykorzystania pamięci odbywa się w stosunku do pojedynczo połączonej listy i najlepszego wykorzystania pamięci (w rzeczywistych implementacjach czynnik może się nieco zmienić, ale wciąż jest wystarczająco duży, aby zsumować całkiem sporo zmarnowanej pamięci).
W odniesieniu do „złej praktyki”:
Zdecydowanie. To nie jest standardowym sposobem realizacji wykres z prostego powodu: wykres-węzeł ma podrzędne węzły, nie jest to lista dzieci-węzłów. Precyzyjne wyrażanie tego, co masz na myśli w kodzie, to główna umiejętność. Czy to w nazwach zmiennych, czy w strukturach wyrażających, takich jak ta. Następny punkt: utrzymywanie interfejsów na minimalnym poziomie:
ArrayList
Udostępniłeś każdą metodę użytkownikowi klasy poprzez dziedziczenie. Nie można tego zmienić bez zerwania całej struktury kodu. Zamiast tego zapiszList
jako zmienną wewnętrzną i udostępnij wymagane metody za pomocą metody adaptera. W ten sposób możesz łatwo dodawać i usuwać funkcje z klasy bez zepsucia wszystkiego.źródło
new Object[0]
używa łącznie 4 słów, anew Object[10]
użyłoby tylko 14 słów pamięci.ArrayList
s rezygnują z dość dużej ilości pamięci.Wygląda to na semantyczny wzór złożony . Służy do modelowania rekurencyjnych struktur danych.
źródło
public class Node extends ArrayList<Node> { public string Item; }
jest dziedziczoną wersją twojej odpowiedzi. Twoje jest łatwiejsze do uzasadnienia, ale oba osiągają jedno: definiują drzewa niebinarne.