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

преди 3 години от David Grudl  

Използването на префикс 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 без определение.

Дори това кара хората да се чувстват несигурни и да не знаят дали трябва да пишат typehint за Translator или TranslatorInterface. В клиентския код след това се смесва и двете, сигурно сте се сблъсквали с това много пъти (в Nette например във връзка с Nette\Http\Request срещу IRequest).

Не би ли било по-добре да се отървем от изключителната имплементация и да оставим общото име Translator за интерфейса? Тоест, да имаме конкретни имплементации с конкретно име + общ интерфейс с общо име. Това все пак има смисъл.

Тежестта на описателното име тогава лежи изцяло върху имплементациите. Ако преименуваме TranslatorInterface на Translator, нашата бивша класа Translator се нуждае от ново име. Хората имат тенденция да решават този проблем, като я наричат DefaultTranslator, и аз съм виновен за това. Но отново, с какво е толкова специална, че се нарича Default? Не бъдете мързеливи и се замислете добре какво прави и защо се различава от другите възможни имплементации.

Ами ако не мога да си представя повече имплементации? Ами ако ми идва наум само един валиден начин? Тогава просто не създавайте интерфейс. Поне засега.

Ето, появи се друга имплементация

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

Това е и моментът, в който ясно осъзнавате каква е била спецификата на първоначалния единствен преводач. Това беше преводач за база данни, никакъв default.

Какво да правим с това?

  1. От името Translator ще направим интерфейс
  2. Първоначалния клас ще преименувате на DatabaseTranslator и той ще имплементира Translator
  3. И ще създадете нови класове GettextTranslator и например NeonTranslator

Всички тези промени се правят много удобно и лесно, особено ако приложението е изградено в съответствие с принципите на dependency injection. В кода не е необходимо нищо да се променя, само в конфигурацията на DI контейнера ще променим Translator на DatabaseTranslator. Това е страхотно!

Диаметрално различна ситуация обаче би възникнала, ако настоявахме за префиксиране/суфиксиране. Трябваше да преименуваме типовете в кода в цялото приложение от Translator на TranslatorInterface. Такова преименуване би било чисто целесъобразно заради спазването на конвенцията, но би противоречало на смисъла на ООП, както показахме преди малко. Интерфейсът не се е променил, потребителският код не се е променил, но конвенцията изисква преименуване? Тогава става дума за грешна конвенция.

Ако освен това с времето се окажеше, че по-добре от интерфейс би бил абстрактен клас, щяхме да преименуваме отново. Такава намеса изобщо не е задължително да бъде тривиална, например когато кодът е разпределен в повече пакети или го използват трети страни.

Но нали всички го правят така

Не всички. Вярно е, че в света на PHP популяризира разграничаването на имената на интерфейсите и абстрактните класове Zend Framework, а след него и Symfony, т.е. големи играчи. Този подход беше приет и от PSR, която парадоксално публикува само интерфейси, и въпреки това при всеки посочва в името думата интерфейс.

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