Зачем избегать последнего ключевого слова? - PullRequest
11 голосов
/ 06 февраля 2009

В java есть ли случаи, когда можно разрешить расширение неабстрактного класса?

Это всегда указывает на плохой код, когда есть иерархии классов. Вы согласны, и почему / почему нет?

Ответы [ 10 ]

12 голосов
/ 06 февраля 2009

В определенное время есть смысл иметь неконкретные конкретные классы. Однако я согласен с Кентом - я считаю, что классы должны быть окончательными (запечатаны в C #) по умолчанию, и что методы Java должны быть окончательными по умолчанию (как они есть в C #).

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

См. "Как вы разрабатываете класс для наследования" , чтобы подробнее обсудить это.

11 голосов
/ 06 февраля 2009

Я согласен с Джоном и Кентом, но, как и Скотт Майерс (в Effective C ++), я иду гораздо дальше. Я считаю, что каждый класс должен быть либо abstract, либо final. Таким образом, только листовые классы в любой иерархии действительно пригодны для непосредственной реализации. Все остальные классы (т. Е. Внутренние узлы в наследовании) являются «незавершенными» и, следовательно, должны быть abstract.

Просто не имеет смысла расширять обычные классы. Если какой-либо аспект класса стоит расширить и / или изменить, то более понятным способом было бы взять этот один класс и разделить его на один abstract базовый класс и одну конкретную сменную реализацию.

5 голосов
/ 06 февраля 2009

Этот вопрос в равной степени применим и к другим платформам, таким как C # .NET. Есть те (включая меня), которые считают, что типы должны быть окончательными / запечатанными по умолчанию и должны быть явно распечатаны, чтобы разрешить наследование.

Расширение через наследование - это то, что требует тщательного проектирования и не так просто, как просто оставить тип незапечатанным. Поэтому я думаю, что это должно быть явным решением разрешить наследование.

4 голосов
/ 06 февраля 2009

Ваша лучшая ссылка здесь - это пункт 15 из превосходной книги Джошуа Блоха «Эффективная Ява» под названием «Дизайн и документ для наследования, или же запретить его». Однако ключ к тому, следует ли разрешить расширение класса, заключается не в том, «является ли он абстрактным», а «был ли он разработан с учетом наследования». Иногда между ними есть корреляция, но важна вторая. Чтобы взять простой пример, большинство классов AWT предназначены для расширения, даже те, которые не являются абстрактными.

Резюме главы Блоха заключается в том, что взаимодействие наследуемых классов с их родителями может быть удивительным и непредсказуемым, если предок не был предназначен для наследования. Поэтому классы должны быть двух видов: а) классы, предназначенные для расширения, и с достаточным количеством документации, чтобы описать, как это должно быть сделано; б) классы, помеченные как окончательные. Классы в (а) часто будут абстрактными, но не всегда. Для

4 голосов
/ 06 февраля 2009

есть веские причины, чтобы ваш код не был окончательным. многие фреймворки, такие как hibernate, spring, guice, иногда зависят от не финальных классов, которые они динамически расширяют во время выполнения.

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

3 голосов
/ 06 февраля 2009

В некоторых случаях вы хотите убедиться, что нет подклассов, в других случаях вы хотите обеспечить подклассы (абстрактные). Но всегда есть большое подмножество классов, в которых вам, как первоначальному автору, все равно и это не должно волновать. Это часть того, чтобы быть открытым / закрытым. Решить, что что-то должно быть закрыто, это также , что должно быть сделано по причине.

2 голосов
/ 06 февраля 2009

Я не мог не согласиться больше. Иерархии классов имеют смысл для конкретных классов, когда конкретные классы знают возможные возвращаемые типы методов, которые они не пометили как финальные. Например, конкретный класс может иметь хук подкласса:

protected SomeType doSomething() {
return null;
}

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

  • Абстрактный базовый класс
    • Класс по умолчанию (класс, который мог быть неабстрактным, реализует только защищенный метод и ничего больше)
    • Другие подклассы.

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

  • Класс по умолчанию
    • Другие подклассы.

Теперь, конечно, иерархии можно использовать и злоупотреблять, и, если что-то не документировано четко или классы не разработаны должным образом, подклассы могут столкнуться с проблемами. Но те же проблемы существуют и с абстрактными классами, вы не избавляетесь от проблемы только потому, что добавляете «абстрактный» в свой класс. Например, если в контракте метода "doSomething ()", описанном выше, требуется, чтобы SomeType заполнил поля x, y и z, когда к ним обращались через методы получения и установки, ваш подкласс взорвался бы независимо от того, использовали ли вы конкретный класс, который возвратил ноль как ваш базовый класс или абстрактный класс.

Общее практическое правило для разработки иерархии классов - довольно простой вопрос:

  1. Нужно ли мне поведение предложенного суперкласса в моем подклассе? (Y / N) Это первый вопрос, который вам нужно задать себе. Если вам не нужно поведение, нет аргумента для подкласса.

  2. Нужно ли мне состояние предложенного суперкласса в моем подклассе? (Y / N) Это второй вопрос. Если состояние соответствует модели того, что вам нужно, это может быть кандидатом на создание подклассов.

  3. Если подкласс был создан из предложенного суперкласса, действительно ли это будет отношение IS-A или это просто ярлык для наследования поведения и состояния? Это последний вопрос. Если это просто ярлык, и вы не можете квалифицировать предложенный подкласс «как-а» суперкласс, тогда следует избегать наследования. Состояние и логику можно скопировать и вставить в новый класс с другим корнем или использовать делегирование.

Только если классу нужно поведение, состояние и можно считать, что экземпляр подкласса IS-A (n) суперкласса должен считаться наследником суперкласса. В противном случае существуют другие варианты, которые лучше подходят для этой цели, хотя для этого может потребоваться дополнительная работа, в долгосрочной перспективе это чище.

2 голосов
/ 06 февраля 2009

Я не согласен. Если бы иерархия была плохой, не было бы никаких причин для существования объектно-ориентированных языков. Если вы посмотрите на библиотеки виджетов UI от Microsoft и Sun, вы наверняка найдете наследство. Это все "плохой код" по определению? Нет, конечно нет.

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

1 голос
/ 06 февраля 2009

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

1 голос
/ 06 февраля 2009

Есть несколько случаев, когда мы не хотим позволять изменять поведение. Например, класс String, Math.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...