Präfixe und Suffixe gehören nicht in Interface-Namen

vor 3 Jahren von David Grudl  

Die Verwendung des Präfixes I oder des Suffixes Interface bei Interfaces, ebenso wie Abstract bei abstrakten Klassen, ist ein Antipattern. In sauberem Code hat es nichts zu suchen. Die Unterscheidung von Interface-Namen verschleiert tatsächlich die Prinzipien der OOP, bringt Rauschen in den Code und verursacht Komplikationen bei der Entwicklung. Die Gründe sind folgende.

Typ = Klasse + Interface + Nachkommen

In der Welt der OOP werden sowohl Klassen als auch Interfaces als Typen betrachtet. Wenn ich einen Typ bei der Deklaration einer Property oder eines Parameters verwende, gibt es aus Entwicklersicht keinen Unterschied, ob der Typ, auf den man sich verlässt, eine Klasse oder ein Interface ist. Das ist eine großartige Sache, dank der Interfaces eigentlich so nützlich sind. Das gibt ihrer Existenz einen Sinn. (Ernsthaft: Wozu wären Interfaces gut, wenn dieses Prinzip nicht gelten würde? Versuchen Sie, darüber nachzudenken.)

Schauen Sie sich diesen Code an:

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 völlig egal, ob er ein GettextTranslator- oder DatabaseTranslator-Objekt bekommt. Und gleichzeitig ist es mir als Benutzer völlig egal, ob Translator ein Interface, eine abstrakte Klasse oder eine konkrete Klasse ist.

Ist es mir völlig egal? Eigentlich nicht, ich gebe zu, ich bin ziemlich neugierig, also wenn ich eine fremde Bibliothek untersuche, schaue ich nach, was sich hinter dem Typ verbirgt, und fahre mit der Maus darüber:

Aha, jetzt weiß ich es. Und damit endet es. Das Wissen, ob es sich um eine Klasse oder ein Interface handelt, wäre wichtig, wenn ich eine Instanz davon erstellen wollte, aber das ist hier nicht der Fall, ich spreche nur über den Typ der Variablen. Und hier möchte ich von diesen Details abgeschirmt sein. Und ich will sie schon gar nicht in meinen Code einschleppen! Was sich hinter dem Typ verbirgt, ist Teil seiner Definition, nicht des Typs selbst.

Und jetzt schauen Sie sich den nächsten Code an:

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

Diese Konstruktordefinition sagt wörtlich: “Ich brauche ein abstraktes Formular und ein Interface für den Übersetzer.” Aber das ist Unsinn. Er braucht ein konkretes Formular, das er rendern soll. Kein abstraktes Formular. Und er braucht ein Objekt, das die Aufgabe des Übersetzers erfüllt. Er braucht kein Interface.

Sie wissen, dass die Wörter Interface und Abstract ignoriert werden sollen. Dass der Konstruktor dasselbe will wie im vorherigen Beispiel. Aber… ernsthaft? Finden Sie es wirklich eine gute Idee, in Namenskonventionen die Verwendung von Wörtern einzuführen, die übersehen werden sollen?

Denn es erzeugt eine falsche Vorstellung von den Prinzipien der OOP. Ein Anfänger muss verwirrt sein: „Wenn unter dem Typ Translator entweder 1) ein Objekt der Klasse Translator 2) ein Objekt, das das Interface Translator implementiert oder 3) ein davon erbendes Objekt verstanden wird, was wird dann unter TranslatorInterface verstanden?“ Darauf gibt es keine vernünftige Antwort.

Wenn wir TranslatorInterface schreiben, obwohl auch Translator ein Interface sein kann, begehen wir eine Tautologie. Dasselbe, wenn wir interface TranslatorInterface deklarieren. Und so weiter. Bis ein Programmiererwitz entsteht:

interface TranslatorInterface
{
}

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

Die außergewöhnliche Implementierung

Wenn ich so etwas wie TranslatorInterface sehe, ist es wahrscheinlich, dass es auch eine Implementierung namens Translator implements TranslatorInterface geben wird. Das bringt mich zum Nachdenken: Was macht Translator so besonders, dass er das alleinige Recht hat, sich Translator zu nennen? Jede andere Implementierung benötigt einen beschreibenden Namen, zum Beispiel GettextTranslator oder DatabaseTranslator, aber diese ist irgendwie “Standard”, wie ihre bevorzugte Stellung andeutet, wenn sie Translator ohne Zusatz heißt.

Das verunsichert die Leute sogar und sie wissen nicht, ob sie den Typehint für Translator oder TranslatorInterface schreiben sollen. Im Client-Code wird dann beides gemischt, sicher sind Sie schon oft darauf gestoßen (in Nette zum Beispiel im Zusammenhang mit Nette\Http\Request vs IRequest).

