Präfixe und Suffixe gehören nicht in Schnittstellennamen
Die Verwendung des Suffixes I
prefix or Interface
für Schnittstellen und Abstract
für abstrakte Klassen ist ein
Antipattern. Es gehört nicht in reinen Code. Die Unterscheidung von
Schnittstellennamen verwischt die OOP-Prinzipien, bringt Unordnung in den Code
und verursacht Komplikationen bei der Entwicklung. Hier sind die
Gründe dafür.
Typ = Klasse + Schnittstelle + Abkömmlinge
In der OOP-Welt werden sowohl Klassen als auch Schnittstellen als Typen betrachtet. Wenn ich bei der Deklaration einer Eigenschaft oder eines Parameters einen Typ verwende, macht es aus der Sicht des Entwicklers keinen Unterschied, ob der Typ, auf den er sich stützt, eine Klasse oder eine Schnittstelle ist. Das ist das Tolle, was Schnittstellen eigentlich so nützlich macht. Es ist das, was ihrer Existenz einen Sinn gibt. (Im Ernst: Wozu wären Schnittstellen gut, wenn dieses Prinzip nicht gelten würde? Denken Sie einmal darüber nach.)
Werfen Sie einen Blick auf diesen Code:
class FormRenderer
{
public function __construct(
private Form $form,
private Translator $translator,
) {
}
}
Der Konstruktor sagt: "Ich brauche ein Formular und einen Übersetzer.
" Und es ist ihm egal, ob er ein GettextTranslator
Objekt oder
ein DatabaseTranslator
Objekt erhält. Und gleichzeitig ist es mir
als Benutzer egal, ob Translator
eine Schnittstelle, eine abstrakte
Klasse oder eine konkrete Klasse ist.
Ist es mir wirklich egal? Nein, ich gebe zu, dass ich ziemlich neugierig bin, und wenn ich die Bibliothek eines anderen Nutzers erkunde, schaue ich mir an, was sich hinter dem Typ verbirgt, und schwebe darüber:
Aha, ich verstehe. Und das ist das Ende der Geschichte. Zu wissen, ob es sich um eine Klasse oder eine Schnittstelle handelt, wäre wichtig, wenn ich eine Instanz davon erstellen wollte, aber das ist nicht der Fall, ich rede jetzt nur über den Typ der Variablen. Und das ist der Punkt, an dem ich mich aus diesen Implementierungsdetails heraushalten möchte. Und ich möchte sie ganz sicher nicht in meinen Code einbetten! Was sich hinter einem Typ verbirgt, ist Teil seiner Definition, nicht der Typ selbst.
Sehen Sie sich nun den anderen Code an:
class FormRenderer
{
public function __construct(
private AbstractForm $form,
private TranslatorInterface $translator,
) {
}
}
Diese Definition von Konstruktor sagt wörtlich: "Ich brauche eine abstrakte Form und eine Schnittstelle des Übersetzers. " Aber das ist dumm. Es braucht eine konkrete Form zum Rendern. Nicht eine abstrakte Form. Und es braucht ein Objekt, das als Übersetzer fungiert. Es braucht keine Schnittstelle.
Sie wissen, dass die Worte Interface
und Abstract
zu ignorieren sind. Dass der Konstruktor das Gleiche will wie im vorherigen
Beispiel. Aber… wirklich? Ist es wirklich eine gute Idee, in
Namenskonventionen die Verwendung von zu ignorierenden Wörtern
einzuführen?
Schließlich wird dadurch eine falsche Vorstellung von den OOP-Prinzipien
vermittelt. Ein Anfänger muss verwirrt sein: “Wenn der Typ
Translator
entweder 1) ein Objekt der Klasse
Translator
, 2) ein Objekt, das die Schnittstelle
Translator
implementiert, oder 3) ein Objekt, das von ihnen erbt,
bedeutet, was ist dann mit TranslatorInterface
gemeint?” Auf
diese Frage gibt es keine vernünftige Antwort.
Wenn wir TranslatorInterface
verwenden, selbst wenn
Translator
eine Schnittstelle sein kann, begehen wir eine
Tautologie. Dasselbe geschieht, wenn wir
interface TranslatorInterface
deklarieren. Allmählich wird es zu
einem Programmierwitz kommen:
interface TranslatorInterface
{
}
class FormRendererClass
{
/**
* Constructor
*/
public function __construct(
private AbstractForm $privatePropertyForm,
private TranslatorInterface $privatePropertyTranslator,
) {
// 🤷♂️
}
}
Hervorragende Implementierung
Wenn ich etwas wie TranslatorInterface
sehe, ist es
wahrscheinlich, dass es eine Implementierung namens
Translator implements TranslatorInterface
gibt. Da frage ich mich:
Was macht Translator
so besonders, dass es das einzigartige
Privileg hat, Translator genannt zu werden? Jede andere Implementierung braucht
einen beschreibenden Namen, wie GettextTranslator
oder
DatabaseTranslator
, aber diese hier ist sozusagen
“standardmäßig”, wie der bevorzugte Status Translator
ohne
die Bezeichnung nahelegt.
Es gibt sogar Leute, die verwirrt sind und nicht wissen, ob sie
Translator
oder TranslatorInterface
eingeben sollen.
Dann werden beide im Client-Code verwechselt, ich bin mir sicher, dass Sie das
schon oft erlebt haben (in Nette zum Beispiel im Zusammenhang mit
Nette\Http\Request
vs. IRequest
).
Wäre es nicht besser, die spezielle Implementierung loszuwerden und den
generischen Namen Translator
für die Schnittstelle beizubehalten?
Das heißt, spezifische Implementierungen mit einem spezifischen Namen + eine
generische Schnittstelle mit einem generischen Namen zu haben. Das
macht Sinn.
Die Last eines beschreibenden Namens liegt dann allein bei den
Implementierungen. Wenn wir TranslatorInterface
in
Translator
umbenennen, braucht unsere ehemalige Klasse
Translator
einen neuen Namen. Die Leute neigen dazu, dieses Problem
zu lösen, indem sie sie DefaultTranslator
nennen, selbst ich habe
mich dessen schuldig gemacht. Aber noch einmal: Was macht sie so besonders, dass
sie Default genannt wird? Seien Sie nicht faul und denken Sie genau darüber
nach, was sie tut und warum sie sich von anderen möglichen Implementierungen
unterscheidet.
Und was ist, wenn ich mir nicht mehrere mögliche Implementierungen vorstellen kann? Was ist, wenn ich mir nur eine einzige gültige Möglichkeit vorstellen kann? Also erstellen Sie die Schnittstelle einfach nicht. Zumindest für den Moment.
Eh, es gibt eine andere Implementierung
Und sie ist hier! Wir brauchen eine zweite Implementierung. Das passiert die ganze Zeit. Es war noch nie notwendig, Übersetzungen auf mehr als eine bewährte Weise zu speichern, z. B. in einer Datenbank, aber jetzt gibt es eine neue Anforderung, und wir brauchen mehr Übersetzer in der Anwendung.
An dieser Stelle wird auch deutlich, was die Besonderheiten des ursprünglichen Einzelübersetzers waren. Es war ein Datenbankübersetzer, kein Standard.
Was ist damit?
- Erstelle den Namen
Translator
der Schnittstelle - Benennen Sie die ursprüngliche Klasse in
DatabaseTranslator
um und sie wird implementiertTranslator
- Und Sie erstellen neue Klassen
GettextTranslator
und vielleichtNeonTranslator
All diese Änderungen sind sehr bequem und einfach zu bewerkstelligen,
insbesondere wenn die Anwendung nach den Grundsätzen der Dependency
Injection aufgebaut ist. Es ist nicht nötig, irgendetwas am Code zu
ändern, es genügt, in der DI-Container-Konfiguration
Translator
in DatabaseTranslator
zu ändern. Das ist
großartig!
Eine diametral andere Situation würde jedoch entstehen, wenn wir auf
Präfixierung/Suffixierung bestehen würden. Dann müssten wir die Typen im Code
der gesamten Anwendung von Translator
in
TranslatorInterface
umbenennen. Eine solche Umbenennung wäre rein
konventionsbedingt, würde aber, wie wir gerade gezeigt haben, dem Geist von OOP
widersprechen. Die Schnittstelle hat sich nicht geändert, der Benutzercode hat
sich nicht geändert, aber die Konvention erfordert eine Umbenennung? Dann ist
es eine fehlerhafte Konvention.
Und wenn sich im Laufe der Zeit herausstellen sollte, dass eine abstrakte Klasse besser ist als die Schnittstelle, würden wir sie wieder umbenennen. Eine solche Aktion ist unter Umständen nicht trivial, zum Beispiel wenn der Code über mehrere Pakete verteilt ist oder von Dritten verwendet wird.
Aber jeder macht es
Nicht jeder tut es. Es stimmt, dass in der PHP-Welt das Zend Framework, gefolgt von Symfony, den großen Playern, die Unterscheidung zwischen Schnittstellen- und abstrakten Klassennamen popularisiert hat. Dieser Ansatz wurde von PSR übernommen, das ironischerweise nur Schnittstellen veröffentlicht, aber das Wort Schnittstelle in den Namen jeder Klasse aufnimmt.
Ein anderes großes Framework, Laravel, unterscheidet hingegen nicht zwischen
Schnittstellen und abstrakten Klassen. Auch die populäre
Doctrine-Datenbankschicht tut dies nicht, zum Beispiel. Und auch die
Standardbibliothek in PHP tut dies nicht (so gibt es die Schnittstellen
Throwable
oder Iterator
, die abstrakte Klasse
FilterIterator
usw.).
Wenn wir uns die Welt außerhalb von PHP ansehen, verwendet C# das Präfix “I” für Schnittstellen, während in Java oder TypeScript die Namen nicht anders lauten.
Es macht also nicht jeder, aber selbst wenn, heißt das nicht, dass es eine gute Sache ist. Es ist nicht klug, gedankenlos zu übernehmen, was andere tun, weil man damit auch Fehler übernehmen könnte. Fehler, die der andere am liebsten loswerden würde, denn das ist heutzutage einfach zu viel des Guten.
Ich weiß im Code nicht, was die Schnittstelle ist
Viele Programmierer werden argumentieren, dass Präfixe/Suffixe für sie nützlich sind, weil sie ihnen ermöglichen, im Code sofort zu erkennen, was eine Schnittstelle ist. Sie sind der Meinung, dass sie eine solche Unterscheidung vermissen würden. Können Sie also in diesen Beispielen erkennen, was eine Klasse und was eine Schnittstelle ist?
$o = new X;
class X extends X implements Y
{}
interface Y
{}
X::fn();
X::$v;
X
is always a class, Y
ist eine Schnittstelle, das
ist auch ohne Präfixe/Postfixe eindeutig. Natürlich weiß das auch die IDE und
gibt Ihnen immer den richtigen Hinweis in einem bestimmten Kontext.
Aber wie sieht es hier aus:
function foo(A $param): A
{}
public A $property;
$o instanceof A
A::CONST
try { ... } catch (A $x) { ... }
In diesen Fällen werden Sie es nicht wissen. Wie wir bereits zu Beginn gesagt haben, sollte es aus der Sicht eines Entwicklers keinen Unterschied zwischen einer Klasse und einer Schnittstelle geben. Das ist es, was den Sinn von Schnittstellen und abstrakten Klassen ausmacht.
Wenn man hier zwischen einer Klasse und einer Schnittstelle unterscheiden könnte, würde man das Grundprinzip von OOP verleugnen. Und Schnittstellen würden sinnlos werden.
Daran bin ich gewöhnt
Gewohnheiten zu ändern tut einfach weh 🙂 Oft tut schon die Vorstellung davon weh. Machen wir den Menschen, die sich zu Veränderungen hingezogen fühlen und sich darauf freuen, keinen Vorwurf, denn für die meisten gilt das tschechische Sprichwort: Gewohnheit ist ein bügelndes Hemd.
Aber schauen Sie sich nur die Vergangenheit an, wie manche Gewohnheiten von
der Zeit hinweggefegt wurden. Die vielleicht bekannteste ist die so genannte
ungarische Notation, die seit den achtziger Jahren verwendet und von Microsoft
populär gemacht wurde. Die Notation bestand darin, den Namen jeder Variablen
mit einer Abkürzung zu beginnen, die ihren Datentyp symbolisierte. In PHP
würde das so aussehen: echo $this->strName
oder
$this->intCount++
. Die ungarische Notation wurde in den
neunziger Jahren aufgegeben, und heute raten die Richtlinien von Microsoft den
Entwicklern direkt davon ab, sie zu verwenden.
Früher war sie ein wesentliches Merkmal und heute vermisst sie niemand mehr.
Aber warum ist das so lange her? Vielleicht erinnern Sie sich, dass es in PHP üblich war, nicht-öffentliche Mitglieder von Klassen mit einem Unterstrich zu kennzeichnen (Beispiel aus Zend Framework). Das war damals, als es PHP 5 gab, das die Sichtbarkeitsmodifikatoren public/protected/private hatte. Aber die Programmierer taten es aus Gewohnheit. Sie waren überzeugt, dass sie ohne Unterstriche den Code nicht mehr verstehen würden. “Wie soll ich öffentliche von privaten Eigenschaften im Code unterscheiden?”
Heute verwendet niemand mehr Unterstriche. Und niemand vermisst sie. Die Zeit hat eindeutig bewiesen, dass die Befürchtungen falsch waren.
Und doch ist es genau dasselbe wie der Einwand: “Wie soll ich denn im Code eine Schnittstelle von einer Klasse unterscheiden, hm?”
Ich habe vor zehn Jahren aufgehört, Präfixe/Postfixe zu verwenden. Ich würde nie wieder zurückgehen, es war eine gute Entscheidung. Ich kenne auch keinen anderen Programmierer, der zurückkehren möchte. Wie ein Freund sagte: “Probieren Sie es aus und in einem Monat werden Sie nicht mehr verstehen, dass Sie es jemals anders gemacht haben.”
Ich möchte Beständigkeit beibehalten
Ich kann mir vorstellen, dass ein Programmierer sagt: “Die Verwendung von Präfixen und Suffixen ist wirklich unsinnig, ich verstehe das, es ist nur so, dass ich meinen Code bereits so aufgebaut habe und es sehr schwierig ist, ihn zu ändern. Und wenn ich anfange, neuen Code korrekt ohne sie zu schreiben, werde ich mit Inkonsistenz enden, was vielleicht noch schlimmer ist als schlechte Konventionen.”
In der Tat ist Ihr Code bereits inkonsistent, weil Sie die PHP-Systembibliothek verwenden:
class Collection implements ArrayAccess, Countable, IteratorAggregate
{
public function add(string|Stringable $item): void
{
}
}
Und Hand aufs Herz, ist das wichtig? Haben Sie jemals daran gedacht, dass dies konsistenter sein würde?
class Collection implements ArrayAccessInterface, CountableInterface, IteratorAggregateInterface
{
public function add(string|StringableInterface $item): void
{
}
}
Oder so?
try {
$command = $this->find($name);
} catch (ThrowableInterface $e) {
return $e->getMessage();
}
Nein, das glaube ich nicht. Die Konsistenz spielt keine so große Rolle, wie es vielleicht den Anschein hat. Im Gegenteil, das Auge bevorzugt weniger visuelles Rauschen, das Gehirn bevorzugt ein klares Design. Es macht also Sinn, die Konvention anzupassen und neue Schnittstellen ohne Präfixe und Suffixe korrekt zu schreiben.
Sie können sogar bei großen Projekten absichtlich entfernt werden. Ein Beispiel dafür ist das Nette Framework, das in der Vergangenheit I-Präfixe in Schnittstellennamen verwendet hat, die es vor ein paar Jahren unter Beibehaltung der vollen Abwärtskompatibilität auslaufen ließ.
Um einen Kommentar abzugeben, loggen Sie sich bitte ein