Префиксам и суффиксам не место в именах интерфейсов

3 года назад от David Grudl  

Использование префикса I или суффикса Interface у интерфейсов, а также Abstract у абстрактных классов, является антипаттерном. В чистом коде ему не место. Различение имен интерфейсов на самом деле затуманивает принципы ООП, вносит шум в код и вызывает сложности при разработке. Причины следующие.

Тип = класс + интерфейс + потомки

В мире ООП классы и интерфейсы считаются типами. Если я использую тип при объявлении свойства или параметра, с точки зрения разработчика нет разницы между тем, является ли тип, на который он полагается, классом или интерфейсом. Это замечательная вещь, благодаря которой интерфейсы, собственно, так полезны. Это придает их существованию смысл. (Серьезно: зачем нужны были бы интерфейсы, если бы этот принцип не действовал? Попробуйте задуматься над этим.)

Посмотрите на этот код:

class FormRenderer
{
	public function __construct(
		private Form $form,
		private Translator $translator,
	) {
	}
}

Конструктор говорит: “Мне нужна форма и переводчик.” И ему совершенно все равно, получит ли он объект GettextTranslator или DatabaseTranslator. И в то же время мне, как пользователю, совершенно все равно, является ли Translator интерфейсом, абстрактным классом или конкретным классом.

Мне это совершенно все равно? На самом деле нет, признаюсь, я довольно любопытен, поэтому, когда изучаю чужую библиотеку, заглядываю, что скрывается за типом, и навожу на него мышь:

Ага, теперь я знаю. И на этом все заканчивается. Знание того, является ли это классом или интерфейсом, было бы важно, если бы я хотел создать его экземпляр, но это не тот случай, сейчас я просто говорю о типе переменной. И здесь я хочу быть изолирован от этих деталей. И уж тем более не хочу вносить их в свой код! То, что скрывается за типом, является частью его определения, а не самого типа.

А теперь посмотрите на другой код:

class FormRenderer
{
	public function __construct(
		private AbstractForm $form,
		private TranslatorInterface $translator,
	) {
	}
}

Это определение конструктора буквально говорит: “Мне нужна абстрактная форма и интерфейс переводчика.” Но это глупость. Ему нужна конкретная форма, которую он должен отобразить. Не абстрактная форма. И ему нужен объект, выполняющий роль переводчика. Ему не нужен интерфейс.

Вы знаете, что слова Interface и Abstract следует игнорировать. Что конструктор хочет того же, что и в предыдущем примере. Но… серьезно? Вам действительно кажется хорошей идеей вводить в соглашения об именах использование слов, которые следует игнорировать?

Ведь это создает ложное представление о принципах ООП. Новичок должен быть сбит с толку: «Если под типом Translator понимается либо 1) объект класса Translator, 2) объект, реализующий интерфейс Translator, либо 3) объект, наследующий от них, то что тогда понимается под TranslatorInterface?» На это невозможно разумно ответить.

Когда мы пишем TranslatorInterface, хотя и Translator может быть интерфейсом, мы допускаем тавтологию. То же самое, когда мы объявляем interface TranslatorInterface. И так далее. В итоге получается программистская шутка:

interface TranslatorInterface
{
}

class FormRendererClass
{
	/**
	 * Конструктор
	 */
	public function __construct(
		private AbstractForm $privatePropertyForm,
		private TranslatorInterface $privatePropertyTranslator,
	) {
		// 🤷‍♂️
	}
}

Исключительная реализация

Когда я вижу что-то вроде TranslatorInterface, вероятно, будет существовать и реализация с именем Translator implements TranslatorInterface. Это заставляет меня задуматься: чем Translator так исключителен, что имеет уникальное право называться Translator? Каждая другая реализация нуждается в описательном имени, например GettextTranslator или DatabaseTranslator, но эта как бы “по умолчанию”, как указывает ее привилегированное положение, когда она называется Translator без прилагательного.

