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


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поле, было поле.


или вы можете уменьшить вес, чтобы иметь возможность отражать оба, но это, вероятно, слишком много.
— Вальфрат

3
@Walfrat: Было бы очень плохо, если бы одно и то же поле представляло совершенно разные вещи в разных подклассах.
— JacquesB

Что если в книге есть как физические, так и цифровые издания? Вам придется создавать разные классы для каждой перестановки наличия или отсутствия каждого необязательного поля. Я думаю, что лучшим решением было бы просто пропустить некоторые поля или нет в зависимости от книги. Вы процитировали совершенно другую ситуацию, когда два класса будут вести себя по-разному функционально. Это не то, что подразумевает этот вопрос. Им просто нужно смоделировать структуру данных.
— 4каста

@ 4castle: Вам понадобятся отдельные классы для каждого издания, так как каждое издание также может иметь разную цену. Вы не можете решить это с помощью дополнительных полей. Но мы не знаем из примера, если оператору нужно другое поведение для книг с цветными или «бесцветными книгами», поэтому невозможно узнать, каково правильное моделирование в этом случае.
— JacquesB

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 Давайте согласимся не соглашаться, потому что я могу привести несколько людей, которые сыграли свою роль в добавлении 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 стал довольно популярным в кругах разработчиков игр. Это также интересный подход, на который вы, возможно, захотите взглянуть, когда ожидаете много расширений для вашей объектной модели.


И это не решило вопрос «как выбирать между этими вариантами», это просто еще один вариант. Почему это всегда лучше, чем все варианты, представленные в ОП?
— 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каста
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.