Спадщина проти додаткової властивості з нульовим значенням


12

Чи краще для класів з необов’язковими полями використовувати успадкування або властивість, що зводиться нанівець? Розглянемо цей приклад:

class Book {
    private String name;
}
class BookWithColor extends Book {
    private String color;
}

або

class Book {
    private String name;
    private String color; 
    //when this is null then it is "Book" otherwise "BookWithColor"
}

або

class Book {
    private String name;
    private Optional<String> color;
    //when isPresent() is false it is "Book" otherwise "BookWithColor"
}

Код залежно від цих трьох варіантів буде:

if (book instanceof BookWithColor) { ((BookWithColor)book).getColor(); }

або

if (book.getColor() != null) { book.getColor(); }

або

if (book.getColor().isPresent()) { book.getColor(); }

Перший підхід для мене виглядає більш природно, але, можливо, він менш читабельний через необхідність робити кастинг. Чи є якийсь інший спосіб досягти такої поведінки?


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

Щоб зробити його трохи більш конкретним - є 2 типи книг - одна з кольором та одна без. Тому не всі книги можуть мати колір.
Bojan VukasovicTest

1
Якщо дійсно є випадок, коли книга зовсім не має кольору, у вашому випадку успадкування - це найпростіший спосіб розширити властивості барвистої книги. У цьому випадку не схоже, що ви щось порушите, успадкувавши базовий клас книги та додавши до нього властивість кольору. Ви можете прочитати про принцип заміщення лісков, щоб дізнатися більше про випадки, коли розширення класу дозволено та коли це не так.
Енді

4
Чи можете ви визначити поведінку, яку ви хочете з книг за кольором? Чи можете ви знайти спільну поведінку серед книг із забарвленням та без нього, де книги з забарвленням повинні поводитись трохи інакше? Якщо це так, це хороший випадок для OOP з різними типами, і ідеєю було б перенести поведінку в класи замість того, щоб допитувати наявність властивості та значення зовні.
Ерік Ейдт

1
Якщо можливі кольори книг відомі достроково, як щодо поля Enum для BookColor з опцією для BookColor.NoColor?
Грем

Відповіді:


7

Це залежить від обставин. Конкретний приклад нереальний, оскільки у вас не було б підкласу, названого BookWithColorв реальній програмі.

Але в цілому властивість, яка має сенс лише для певних підкласів, повинна існувати лише в цих підкласах.

Наприклад , якщо Bookє PhysicalBookі в DigialBookякості нащадків, то PhysicalBookможе мати weightвластивість, і DigitalBookв sizeInKbвласність. Але DigitalBookне матиме weightі навпаки. Bookне матиме жодної властивості, оскільки клас повинен мати лише властивості, якими поділяються всі нащадки.

Кращим прикладом є перегляд занять із реального програмного забезпечення. JSliderКомпонент має поле majorTickSpacing. Оскільки лише у повзуна є "кліщі", це поле має сенс лише для JSliderйого нащадків. Було б дуже заплутано, якби інші компоненти, такі як брат, JButtonмали majorTickSpacingполе.


або ви могли б збільшити вагу, щоб мати можливість відобразити і те, і інше, але це, мабуть, занадто багато.
Вальфрат

3
@Walfrat: Було б дуже поганою ідеєю, щоб одне і те ж поле представляло абсолютно різні речі в різних підкласах.
ЖакБ

Що робити, якщо книга має як фізичне, так і цифрове видання? Вам потрібно було б створити інший клас для кожної перестановки щодо наявності чи відсутності кожного необов'язкового поля. Я думаю, що найкращим рішенням було б лише дозволити пропущення деяких полів або не залежно від книги. Ви цитували зовсім іншу ситуацію, коли два класи поводилися б по-різному функціонально. Це не те, що має на увазі це питання. Їм просто потрібно моделювати структуру даних.
4castle

@ 4castle: Вам потрібні окремі класи на видання, оскільки кожне видання також може мати різну ціну. Ви не можете вирішити це за допомогою додаткових полів. Але ми не знаємо з прикладу, чи бажають оператори різної поведінки для книг з кольором або "безбарвних книжок", тому неможливо дізнатися, яке правильне моделювання в цьому випадку.
ЖакБ