Это даже сбивает людей с толку, и они не знают, следует ли писать typehint для Translator или TranslatorInterface. В клиентском коде затем смешивается и то, и другое, вы наверняка с этим уже много раз сталкивались (в Nette, например, в связи с Nette\Http\Request vs IRequest).

Не лучше ли было бы избавиться от исключительной реализации и оставить общее имя Translator для интерфейса? То есть иметь конкретные реализации с конкретным именем + общий интерфейс с общим именем. Это ведь имеет смысл.

Бремя описательного имени тогда ложится исключительно на реализации. Если мы переименуем TranslatorInterface в Translator, наш бывший класс Translator нуждается в новом имени. Люди склонны решать эту проблему, называя его DefaultTranslator, я и сам виноват. Но опять же, чем он так исключителен, что называется Default? Не ленитесь и хорошенько подумайте над тем, что он делает и почему это отличается от других возможных реализаций.

А что, если я не могу представить себе несколько реализаций? Что, если мне приходит в голову только один действительный способ? Тогда просто не создавайте интерфейс. По крайней мере, пока.

Смотрите, появилась еще одна реализация

И вот оно! Нам нужна вторая реализация. Это случается часто. Никогда не возникало необходимости хранить переводы иначе, чем одним проверенным способом, например, в базе данных, но теперь появилось новое требование, и нужно иметь в приложении больше переводчиков.

Это также момент, когда вы ясно осознаете, в чем заключалась специфика исходного единственного переводчика. Это был переводчик базы данных, никакой не default.

Что с этим делать?

  1. Из имени Translator сделаем интерфейс
  2. Исходный класс переименуем в DatabaseTranslator и он будет реализовывать Translator
  3. И создадим новые классы GettextTranslator и, например, NeonTranslator

Все эти изменения делаются очень удобно и легко, особенно если приложение построено в соответствии с принципами внедрения зависимостей. В коде ничего менять не нужно, только в конфигурации DI-контейнера изменим Translator на DatabaseTranslator. Это здорово!

Диаметрально противоположная ситуация возникла бы, если бы мы настаивали на префиксации/суффиксации. Нам пришлось бы в коде по всему приложению переименовывать типы с Translator на TranslatorInterface. Такое переименование было бы чисто целевым ради соблюдения конвенции, но шло бы вразрез со смыслом ООП, как мы показали только что. Интерфейс не изменился, пользовательский код не изменился, но конвенция требует переименовывать? Тогда это ошибочная конвенция.

Если бы к тому же со временем выяснилось, что лучше интерфейса был бы абстрактный класс, мы бы переименовывали снова. Такое вмешательство вовсе не обязательно будет тривиальным, например, если код распределен по нескольким пакетам или его используют третьи стороны.

Но ведь все так делают

Не все. Правда, что в мире PHP популяризировали различение имен интерфейсов и абстрактных классов Zend Framework, а за ним и Symfony, то есть крупные игроки. Этот подход приняла и PSR, которая, парадоксально, публикует только интерфейсы, и тем не менее у каждого указывает в имени слово интерфейс.

С другой стороны, другой значимый фреймворк Laravel интерфейсы и абстрактные классы никак не различает. Не делает этого, например, и популярный слой баз данных Doctrine. И не делает этого и стандартная библиотека PHP (у нас есть интерфейсы Throwable или Iterator, абстрактный класс FilterIterator и т.п.).

Если бы мы посмотрели на мир за пределами PHP, то, например, C# использует префикс I для интерфейсов, напротив, в Java или TypeScript имена не различаются.

Так что делают это не все, однако даже если бы делали, это не значит, что это правильно. Бездумно перенимать то, что делают другие, неразумно, потому что можно перенять и ошибки. Ошибки, от которых другой, возможно, и сам бы с радостью избавился, только это слишком сложно.

Я не узнаю в коде, что является интерфейсом

