Префікси та суфікси не потрібні в назвах інтерфейсів

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 настільки особливим, що він має унікальний привілей називатися Перекладачем? Кожна інша реалізація потребує описової назви, наприклад, GettextTranslator або DatabaseTranslator, але ця реалізація є чимось на кшталт “за замовчуванням”, про що свідчить її привілейований статус, коли її називають Translator без етикетки.

Навіть люди плутаються і не знають, чи вводити підказку для Translator чи TranslatorInterface. Потім обидва варіанти змішуються у клієнтському коді, я впевнений, що ви стикалися з цим багато разів (у Nette, наприклад, у зв'язку з Nette\Http\Request vs 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, який іронічно публікує тільки інтерфейси, але включає слово interface в назву кожного з них.

З іншого боку, інший великий фреймворк, 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 було прийнято виділяти непублічні члени класів символом підкреслення (зразок з 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 в назвах інтерфейсів, від яких почав поступово відмовлятися кілька років тому, зберігаючи при цьому повну зворотну сумісність.