Наследование, композиция и методы по умолчанию

Обычно признается, что расширение реализации интерфейса посредством наследования не является лучшей практикой, и что композиция (например, повторная реализация интерфейса с нуля) более удобна в сопровождении.

Это работает, потому что контракт интерфейса заставляет пользователя реализовать все желаемые функции. Однако в java 8 методы по умолчанию обеспечивают некоторое поведение по умолчанию, которое можно переопределить «вручную». Рассмотрим следующий пример: я хочу создать пользовательскую базу данных, которая должна иметь функциональные возможности списка. Я решил, в целях эффективности, поддержать его ArrayList.

public class UserDatabase extends ArrayList<User>{}

Обычно это не считается хорошей практикой, и было бы предпочтительнее, если бы на самом деле желали использовать все возможности списка и следовали обычному девизу «композиция вместо наследования»:

public class UserDatabase implements List<User>{
  //implementation here, using an ArrayList type field, or decorator pattern, etc.
}

Однако, если не обращать внимания, некоторые методы, такие как spliterator(), не потребуется переопределять, так как они являются методами интерфейса List по умолчанию. Загвоздка в том, что метод spliterator() в List работает намного хуже, чем метод spliterator() в ArrayList, оптимизированный для конкретной структуры ArrayList.

Это заставляет разработчика

  1. имейте в виду, что ArrayList имеет свою собственную, более эффективную реализацию spliterator(), и вручную переопределяет метод spliterator() своей собственной реализации List или
  2. потерять огромное количество производительности, используя метод по умолчанию.

Итак, вопрос: остается ли «верным» то, что в таких ситуациях следует предпочесть композицию наследованию?


person Ulysse Mizrahi    schedule 06.07.2015    source источник
comment
В некоторой степени предпочтение композиции наследованию связано с лучшей стратегией реализации (вы упомянули некоторые проблемы). Но настоящая ценность — это концептуальная выразительность: база данных — это не список! Это приводит к гораздо лучшему API вашего класса базы данных.   -  person Seelenvirtuose    schedule 06.07.2015
comment
Я не об этом, я выбрал слово «база данных», но это могло быть что-то более близкое к реальному списку, скажем, GroceryList.   -  person Ulysse Mizrahi    schedule 06.07.2015
comment
На самом деле пример, который вы привели сюда, не имеет ничего общего с «композицией по наследству». Эта фраза означает, что вместо расширения класса с помощью 'extends' вы должны хранить экземпляр этого класса и использовать его там, где это уместно.   -  person Tomer    schedule 06.07.2015
comment
Преимущество композиции по сравнению с наследованием в основном заключается в гибкости, поэтому проблема, о которой вы упомянули, не влияет на преимущество. Имейте в виду, что композиция над наследованием не является вопросом истины, она более адекватна или нет в данной ситуации.   -  person Jean-Baptiste Yunès    schedule 06.07.2015
comment
Итак, вы выбрали ArrayList, потому что знаете, что это хорошо, но вам лень переопределять методы по умолчанию и ожидать чего-то от разработчиков языка?   -  person AdamSkywalker    schedule 06.07.2015
comment
если вы хотите лучших результатов, вам нужно лучше знать код, который вы используете, в методах по умолчанию нет ничего нового, что противоречило бы этому утверждению.   -  person AdamSkywalker    schedule 06.07.2015
comment
Как указал @fatman, ваш пример композиции должен был быть public class UserDatabase{ List<User> aList = new ArrayList<>(); //Other code}   -  person Blip    schedule 06.07.2015
comment
Обратите внимание, что разделитель по умолчанию становится намного хуже только для параллельной обработки. Для последовательного не намного хуже, чем ArrayList сплитератор.   -  person Tagir Valeev    schedule 06.07.2015
comment
@UlysseMizrahi Я не понимаю твоей дилеммы. Почему тот факт, что ArrayList имеет более быструю реализацию splititerator, влияет на ваше решение сделать выбор между композицией и наследованием? Какое отношение детали реализации имеют к выбору между наследованием и композицией? Какое отношение default методов имеет к выбору между ними? Полагаясь на детали реализации для выбора композиции, вы, в первую очередь, бросаете вызов самой цели композиции. Не путайте себя. Держи это просто глупо.   -  person CKing    schedule 06.07.2015
comment
Если вы сочиняете поверх ArrayList, почему нельзя делегировать метод spliterator() UserDatabase методу spliterator() базового ArrayList?   -  person Brian Goetz    schedule 06.07.2015
comment
@Brian Goetz: насколько я понял, это теоретический вопрос, касающийся возможности того, что это делегирование не происходит, потому что а) разработчик пропустил это или б) такой метод default был добавлен после код делегирования был написан. Таким образом, spliterator() — это только пример для обсуждения шаблона проектирования в целом.   -  person Holger    schedule 07.07.2015


