Автор фото: Stable Diffusion, Deliberate v2.0 (8-bit)
Объявляйте классы final по-умолчанию
Ключевое слово final появилось в PHP вместе с понятием классов, однако зачастую им пренебрегают.
TL;DR: Всегда объявляйте класс как final, если все его публичные методы являются реализацией интерфейсов класса.
Принцип Open-closed
Вспомним принцип Open-Closed из аббревиатуры SOLID:
Программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения
Закрытость для изменения в этом контексте означает что когда ваш код предоставляет какой-то интерфейс, этот интерфейс не должен меняться. В конце концов API это контракт, это обещание совместимости, это ответственность. Это не означает что реализация не может меняться — как раз наоборот!
Однако “открытость к расширению” не означает “открытость к наследованию”. Композиция, Стратегия, EventListener, обратные вызовы - всё это рабочие способы расширять поведение без использования наследования, которые чаще всего ещё и более предпочтительны. Часто наследование делает код только более сложным и менее гибким.
Наследование vs Интерфейсы
Если ваш класс изначально не подразумевает наследование, то стоит сразу объявить его final. Но как понять что наследование не потребуется в дальнейшем?
Исходя из принципа “программировать на уровне интерфейсов, а не реализаций” следует сразу принять что либо наш класс изначально пишется как конечная реализация набора интерейсов, либо представляет собой интерфейс с частичной реализацией логики, т.е. является абстрактным классом.
Само определение абстрактного класса подразумевает что его будут наследовать, final здесь точно неприменим.
Класс, реализующий интерфейс же может иметь наследников, однако таковые либо полностью заново реализуют всё тот же интерфейс, а значит наследование здесь лишнее, либо общие элементы реализации просятся в абстрактный класс.
Таким образом получается что если ваш класс реализует интерфейс, то вполне уместно сразу же объявить его final.
Проблемы с тестированием
Тестовые фреймворки полагаются на возможность наследования классов для создания моков и заглушек. Как с этим быть?
Можно конечно вместо объявления класса как final добавить ему аннотацию (или phpdoc) @internal что пометит класс, как часть внутренней реализации и хотя технически его всё ещё можно будет наследовать, мы явно объявим о своей интенции и сделаем вид что всё хорошо.
Но есть способ лучше - покрыть все публичные методы такого класса соответствующим набором интерфейсов. В таком случае наш код будет работать с абстракциями, в то время как тесты смогут сгенерировать на основе интерфейсов необходимые заглушки.
Почему final это хорошо
- Предотвращает формирования адских цепочек наследования вида
MailDeliveryBot→Bot→Admin→User→Core→Db. - Подталкивает к использованию композиции. Скорее всего вам не нужно наследование при работе с этим классом, если изначально он не планировался как абстрактный класс.
- Заставляет продумывать API библиотек и решений. Наследование нарушает инкапсуляцию. Если класс явно не создавался под наследование, то можно получить проблемы с совместимостью. Трижды думайте перед тем как использовать
extendsи используйтеfinalчтобы не заставлять других ломать голову. - Подталкивает к сужению публичного API классов
- При необходимости
finalможно убрать, но придётся привести веские доводы и принять это решение явно
В каких случаях применять final
Классы объявленные как final эффективно работают только в при соблюдении следующих условий:
- Класс, объявленный
finalреализует некоторый набор интерфейсов - Все публичные методы класса покрыты этими интерфейсами
Если хотя бы один из пунктов отсутствует, то это явный признак того, что ваш код не в полной мере полагается на абстракции, а значит рано или поздно вы можете прийти к решению через наследование. В таком случае лучше сделать шаг назад и, взглянув на холст вашего проекта целиком, придумать как решить проблему с помощью введения соответствующих абстракций.
Бонус
Шаблон для создания PHP классов в PhpStorm:
<?php
declare(strict_types=1);
#if (${NAMESPACE})
namespace ${NAMESPACE};
#end
final class ${NAME}
{
}