3
погоджуюся з @JacquesB. ви не можете дати універсально правильне рішення для "моделювання книг", оскільки це залежить від того, яку проблему ви намагаєтеся вирішити та який ваш бізнес. Я думаю, що цілком безпечно сказати, що визначення ієрархій класів, де ви порушуєте лисков, є категорично неправильним (тм), хоча (так, так, ваш випадок, мабуть, дуже особливий і це вимагає (хоча я сумніваюсь у цьому))
Сара

5

Важливий момент, який, здається, не згадується: У більшості мов екземпляри класу не можуть змінити, для якого класу вони є. Отже, якщо у вас була книга без кольору, і ви хотіли додати колір, вам потрібно створити новий об’єкт, якщо ви використовуєте різні класи. І тоді вам, ймовірно, потрібно буде замінити всі посилання на старий об’єкт посиланнями на новий об’єкт.

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


Це дійсно важливий момент! Він визначає, чи слід розглядати колекцію необов’язкових властивостей замість атрибутів класу.
хроманоїд

1

Подумайте про JavaBean (як ваш Book) як запис у базі даних. Необов’язкові стовпці - це nullколи вони не мають значення, але цілком законно. Тому ваш другий варіант:

class Book {
    private String name;
    private String color; // null when not applicable
}

Є найбільш розумним. 1

Будьте уважні, хоча ви використовуєте Optionalклас. Наприклад, це не так Serializable, як правило, характерно для JavaBean. Ось кілька порад від Стівена Колбурна :

  1. Не оголошуйте жодної змінної примірника типу Optional.
  2. Використовуйте nullдля вказівки необов'язкових даних у приватному масштабі класу.
  3. Використовуйте Optionalдля одержувачів, які отримують доступ до необов'язкового поля.
  4. Не використовуйте Optionalв установниках або конструкторах.
  5. Використовувати Optionalяк тип повернення для будь-яких інших методів ділової логіки, які мають необов'язковий результат.

Таким чином, в своєму класі , ви повинні використовувати , nullщоб представити , що поле немає, але коли color листя на Book(як тип повертається значення ) він повинен бути обгорнутий з Optional.

return Optional.ofNullable(color); // inside the class

book.getColor().orElse("No color"); // outside the class

Це забезпечує чіткий дизайн та більш читабельний код.


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


1
Хто щось сказав про базу даних? Якби всі шаблони ОО мали вписатись у парадигму реляційних баз даних, нам довелося б змінити багато моделей ОО.
alexanderbird

Я заперечую, що додавання невикористаних властивостей "про всяк випадок" є найменш зрозумілим варіантом. Як говорили інші, це залежить від випадку використання, але найбільш бажаною ситуацією було б не переносити властивості, коли зовнішній додаток повинен знати, чи використовується власне існуюча властивість.
Volker Kueffel

@Volker Давайте погоджуємося не погоджуватися, тому що я можу навести кілька людей, які грали руку, додаючи Optionalклас до Java для цієї точної мети. Це може бути не ОО, але це, безумовно, справедлива думка.
4касл

@Alexanderbird Я спеціально вирішував JavaBean, який повинен бути серіалізаційним. Якщо я був поза темою, виховуючи JavaBeans, то це була моя помилка.
4касл

@Alexanderbird Я не очікую, що всі шаблони відповідають реляційній базі даних, але я очікую, що Java Bean
4castle

0

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

Так чи

interface Colored {
  Color getColor();
}
class ColoredBook extends Book implements Colored {
  ...
}

або

class PropertiesHolder {
  <T> extends Property<?>> Optional<T> getProperty( Class<T> propertyClass ) { ... }
  <V, T extends Property<V>> Optional<V> getPropertyValue( Class<T> propertyClass ) { ... }
}
interface Property<T> {
  String getName();
  T getValue();
}
class ColorProperty implements Property<Color> {
  public Color getValue() { ... }
  public String getName() { return "color"; }
}
class Book extends PropertiesHolder {
}

Пояснення (редагування):

Просто додайте необов'язкові поля в клас сутності

