Що було б недоліком у визначенні класу як підкласу самого списку?


48

У недавньому моєму проекті я визначив клас із таким заголовком:

public class Node extends ArrayList<Node> {
    ...
}

Однак, обговоривши з моїм професором CS, він заявив, що і клас буде "жахливим для пам'яті" та "поганою практикою". Я не вважав, що перша є особливо правдивою, а друга - суб’єктивною.

Моє міркування цього використання полягає в тому, що у мене була ідея для об'єкта, який потрібно визначити як щось, що може мати довільну глибину, де поведінку екземпляра можна визначити або за допомогою власної реалізації, або за поведінкою кількох подібних об'єктів, що взаємодіють . Це дозволило б абстрагувати об'єкти, фізична реалізація яких складалася б з багатьох взаємодіючих взаємодіючих компонентів.¹

З іншого боку, я бачу, як це може бути поганою практикою. Ідея визначити щось як перелік себе не є простою або фізично реалізованою.

Чи є якась поважна причина, чому я не повинен використовувати це в своєму коді, враховуючи моє використання для цього?


¹ Якщо мені потрібно пояснити це далі, я би радий; Я просто намагаюся зробити це питання стислим.



1
@gnat Це більше стосується суворого визначення класу як списку самого себе, а не списків, що містять списки. Я думаю, що це варіант подібної речі, але не зовсім той самий. Це питання стосується чогось , що відповідає відповіді Роберта Харві .
Аддісон Крим

16
Спадщина від чогось, що параметризується само собою, не є незвичним, як показано загальновживану ідіому CRTP в C ++. Що стосується контейнера, головне питання полягає в тому, щоб виправдати, чому ви повинні використовувати спадщину замість складу.
Крістоф