Wäre es nicht besser, die außergewöhnliche Implementierung loszuwerden und den allgemeinen Namen Translator für das Interface beizubehalten? Also konkrete Implementierungen mit konkretem Namen + allgemeines Interface mit allgemeinem Namen. Das ergibt doch Sinn.

Die Last des beschreibenden Namens liegt dann rein bei den Implementierungen. Wenn wir TranslatorInterface in Translator umbenennen, braucht unsere ehemalige Klasse Translator einen neuen Namen. Leute neigen dazu, dieses Problem zu lösen, indem sie sie DefaultTranslator nennen, auch ich bin schuldig. Aber wieder, was macht sie so besonders, dass sie Default heißt? Seien Sie nicht faul und denken Sie gründlich darüber nach, was sie tut und warum sie sich von anderen möglichen Implementierungen unterscheidet.

Und was, wenn ich mir nicht mehrere Implementierungen vorstellen kann? Was, wenn mir nur eine gültige Methode einfällt? Dann erstellen Sie einfach kein Interface. Zumindest vorerst.

Siehe da, eine weitere Implementierung ist aufgetaucht

Und da ist es! Wir brauchen eine zweite Implementierung. Das passiert häufig. Es gab nie die Notwendigkeit, Übersetzungen anders als auf eine bewährte Weise zu speichern, z.B. in einer Datenbank, aber jetzt gibt es eine neue Anforderung und es müssen mehrere Übersetzer in der Anwendung vorhanden sein.

Das ist auch der Moment, in dem Sie klar erkennen, was die Spezifität des ursprünglichen einzigen Übersetzers war. Es war ein Datenbank-Übersetzer, kein Standard.

Was tun?

  1. Aus dem Namen Translator machen wir ein Interface
  2. Die ursprüngliche Klasse benennen Sie in DatabaseTranslator um und sie implementiert Translator
  3. Und Sie erstellen neue Klassen GettextTranslator und vielleicht NeonTranslator

All diese Änderungen lassen sich sehr bequem und einfach durchführen, insbesondere wenn die Anwendung gemäß den Prinzipien der Dependency Injection aufgebaut ist. Im Code muss nichts geändert werden, nur in der Konfiguration des DI-Containers ändern wir Translator in DatabaseTranslator. Das ist großartig!

Eine diametral andere Situation würde jedoch eintreten, wenn wir auf Präfixen/Suffixen bestehen würden. Wir müssten die Typen in der gesamten Anwendung von Translator in TranslatorInterface umbenennen. Eine solche Umbenennung wäre rein zweckmäßig, um die Konvention einzuhalten, würde aber dem Sinn der OOP widersprechen, wie wir gerade gezeigt haben. Das Interface hat sich nicht geändert, der Benutzercode hat sich nicht geändert, aber die Konvention erfordert eine Umbenennung? Dann ist es eine fehlerhafte Konvention.

Wenn sich außerdem im Laufe der Zeit herausstellen würde, dass eine abstrakte Klasse besser wäre als ein Interface, würden wir erneut umbenennen. Ein solcher Eingriff muss keineswegs trivial sein, etwa wenn der Code auf mehrere Pakete verteilt ist oder von Dritten verwendet wird.

Aber das machen doch alle so

Nicht alle. Es stimmt, dass in der PHP-Welt das Zend Framework und danach Symfony, also große Player, die Unterscheidung von Interface- und abstrakten Klassennamen popularisiert haben. Dieser Ansatz wurde auch von PSR übernommen, die paradoxerweise nur Interfaces veröffentlicht und dennoch bei jedem das Wort Interface im Namen angibt.

Andererseits unterscheidet ein anderes bedeutendes Framework, Laravel, Interfaces und abstrakte Klassen in keiner Weise. Das tut zum Beispiel auch die populäre Datenbankschicht Doctrine nicht. Und das tut auch die Standardbibliothek in PHP nicht (wir haben Interfaces Throwable oder Iterator, die abstrakte Klasse FilterIterator, usw.).

Wenn wir einen Blick über die PHP-Welt hinaus werfen, verwendet C# das Präfix I für Interfaces, während in Java oder TypeScript die Namen nicht unterschieden werden.

Es machen also nicht alle, aber selbst wenn sie es täten, bedeutet das nicht, dass es gut ist. Gedankenlos zu übernehmen, was andere tun, ist nicht vernünftig, denn man kann auch Fehler übernehmen. Fehler, die der andere vielleicht selbst gerne loswerden würde, aber es ist zu aufwendig.

Ich erkenne im Code nicht, was ein Interface ist

Viele Programmierer werden einwenden, dass Präfixe/Suffixe für sie nützlich sind, da sie dank ihnen sofort im Code erkennen, was Interfaces sind. Sie haben das Gefühl, dass ihnen eine solche Unterscheidung fehlen würde. Mal sehen, erkennen Sie in diesen Beispielen, was eine Klasse und was ein Interface ist?

$o = new X;