Особливо з додатковою обгорткою (редагуйте: див. Відповідь 4castle ). Я думаю, що це (додавання полів у оригінальну сутність) є життєздатним способом додавання нових властивостей у невеликому масштабі. Найбільшою проблемою такого підходу є те, що він може працювати проти низької зчеплення.

Уявіть, що ваш клас книг визначений у спеціальному проекті для вашої доменної моделі. Тепер ви додаєте ще один проект, який використовує модель домену для виконання спеціального завдання. Це завдання вимагає додаткової властивості в класі книги. Або ви закінчите спадщину (див. Нижче), або вам доведеться змінити загальну модель домену, щоб зробити нове завдання можливим. В останньому випадку ви можете закінчити купу проектів, які залежать від їх власних властивостей, доданих до класу книг, тоді як сам клас книги певним чином залежить від цих проектів, оскільки ви не в змозі зрозуміти клас книги без додаткові проекти.

Чому успадкування проблематично, коли мова йде про спосіб надання додаткових властивостей?

Коли я бачу ваш клас прикладу "Книга", я замислююся про об'єкт домену, який часто виникає з багатьма необов'язковими полями і підтипами. Уявіть собі, що ви хочете додати властивість для книг, які містять компакт-диск. Зараз існує чотири види книг: книги, книги з кольором, книги з компакт-дисками, книги з кольором та компакт-диск. Ви не можете описати цю ситуацію при спадкуванні на Java.

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

Далі читайте, чому спадкування часто є проблематичною ідеєю:

Чому прихильники ООП взагалі сприймають спадщину як погану річ

JavaWorld: Чому поширюється - це зло

Проблема з реалізацією набору інтерфейсів для складання набору властивостей

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

редагувати: Ще один важливий аспект згадується gnasher729 . Вам часто хочеться динамічно додавати необов'язкові властивості до вже наявного об'єкта. За допомогою успадкування або інтерфейсів вам доведеться відтворити весь об'єкт за допомогою іншого класу, який далеко не є обов'язковим.

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

У процесі розвитку ігор ви часто стикаєтеся з ситуаціями, коли ви хочете складати поведінку та дані у багатьох варіантах. Ось чому структура -компонентна система архітектури отримала досить велику популярність у колах розвитку ігор. Це також цікавий підхід, який ви хочете переглянути, коли очікуєте багато розширень вашої об'єктної моделі.


І це не стосувалося питання "як визначитися між цими варіантами", це просто інший варіант. Чому це завжди краще, ніж усі запропоновані ОП варіанти?
alexanderbird

Ви праві, я вдосконалю відповідь.
хроманоїд

@ 4castle В Java інтерфейси не є спадщиною! Реалізація інтерфейсів - це спосіб домогтися контракту, а успадковування класу в основному розширює його поведінку. Ви можете реалізувати декілька інтерфейсів, хоча ви не можете розширити кілька класів в одному класі. У моєму прикладі ви розширюєте інтерфейси, в той час як використовуєте спадщину для успадкування поведінки оригінальної сутності.
хроманоїд

Має сенс. У вашому прикладі, PropertiesHolderмабуть, був би абстрактний клас. Але що б ви запропонували бути реалізацією для getPropertyабо getPropertyValue? Дані все-таки повинні десь зберігатися в якомусь полі екземпляра. Або ви б використовувати a Collection<Property>для зберігання властивостей замість поля?
4castle

1
Я зробив Bookпродовження PropertiesHolderз ясності. PropertiesHolderЗазвичай повинен бути членом у всіх класах , які потребують в цій функції. Впровадження матиме Map<Class<? extends Property<?>>,Property<?>>щось подібне. Це потворна частина реалізації, але вона забезпечує дійсно приємну основу, яка працює навіть з JAXB та JPA.
хроманоїд

-1

Якщо Bookклас можна отримати без властивості кольору, як показано нижче

class E_Book extends Book{
   private String type;
}

то успадкування розумне.


Я не впевнений, що я розумію ваш приклад? Якщо colorприватний, то будь-який похідний клас ні в colorякому разі не повинен стосуватися . Просто додайте кілька конструкторів до Bookцього ігнору colorвзагалі, і це буде за замовчуванням null.
4castle
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.