1
Мені подобається ідея. Адже кожен вузол на дереві - це (під) дерево. Зазвичай дерев яність вузла приховується від виду типу (класичний вузол - це не дерево, а окремий об'єкт) і замість цього виражається у вигляді даних (один вузол дозволяє отримати доступ до гілок). Ось навпаки; ми не побачимо даних філій, вони входять у тип. За іронією долі, власне макет об’єкта в C ++, ймовірно, буде дуже схожим (успадкування реалізується дуже аналогічно вмісту даних), що змушує мене думати, що ми маємо справу з двома способами вираження одного і того ж.
Пітер - Відновіть Моніку

1
Можливо, я трохи пропускаю, але чи справді вам потрібно розширити ArrayList<Node> , порівняно з впроваджувати List<Node> ?
Маттіас

Відповіді:


107

Чесно кажучи, я не бачу тут необхідності в спадкуванні. Це не має сенсу; Node це ArrayList з Node?

Якщо це лише рекурсивна структура даних, ви просто напишіть щось на зразок:

public class Node {
    public String item;
    public List<Node> children;
}

Що має сенс; вузол має список дітей або нащадків.


34
Це не одне і те ж. Список списку списку ad-infinitum є безглуздим, якщо ви не зберігаєте щось, крім нового списку в списку. Це те, що Item існує, і Itemдіє незалежно від списків.
Роберт Харві

6
О, я бачу зараз. Я підійшов Node це ArrayList в Nodeякості Node містить 0 для багатьох Node об'єктів. Їх функція по суті однакова, але замість того, щоб бути списком подібних об'єктів, у неї є список подібних об’єктів. Оригінальним дизайном для написаного класу було переконання, що будь-який Nodeможе бути виражений як набір підрядів Node, що і обидві реалізації, але це, безумовно, менш заплутано. +1
Addison Crump

11
Я б сказав, що ідентичність між вузлом і списком має тенденцію виникати "природно", коли ви пишете окремо пов'язані списки в C або Lisp або будь-що інше: у певних проектах головний вузол "це" список, в сенсі є єдиним річ, що колись використовувалася для представлення списку Тому я не можу повністю розвіяти відчуття, що в деяких контекстах, можливо, вузол "є" (а не просто "має") колекцію вузлів, хоча я згоден, це відчуває EvilBadWrong на Java.
Стів Джессоп

12
@ nl-x Це те, що один синтаксис коротший, не означає, що він кращий. Також, до речі, досить рідко можна отримати доступ до таких елементів у такому дереві. Зазвичай структура не відома під час компіляції, тому ви, як правило, не обходите такі дерева постійними значеннями. Глибина також не фіксована, тому ви навіть не можете повторити всі значення за допомогою цього синтаксису. Коли ви адаптуєте код для використання традиційної проблеми обходу дерев, ви закінчуєте писати Childrenлише один-два рази, і зазвичай бажано виписувати його в таких випадках задля ясності коду.
Сервіс


57

Аргумент "жахливий для пам'яті" абсолютно невірний, але це об'єктивно "погана практика". Коли ви успадковуєте клас, ви не успадковуєте лише поля та методи, які вас цікавлять. Натомість ви успадковуєте все . Кожен метод, який він оголошує, навіть якщо він не корисний для вас. І найголовніше, ви також успадковуєте всі його контракти та гарантії, які дає клас.

Акронім 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класу? Об'єктно-орієнтований дизайн полягає не в тому, щоб відображати реальний світ саме в нашому коді. Ми не можемо легко кодувати багаті таксономії реального світу у нашому вихідному коді.

Одним із таких прикладів є проблема моделювання працівник-начальник. У нас є декілька Persons, кожен з іменем та адресою. А Employeeє Personі має Boss. А Bossтакож є Person. Тож я повинен створити Personклас, який успадковується Bossі Employee? Зараз у мене проблема: начальник також є працівником і має іншого начальника. Тому, схоже, Bossслід поширюватися Employee. Але це CompanyOwnerє, Bossале не є Employee? Тут будь-який графік спадкування якось зламається.

OOP не стосується ієрархій, успадкування та повторного використання існуючих класів, це узагальнення поведінки . OOP - це "я маю купу об'єктів і хочу, щоб вони робили конкретну справу - і мені байдуже як". Ось для чого призначені інтерфейси . Якщо ви реалізуєте Iterableінтерфейс для свого, Nodeтому що хочете зробити його доступним, це абсолютно чудово. Якщо ви реалізуєте Collectionінтерфейс, оскільки ви хочете додати / видалити дочірні вузли тощо, то це добре. Але успадковуючи від іншого класу, оскільки трапляється дати тобі все, що ні, або, принаймні, ні, якщо ви не задумалися про це, як було зазначено вище.


10
Я надрукував ваші останні 4 абзаци, назвав їх « Про OOP та спадщину» та наклеїв їх на стіну під час роботи. Дехто з моїх колег повинен бачити, що як я знаю, вони оцінять так, як ви це висловили :) Дякую.
YSC

17

Подовження контейнера само по собі зазвичай вважається поганою практикою. Насправді дуже мало причин розширити контейнер, а не просто мати. Розтягування контейнера з собою просто робить його дивним.


14

Додавши до сказаного, є дещо специфічна для Java причина, щоб уникнути подібних структур.

Договір equalsметоду для списків вимагає, щоб список вважався рівним іншому об'єкту

якщо і лише якщо вказаний об'єкт також є списком, обидва списки мають однаковий розмір, і всі відповідні пари елементів у двох списках рівні .

Джерело: https://docs.oracle.com/javase/7/docs/api/java/util/List.html#equals(java.lang.Object)

Зокрема, це означає, що наявність класу, розробленого як перелік самих, може зробити порівняння рівності дорогим (і хеш-обчислення, а також якщо списки є змінними), а якщо клас має деякі екземплярні поля, добре, що їх слід ігнорувати при порівнянні рівності .


1
Це відмінний привід усунути це з мого коду, окрім поганої практики. : P
Addison Crump

3

Щодо пам’яті:

Я б сказав, що це питання перфекціонізму. Конструктор за замовчуванням ArrayListвиглядає так:

public ArrayList(int initialCapacity) {
     super();

     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);

     this.elementData = new Object[initialCapacity];
 }

public ArrayList() {
     this(10);
}

Джерело . Цей конструктор також використовується в Oracle-JDK.

