Präfixe und Suffixe gehören nicht in Schnittstellennamen

vor 2 Jahren von David Grudl  

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?

  1. Erstelle den Namen Translator der Schnittstelle
  2. Benennen Sie die ursprüngliche Klasse in DatabaseTranslator um und sie wird implementiert Translator
  3. Und Sie erstellen neue Klassen GettextTranslator und vielleicht NeonTranslator

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ß.