Ответы (5)


Прежде чем начать думать о производительности, мы всегда должны думать о корректности, т.е. в вашем вопросе мы должны учитывать, что подразумевает использование наследования вместо делегирования. Это уже проиллюстрировано этой проблемой EclipseLink/JPA. Из-за наследования сортировка (то же относится и к потоковой операции) не работает, если лениво заполняемый список еще не заполнен.

Таким образом, мы должны найти компромисс между возможностью того, что специализации, переопределяющие новые методы default, полностью сломаются в случае наследования, и возможностью того, что методы default не будут работать с максимальной производительностью в случае делегирования. Думаю, ответ должен быть очевиден.

Поскольку ваш вопрос касается того, меняют ли новые методы default ситуацию, следует подчеркнуть, что вы говорите о снижении производительности по сравнению с чем-то, чего раньше даже не существовало. Давайте остановимся на примере sort. Если вы используете делегирование и не переопределяете метод сортировки default, метод default может иметь меньшую производительность, чем оптимизированный метод ArrayList.sort, но до Java 8 последнего не существовало, а алгоритм, не оптимизированный для ArrayList, был стандартом< /эм> поведение.

Таким образом, вы не теряете производительность при делегировании в Java 8, вы просто не получаете больше, если не переопределяете метод default. За счет других доработок, полагаю, производительность все равно будет лучше, чем под Java 7 (без default методов).

API Stream сложно сравнивать, так как API не существовало до Java 8. Однако ясно, что аналогичные операции, например. если вы выполняете сокращение вручную, у вас не было другого выбора, кроме как пройти через Iterator вашего списка делегирования, который должен был быть защищен от remove() попыток, следовательно, обернуть ArrayList Iterator или использовать size() и get(int) которые делегируют поддержку List. Таким образом, не существует сценария, в котором API методов до default мог бы демонстрировать лучшую производительность, чем методы default API Java 8, поскольку в прошлом в любом случае не было оптимизации, специфичной для ArrayList.

Тем не менее, дизайн вашего API можно улучшить, если использовать композицию по-другому: вообще не позволяя UserDatabase реализовывать List<User>. Просто предложите List через метод доступа. Затем другой код не будет пытаться выполнять потоковую передачу по экземпляру UserDatabase, а по списку, возвращаемому методом доступа. Возвращаемый список может быть оболочка только для чтения, которая обеспечивает оптимальную производительность, поскольку она обеспечивается самой JRE, и заботится о переопределении методов default, где это возможно.

