Префиксите и суфиксите не принадлежат на имената на интерфейсите

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