Prefiksy i sufiksy nie należą do nazw interfejsów

3 lata temu przez David Grudl  

Używanie prefiksu I lub sufiksu Interface w interfejsach, a także Abstract w klasach abstrakcyjnych, jest antywzorcem. W czystym kodzie nie ma dla nich miejsca. Rozróżnianie nazw interfejsów w rzeczywistości zaciemnia zasady OOP, wprowadza szum do kodu i powoduje komplikacje podczas rozwoju. Powody są następujące.

Typ = klasa + interfejs + potomkowie

W świecie OOP klasy i interfejsy są uważane za typy. Jeśli użyję typu przy deklaracji właściwości lub parametru, z punktu widzenia programisty nie ma różnicy, czy typ, na którym się opiera, jest klasą czy interfejsem. To wspaniała rzecz, dzięki której interfejsy są właściwie tak użyteczne. To nadaje ich istnieniu sens. (Poważnie: do czego byłyby interfejsy, gdyby ta zasada nie obowiązywała? Spróbuj się nad tym zastanowić.)

Spójrz na ten kod:

class FormRenderer
{
	public function __construct(
		private Form $form,
		private Translator $translator,
	) {
	}
}

Konstruktor mówi: “Potrzebuję formularza i tłumacza.” I jest mu zupełnie obojętne, czy dostanie obiekt GettextTranslator czy DatabaseTranslator. A jednocześnie jako użytkownikowi jest mi zupełnie obojętne, czy Translator jest interfejsem, klasą abstrakcyjną czy klasą konkretną.

Jest mi to zupełnie obojętne? Właściwie nie, przyznaję się, że jestem dość ciekawy, więc kiedy badam obcą bibliotekę, zaglądam, co kryje się za typem, i najeżdżam na niego myszką:

Aha, już wiem. I na tym koniec. Wiedza, czy jest to klasa czy interfejs, byłaby ważna, gdybym chciał tworzyć jej instancję, ale to nie ten przypadek, teraz tylko mówię o typie zmiennej. I tutaj chcę być odizolowany od tych szczegółów. A już na pewno nie chcę ich wprowadzać do swojego kodu! To, co kryje się za typem, jest częścią jego definicji, a nie samego typu.

A teraz spójrz na kolejny kod:

class FormRenderer
{
	public function __construct(
		private AbstractForm $form,
		private TranslatorInterface $translator,
	) {
	}
}

Ta definicja konstruktora dosłownie mówi: “Potrzebuję abstrakcyjnego formularza i interfejsu tłumacza.” Ale to nonsens. Potrzebuje konkretnego formularza, który ma wyrenderować. Nie abstrakcyjnego formularza. I potrzebuje obiektu, który pełni rolę tłumacza. Nie potrzebuje interfejsu.

Wiesz, że słowa Interface i Abstract należy ignorować. Że konstruktor chce tego samego, co w poprzednim przykładzie. Ale… naprawdę? Naprawdę uważasz za dobry pomysł wprowadzenie do konwencji nazewnictwa używania słów, które należy pomijać?

Przecież tworzy to fałszywe wyobrażenie o zasadach OOP. Początkujący musi być zdezorientowany: „Jeśli przez typ Translator rozumie się albo 1) obiekt klasy Translator, 2) obiekt implementujący interfejs Translator lub 3) obiekt od nich dziedziczący, co w takim razie rozumie się przez TranslatorInterface?” Na to nie da się rozsądnie odpowiedzieć.

Kiedy piszemy TranslatorInterface, chociaż również Translator może być interfejsem, dopuszczamy się tautologii. To samo, gdy deklarujemy interface TranslatorInterface. I tak dalej. Aż powstanie programistyczny żart:

interface TranslatorInterface
{
}

class FormRendererClass
{
	/**
	 * Konstruktor
	 */
	public function __construct(
		private AbstractForm $privatePropertyForm,
		private TranslatorInterface $privatePropertyTranslator,
	) {
		// 🤷‍♂️
	}
}

Wyjątkowa implementacja

Kiedy widzę coś takiego jak TranslatorInterface, jest prawdopodobne, że będzie istnieć również implementacja o nazwie Translator implements TranslatorInterface. Zmusza mnie to do zastanowienia: czym Translator jest tak wyjątkowy, że ma wyłączne prawo nazywać się Translator? Każda inna implementacja potrzebuje opisowej nazwy, na przykład GettextTranslator lub DatabaseTranslator, ale ta jest jakby “domyślna”, jak sugeruje jej uprzywilejowana pozycja, gdy nazywa się Translator bez przymiotnika.

Nawet to sprawia, że ludzie są niepewni i nie wiedzą, czy powinni pisać typehint dla Translator czy TranslatorInterface. W kodzie klienckim miesza się wtedy jedno i drugie, na pewno już wielokrotnie na to natrafiliście (w Nette na przykład w związku z Nette\Http\Request vs IRequest).

