Аргумент "жахливий для пам'яті" абсолютно невірний, але це об'єктивно "погана практика". Коли ви успадковуєте клас, ви не успадковуєте лише поля та методи, які вас цікавлять. Натомість ви успадковуєте все . Кожен метод, який він оголошує, навіть якщо він не корисний для вас. І найголовніше, ви також успадковуєте всі його контракти та гарантії, які дає клас.
Акронім SOLID надає деякі евристики для гарного об'єктно-орієнтованого дизайну. Тут я nterface Сегрегація принцип (ISP) і L Іськів Заміна Pricinple (LSP) є що - то сказати.
Інтернет-провайдер пропонує нам зберігати наші інтерфейси якомога менше. Але успадковуючи від ArrayList
, ви отримуєте багато-багато методів. Чи є сенс get()
, remove()
, set()
(замінити), або add()
(вставка) дочірній вузол з певним індексом? Чи розумне ensureCapacity()
цього основного списку? Що це означає для sort()
Вузла? Чи дійсно повинні отримати користувачі вашого класу subList()
? Оскільки ви не можете приховати методи, які ви не хочете, єдине рішення - це мати ArrayList як змінну члена та переслати всі потрібні вам методи:
private final ArrayList<Node> children = new ArrayList();
public void add(Node child) { children.add(child); }
public Iterator<Node> iterator() { return children.iterator(); }
Якщо ви дійсно хочете, щоб усі методи ви бачили в документації, ми можемо перейти до LSP. LSP говорить нам, що ми повинні мати можливість використовувати підклас там, де очікується батьківський клас. Якщо функція приймається ArrayList
як параметр, і ми передаємо Node
натомість, нічого не повинно змінюватися.
Сумісність підкласів починається з простих речей, таких як підписи типів. Коли ви переосмислюєте метод, ви не можете робити типи параметрів більш суворими, оскільки це може виключати використання, які були законними для батьківського класу. Але це те, що компілятор перевіряє на нас на Java.
Але LSP працює набагато глибше: ми повинні підтримувати сумісність із усім, що обіцяно документацією всіх батьківських класів та інтерфейсів. У своїй відповіді Лінн знайшов один такий випадок, коли List
інтерфейс (який ви успадкували через ArrayList
) гарантує, як equals()
і hashCode()
методи повинні працювати. Для hashCode()
вас навіть дається певний алгоритм, який потрібно точно реалізувати. Припустимо, ви написали це 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);
}
...
}
Це вимагає, щоб value
не могли сприяти hashCode()
і не можуть впливати equals()
. List
Інтерфейс - що ви обіцяєте честь, успадкувавши від нього - вимагає , new Node(0).equals(new Node(123))
щоб бути правдою.
Оскільки успадкування від класів робить занадто легким випадкове порушення обіцянки батьківського класу, і тому що, як правило, викладається більше методів, ніж ви задумали, зазвичай пропонується віддати перевагу складу над спадщиною . Якщо вам потрібно щось успадкувати, пропонується успадкувати лише інтерфейси. Якщо ви хочете повторно використовувати поведінку певного класу, ви можете зберігати його як окремий об'єкт у змінній екземпляра, таким чином, всі його обіцянки та вимоги вам не вимушені.
Іноді наша природна мова говорить про спадкові відносини: Автомобіль - це транспортний засіб. Мотоцикл - транспортний засіб. Повинен чи я визначити класи Car
і Motorcycle
які успадковують від Vehicle
класу? Об'єктно-орієнтований дизайн полягає не в тому, щоб відображати реальний світ саме в нашому коді. Ми не можемо легко кодувати багаті таксономії реального світу у нашому вихідному коді.
Одним із таких прикладів є проблема моделювання працівник-начальник. У нас є декілька Person
s, кожен з іменем та адресою. А Employee
є Person
і має Boss
. А Boss
також є Person
. Тож я повинен створити Person
клас, який успадковується Boss
і Employee
? Зараз у мене проблема: начальник також є працівником і має іншого начальника. Тому, схоже, Boss
слід поширюватися Employee
. Але це CompanyOwner
є, Boss
але не є Employee
? Тут будь-який графік спадкування якось зламається.
OOP не стосується ієрархій, успадкування та повторного використання існуючих класів, це узагальнення поведінки . OOP - це "я маю купу об'єктів і хочу, щоб вони робили конкретну справу - і мені байдуже як". Ось для чого призначені інтерфейси . Якщо ви реалізуєте Iterable
інтерфейс для свого, Node
тому що хочете зробити його доступним, це абсолютно чудово. Якщо ви реалізуєте Collection
інтерфейс, оскільки ви хочете додати / видалити дочірні вузли тощо, то це добре. Але успадковуючи від іншого класу, оскільки трапляється дати тобі все, що ні, або, принаймні, ні, якщо ви не задумалися про це, як було зазначено вище.