Архитектура многих фреймворков зачастую требует расширения своих классов. Но сегодня речь только про чистый от фреймворков код, за который вы и/или ваша команда несет ответственность.
В первую очередь хочется сказать всем: “Ставьте ключевое слово final классам всегда, когда это возможно. В первую очередь тем, кто реализует какой либо интерфейс”. Читатель может возразить: “Ведь это снижает гибкость”. На что отвечу, что гибкость, произрастает из хороших абстракций, а не из наследований.
- 8 причин использования ключевого слова final
- 1. Предотвратить Extend Hell
- Перестаньте бояться дублированного кода
- 2. Поощрение композиции
- 3. Разработчик начинает задумываться над общедоступном API
- 4. Разработчик теперь будет обязан думать над упрощением общедоступного API
- 5. final всегда можно сделать расширяемым
- 6. extends нарушает инкапсуляцию
- 7. Вам не нужна такая гибкость
- 8. Вы можете свободно изменять код
- Когда следует избегать final:
8 причин использования ключевого слова final
Существует множество причин выставлять ключевое слово final для ваших классов:
1. Предотвратить Extend Hell
У разработчиков есть плохая привычка решать проблемы через наследования, подставляя для работы новый исправленный унаследованный класс
<?php
class Db { /* ... */ }
class Core extends Db { /* ... */ }
class User extends Core { /* ... */ }
class Admin extends User { /* ... */ }
class Bot extends Admin { /* ... */ }
class BotThatDoesSpecialThings extends Bot { /* ... */ }
class PatchedBot extends BotThatDoesSpecialThings { /* ... */ }
Это, без каких-либо сомнений, то, как вы НЕ должны разрабатывать свой код.
Описанный выше подход обычно используется разработчиками, которые путают ООП с “способом решения проблем с помощью наследования” (может быть, “программирование, ориентированное на наследование”?).
<?php
interface UserInterface {}
final class RegularWebUser implements UserInterface {}
final class AdminUser implements UserInterface {}
final class PowerAdminUser implements UserInterface {}
final class WipFunctionalityUser implements UserInterface {}
Многие возразят мне: “Почему, посмотри, сколько кода будет дублироваться, можем ли мы хотя бы использовать трейты?” Но тогда этот трейт станет узким горлышком.
Перестаньте бояться дублированного кода
Так работает наш мозг: он заставляет вас искать шаблоны. Избегание абстрактных классов и наследования поможет вам избежать неудачных решений в коде.
Вам нужны какие-либо новые функции? Начните с интерфейса. Используйте doc-блоки для описания ввода, вывода и причины, стоящей за этим. Это может показаться замедлением, но это поможет вам спланировать то, что вам действительно нужно.
2. Поощрение композиции
В целом, принудительное предотвращение наследования (по умолчанию) имеет приятное преимущество, заставляя разработчиков больше думать о композиции.
В существующем коде будет меньше функций наполнения через наследование, что, на мой взгляд, является признаком спешки в сочетании с ползучестью функций.
Ползучесть функций — это чрезмерное постоянное расширение или добавление новых функций в продукт, особенно в компьютерном программном обеспечении, видеоиграх и бытовой и деловой электронике.
https://en.wikipedia.org/wiki/Feature_creep
Возьмем следующий пример:
<?php
class RegistrationService implements RegistrationServiceInterface
{
public function registerUser(/* ... */) { /* ... */ }
}
class EmailingRegistrationService extends RegistrationService
{
public function registerUser(/* ... */)
{
$user = parent::registerUser(/* ... */);
$this->sendTheRegistrationMail($user);
return $user;
}
// ...
}
Добавим классу ключевое слово final, что заставит нас использовать композицию:
<?php
final class EmailingRegistrationService implements RegistrationServiceInterface
{
public function __construct(RegistrationServiceInterface $mainRegistrationService)
{
$this->mainRegistrationService = $mainRegistrationService;
}
public function registerUser(/* ... */)
{
$user = $this->mainRegistrationService->registerUser(/* ... */);
$this->sendTheRegistrationMail($user);
return $user;
}
// ...
}
3. Разработчик начинает задумываться над общедоступном API
Разработчики, как правило, используют наследование для добавления средств доступа и дополнительных API к существующим классам:
<?php
class RegistrationService implements RegistrationServiceInterface
{
protected $db;
public function __construct(DbConnectionInterface $db)
{
$this->db = $db;
}
public function registerUser(/* ... */)
{
// ...
$this->db->insert($userData);
// ...
}
}
class SwitchableDbRegistrationService extends RegistrationService
{
public function setDb(DbConnectionInterface $db)
{
$this->db = $db;
}
}
Этот пример показывает ряд недостатков в мыслительном процессе, которые привели к SwitchableDbRegistrationService
:
- Этот
setDb
метод используется для измененияDbConnectionInterface
во время выполнения, что, по-видимому, скрывает другую решаемую проблему: может быть, нам нужноMasterSlaveConnection
? - Этот метод
setDb
не подпадает под действиеRegistrationServiceInterface
, поэтому мы можем использовать его только тогда, когда мы строго связываем наш код сSwitchableDbRegistrationService
, что в некоторых контекстах противоречит цели самого контракта. setDb
изменяет зависимости во время выполнения, и это может не поддерживаться логикойRegistrationService
, а также может привести к ошибкам.- Возможно, метод
setDb
был введен из-за ошибки в первоначальной реализации: почему исправление было предоставлено таким образом? Действительно ли это решение проблемы или оно устраняет только симптом?
В этом примере есть и другие проблемы, но это наиболее важные из них для нашей цели объяснить, почему с помощью ключевого слова final
мы могли бы предотвратить подобную ситуацию заранее.
4. Разработчик теперь будет обязан думать над упрощением общедоступного API
Поскольку классы с большим количеством общедоступных методов с большой вероятностью нарушат SRP, часто бывает, что разработчик захочет переопределить определенный API этих классов.
Начало использования final
заставляет разработчика заранее думать о новых API и о том, чтобы сделать их как можно меньше.
5. final
всегда можно сделать расширяемым
Вы можете сделать любой final класс расширяемым в любой момент времени (если это действительно требуется).
Недостатков нет, но вам придется объяснить свои доводы в пользу такого изменения себе и другим членам вашей команды, и это обсуждение может привести к лучшим решениям, прежде чем что-либо будет изменено.
6. extends
нарушает инкапсуляцию
Расширение класса нарушает инкапсуляцию и может привести к непредвиденным последствиям: дважды подумайте, прежде чем использовать ключевое слово extends
, или, что еще лучше, создайте свои классы final
и избавьте других от необходимости думать об этом.
7. Вам не нужна такая гибкость
Один аргумент, который мне всегда приходится опровергать, заключается в том, что final
снижает гибкость использования кодовой базы.
Мой контраргумент очень прост: вам не нужна такая гибкость.
Зачем вам это нужно в первую очередь? Почему вы не можете написать свою собственную индивидуальную реализацию контракта? Почему вы не можете использовать композицию? Вы тщательно обдумали проблему?
Если вам все еще нужно удалить ключевое слово final
из реализации, то здесь может быть задействован какой-то другой запах кода.
8. Вы можете свободно изменять код
После того, как вы создали класс final
, вы можете изменять его так, как вам заблагорассудится.
Поскольку инкапсуляция гарантированно сохраняется, единственное, о чем вам нужно заботиться, – это общедоступный API.
Теперь вы можете переписывать все, столько раз, сколько захотите.
Когда следует избегать final
:
Final классы эффективно работают только при следующих предположениях:
- Существует абстракция (интерфейс), которую реализует конечный класс
- Все общедоступные API final класса являются частью этого интерфейса
Если одно из этих двух предварительных условий отсутствует, то вы, вероятно, достигнете момента времени, когда сделаете класс расширяемым, поскольку ваш код на самом деле не полагается на абстракции.
Исключение может быть сделано, если конкретный класс представляет набор ограничений или концепций, которые являются полностью неизменяемыми, негибкими и глобальными для всей системы. Хорошим примером является математическая операция: $calculator->sum($a, $b)
вряд ли изменится со временем. В этих случаях можно с уверенностью предположить, что мы можем использовать ключевое слово final
без абстракции, на которую нужно полагаться в первую очередь.
Другой случай, когда вы не будете использовать ключевое слово final
, относится к существующим классам: это можно сделать, только если вы следуете semver и используете основную версию для затронутой кодовой базы.