Ключевое слово
final
в объявлении класса PHP запрещает наследовать этот класс.
Я предлагаю внести изменение в PHP и запретить наследование для всех классов по умолчанию.
Что это значит на практике?
class User {}
Такой класс нельзя будет унаследовать. Для наследования нужно будет объявить его абстрактным.
В статье рассматриваю, как я пришёл к такому заключению.
Плохое наследование
Трудно найти что-то более вредное в ООП, чем безудержное использование наследования везде где захочется разработчику.
Это излюбленный способ расширения кода для новичков. Выучив “три кита ООП” — инкапсуляцию, наследование и полиморфизм, разработчик применяет наследование и радуется тому как легко расширить функциональность класса. “Если я использую наследование, значит это точно ООП 😄”
При обучении программированию учат как можно применять наследование, но не учат тому, как его не применять.
Если некорректно применять наследование, поначалу вред незаметен, но с ростом и усложнением проекта возникает всё больше и больше проблем. Код постепенно становится хрупким, чаще ломается, его становится тяжелее изменять. В конце концов проблемные классы нужно будет “распиливать” на части и устранять наследование, что сразу упрощает и улучшает код, но потребует больших усилий на рефакторинг.
По большей части, вред от наследования связан с тем, что программист объединяет в общий класс то, что объединять не следовало.
Хорошее наследование
Классы, которые стоит наследовать, редко встречаются. Обычно это классы, изначально созданные для наследования, и не несущие никакого смысла сами по себе. Пример — базовый класс контроллера в MVC фреймворке Yii2.
Так как полезная функциональность реализуется только в наследниках, объект родительского класса создавать не следует. Запрет на создание объекта легко реализуется: просто объявляем родительский класс абстрактным (abstract
).
Как защититься?
Опытный программист может защитить классы от наследования, прописав для них ключевое слово “final”. Финальный (final
) класс уже нельзя будет наследовать просто так, интерпретатор выдаст ошибку.
Конечно, другой разработчик может изменить объявление класса и убрать “final”, но это действие уже будет заметно остальным членам команды.
Если класс объявлен как “final” то автор этого класса явно выразил своё намерение оставить класс без наследников, и хорошим тоном будет согласовать “расфинализацию” с автором класса.
Защитив классы от наследования, мы можем сохранить код проекта чистым и выстроить более правильную архитектуру.
Как переиспользовать код?
Закономерный вопрос — а как же переиспользовать код без наследования?
Подробно останавливаться на этом не будем, но существует несколько способов, например, переиспользование кода через композицию. Проще говоря, вынести общий код в отдельный класс и обращаться к экземпляру этого класса.
В уроках по программированию наследование ошибочно преподают как приём переиспользования и расширения кода. Это приводит к неправильному пониманию и как следствие архитектурным ошибкам новичков.
Что защищаем?
Хорошим правилом будет не использовать наследование без большой необходимости.
Таким образом, мы используем наследование где это оправдано, и не используем во всех остальных случаях.
- Общий код — переиспользование через композицию или другим методом
- Классы которые определяются в наследниках — используем абстрактные классы (
abstract
) - Определение интерфейса — используем интерфейс (
interface
) - Всё остальное, где наследование не нужно — финализируем (
final
)
final
для всех классов
Получается, что все хорошие кейсы наследования реализуются либо через интерфейс, либо через абстрактный класс.
Да, сейчас в кодовой базе проектов есть классы которые наследуются без объявления родительского абстрактным. Но такой класс может быть приведён к абстрактному либо разделён на несколько. Либо заменяем наследование композицией.
Избавление от наследования во всех не-абстрактных классах только улучшит код.
Мы можем объявить все не-абстрактные классы финальными.
final
в Yii 3
Разработчики фреймворка Yii 3 решили сделать все (то есть почти все) классы фреймворка финальными, чтобы удержать пользователей от злоупотреблений наследованием хотя бы от классов фреймворка.
Это хорошее решение.
Нужен ли final
?
При всей пользе запрета наследования, нужно ли само ключевое слово final
?
Я считаю, что не нужно.
Если все не-абстрактные классы могут быть финальными, то пусть ими и будут. Без явного указания final
.
Наследование будет запрещено для всех не-абстрактных классов на уровне языка.
Попытка наследовать не-абстрактный класс будет приводить к ошибке интерпретатора.
Нельзя будет сделать так:
class User {}
class ProductOwner extends User {}
Можно будет только так:
abstract class User {}
class ProductOwner extends User {}
class Customer extends User {}
Что получим:
- Избавление от лишнего слова
final
в 99% классов, как следствие улучшение читаемости кода - Явное указание наследования в каждом наследуемом классе, делаем неявное явным
Но всё сломается!
Конечно, если просто взять и заменить поведение по умолчанию, всё поломается.
Но не сломается, если грамотно подойти к вопросу.
Во-первых, нужен RFC и обсуждение с сообществом.
Здесь опубликован мой вариант RFC (автор идеи Brent Roose): PHP-RFC Final by default
Этот ресурс для RFC неофициальный, я сам его создал, зато он доступен для обсуждения любому пользователю GitHub без каких-либо сложных манипуляций, в отличие от официального Mailing List.
Во-вторых, подобные изменения могут быть реализованы в мажорной версии PHP, например в PHP 9.
В мажорных версиях согласно SemVer допускаются изменения, ломающие обратную совместимость, поэтому если сообщество признает изменение полезным, его можно будет ввести.
В-третьих, сначала необходимо распространить саму идею финализации по умолчанию. Можно, например, сделать правило статического анализа “запрет наследования не-абстрактных классов” и постепенно распространять эту практику.
Когда сообщество на практике убедится в пользе подобного подхода, тогда можно будет закрепить его на уровне языка.
Бонус
Ссылки по теме:
Final by default (Brent Roose) — источник идеи, прочитав эту статью я переосмыслил значение final
и в итоге согласился с автором