Префікси та суфікси не потрібні в назвах інтерфейсів
Використання суфікса 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? Не
полінуйтеся і добре подумайте про те, що він
робить і чому він відрізняється від інших
можливих реалізацій.
А що, якщо я не можу уявити собі кілька можливих реалізацій? Що, якщо я можу придумати тільки один правильний спосіб? Тоді просто не створюйте інтерфейс. Принаймні, поки що.
О, є ще одна реалізація
І вона тут! Нам потрібна друга реалізація. Таке трапляється постійно. Ніколи не було потреби зберігати переклади більш ніж одним перевіреним способом, наприклад, у базі даних, але зараз з'явилася нова вимога, і нам потрібно мати більше перекладачів у додатку.
Це також той випадок, коли ви чітко усвідомлюєте, в чому полягала специфіка початкового єдиного перекладача. Це був перекладач бази даних, без за замовчуванням.
І що з ним?
- Зробіть ім'я
Translator
інтерфейсом - Перейменуйте оригінальний клас на
DatabaseTranslator
і він буде реалізовуватиTranslator
- А ви створюєте нові класи
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 в назвах інтерфейсів, від яких почав поступово відмовлятися кілька років тому, зберігаючи при цьому повну зворотну сумісність.
Щоб залишити коментар, будь ласка, увійдіть до системи