Przedrostki i przyrostki nie są częścią nazw interfejsów
Używanie przyrostka I
prefix or Interface
dla
interfejsów, oraz Abstract
dla klas abstrakcyjnych, jest
antypatycznym wzorcem. Nie należy do czystego kodu. Rozróżnianie nazw
interfejsów w rzeczywistości rozmywa zasady OOP, dodaje szumu do kodu
i powoduje komplikacje podczas rozwoju. Oto powody.
Typ = Klasa + Interfejs + Potomkowie
W świecie OOP, zarówno klasy jak i interfejsy są uważane za typy. Jeśli używam typu podczas deklarowania właściwości lub parametru, z perspektywy dewelopera nie ma różnicy między tym, czy typ, na którym się opiera, jest klasą czy interfejsem. To jest ta fajna rzecz, która sprawia, że interfejsy są tak użyteczne, w rzeczywistości. To właśnie nadaje sens ich istnieniu. (Poważnie: po co byłyby interfejsy, gdyby ta zasada nie miała zastosowania? Spróbuj o tym pomyśleć).
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 nie
obchodzi go, czy dostanie obiekt GettextTranslator
czy obiekt
DatabaseTranslator
. A jednocześnie ja jako użytkownik nie dbam
o to, czy Translator
jest interfejsem, klasą abstrakcyjną czy
klasą konkretną.
Naprawdę mnie to nie obchodzi? Właściwie nie, przyznaję, że jestem dość ciekawski, więc kiedy eksploruję czyjąś bibliotekę, zerkam na to, co kryje się za typem i najeżdżam na niego:
O, widzę. I na tym kończy się historia. Wiedza o tym, czy jest to klasa czy interfejs byłaby ważna, gdybym chciał stworzyć jej instancję, ale tak nie jest, mówię teraz tylko o typie zmiennej. I to jest miejsce, w którym chcę pozostać z dala od tych szczegółów implementacji. I na pewno nie chcę, aby były one osadzone w moim kodzie! To, co jest za typem, jest częścią jego definicji, a nie samym typem.
Teraz spójrz na inny kod:
class FormRenderer
{
public function __construct(
private AbstractForm $form,
private TranslatorInterface $translator,
) {
}
}
Ta definicja konstruktora dosłownie mówi: "Potrzebuję abstrakcyjnej formy i interfejsu tłumacza. " Ale to głupie. Potrzebuje konkretnej formy do renderowania. Nie abstrakcyjnej formy. I potrzebuje obiektu, który działa jako tłumacz. Nie potrzebuje interfejsu.
Wiesz, że słowa Interface
i Abstract
mają być
ignorowane. Że konstruktor chce tego samego, co w poprzednim przykładzie.
Ale… naprawdę? Czy naprawdę wydaje się, że wprowadzenie do konwencji
nazewniczych użycia słów, które mają być ignorowane, jest dobrym
pomysłem?
Przecież tworzy to fałszywe wyobrażenie o zasadach OOP. Początkujący
użytkownik musi być zdezorientowany: “Jeśli typ Translator
oznacza albo 1) obiekt klasy Translator
2) obiekt implementujący
interfejs Translator
albo 3) obiekt dziedziczący po nich, to co
oznacza TranslatorInterface
?”. Nie ma na to sensownej
odpowiedzi.
Kiedy używamy TranslatorInterface
, nawet jeśli
Translator
może być interfejsem, popełniamy tautologię. To samo
dzieje się, gdy deklarujemy interface TranslatorInterface
.
Stopniowo dojdzie do żartu programistycznego:
interface TranslatorInterface
{
}
class FormRendererClass
{
/**
* Constructor
*/
public function __construct(
private AbstractForm $privatePropertyForm,
private TranslatorInterface $privatePropertyTranslator,
) {
// 🤷♂️
}
}
Outstanding Implementation
Kiedy widzę coś takiego jak TranslatorInterface
,
prawdopodobnie istnieje implementacja o nazwie
Translator implements TranslatorInterface
. To sprawia, że
zastanawiam się: co czyni Translator
tak wyjątkowym, że ma
unikalny przywilej bycia nazywanym Tłumaczem? Każda inna implementacja
potrzebuje opisowej nazwy, takiej jak GettextTranslator
lub
DatabaseTranslator
, ale ta jest jakby “domyślna”, jak sugeruje
jej preferowany status bycia nazywanym Translator
bez etykiety.
Nawet ludzie są zdezorientowani i nie wiedzą, czy wpisać typehint dla
Translator
lub TranslatorInterface
. Następnie oba są
mieszane w kodzie klienta, jestem pewien, że natknąłeś się na to wiele razy
(w Nette na przykład w połączeniu z Nette\Http\Request
vs
IRequest
).
Czy nie byłoby lepiej pozbyć się specjalnej implementacji i zachować
ogólną nazwę Translator
dla interfejsu? Czyli posiadanie
konkretnych implementacji z konkretną nazwą + generyczny interfejs
z generyczną nazwą. To ma sens.
Ciężar opisowej nazwy leży wtedy wyłącznie na implementacjach. Jeśli
zmienimy nazwę TranslatorInterface
na Translator
,
nasza dawna klasa Translator
potrzebuje nowej nazwy. Ludzie mają
tendencję do rozwiązywania tego problemu poprzez nazywanie jej
DefaultTranslator
, nawet ja jestem temu winny. Ale znowu, co czyni
go tak wyjątkowym, że nazywa się Default? Nie bądź leniwy i zastanów się
dobrze, co to robi i dlaczego różni się od innych możliwych
implementacji.
A co jeśli nie mogę sobie wyobrazić kilku możliwych implementacji? Co jeśli mogę myśleć tylko o jednym poprawnym sposobie? Więc po prostu nie twórz interfejsu. Przynajmniej na razie.
Eh, jest inna implementacja
I to jest tutaj! Potrzebujemy drugiej implementacji. To się zdarza cały czas. Nigdy nie było potrzeby przechowywania tłumaczeń w więcej niż jeden sprawdzony sposób, np. w bazie danych, ale teraz jest nowe wymaganie i musimy mieć więcej tłumaczy w aplikacji.
Wtedy też wyraźnie widać, jaka była specyfika pierwotnego pojedynczego translatora. Był to translator bazodanowy, bez default.
Co w związku z tym?
- Nadać nazwę
Translator
interfejsowi. - Zmień nazwę oryginalnej klasy na
DatabaseTranslator
i będzie ona implementowaćTranslator
- I tworzysz nowe klasy
GettextTranslator
i możeNeonTranslator
Wszystkie te zmiany są bardzo wygodne i łatwe do wykonania, zwłaszcza
jeśli aplikacja jest zbudowana zgodnie z zasadami dependency
injection trzeba nic zmieniać w kodzie**, wystarczy zmienić
Translator
na DatabaseTranslator
w konfiguracji
kontenera DI. Świetnie!!!
Jednak diametralnie inna sytuacja powstałaby, gdybyśmy uparli się na
prefiksowanie/sufiksowanie. Musielibyśmy zmienić nazwy typów z
Translator
na TranslatorInterface
w kodzie w całej
aplikacji. Taka zmiana nazwy byłaby czysto dla dobra konwencji, ale byłaby
sprzeczna z duchem OOP, jak właśnie pokazaliśmy. Interfejs się nie
zmienił, kod użytkownika się nie zmienił, ale konwencja wymaga zmiany nazwy?
W takim razie jest to wadliwa konwencja.
Co więcej, gdyby z czasem okazało się, że klasa abstrakcyjna byłaby lepsza od interfejsu, zmienilibyśmy nazwę ponownie. Takie działanie może wcale nie być trywialne, na przykład gdy kod jest rozłożony na wiele pakietów lub używany przez osoby trzecie.
Ale wszyscy to robią
Nie wszyscy tak robią. To prawda, że w świecie PHP, Zend Framework, a następnie Symfony, czyli wielcy gracze, spopularyzowali rozróżnienie nazw interfejsów i klas abstrakcyjnych. To podejście zostało przyjęte przez PSR, który jak na ironię publikuje tylko interfejsy, a mimo to w nazwie każdego z nich znajduje się słowo interface.
Z drugiej strony, inny duży framework, Laravel, nie rozróżnia
interfejsów i klas abstrakcyjnych. Nie robi tego na przykład nawet popularna
warstwa bazodanowa Doctrine. Nie robi tego również standardowa biblioteka w
PHP (mamy więc interfejsy Throwable
lub Iterator
,
klasę abstrakcyjną FilterIterator
itd.)
Jeśli spojrzymy na świat poza PHP, to C# używa przedrostka I
dla interfejsów, natomiast w Javie czy TypeScript nazwy te nie różnią się
niczym.
Tak więc nie wszyscy to robią, ale nawet gdyby tak było, to nie znaczy, że jest to dobre rozwiązanie. Nie jest mądre bezmyślne przyjmowanie tego, co robią inni, ponieważ możesz również przyjmować błędy. Błędy, których ten drugi wolałby się pozbyć, to po prostu zbyt duży zgryz w dzisiejszych czasach.
Nie wiem w kodzie, jaki jest interfejs
Wielu programistów będzie twierdzić, że prefiksy/sufiksy są dla nich przydatne, bo dzięki nim od razu w kodzie wiedzą, czym są interfejsy. Czują, że brakowałoby im takiego rozróżnienia. Zobaczmy więc, czy potrafisz powiedzieć, co jest klasą, a co interfejsem w tych przykładach?
$o = new X;
class X extends X implements Y
{}
interface Y
{}
X::fn();
X::$v;
X
is always a class, Y
jest interfejsem, jest
jednoznaczny nawet bez prefiksów / postfiksów. Oczywiście IDE też o tym wie
i zawsze da ci poprawną podpowiedź w danym kontekście.
Ale co w tym przypadku:
function foo(A $param): A
{}
public A $property;
$o instanceof A
A::CONST
try { ... } catch (A $x) { ... }
W tych przypadkach nie będziesz tego wiedział. Jak powiedzieliśmy na samym początku, z punktu widzenia dewelopera nie powinno być różnicy między tym, co jest klasą, a tym, co jest interfejsem. Co właśnie nadaje interfejsom i klasom abstrakcyjnym ich sens.
Gdyby udało się tutaj odróżnić klasę od interfejsu, zaprzeczyłoby to podstawowej zasadzie OOP. A interfejsy stałyby się bez znaczenia.
Jestem do tego przyzwyczajony
Zmiana nawyków po prostu boli 🙂 Często boli sama myśl o niej. Nie obwiniajmy ludzi, których pociągają zmiany i cieszą się na nie, dla większości jak mówi czeskie przysłowie, że przyzwyczajenie to żelazna koszula.
Wystarczy jednak spojrzeć w przeszłość, jak niektóre przyzwyczajenia
zostały zmiecione przez czas. Chyba najbardziej znanym jest tzw. notacja
węgierska stosowana od lat osiemdziesiątych i spopularyzowana przez
Microsoft. Notacja ta polegała na rozpoczynaniu nazwy każdej zmiennej od
skrótu symbolizującego jej typ danych. W PHP wyglądałoby to jak
echo $this->strName
lub $this->intCount++
.
Notacja węgierska zaczęła być porzucana w latach dziewięćdziesiątych, a
dziś wytyczne Microsoftu wprost zniechęcają programistów do jej
stosowania.
Kiedyś była to niezbędna funkcja i dziś nikt za nią nie tęskni.
Ale po co sięgać do tak odległych czasów? Być może pamiętasz, że w PHP zwyczajowo wyróżniano niepublicznych członków klas podkreślnikiem (sample from Zend Framework). Było to z powrotem, gdy istniał PHP 5, który miał modyfikatory widoczności public/protected/private. Jednak programiści robili to z przyzwyczajenia. Byli przekonani, że bez podkreślników przestaną rozumieć kod. “Jak miałbym odróżnić właściwości publiczne od prywatnych w kodzie, co?”
Dziś nikt nie używa podkreślników. I nikt ich nie omija. Czas doskonale udowodnił, że obawy były fałszywe.
A jednak to dokładnie to samo, co zarzut: “Jak miałbym odróżnić w kodzie interfejs od klasy, co?”.
Przestałem używać prefiksów/postfiksów dziesięć lat temu. Nigdy bym nie wrócił, to była świetna decyzja. Nie znam żadnego innego programisty, który chciałby również wrócić. Jak powiedział przyjaciel: “Spróbuj tego, a za miesiąc nie zrozumiesz, że kiedykolwiek zrobiłeś to inaczej”.
Chcę zachować konsekwencję
Mogę sobie wyobrazić programistę, który mówi: “Używanie prefiksów i sufiksów jest naprawdę nonsensowne, rozumiem to, tylko że ja już mam swój kod zbudowany w ten sposób i zmiana go jest bardzo trudna. A jeśli zacznę pisać nowy kod poprawnie bez nich, skończę z niespójnością, co jest chyba nawet gorsze niż zła konwencja.”
W rzeczywistości twój kod jest już niespójny, ponieważ używasz biblioteki systemowej PHP:
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
public function add(string|Stringable $item): void
{
}
}
I ręka na sercu, czy to ma znaczenie? Czy kiedykolwiek myślałeś, że to będzie bardziej spójne?
class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
public function add(string|StringableInterface $item): void
{
}
}
Albo to?
try {
$command = $this->find($name);
} catch (ThrowableInterface $e) {
return $e->getMessage();
}
Nie wydaje mi się. Spójność nie odgrywa tak dużej roli, jak mogłoby się wydawać. Wręcz przeciwnie, oko woli mniej wizualnego hałasu, mózg woli jasność projektu. Tak więc dostosowanie 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, które zaczął wycofywać kilka lat temu, zachowując pełną kompatybilność wsteczną.
Aby przesłać komentarz, proszę się zalogować