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 sensowne get()
, remove()
, set()
(wymienić) albo add()
(wkładka) węzeł dziecko w określonym indeksie? Czy ma to sens ensureCapacity()
w przypadku bazowej listy? Co to oznacza dla sort()
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:
private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }
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, a Node
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średnictwem ArrayList
) gwarantuje, jak powinny działać metody equals()
i hashCode()
. Na hashCode()
jesteś nawet biorąc pod uwagę konkretny algorytm, który musi być realizowany dokładnie. Załóżmy, że napisałeś Node
:
public class Node extends ArrayList<Node> {
public final int value;
public Node(int value, Node... children) {
this.value = Value;
for (Node child : children)
add(child);
}
...
}
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 - wymaga new 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
i Motorcycle
dziedziczyć je po Vehicle
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. An Employee
jest a Person
i ma Boss
. A Boss
jest także Person
. Czy powinienem więc stworzyć Person
klasę dziedziczoną przez Boss
i Employee
? Teraz mam problem: szef jest także pracownikiem i ma innego przełożonego. Wygląda na to, że Boss
powinien się rozszerzyć Employee
. Ale to CompanyOwner
jest Boss
ale nie jest Employee
? 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 implementujesz Collection
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.