Czy nie byłoby lepiej pozbyć się wyjątkowej implementacji i pozostawić ogólną nazwę Translator dla interfejsu? Czyli mieć konkretne implementacje z konkretną nazwą + ogólny interfejs z ogólną nazwą. To przecież ma sens.

Ciężar opisowej nazwy spoczywa wtedy wyłącznie na implementacjach. Jeśli zmienimy nazwę TranslatorInterface na Translator, nasza była klasa Translator potrzebuje nowej nazwy. Ludzie mają tendencję do rozwiązywania tego problemu, nazywając ją DefaultTranslator, ja też jestem winny. Ale znowu, czym jest tak wyjątkowa, że nazywa się Default? Nebuďte líní a pořádně se zamyslete nad tím, co dělá a proč se to liší od ostatních možných implementací.

A co jeśli nie potrafię sobie wyobrazić więcej implementacji? Co jeśli przychodzi mi do głowy tylko jeden słuszny sposób? To po prostu nie twórzcie interfejsu. Przynajmniej na razie.

Oto pojawiła się kolejna implementacja

I stało się! Potrzebujemy drugiej implementacji. Zdarza się to często. Nigdy nie było potrzeby przechowywania tłumaczeń w inny niż jeden sprawdzony sposób, np. w bazie danych, ale teraz pojawiło się nowe wymaganie i trzeba mieć w aplikacji więcej tłumaczy.

To także chwila, w której jasno zdajesz sobie sprawę, jaka była specyfika pierwotnego, jedynego tłumacza. Był to tłumacz bazodanowy, żaden domyślny.

Co z tym zrobić?

  1. Z nazwy Translator zrobimy interfejs
  2. Pierwotną klasę zmienisz nazwę na DatabaseTranslator i będzie implementować Translator
  3. I stworzysz nowe klasy GettextTranslator i na przykład NeonTranslator

Wszystkie te zmiany robi się bardzo wygodnie i łatwo, zwłaszcza jeśli aplikacja jest zbudowana zgodnie z zasadami dependency injection. W kodzie nie trzeba nic zmieniać, tylko w konfiguracji kontenera DI zmienimy Translator na DatabaseTranslator. To wspaniałe!

Diametralnie inna sytuacja byłaby jednak, gdybyśmy upierali się przy prefiksowaniu/sufiksowaniu. Musielibyśmy w kodzie w całej aplikacji zmieniać nazwy typów z Translator na TranslatorInterface. Taka zmiana nazwy byłaby czysto celowa ze względu na przestrzeganie konwencji, ale byłaby sprzeczna z sensem OOP, jak pokazaliśmy przed chwilą. Interfejs się nie zmienił, kod użytkownika się nie zmienił, ale konwencja wymaga zmiany nazw? W takim razie jest to błędna konwencja.

Gdyby dodatkowo z czasem okazało się, że lepsza niż interfejs byłaby klasa abstrakcyjna, zmienialibyśmy nazwy ponownie. Taka ingerencja wcale nie musi być trywialna, na przykład gdy kod jest rozłożony na wiele pakietów lub używają go strony trzecie.

Ale przecież wszyscy tak robią

Nie wszyscy. Prawdą jest, że w świecie PHP spopularyzował rozróżnianie nazw interfejsów i klas abstrakcyjnych Zend Framework, a po nim Symfony, czyli duzi gracze. To podejście przejęła również PSR, która paradoksalnie publikuje tylko interfejsy, a mimo to przy każdym podaje w nazwie słowo interfejs.

Z drugiej strony inny znaczący framework Laravel nie rozróżnia w żaden sposób interfejsów i klas abstrakcyjnych. Nie robi tego na przykład popularna warstwa bazodanowa Doctrine. I nie robi tego również standardowa biblioteka w PHP (mamy więc interfejsy Throwable czy Iterator, klasę abstrakcyjną FilterIterator, itp.).

Gdybyśmy spojrzeli na świat poza PHP, to na przykład C# używa prefiksu I dla interfejsów, natomiast w Javie czy TypeScript nazwy się nie rozróżnia.

Nie robią tego więc wszyscy, niemniej jednak nawet gdyby robili, nie oznacza to, że jest to dobre. Bezmyślne przejmowanie tego, co robią inni, nie jest rozsądne, ponieważ można przejmować również błędy. Błędy, których drugi być może bardzo chętnie sam by się pozbył, tylko jest to zbyt trudne.

Nie rozpoznam w kodzie, co jest interfejsem