person Holger    schedule 06.07.2015
comment
Это то, что я пытался сказать в своем ответе, но это дало мне отрицательный голос. Надеюсь, что ваша многословность (и гораздо более аргументированный ответ!) послужит вам лучше :-) - person oligofren; 06.07.2015
comment
Интересно, поменяли ли вы термины в этом предложении: ... новые методы по умолчанию полностью ломаются в случае наследования и вероятность того, что они не работают с максимальной производительностью в случае делегирования. Все наоборот, если использовать случай OP. Он хотел наследовать ArrayList, чтобы получить производительность, а при использовании делегирования (реализации интерфейса) вы бы получили метод по умолчанию, ухудшение производительности, возможно, полную поломку. - person oligofren; 06.07.2015
comment
@oligofren: это не поменялось местами, но, возможно, это было недостаточно четко. Если вы используете наследование, вы можете получить специализацию методов default, которые работают с внутренними компонентами, игнорируя любую причину создания подклассов, что может привести к поломке. В сценарии делегирования вы получаете default методов, надежно работающих только с интерфейсом. - person Holger; 06.07.2015

Я не очень понимаю здесь большую проблему. Вы по-прежнему можете поддерживать свой UserDatabase с помощью ArrayList, даже если не расширяете его, и получить производительность за счет делегирования. Вам не нужно расширять его, чтобы получить производительность.

public class UserDatabase implements List<User>{
   private ArrayList<User> list = new ArrayList<User>();

   // implementation ...

   // delegate
   public Spliterator() spliterator() { return list.spliterator(); }
}

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

Я до сих пор не уверен, имеет ли смысл реализовывать интерфейс List, если только вы явно не создаете повторно используемую библиотеку Collection. Лучше создайте свой собственный API для таких одноразовых проектов, который не приведет к будущим проблемам через цепочку наследования (или интерфейса).

person oligofren    schedule 06.07.2015
comment
Это действительно упоминается в моем вопросе. Все дело в том, что, поскольку разработчик не обязан реализовывать метод spliterator(), это вынуждает его полагаться на свои знания о реализации метода spliterator() в ArrayList, чтобы использовать его производительность, что обычно нежелательно. - person Ulysse Mizrahi; 06.07.2015
comment
Вопрос заключался в том, верно ли, что в таких ситуациях следует предпочесть композицию наследованию? Ответ на это был да. Методы по умолчанию не меняют этого. Неважно, используете ли вы наследование или композицию, вам все равно нужно знать о снижении производительности, чтобы проектировать с учетом этого. - person oligofren; 06.07.2015
comment
@oligofren Элегантный ответ. Я разместил комментарий к вопросу только для того, чтобы понять, что кто-то уже опубликовал ответ с точно такими же рассуждениями, что и у меня. Грустно видеть, что даже после того, как вы попали в самую точку с таким элегантным ответом, вы все равно получили отрицательный голос. +1 от меня, чтобы аннулировать отрицательный голос. - person CKing; 06.07.2015

Я не могу дать совет для каждой ситуации, но для этого конкретного случая я бы предложил вообще не реализовывать List. Какова будет цель UserDatabase.set(int, User)? Вы действительно хотите заменить i-ю запись в резервной базе данных совершенно новым пользователем? А add(int, User)? Мне кажется, что вы должны либо реализовать его как список только для чтения (выбрасывая UnsupportedOperationException при каждом запросе модификации), либо поддерживать только некоторые методы модификации (например, add(User) поддерживается, а add(int, User) — нет). Но последний случай сбил бы пользователей с толку. Лучше предоставить свой собственный API модификации, который больше подходит для вашей задачи. Что касается запросов на чтение, наверное, было бы лучше возвращать поток пользователей:

Я бы предложил создать метод, который возвращает Stream:

public class UserDatabase {
    List<User> list = new ArrayList<>();

    public Stream<User> users() {
        return list.stream();
    }
}

Обратите внимание, что в этом случае вы можете полностью изменить реализацию в будущем. Например, замените ArrayList на TreeSet или ConcurrentLinkedDeque или что-то еще.

person Tagir Valeev    schedule 06.07.2015
comment
@AdamSkywalker Ответ на самом деле не имел ничего общего с базой данных как таковой, а касался проблем проектирования, связанных с отношениями IS-A. - person oligofren; 06.07.2015