Ряд программистов будут возражать, что для них префиксы/суффиксы полезны, так как благодаря им они сразу в коде узнают, что является интерфейсом. У них есть ощущение, что им бы не хватало такого различения. Давайте проверим, узнаете ли вы в этих примерах, что является классом, а что интерфейсом?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X — это всегда класс, Y — интерфейс, это однозначно и без префиксов/постфиксов. Конечно, это знает и IDE, и в данном контексте вам всегда будет правильно подсказывать.

А что здесь:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try {
} catch (A $x) {
}

В этих случаях вы не узнаете. Как мы сказали в самом начале, здесь с точки зрения разработчика не должно быть разницы между тем, что является классом, а что интерфейсом. Что как раз и придает смысл интерфейсам и абстрактным классам.

Если бы вы здесь были способны различить класс от интерфейса, это опровергло бы основной принцип ООП. И интерфейсы потеряли бы смысл.

Я к этому привык

Менять привычки просто больно 🙂 Сколько раз даже само представление об этом. Но чтобы не быть несправедливыми, ряд людей изменения, наоборот, привлекают, и они их ждут, однако для большинства действует правило, что привычка — вторая натура.

Но достаточно посмотреть в прошлое, как некоторые привычки унесло время. Наверное, самая известная — это так называемая венгерская нотация, использовавшаяся с восьмидесятых годов и популяризированная Microsoft. Нотация заключалась в том, что имя каждой переменной начиналось с аббревиатуры, символизирующей ее тип данных. В PHP это выглядело бы так: echo $this->strName или $this->intCount++. От венгерской нотации начали отходить в девяностых годах, и сегодня Microsoft в своих рекомендациях разработчиков прямо отговаривает от нее.

Когда-то это была неотъемлемая часть, а сегодня никому ее не хватает.

Но зачем ходить в такое далекое прошлое? Возможно, вы помните, что в PHP было принято отличать непубличные члены классов подчеркиванием (пример из Zend Framework). Это было во времена, когда уже давно существовал PHP 5, у которого были модификаторы видимости public/protected/private. Но программисты делали это по привычке. Они были убеждены, что без подчеркиваний перестали бы ориентироваться в коде. «Как бы я в коде различил публичные от приватных переменных, а?»

Сегодня подчеркивания не использует никто. И никому их не хватает. Время отлично проверило, что опасения были напрасны.

При этом это совершенно то же самое, что и возражение: «Как бы я в коде различил интерфейс от класса, а?»

Я перестал использовать префиксы/постфиксы десять лет назад. Никогда бы не вернулся, это было отличное решение. Я не знаю ни одного другого программиста, который хотел бы вернуться. Как сказал один друг: «Попробуй, и через месяц не будешь понимать, как ты когда-то делал иначе.»

Я хочу поддерживать последовательность

Могу представить, что программист скажет: «Использовать префиксы и суффиксы действительно бессмысленно, я это понимаю, но у меня уже так построен код, и изменение очень сложно. А если бы я начал новый код писать правильно без них, у меня возникнет неконсистентность, которая, возможно, еще хуже, чем плохая конвенция.»

На самом деле уже сейчас ваш код неконсистентен, потому что вы используете системную библиотеку PHP, у которой нет никаких префиксов и постфиксов:

class Collection implements ArrayAccess, Countable, IteratorAggregate
{
	public function add(string|Stringable $item): void
	{
	}
}

И положа руку на сердце, это мешает? Вам когда-нибудь приходило в голову, что было бы консистентнее так?

class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
	public function add(string|StringableInterface $item): void
	{
	}
}

Или так?

try {
	$command = $this->find($name);
} catch (ThrowableInterface $e) {
	return $e->getMessage();
}

Думаю, что нет. Консистентность не играет такой значительной роли, как могло бы показаться. Наоборот, глаз предпочитает меньше визуального шума, мозг — чистоту дизайна. Таким образом, изменить конвенцию и начать писать новые интерфейсы правильно без префиксов и суффиксов имеет смысл.

Их можно целенаправленно удалить и из крупных проектов. Примером является Nette Framework, который исторически использовал префиксы I в именах интерфейсов, от чего несколько лет назад начал постепенно и с полным сохранением обратной совместимости избавляться.