В java существует ли когда-нибудь возможность расширения не-абстрактного класса?
Кажется, он всегда указывает на плохой код, когда есть иерархии классов. Согласны ли вы и почему/почему нет?
В java существует ли когда-нибудь возможность расширения не-абстрактного класса?
Кажется, он всегда указывает на плохой код, когда есть иерархии классов. Согласны ли вы и почему/почему нет?
Я согласен с Джоном и Кентом, но, как и Скотт Майерс (в Effective С++), я иду гораздо дальше. Я считаю, что каждый класс должен быть либо abstract
, либо final
. То есть, только листовые классы в любой иерархии действительно склонны к прямому инстанцированию. Все остальные классы (т.е. Внутренние узлы в наследовании) являются "незавершенными" и поэтому должны быть abstract
.
Просто нет смысла, чтобы обычные классы были расширены. Если какой-либо аспект класса стоит расширять и/или модифицировать, более чистым способом было бы взять этот один класс и разделить его на один базовый класс abstract
и одну конкретную взаимозаменяемую реализацию.
Конечно, есть моменты, когда имеет смысл иметь неконкретные конкретные классы. Однако я согласен с Kent - я считаю, что классы по умолчанию должны быть окончательными (опечатаны на С#), а методы Java должны быть окончательными по умолчанию (как они есть на С#).
Как говорит Кент, наследование требует тщательной разработки и документации - очень легко думать, что вы можете просто переопределить один метод, но не знаете ситуаций, в которых этот метод может быть вызван из базового класса, как часть остальной части реализация.
См. "Как вы создаете класс для наследования" для более подробного обсуждения этого вопроса.
Этот вопрос одинаково применим к другим платформам, таким как С#.NET. Есть те (включая меня), которые считают, что типы должны быть окончательными/опечатаны по умолчанию и должны быть явно распечатаны, чтобы разрешить наследование.
Расширение через наследование - это то, что требует тщательной разработки и не так просто, как просто оставить тип незапечатанным. Поэтому я считаю, что это должно быть явное решение разрешить наследование.
есть веские причины, чтобы ваш код не был окончательным. многие фреймворки, такие как спящий режим, spring, иногда зависят от нечетких классов, которые динамически расширяются во время выполнения.
например, hibernate использует прокси для ленивой ассоциации. особенно когда дело доходит до АОП, вы захотите, чтобы ваши классы были нефинальными, чтобы перехватчики могли прикрепляться к нему. см. также вопрос в SO
Ваша лучшая ссылка здесь - пункт 15 Джошуа Блоха, отличная книга "Эффективная Java", называемая "Дизайн и документ для наследования или запрещающая". Однако ключ к тому, чтобы разрешить расширение класса, не является "абстрактным", но "он был разработан с учетом наследования". Иногда между ними существует корреляция, но вторая важна. Чтобы сделать простой пример, большинство классов AWT предназначены для расширения, даже те, которые не являются абстрактными.
Резюме главы Блоха заключается в том, что взаимодействие унаследованных классов с родителями может быть неожиданным и непредсказуемым, если предок не был унаследован. Поэтому классы должны иметь два типа: а) классы, предназначенные для расширения, и с достаточной документацией для описания того, как это должно быть сделано; б) классы, обозначенные как final. Занятия в (а) часто будут абстрактными, но не всегда. Для
В некоторых случаях вы хотите убедиться, что нет подкласса, в других случаях вы хотите обеспечить подклассу (абстрактную). Но всегда есть большое подмножество классов, где вам, как оригинальному автору, все равно, и все равно. Это часть открытия/закрытия. Решение о том, что что-то должно быть закрыто, также должно быть сделано по какой-то причине.
Я не согласен. Если иерархии были плохими, не было бы причин для существования объектно-ориентированных языков. Если вы посмотрите на библиотеки виджета пользовательского интерфейса от Microsoft и Sun, вы обязательно найдете наследование. Это все "плохой код" по определению? Нет, конечно, нет.
Наследование можно злоупотреблять, но так может быть и любой язык. Трюк заключается в том, чтобы научиться правильно делать вещи.
Я не мог больше не согласиться. Иерархии классов имеют смысл для конкретных классов, когда конкретные классы знают возможные возвращаемые типы методов, которые они не отметили окончательно. Например, конкретный класс может иметь подклассу подкласса:
protected SomeType doSomething() {
return null;
}
Это doSomething гарантируется как null, так и экземпляр SomeType. Скажите, что у вас есть возможность обрабатывать экземпляр SomeType, но у него нет прецедента для использования экземпляра SomeType в текущем классе, но знаете, что эта функциональность будет действительно хорошей в подклассах, и большинство из них конкретнее. Нет смысла делать текущий класс абстрактным классом, если его можно использовать напрямую со значением по умолчанию, ничего не делая с его нулевым значением. Если вы сделали его абстрактным классом, то у вас будут свои дети в иерархии этого типа:
Таким образом, у вас есть абстрактный базовый класс, который нельзя использовать напрямую, когда класс по умолчанию может быть наиболее распространенным. В другой иерархии существует еще один класс, поэтому функциональность может быть использована без создания практически бесполезного класса по умолчанию, потому что абстракция просто должна быть принудительно введена в класс.
Теперь, конечно, иерархий можно использовать и злоупотреблять, и если вещи не документированы четко или классы, которые не хорошо разработаны, подклассы могут столкнуться с проблемами. Но эти же проблемы существуют и с абстрактными классами, вы не избавляетесь от проблемы только потому, что добавляете "абстрактный" к вашему классу. Например, если в контракте метода doSomething() выше SomeType имел заполненные поля x, y и z, когда они были доступны через getters и seters, ваш подкласс мог бы взорваться независимо от того, используете ли вы конкретный класс, который возвращает null как ваш базовый класс или абстрактный класс.
Общее правило для проектирования иерархии классов - довольно простой вопрос:
Нужно ли мне поведение моего предложенного суперкласса в моем подклассе? (Y/N) Это первый вопрос, который вам нужно задать самому себе. Если вам не нужно поведение, нет аргументов для подкласса.
Нужно ли состояние моего предложенного суперкласса в моем подклассе? (Y/N) Это второй вопрос. Если состояние соответствует модели того, что вам нужно, это может быть привязкой для подкласса.
Если подкласс был создан из предлагаемого суперкласса, действительно ли это было бы отношением IS-A, или это просто ярлык для наследования поведения и состояния? Это последний вопрос. Если это просто ярлык, и вы не можете квалифицировать свой предлагаемый подкласс как "супер-класс", тогда наследование следует избегать. Состояние и логику можно скопировать и вставить в новый класс с другим корнем или использовать делегат.
Только если классу необходимо поведение, состояние и можно считать, что подкласс IS-A (n) экземпляр суперкласса должен считаться наследуемым от суперкласса. В противном случае существуют другие варианты, которые лучше подходят для этой цели, хотя для этого может потребоваться немного больше работы, поэтому в конечном итоге она более чистая.
Есть несколько случаев, когда мы не хотим позволять изменять поведение. Например, класс String, Math.
Мне не нравится наследование, потому что всегда есть лучший способ сделать то же самое, но когда вы делаете изменения обслуживания в огромной системе, иногда лучший способ исправить код с минимальными изменениями - немного расширить класс, Да, это обычно приводит к плохому коду, но к рабочему и без месяцев переписывания. Таким образом, предоставление человеку технического обслуживания такой же гибкости, как он может справиться, - это хороший способ пойти.