Наследование против дополнительного свойства с нулевым значением

12

Для классов с необязательными полями лучше использовать наследование или свойство, допускающее значение NULL? Рассмотрим этот пример:

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"
}

Код в зависимости от этих 3 вариантов будет:

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

или

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

или

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

Первый подход выглядит более естественным для меня, но, возможно, он менее читабелен из-за необходимости выполнять приведение. Есть ли другой способ добиться этого поведения?

Боян ВукасовичТест
источник
1
Боюсь, что решение сводится к вашему определению бизнес-правил. Я имею в виду, что если все ваши книги могут иметь цвет, но цвет является необязательным, то наследование является излишним и фактически ненужным, потому что вы хотите, чтобы все ваши книги знали, что свойство color существует. С другой стороны, если у вас могут быть как книги, которые ничего не знают о цвете, так и книги, у которых есть цвет, создание специализированных классов неплохо. Даже тогда я, вероятно, пошел бы за композицией, а не наследованием.
Энди
Чтобы сделать это немного более конкретным - есть 2 типа книг - одна с цветом и одна без. Так что не все книги могут иметь цвет.
Боян ВукасовичТест
1
Если действительно есть случай, когда книга вообще не имеет цвета, в вашем случае наследование - это самый простой способ расширить свойства красочной книги. В этом случае не похоже, что вы что-то сломаете, унаследовав базовый класс книги и добавив к нему свойство color. Вы можете прочитать о принципе замены Лискова, чтобы узнать больше о случаях, когда расширение класса разрешено, а когда нет.
Энди
4
Можете ли вы определить поведение, которое вы хотите из книг с раскраской? Можете ли вы найти обычное поведение среди книг с раскраской и без раскраски, где книги с раскраской должны вести себя немного иначе? Если это так, то это хороший случай для ООП с разными типами, и идея заключается в том, чтобы перенести поведение в классы вместо внешнего запроса наличия и значения свойства.
Эрик Эйдт
1
Если возможные цвета книг известны заранее, как насчет поля Enum для BookColor с опцией для BookColor.NoColor?
Грэм

Ответы:

7

Это зависит от обстоятельств. Конкретный пример нереалистичен, так как у вас не будет подкласса, вызываемого BookWithColorв реальной программе.

Но в целом свойство, которое имеет смысл только для определенных подклассов, должно существовать только в этих подклассах.

Например , если Bookесть PhysicalBookи в DigialBookкачестве потомков, то PhysicalBookможет иметь weightсвойство, и DigitalBookв sizeInKbсобственность. Но DigitalBookне будет weightи наоборот. Bookне будет иметь ни одного свойства, так как класс должен иметь свойства, общие для всех потомков.

Лучший пример - смотреть на классы из реального программного обеспечения. JSliderКомпонент имеет поле majorTickSpacing. Так как только у слайдера есть «галочки», это поле имеет смысл только для JSliderи его потомков. Было бы очень странно, если бы у других родственных компонентов, таких JButtonкак majorTickSpacingполе, было поле.

JacquesB
источник
или вы можете уменьшить вес, чтобы иметь возможность отражать оба, но это, вероятно, слишком много.
Вальфрат
3
@Walfrat: Было бы очень плохо, если бы одно и то же поле представляло совершенно разные вещи в разных подклассах.
JacquesB
Что если в книге есть как физические, так и цифровые издания? Вам придется создавать разные классы для каждой перестановки наличия или отсутствия каждого необязательного поля. Я думаю, что лучшим решением было бы просто пропустить некоторые поля или нет в зависимости от книги. Вы процитировали совершенно другую ситуацию, когда два класса будут вести себя по-разному функционально. Это не то, что подразумевает этот вопрос. Им просто нужно смоделировать структуру данных.
4каста
@ 4castle: Вам понадобятся отдельные классы для каждого издания, так как каждое издание также может иметь разную цену. Вы не можете решить это с помощью дополнительных полей. Но мы не знаем из примера, если оператору нужно другое поведение для книг с цветными или «бесцветными книгами», поэтому невозможно узнать, каково правильное моделирование в этом случае.
JacquesB
3
согласен с @JacquesB. Вы не можете дать универсально правильное решение для «книг по моделированию», потому что это зависит от того, какую проблему вы пытаетесь решить и чем занимается ваш бизнес. Я думаю, что можно с уверенностью сказать, что определение иерархии классов, в которой вы нарушаете Лискова, категорически неверно (тм), хотя (да, ваш случай, вероятно, очень особенный и заслуживает его (хотя я сомневаюсь в этом))
Сара
5

Важный момент, который, кажется, не был упомянут: в большинстве языков экземпляры класса не могут изменить класс, к которому они относятся. Так что если у вас была книга без цвета, и вы хотели добавить цвет, вам нужно было бы создать новый объект, если вы используете разные классы. И тогда вам, вероятно, придется заменить все ссылки на старый объект ссылками на новый объект.

Если «книги без цвета» и «книги с цветом» являются экземплярами одного и того же класса, то добавление цвета или удаление цвета будет намного меньшей проблемой. (Если ваш пользовательский интерфейс отображает список «книг с цветом» и «книг без цвета», то этот пользовательский интерфейс, очевидно, должен был бы измениться, но я ожидаю, что с этим вам все равно придется справиться, как в «красном списке»). книги "и" список зеленых книг ").

gnasher729
источник
Это действительно важный момент! Он определяет, следует ли рассматривать набор необязательных свойств вместо атрибутов класса.
хроманоид
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инкапсулировать целый «класс» книг, обладающих специализированными возможностями по сравнению с другими книгами, то имеет смысл использовать наследование.