class X extends X implements Y
{}

interface Y
{}

X::fn();

X::$v;

X ist immer eine Klasse, Y ist ein Interface, es ist eindeutig auch ohne Präfixe/Postfixe. Natürlich weiß das auch die IDE und wird Ihnen im jeweiligen Kontext immer korrekt Vorschläge machen.

Aber was ist hier:

function foo(A $param): A
{}

public A $property;

$o instanceof A

A::CONST

try {
} catch (A $x) {
}

In diesen Fällen erkennen Sie es nicht. Wie wir ganz am Anfang gesagt haben, soll hier aus Entwicklersicht kein Unterschied bestehen, was eine Klasse und was ein Interface ist. Was eben Interfaces und abstrakten Klassen ihren Sinn gibt.

Wenn Sie hier in der Lage wären, eine Klasse von einem Interface zu unterscheiden, würde dies das grundlegende Prinzip der OOP negieren. Und Interfaces würden ihren Sinn verlieren.

Ich bin daran gewöhnt

Gewohnheiten zu ändern tut einfach weh 🙂 Manchmal schon die Vorstellung davon. Aber um fair zu sein, viele Menschen werden von Veränderungen angezogen und freuen sich darauf, aber für die Mehrheit gilt, dass Gewohnheit eine eiserne Fessel ist.

Aber schauen Sie einfach in die Vergangenheit, wie einige Gewohnheiten von der Zeit verweht wurden. Die wohl berühmteste ist die sogenannte Ungarische Notation, die seit den achtziger Jahren verwendet und von Microsoft popularisiert wurde. Die Notation bestand darin, dass der Name jeder Variablen mit einer Abkürzung begann, die ihren Datentyp symbolisierte. In PHP sähe das so aus: echo $this->strName oder $this->intCount++. Von der Ungarischen Notation begann man sich in den neunziger Jahren zu lösen, und heute rät Microsoft in seinen Richtlinien Entwicklern direkt davon ab.

Einst war sie unverzichtbar, und heute vermisst sie niemand.

Aber warum so weit in die Vergangenheit gehen? Vielleicht erinnern Sie sich, dass es in PHP üblich war, nicht-öffentliche Klassenmitglieder mit einem Unterstrich zu unterscheiden (Beispiel aus dem Zend Framework). Das war zu einer Zeit, als es längst PHP 5 gab, das die Sichtbarkeitsmodifikatoren public/protected/private hatte. Aber Programmierer taten es aus Gewohnheit. Sie waren überzeugt, dass sie sich ohne Unterstriche im Code nicht mehr zurechtfinden würden. „Wie würde ich im Code öffentliche von privaten Variablen unterscheiden, aha?“

Heute verwendet niemand mehr Unterstriche. Und niemand vermisst sie. Die Zeit hat hervorragend bewiesen, dass die Befürchtungen unbegründet waren.

Dabei ist es genau dasselbe wie der Einwand: „Wie würde ich im Code Interfaces von Klassen unterscheiden, aha?“

Ich habe vor zehn Jahren aufgehört, Präfixe/Postfixe zu verwenden. Ich würde niemals zurückkehren, es war eine großartige Entscheidung. Ich kenne auch keinen anderen Programmierer, der zurückkehren möchte. Wie ein Freund sagte: „Probier es aus, und in einem Monat wirst du nicht verstehen, dass du es jemals anders gemacht hast.“

Ich möchte Konsistenz wahren

Ich kann mir vorstellen, dass ein Programmierer sagt: „Präfixe und Suffixe zu verwenden ist wirklich Unsinn, ich verstehe das, aber ich habe meinen Code bereits so aufgebaut und eine Änderung ist sehr schwierig. Und wenn ich anfangen würde, neuen Code korrekt ohne sie zu schreiben, entsteht eine Inkonsistenz, die vielleicht noch schlimmer ist als eine schlechte Konvention.“

Tatsächlich ist Ihr Code bereits jetzt inkonsistent, da Sie die PHP-Systembibliothek verwenden, die keine Präfixe und Postfixe hat:

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

Und Hand aufs Herz, stört das? Ist Ihnen jemals eingefallen, dass dies konsistenter wäre?

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

Oder dies?

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

Ich denke nicht. Konsistenz spielt keine so bedeutende Rolle, wie es scheinen mag. Im Gegenteil, das Auge bevorzugt weniger visuelles Rauschen, das Gehirn die Sauberkeit des Entwurfs. Daher ergibt es Sinn, die Konvention anzupassen und neue Interfaces korrekt ohne Präfixe und Suffixe zu schreiben.

Sie können auch gezielt aus großen Projekten entfernt werden. Ein Beispiel ist das Nette Framework, das historisch Präfixe I in Interface-Namen verwendete, wovon es sich vor einigen Jahren schrittweise und unter vollständiger Wahrung der Abwärtskompatibilität entfernt.