Префікси та суфікси не належать до назв інтерфейсів
Використання префікса 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
без
прикметника.
Навіть це збиває людей з пантелику, і вони
не знають, чи писати підказку типу для
Translator
чи TranslatorInterface
. У
клієнтському коді потім змішується і те, і
інше, ви напевно на це вже багато разів
натрапляли (у Nette, наприклад, у зв'язку з
Nette\Http\Request
проти IRequest
).
Чи не було б краще позбутися виняткової
реалізації та залишити загальну назву
Translator
для інтерфейсу? Тобто мати
конкретні реалізації з конкретною назвою +
загальний інтерфейс із загальною назвою. Це
ж має сенс.
Тягар описової назви тоді лежить виключно
на реалізаціях. Якщо ми перейменуємо
TranslatorInterface
на Translator
, наш
колишній клас Translator
потребує нової
назви. Люди схильні вирішувати цю проблему,
називаючи його DefaultTranslator
, я теж
винен. Але знову ж таки, чим він такий
винятковий, що називається Default? Не
лінуйтеся і добре подумайте над тим, що він
робить і чому це відрізняється від інших
можливих реалізацій.
А що, якщо я не можу уявити собі більше реалізацій? Що, якщо мені спадає на думку лише один дійсний спосіб? Тоді просто не створюйте інтерфейс. Принаймні поки що.
Ось, з'явилася ще одна реалізація
І ось воно! Нам потрібна друга реалізація. Це трапляється часто. Ніколи не виникало потреби зберігати переклади іншим способом, крім одного перевіреного, наприклад, у базі даних, але тепер з'явилася нова вимога, і потрібно мати в додатку більше перекладачів.
Це також момент, коли ви чітко усвідомлюєте, якою була специфіка початкового єдиного перекладача. Це був перекладач баз даних, жодний стандартний.
Що з цим робити?
- З назви
Translator
зробимо інтерфейс - Початковий клас перейменуєте на
DatabaseTranslator
і він реалізовуватимеTranslator
- І створите нові класи
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
в назвах інтерфейсів, чого кілька років
тому почав поступово
і з повним збереженням зворотної
сумісності позбуватися.
Щоб залишити коментар, будь ласка, увійдіть до системи