А что, если вам сказали бы, что класс без final — это ошибка проектирования? Что возможность наследования должна быть исключением, а не правилом? В новом переводе от команды Spring АйО автор предлагает радикальную идею: все классы должны быть final по умолчанию.
⚠️ Статья была написана ещё до появления Kotlin`а. Цель перевода данной статьи — поднять запылившийся на полке вопрос в ��изайне API: «Нужно ли оставлять API открытым по‑умолчанию для внешнего расширения?»
На днях у нас состоялась интересная дискуссия, в ходе которой всплыла тема final-классов. По какой-то причине меня вдруг осенило: все классы должны быть final по умолчанию. То есть классы должны быть неявно final, без необходимости явного указания. К примеру, следующий код, на мой взгляд, должен считаться недопустимым:
class Parent { ... }
class Child extends Parent { ... } // недопустимо: родитель неявно finalВместо final можно было бы ввести другой модификатор, скажем, open. Это позволило бы нам расширять классы следующим образом:
open class Parent { ... }
class Child extends Parent { ... } // допустимо: родитель явно открытТеперь возникает вопрос: почему я считаю, что final должен быть значением по умолчанию? Это не связано с производительностью. Подсказку дают следующие цитаты из великолепного выступления Джоша Блоха о проектировании API (см. [1][2][3]):
«When in doubt — leave it out»
«You can always add, but you can never remove»
Что это означает? Если вы не уверены, стоит ли включать ту или иную функцию в API, то лучше её не включать. Потому что как только вы добавите функцию в публичный API, пользователи начнут на неё полагаться, и вам придётся её поддерживать. Если она плохо спроектирована, вы можете столкнуться с проблемами. Конечно, можно попробовать её пометить как deprecated — но, скорее всего, она останется навсегда.
Комментарий от Михаила Поливахи
В разработке программных продуктов уже долгие существует закон, который популяризовал Google в своей книжке Software Engineering in Google. Закон этот
Имея достаточное количество пользователей вашего API, это совершенно не важно, что вы обещаете конечным пользователям в контракте, а что — нет. Все наблюдаемые side-эффекты Вашего API будут кем-либо проэксплуатированы.
Вот тут про него можно почитать детальнее: https://www.hyrumslaw.com/
Как это всё связано с final-классами? Всё просто: не-final класс можно расширять! Любые его public или protected методы можно переопределить, а protected поля — читать и изменять. Более того, вы не можете отменить это решение — то есть, если класс однажды стал публичным и не-final, то таким он и останется. В то же время, если использовать final как значение по умолчанию, то это решение можно изменить — то есть, класс всегда можно «открыть», но нельзя будет «закрыть» обратно.
Это наблюдение — вовсе не открытие! Джош Блох в своей (тоже отличной) книге Effective Java утверждает: «старайтесь делать каждый класс или член максимально недоступным». Например, всегда следует отдавать предпочтение private перед protected полями. Однако, по какой-то причине, он не распространяет этот принцип на выбор между final и не-final классами.
Тем не менее, похоже, что в мире есть единомышленники. И, конечно же, есть те, кто придерживается иного мнения…
Комментарий от Ильи Сазонова
Статья написана в далёком 2011 году. С тех пор много поменялось. На вопрос, надо ли писать final везде, где только можно, индустрия дала однозначный ответ - надо!
Оксюморон неизменяемая переменная прочно вошёл в нашу жизнь.
У нас появились records, в Kotlin уже давно есть val. И даже в Java этот самый val доступен через Lombok.
В Rust вообще нужно специально писать mut, для того, чтобы появилась возможность вписать в переменную новое значение.
Но это что касается переменных. Что касается классов, то, хотя просто так сделать их по умолчанию final нельзя, в Java появились sealed classes, которые делают примерно то, о чём рассуждает автор статьи.
Многие этого не заметили, но уже сейчас разработчикам часто приходится доказывать обратную позицию, что если это улучшает читаемость кода, то можно делать переменные изменяемыми, а классы открывать для наследования.
Будущее наступило!
Комментарий от Евгения Сулейманова
Идея final-классов по умолчанию здравая, но в «проде» она сразу сталкивается с экосистемой прокси и ленивых прокладок.
Минимум 2 примера:
1. AOP/прокси в Spring. CGLIB не любит final классы/методы.
2. JPA/Hibernate (ленивая загрузка). Прокси-сабклассы не сгенерятся поверх final.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.