Prefiksy i sufiksy nie należą do nazw interfejsów
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ć?
- Z nazwy
Translator
zrobimy interfejs - Pierwotną klasę zmienisz nazwę na
DatabaseTranslator
i będzie implementowaćTranslator
- I stworzysz nowe klasy
GettextTranslator
i na przykładNeonTranslator
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ć.
Aby przesłać komentarz, proszę się zalogować