Wielu programistów będzie argumentować, że prefiksy/sufiksy są dla nich użyteczne, ponieważ dzięki nim od razu rozpoznają w kodzie, co jest interfejsem. Mają poczucie, że brakowałoby im takiego rozróżnienia. Zobaczmy więc, czy rozpoznasz w tych przykładach, co jest klasą, a co interfejsem?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X jest zawsze klasą, Y jest interfejsem, jest to jednoznaczne nawet bez prefiksów/postfixów. Oczywiście wie to również IDE i w danym kontekście zawsze będzie poprawnie podpowiadać.

Ale co tutaj:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try {
} catch (A $x) {
}

W tych przypadkach tego nie rozpoznasz. Jak powiedzieliśmy na samym początku, tutaj z punktu widzenia programisty nie powinno być różnicy między tym, co jest klasą, a co interfejsem. Co właśnie nadaje sens interfejsom i klasom abstrakcyjnym.

Gdybyś tutaj był w stanie rozróżnić klasę od interfejsu, zaprzeczyłoby to podstawowej zasadzie OOP. A interfejsy straciłyby sens.

Jestem do tego przyzwyczajony

Zmiana nawyków po prostu boli 🙂 Ile razy już sama myśl o tym. Ale żeby nie być niesprawiedliwym, wielu ludzi zmiany wręcz przyciągają i cieszą się na nie, niemniej jednak dla większości obowiązuje zasada, że przyzwyczajenie jest drugą naturą.

Ale wystarczy spojrzeć w przeszłość, jak niektóre zwyczaje rozwiał czas. Chyba najsłynniejsza jest tzw. notacja węgierska używana od lat osiemdziesiątych i spopularyzowana przez Microsoft. Notacja polegała na tym, że nazwa każdej zmiennej zaczynała się od skrótu symbolizującego jej typ danych. W PHP wyglądałoby to tak: echo $this->strName lub $this->intCount++. Od notacji węgierskiej zaczęto odchodzić w latach dziewięćdziesiątych, a dzisiaj Microsoft w swoich wytycznych wręcz odradza ją programistom.

Kiedyś była to nieodłączna część, a dzisiaj nikomu jej nie brakuje.

Ale po co sięgać do tak odległej przeszłości? Być może pamiętasz, że w PHP zwyczajem było odróżnianie niepublicznych członków klas podkreślnikiem (ukázka ze Zend Framework). Było to w czasach, gdy już dawno istniało PHP 5, które miało modyfikatory widoczności public/protected/private. Ale programiści robili to z przyzwyczajenia. Byli przekonani, że bez podkreślników przestaliby orientować się w kodzie. „Jak bym w kodzie rozróżnił publiczne od prywatnych zmiennych, aha?”

Dzisiaj podkreślników nie używa nikt. I nikomu ich nie brakuje. Czas świetnie zweryfikował, że obawy były bezpodstawne.

Jest to przecież dokładnie to samo, co zarzut: „Jak bym w kodzie rozróżnił interfejs od klasy, aha?”

Przestałem używać prefiksów/postfixów dziesięć lat temu. Nigdy bym nie wrócił, była to wspaniała decyzja. Nie znam też żadnego innego programisty, który chciałby wrócić. Jak powiedział jeden kolega: „Spróbuj tego, a za miesiąc nie będziesz rozumiał, że kiedykolwiek robiłeś inaczej.”

Chcę zachować spójność

Potrafię sobie wyobrazić, że programista powie: „Używanie prefiksów i sufiksów jest naprawdę bez sensu, rozumiem to, ale mam już tak zbudowany kod i zmiana jest bardzo trudna. A gdybym zaczął nowy kod pisać poprawnie bez nich, powstanie mi niespójność, która jest chyba jeszcze gorsza niż zła konwencja.”

W rzeczywistości już teraz twój kod jest niespójny, ponieważ używasz systemowej biblioteki PHP, która nie ma żadnych prefiksów i postfixów:

class Collection implements ArrayAccess, Countable, IteratorAggregate
{
	public function add(string|Stringable $item): void
	{
	}
}

I ręka na sercu, czy to przeszkadza? Czy kiedykolwiek przyszło ci do głowy, że byłoby spójniej tak?

class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
	public function add(string|StringableInterface $item): void
	{
	}
}

Albo tak?

try {
	$command = $this->find($name);
} catch (ThrowableInterface $e) {
	return $e->getMessage();
}

Myślę, że nie. Spójność nie odgrywa tak znaczącej roli, jak mogłoby się wydawać. Wręcz przeciwnie, oko preferuje mniej szumu wizualnego, mózg czystość projektu. Zatem zmiana konwencji i rozpoczęcie pisania nowych interfejsów poprawnie, bez prefiksów i sufiksów, ma sens.

Można je celowo usunąć nawet z dużych projektów. Przykładem jest Nette Framework, który historycznie używał prefiksów I w nazwach interfejsów, czego kilka lat temu zaczął się stopniowo i z pełnym zachowaniem kompatybilności wstecznej pozbywać.