Ключевое слово 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 допускаются изменения, ломающие обратную совместимость, поэтому если сообщество признает изменение полезным, его можно будет ввести.

В-третьих, сначала необходимо распространить саму идею финализации по умолчанию. Можно, например, сделать правило статического анализа “запрет наследования не-абстрактных классов” и постепенно распространять эту практику.

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

Бонус

Ссылки по теме:

When to declare classes final

PHP-RFC final by default

Final by default (Brent Roose) — источник идеи, прочитав эту статью я переосмыслил значение final и в итоге согласился с автором