Выбор прост в зависимости от ваших требований.

Примечание. Ниже приведен пример использования. чтобы проиллюстрировать разницу.

Если вам нужен список, в котором не будет дубликатов и который будет делать целую кучу вещей, сильно отличающихся от ArrayList, тогда нет смысла расширять ArrayList, потому что вы пишете все с нуля.

В приведенном выше вы должны Implement List. Но если вы просто оптимизируете реализацию ArrayList, вам следует скопировать и вставить всю реализацию ArrayList и следовать оптимизации вместо extending ArrayList. Почему, потому что многоуровневая реализация мешает кому-то разобраться.

Например: компьютер с 4 ГБ оперативной памяти в качестве родителя и ребенка имеет 8 ГБ оперативной памяти. Плохо, если у родителя 4 ГБ, а у нового ребенка 4 ГБ, чтобы получилось 8 ГБ. Вместо детской реализации с 8 Гб ОЗУ.

Я бы предложил composition в этом случае. Но он будет меняться в зависимости от сценария.

person Viswanath Lekshmanan    schedule 06.07.2015

Обычно признается, что расширение реализации интерфейса посредством наследования не является лучшей практикой, и что композиция (например, повторная реализация интерфейса с нуля) более удобна в сопровождении.

Я вообще не думаю, что это точно. Конечно, есть много ситуаций, когда композиция предпочтительнее наследования, но есть много ситуаций, когда наследование предпочтительнее композиции!

Особенно важно понимать, что структура наследования ваших классов реализации не обязательно должна быть похожа на структуру наследования вашего API.

Кто-нибудь действительно верит, например, что при написании графической библиотеки, такой как Java swing, каждый класс реализации должен переопределять метод paintComponent()? На самом деле весь принцип дизайна заключается в том, что при написании методов рисования для новых классов вы можете вызывать super.paint() и это гарантирует, что все элементы в иерархии будут отрисованы, а также дальнейшее решение сложностей, связанных с взаимодействием с собственным интерфейсом. вверх по дереву.

Общепризнано, что расширение классов, не находящихся под вашим контролем, которые не предназначены для поддержки наследования, опасно и потенциально может стать источником раздражающих ошибок при изменении реализации. (Поэтому отметьте свои классы как окончательные, если вы оставляете за собой право изменить свою реализацию!). Я сомневаюсь, что Oracle внесет критические изменения в реализацию ArrayList! Если вы уважаете его документацию, вы должны быть в порядке....

В этом изящество дизайна. Если они решат, что есть проблема с ArrayList, они напишут новый класс реализации, подобно тому, как когда-то заменили Vector, и не будет необходимости вносить критические изменения.

===============

В вашем текущем примере оперативный вопрос: почему этот класс вообще существует?

Если вы пишете класс, который расширяет интерфейс списка, какие другие методы он реализует? Если он не реализует новых методов, что плохого в использовании ArrayList?

Когда вы знаете ответ, вы будете знать, что делать. Если ответ «Мне нужен объект, который в основном представляет собой список, но имеет некоторые дополнительные удобные методы для работы с этим списком», тогда я должен использовать композицию.

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

person phil_20686    schedule 06.07.2015
comment
Свинг — очень плохой пример. Он использует много делегирования, поэтому подклассы часто работают без переопределения методов. Существуют модели данных, модели выбора, UI делегирование, Border и Icon абстракции, дочерние компоненты и т. д. На самом деле, многие из этих подклассов существуют только для того, чтобы упростить работу разработчиков, привыкших к другим средам, таким как простой AWT, и т.д. Например класс JButton почти ничего не добавляет к своему базовому классу, однако разработчики очень удивились, если не было класса кнопки… - person Holger; 06.07.2015
comment
Это честно. Если вы можете придумать лучший пример, я буду рад изменить пример. Я перепишу пример. - person phil_20686; 06.07.2015