4castle
источник
1
Кто сказал что-нибудь о базе данных? Если бы все шаблоны ОО были вписаны в парадигму реляционной базы данных, нам пришлось бы изменить множество шаблонов ОО.
alexanderbird
Я бы сказал, что добавление неиспользуемых свойств «на всякий случай» - наименее понятный вариант. Как говорили другие, это зависит от варианта использования, но наиболее желательной ситуацией будет не носить свойства, когда внешнее приложение должно знать, действительно ли используется существующее свойство.
Фолькер Куеффель
@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 {
}

Пояснение (правка):

Просто добавление необязательных полей в классе сущности

Особенно с помощью Optional-wrapper (правка: см. Ответ 4castle ). Я думаю, что это (добавление полей в исходную сущность) - это жизнеспособный способ добавления новых свойств в небольшом масштабе. Самая большая проблема этого подхода заключается в том, что он может работать против слабой связи.

Представьте, что ваш класс книги определен в отдельном проекте для вашей модели предметной области. Теперь вы добавляете другой проект, который использует модель домена для выполнения специальной задачи. Эта задача требует дополнительного свойства в классе книги. Либо вы получаете наследование (см. Ниже), либо вам нужно изменить модель общего домена, чтобы сделать новую задачу возможной. В последнем случае вы можете получить кучу проектов, которые все зависят от своих собственных свойств, добавленных в класс книги, в то время как сам класс книги в некотором роде зависит от этих проектов, потому что вы не можете понять класс книги без дополнительные проекты.

Почему наследование проблематично, когда речь идет о способе предоставления дополнительных свойств?

Когда я вижу ваш пример класса «Книга», я думаю о доменном объекте, который часто заканчивается множеством необязательных полей и подтипов. Представьте, что вы хотите добавить свойство для книг, которые включают в себя компакт-диск. В настоящее время существует четыре вида книг: книги, цветные книги, книги с компакт-дисками, цветные книги и компакт-диски. Вы не можете описать эту ситуацию с наследованием в Java.

С интерфейсами вы обходите эту проблему. Вы можете легко составить свойства определенного класса книги с помощью интерфейсов. Делегирование и состав позволят легко получить именно тот класс, который вам нужен. С наследованием вы обычно получаете некоторые необязательные свойства в классе, которые существуют только потому, что они нужны классу-брату.

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

Почему сторонники ООП обычно считают наследование плохим

JavaWorld: почему расширяется зло

Проблема с реализацией набора интерфейсов для составления набора свойств

Когда вы используете интерфейсы для расширения, все в порядке, если у вас есть только небольшой набор из них. Особенно, когда ваша объектная модель используется и расширяется другими разработчиками, например, в вашей компании, количество интерфейсов будет расти. И в конечном итоге вы в конечном итоге создаете новый официальный «интерфейс свойств», который добавляет метод, который ваши коллеги уже использовали в своем проекте для клиента X для совершенно не связанного варианта использования - тьфу.

редактировать: еще один важный аспект упоминается gnasher729 . Вы часто хотите динамически добавлять дополнительные свойства к существующему объекту. С наследованием или интерфейсами вам придется воссоздавать весь объект с другим классом, который далеко не обязателен.

Когда вы ожидаете расширения вашей объектной модели до такой степени, вам будет лучше, если вы явно смоделируете возможность динамического расширения. Я предлагаю что-то вроде выше, где каждое «расширение» (в данном случае свойство) имеет свой собственный класс. Класс работает как пространство имен и идентификатор для расширения. Когда вы устанавливаете соглашения об именах пакетов соответствующим образом, этот способ позволяет бесконечное количество расширений, не загрязняя пространство имен для методов в исходном классе сущности.

При разработке игр вы часто сталкиваетесь с ситуациями, когда вы хотите составить поведение и данные во многих вариациях. Вот почему архитектурный шаблон entity-component-system стал довольно популярным в кругах разработчиков игр. Это также интересный подход, на который вы, возможно, захотите взглянуть, когда ожидаете много расширений для вашей объектной модели.

chromanoid
источник
И это не решило вопрос «как выбирать между этими вариантами», это просто еще один вариант. Почему это всегда лучше, чем все варианты, представленные в ОП?
alexanderbird
Вы правы, я улучшу ответ.
хроманоид
@ 4castle В Java интерфейсы не наследуются! Реализация интерфейсов - это способ соблюдения контракта, в то время как наследование класса в основном расширяет его поведение. Вы можете реализовать несколько интерфейсов, в то время как вы не можете расширять несколько классов в одном классе. В моем примере вы расширяете интерфейсы, в то время как вы используете наследование для наследования поведения исходной сущности.
хроманоид
Имеет смысл. В вашем примере, PropertiesHolderвероятно, будет абстрактный класс. Но что бы вы предложили для реализации getPropertyили getPropertyValue? Данные все еще должны храниться в каком-то поле экземпляра где-нибудь. Или вы бы использовали Collection<Property>для хранения свойств вместо использования полей?
4каста
1
Я сделал Bookрасширение PropertiesHolderдля ясности. PropertiesHolderОбычно должен быть членом во всех классах , которые нуждаются в этой функции. Реализация будет содержать Map<Class<? extends Property<?>>,Property<?>>или что-то вроде этого. Это уродливая часть реализации, но она обеспечивает действительно хорошую среду, которая работает даже с JAXB и JPA.
хроманоид
-1

Если Bookкласс выводим без свойства цвета, как показано ниже

class E_Book extends Book{
   private String type;
}

тогда наследование разумно.

Адем Каглин
источник
Я не уверен, что понимаю ваш пример? Если colorэто private, то любой производный класс вообще не должен интересоваться color. Просто добавьте несколько конструкторов к Bookэтому игнорированию color, и он будет по умолчанию null.
4каста