Префиксы и суффиксы не должны присутствовать в именах интерфейсов

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

Использование суффикса I prefix or 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
{
	/**
	 * Constructor
	 */
	public function __construct(
		private AbstractForm $privatePropertyForm,
		private TranslatorInterface $privatePropertyTranslator,
	) {
		// 🤷‍♂️
	}
}

Выдающаяся реализация

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

Даже люди путаются и не знают, что вводить – Translator или TranslatorInterface. Затем оба названия путаются в клиентском коде, я уверен, что вы сталкивались с этим много раз (в Nette, например, в связи с Nette\Http\Request против IRequest).

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

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

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

Эх, есть другая реализация

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

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

И что же?

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

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

Однако диаметрально противоположная ситуация возникла бы, если бы мы настаивали на префиксации/суффиксации. Нам пришлось бы переименовывать типы с 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 is always a class, 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 было принято выделять непубличные члены классов знаком подчеркивания (sample from 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 в названиях интерфейсов, от которых он начал постепенно отказываться несколько лет назад, сохраняя полную обратную совместимость.