Тепер розглянемо можливість створення спільно пов'язаного списку зі своїм кодом. Ви щойно успішно пом'якшили споживання пам'яті коефіцієнтом 10x (якщо бути точним навіть незначно вище). Для дерев це також може бути дуже поганим, без особливих вимог до структури дерева. Використання іншого LinkedListчи іншого класу повинно вирішити це питання.

Якщо чесно, в більшості випадків це не що інше, як просто перфекціонізм, навіть якщо ми ігноруємо кількість наявної пам'яті. A LinkedListтрохи уповільнить код як альтернативу, так що це компроміс між продуктивністю та споживанням пам'яті в будь-якому випадку. І все-таки моя особиста думка з цього приводу полягає в тому, щоб не витрачати стільки пам’яті таким чином, що її можна так просто обійти, як цю.

EDIT: роз'яснення щодо коментарів (@amon, щоб бути точним). Цей розділ відповіді не стосується питання спадкування. Порівняння використання пам’яті проводиться по відношенню до спільно пов’язаного списку та з найкращим використанням пам’яті (у фактичних реалізаціях фактор може дещо змінитись, але він все ще досить великий, щоб підсумувати зовсім небагато витраченої пам’яті).

Щодо "Поганої практики":

Безумовно. Це не стандартний спосіб реалізації графа з простої причини: Graph-Node має дочірні вузли, а не такий, як список дочірніх вузлів. Точне вираження того, що ви маєте на увазі під кодом, є головним вмінням. Будь то в назвах змінних або в виражальних структурах, як ця. Наступний пункт: мінімізація інтерфейсів: Ви зробили кожен метод ArrayListдоступним для користувача класу шляхом успадкування. Неможливо змінити це, не порушивши всю структуру коду. Замість цього зберігайте Listяк внутрішню змінну та надайте необхідні методи через адаптер-метод. Таким чином ви можете легко додавати та видаляти функціональні можливості з класу, не псуючи все.


1
На 10 разів вище порівняно з чим ? Якщо у мене створено за замовчуванням ArrayList як поле екземпляра (замість того, щоб успадковувати його), я фактично використовую трохи більше пам’яті, оскільки мені потрібно зберігати додатковий вказівник на ArrayList у своєму об’єкті. Навіть якщо ми порівнюємо яблука з апельсинами і порівнюємо успадковування від ArrayList з тим, що не використовуємо жодного ArrayList, зауважте, що в Java ми завжди маємо справу з покажчиками на об’єкти, ніколи не об'єкти за значенням, як це робив би C ++. Якщо припустимо, що new Object[0]вживається 4 слова, new Object[10]то пам'ять використовує лише 14 слів.
амон

@amon Я цього не заявив прямо, просимо за це. Хоча контекст повинен був бути достатнім, щоб побачити, що я порівнюю з LinkedList. І це називається посиланням, а не вказівником btw. І справа в пам’яті полягала не в тому, використовується клас через спадкування чи ні, а просто у тому, що (майже) невикористаний ArrayLists затримує зовсім небагато пам’яті.
Павло

-2

Схоже, це виглядає як семантика складеного малюнка . Він використовується для моделювання рекурсивних структур даних.


13
Я не підкажу цього, але саме тому я дуже ненавиджу книгу GoF. Це кровоточить дерево! Чому для опису дерева нам потрібно було винайти термін «складений візерунок»?
Девід Арно

3
@DavidArno Я вважаю, що цілий аспект поліморфного вузла визначає його як "композитний візерунок", використання структури даних про дерево - лише його частина.
JAB

2
@RobertHarvey, я не згоден (і з вашими коментарями до вашої відповіді, і тут). public class Node extends ArrayList<Node> { public string Item; }- версія спадкування вашої відповіді. З вами простіше міркувати, але обидва досягають певної речі: вони визначають небінарні дерева.
Девід Арно

2
@DavidArno: Я ніколи не говорив, що це не спрацює, я просто сказав, що це, мабуть, не ідеально.
Роберт Харві

1
@DavidArno тут не про почуття та смак, а про факти. Дерево складається з вузлів, і кожен вузол має потенційних дітей. Даний вузол - вузол листів лише в даний момент часу. У складі композитний вузол є листовим за конструкцією, і його природа не зміниться.
Крістоф
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.