От: http://www.artima.com/lejava/articles/designprinciples4.html
Эрих Гамма: Я все еще думаю, что это правда, даже после десяти лет. Наследование - это крутой способ изменить поведение. Но мы знаем, что это хрупко, потому что подкласс может легко делать предположения о контексте, в котором вызывается метод, который он переопределяет. Между базовым классом и подклассом существует тесная связь из-за неявного контекста, в который будет вызываться код подкласса, который я подключаю. Композиция обладает более приятным свойством. Связность уменьшается благодаря наличию более мелких вещей, которые вы подключаете к чему-то большему, а более крупный объект просто вызывает меньший объект обратно. С точки зрения API определение того, что метод может быть переопределен, является более строгим обязательством, чем определение того, что метод может быть вызван.
Я не понимаю, что он имеет в виду. Может ли кто-нибудь объяснить это?
Если вы публикуете обычную функцию, вы даете односторонний контракт:
что делает функция, если вызывается?
Если вы публикуете обратный вызов, вы также даете односторонний контракт:
когда и как он будет называться?
И если вы опубликуете переопределяемую функцию, она будет одновременно, и вы получите двусторонний контракт:
когда она будет вызвана и что она должна делать, если она будет вызвана?
Даже если ваши пользователи не злоупотребляют вашим API (нарушая свою часть контракта, что может оказаться слишком дорого для обнаружения), вы можете легко увидеть, что последнему требуется гораздо больше документации, и все , что вы документируете, является обязательством, которое ограничивает ваши дальнейшие выборы.
Примером отказа от такого двустороннего контракта является переход от
show
иhide
кsetVisible(boolean)
в java.awt.Component .источник
show
иhide
до сих пор существуют, они просто@Deprecated
. Таким образом, изменение не нарушает код, который просто вызывает их. Но если вы переопределили их, ваши переопределения не будут вызываться клиентами, которые переходят на новый setVisible. (Я никогда не использовал Swing, поэтому я не знаю, как часто это отменять; но так как это произошло давным-давно, я представляю себе причину, по которой Deduplicator вспоминает, что это причиняло ему / ей боль.)Ответ Килиана Фота превосходен. Я просто хотел бы добавить канонический пример * того, почему это проблема. Представьте себе целочисленный класс Point:
Теперь давайте подкласс это, чтобы быть 3D-точкой.
Супер просто! Давайте использовать наши очки:
Вы, наверное, удивляетесь, почему я публикую такой простой пример. Вот подвох:
Когда мы сравниваем 2D-точку с эквивалентной 3D-точкой, мы получаем истинное значение, но когда мы обращаемся к обратному сравнению, мы получаем ложное значение (потому что p2a не выполняется
instanceof Point3D
)Вывод
Обычно возможно реализовать метод в подклассе таким образом, что он больше не совместим с тем, как суперкласс ожидает его работы.
Как правило, невозможно реализовать equals () для существенно другого подкласса способом, совместимым с его родительским классом.
Когда вы пишете класс, который вы хотите разрешить людям создавать подклассы, очень полезно написать контракт о том, как должен вести себя каждый метод. Еще лучше был бы набор модульных тестов, которые люди могли бы запускать против своих реализаций переопределенных методов, чтобы доказать, что они не нарушают контракт. Почти никто не делает этого, потому что это слишком много работы. Но если вам все равно, это то, что нужно сделать.
Отличным примером хорошо прописанного контракта является Comparator . Просто игнорируйте то, о чем говорится,
.equals()
по причинам, описанным выше. Вот пример того, как Comparator может делать то, что.equals()
нельзя .Заметки
Источником этого примера был пункт 8 «Эффективная Java» Джоша Блоха, но Блох использует ColorPoint, который добавляет цвет вместо третьей оси и использует двойные вместо целых. Пример Java Блоха в основном продублирован Odersky / Spoon / Venners, которые сделали свой пример доступным в Интернете.
Несколько человек возражали против этого примера, потому что, если вы сообщите родительскому классу о подклассе, вы можете исправить эту проблему. Это верно, если имеется достаточно небольшое количество подклассов и если родитель знает обо всех них. Но первоначальный вопрос был о создании API, для которого кто-то еще напишет подклассы. В этом случае вы обычно не можете обновить родительскую реализацию, чтобы она была совместима с подклассами.
бонус
Компаратор также интересен тем, что он решает проблему правильной реализации equals (). Более того, он следует шаблону для решения этого типа проблемы наследования: шаблону разработки стратегии. Классы типов, от которых люди в Haskell и Scala приходят в восторг, также являются образцом стратегии. Наследование не плохо или неправильно, это просто сложно. Для дальнейшего чтения посмотрите статью Филиппа Уодлера « Как сделать специальный полиморфизм менее специальным»
источник
equals
как Map и Set определяют его. Равенство полностью игнорирует порядок, в результате чего, например, два SortedSets с одинаковыми элементами, но разными порядками сортировки, все равно сравниваются.B
является дочерним по отношению к классуA
и если вы создаете экземпляр объекта классаB
, вы должны иметь возможность привестиB
объект класса к его родителю и использовать API приведенной переменной без потери деталей реализации ребенок. Вы нарушили правило, предоставив третье свойство. Как вы планируете получить доступ кz
координате после преобразованияPoint3D
переменной вPoint2D
, когда базовый класс не знает, что такое свойство существует? Если, приведя дочерний класс к его базе, вы нарушите публичный API, ваша абстракция неверна.Наследование ослабляет инкапсуляцию
Когда вы публикуете интерфейс с разрешенным наследованием, вы существенно увеличиваете размер вашего интерфейса. Каждый переопределяемый метод может быть заменен и должен рассматриваться как обратный вызов, предоставляемый конструктору. Реализация, предоставляемая вашим классом, является просто значением обратного вызова по умолчанию. Таким образом, должен быть предоставлен некоторый вид контракта, указывающий, каковы ожидания от метода. Такое редко случается и является основной причиной, по которой объектно-ориентированный код называется хрупким.
Ниже приведен реальный (упрощенный) пример из фреймворка коллекций Java, любезно предоставленный Питером Норвигом ( http://norvig.com/java-iaq.html ).
Так что же произойдет, если мы сделаем это подклассом?
У нас есть ошибка: иногда мы добавляем «dog», и в хеш-таблицу вводится запись «dogss». Причина была в том, что кто-то предоставил реализацию пут, которую не ожидал человек, разрабатывающий класс Hashtable.
Расширяемость наследственных разрывов
Если вы позволяете своему классу быть разделенным на подклассы, вы обязуетесь не добавлять никаких методов в ваш класс. В противном случае это можно сделать, ничего не нарушая.
Когда вы добавляете новые методы в интерфейс, любой, кто унаследовал от вашего класса, должен будет реализовать эти методы.
источник
Если метод должен быть вызван, вам нужно только убедиться, что он работает правильно. Вот и все. Выполнено.
Если метод предназначен для переопределения, вам также необходимо тщательно продумать область действия метода: если область слишком велика, дочерний класс часто должен будет включать вставленный в копию код из родительского метода; если он слишком мал, необходимо переопределить многие методы, чтобы получить желаемую новую функциональность - это добавляет сложности и ненужного количества строк.
Поэтому создатель родительского метода должен сделать предположения о том, как класс и его методы могут быть переопределены в будущем.
Тем не менее, автор говорит о другой проблеме в цитируемом тексте:
Рассмотрим метод,
a
который обычно вызывается из методаb
, но в некоторых редких и неочевидных случаях из методаc
. Если автор переопределяющего метода пропускаетc
метод и его ожиданияa
, очевидно, что все может пойти не так.Поэтому важнее то, что
a
он четко и недвусмысленно определен, хорошо документирован, «делает одно и делает это хорошо» - тем более, чем если бы это был метод, предназначенный только для вызова.источник