Почему избегать наследования Java «расширяет»

40

Джейм Гослинг сказал

«Вы должны по возможности избегать наследования реализации».

и вместо этого используйте наследование интерфейса.

Но почему? Как мы можем избежать наследования структуры объекта, используя ключевое слово «extends», и в то же время сделать наш код «Object Oriented»?

Может ли кто-нибудь привести пример «Объектно-ориентированный», иллюстрирующий эту концепцию в сценарии типа «заказ книги в книжном магазине»?

новичок
источник
4
дубликат programmers.stackexchange.com/questions/65179/…
Стивен А. Лоу
Проблема в том, что вы можете продлить только один класс.

Ответы:

38

Гослинг не сказал, чтобы избежать использования ключевого слова extends ... он просто сказал, что не использует наследование в качестве средства для достижения повторного использования кода.

Наследование само по себе неплохо и является очень мощным (необходимым) инструментом для создания ОО-структур. Однако, когда он не используется должным образом (то есть, когда используется для чего - то другого, кроме создания структур Object), он создает код, который очень тесно связан и очень сложен в обслуживании.

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

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

--- Редактировать Этот ответ является довольно хорошим аргументом, чтобы ответить на вопрос в более общих терминах.

Newtopian
источник
3
(Реализация) наследование не является существенным для создания ОО структур. Вы можете иметь язык ОО без него.
Андрес Ф.
Не могли бы вы привести пример? Я никогда не видел ОО без наследства.
Newtopian
1
Проблема в том, что нет общепринятого определения того, что такое ООП. (Кроме того, даже Алан Кей пожалел об общей интерпретации своего определения с его нынешним акцентом на «объекты» вместо «сообщения»). Вот попытка ответа на ваш вопрос . Я согласен, что редко можно увидеть ООП без наследования; следовательно, мой аргумент состоял просто в том, что это не существенно ;)
Андрес Ф.
9

На заре объектно-ориентированного программирования наследование реализации было золотым молотком повторного использования кода. Если в каком-то классе была какая-то часть функциональности, которая вам была нужна, нужно было публично наследовать от этого класса (это были времена, когда C ++ был более распространенным, чем Java), и вы получали эту функциональность «бесплатно».

Кроме того, это не было действительно бесплатно. Результатом стали чрезвычайно запутанные иерархии наследования, которые было почти невозможно понять, включая деревья наследования в форме ромба, с которыми было трудно обращаться. Это одна из причин, по которой разработчики Java решили не поддерживать множественное наследование классов.

Совет Гослинга (и другие утверждения), как будто это было сильное лекарство от довольно серьезного заболевания, и вполне возможно, что некоторые дети были выброшены с водой из ванны. Нет ничего плохого в использовании наследования для моделирования отношений между классами, которые действительно есть is-a. Но если вы просто хотите использовать функциональность из другого класса, вам лучше сначала изучить другие варианты.

JohnMcG
источник
6

Наследство приобрело довольно страшную репутацию в последнее время. Одна из причин избегать этого связана с принципом «составление по наследованию», который в основном побуждает людей не использовать наследование там, где это не истинные отношения, просто для того, чтобы сохранить некоторую запись кода. Такое наследование может привести к тому, что некоторые проблемы будут трудно найти и исправить. Причина, по которой ваш друг, вероятно, предложил использовать интерфейс, заключается в том, что интерфейсы предоставляют более гибкое и удобное решение для распределенных API-интерфейсов. Они чаще позволяют вносить изменения в API, не нарушая пользовательский код, тогда как использование классов часто нарушает пользовательский код. Лучшим примером этого в .Net является разница в использовании между List и IList.

Морган Херлокер
источник
5

Потому что вы можете расширить только один класс, в то время как вы можете реализовать много интерфейсов.

Однажды я слышал, что наследование определяет, кто вы есть, но интерфейсы определяют роль, которую вы можете играть.

Интерфейсы также позволяют лучше использовать полиморфизм.

Это может быть частью причины.

Махмуд Хоссам
источник
почему отрицание?
Махмуд Хоссам
6
Ответ ставит телегу перед лошадью. Java позволяет расширять только один класс из-за принципа Гослинга (дизайнера Java). Это все равно, что сказать, что дети должны носить шлемы, когда едут на велосипедах, потому что это закон. Это правда, но как закон, так и советы основаны на предотвращении травм головы.
JohnMcG
@JohnMcG Я не объясняю выбор дизайна, который сделал Гослинг, я просто даю советы кодеру, кодеры обычно не беспокоятся о дизайне языка.
Махмуд Хоссам
1

Меня тоже этому учили, и я предпочитаю интерфейсы, где это возможно (конечно, я все еще использую наследование там, где это имеет смысл).

Одна вещь, я думаю, что это делает, это отделить ваш код от конкретных реализаций. Скажем, у меня есть класс ConsoleWriter, который передается в метод для записи чего-либо и записывает это в консоль. Теперь допустим, что я хочу переключиться на печать в окне графического интерфейса. Что ж, теперь я должен либо изменить метод, либо написать новый, который принимает GUIWriter в качестве параметра. Если бы я начал с определения интерфейса IWriter, а метод взял IWriter, я мог бы начать с ConsoleWriter (который будет реализовывать интерфейс IWriter), а затем позже написать новый класс с именем GUIWriter (который также реализует интерфейс IWriter), а затем я нужно было бы просто выключить передаваемый класс.

Еще одна вещь (которая верна для C #, но не для Java) - это то, что вы можете расширить только один класс, но реализовать много интерфейсов. Допустим, у меня были классы под названием Учитель, Учитель математики и Учитель истории. Теперь MathTeacher и HistoryTeacher простираются от Teacher, но что если нам нужен класс, представляющий кого-то, кто является одновременно MathTeacher и HistoryTeacher. Это может стать довольно грязным, когда вы пытаетесь наследовать от нескольких классов, когда вы можете делать это только по одному (есть способы, но они не совсем оптимальны). С помощью интерфейсов вы можете иметь 2 интерфейса, называемых IMathTeacher и IHistoryTeacher, а затем иметь один класс, который расширяется от Teacher и реализует эти 2 интерфейса.

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

Думаю, что основной причиной использования интерфейсов вместо наследования является разделение кода реализации, но не думайте, что наследование является злом, поскольку оно все еще очень полезно.

ryanzec
источник
2
«Вы можете расширить только один класс, но реализовать много интерфейсов». Это верно как для Java, так и для C #.
FrustratedWithFormsDesigner
Одним из возможных решений проблемы дублирования кода является создание абстрактного класса, который реализует интерфейс и расширяет его. Используйте с осторожностью, хотя; я видел только несколько случаев, которые имеют смысл.
Майкл К
0

Если вы наследуете от класса, вы берете зависимость не только от этого класса, но вы также должны соблюдать любые неявные контракты, созданные клиентами этого класса. Дядя Боб дает отличный пример того, как легко нарушить принцип подстановки Лискова, используя базовый квадрат, расширяющий дилемму прямоугольника . Интерфейсы, поскольку они не имеют реализации, также не имеют скрытых контрактов.

Майкл Браун
источник
2
Одно замечание: проблема Круг-эллипс все равно будет возникать, даже если используются только чистые интерфейсы, если объекты должны быть изменяемыми.
Rwong