Naming von Interfaces und Implementierungen
Viele kennen vielleicht die Konstellation, dass man zu einem Interface FormDataValue eine Implementierungsklasse FormDataValueImpl vorfindet. Auf meine leicht überhebliche Kritik bekomme ich dennoch oft Zustimmung und eine Entschuldigung der Form: “Wir haben uns teamintern gegen Interface-Namenskonventionen wie z.B. IFormDataValue entschieden.”
Da fühle ich mich dann etwas missverstanden. Beim Naming gibt es nun mal mehr Möglichkeiten. Warum kommen manche Teams nur auf diese zwei? Ich versuche das Problem einmal etwas tiefer zu analysieren …
Welches Problem wird mit Single-Implementation-Interfaces gelöst?
Die Antwort darauf ist nicht so einfach. Im direkten Gespräch höre ich oft, dass damit zwei Ziele erreicht werden sollen:
- Entkopplung von abstrakter Schnittstelle und konkreter Implementierung
- Langfristig wird es aber auch nur eine Implementierung geben
Interessanterweise dient die Entkopplung dem Zweck in Zukunft flexibler zu sein, die Festlegung auf langfristig nur eine Implementierung widerspricht dem dann aber. Tatsächlich führt die Überlegung, dass es ja ohnehin nur eine Implementierung geben kann oft dazu, dass man dieses Wissen im Code auch nutzt, z.B. indem man direkt auf die Implementierungsklasse castet, z.B.
FormDataValue value = ...;
// other code
FormDataValueImpl impl = (FormDataValueImpl) value;
impl.doNonInterfaceAction();
Aus meiner Sicht sollte man sich also schon entscheiden, ob es wichtiger ist, entkoppelt zu sein oder ob es wirklich nur eine Implementierung geben darf: Es sind widersprüchliche Ziele:
- wer in so einem Design explizit die Entkopplung nutzen will (d.h. einen neuen Typ einführen) wird am Ende langfristig eben doch mehrere Implementierungen haben
- wer das Wissen, dass es nur eine Implementierung gibt, nutzt (z.B. in dem er auf den Implementierungstyp castet) verletzt explizit die Entkopplung
Warum …Impl und I… kein gutes Naming sind
Ganz allgemein erwarten wir beim Naming eine gewisse Präzision und Umschreibung des benamten Objekts. Mit ...Impl und I... beschreiben wir aber nicht das Objekt sondern die programmiertechnische Rolle in unserem Programm, nämlich dass es sich um ein Interface handelt bzw. eine Implementierung handelt, beides sind Informationen,
- die bei der Definition redundant sind (wir sehen ja, ob es das eine oder andere ist)
- die bei der Verwendung keine Rolle spielen sollten (beides sollte sich ja gleich verhalten)
Beim Naming einer Implementierung sollten wir darauf achten, dass in irgendeiner Art und Weise umschrieben wird, wie diese Implementierung arbeitet. Das es ein Problem ist, erkennt man an der Typhierarchie von Collections in .NET:
Zu einem Interface IList findet man einen Untertyp List, allerdings weiß nun niemand welche Eigenschaften er von dieser Liste erwarten darf. Java- und .NET-Entwickler werden vermuten, dass es sich um eine array-basierte Liste handelt, funktionale Entwickler (Haskell, ML) verstehen unter eine Liste dann doch eher eine einfach verkettete Liste. Was F#-Entwickler (funktional und .NET) erwarten kann ich nur mutmaßen. Auch werden Third-Party-Implementierungen hier in der Zwickmühle, wie sie ihre Typen nennen (List in einem anderen package führt vermutlich zu Verwechselungen). Die Collection-Bibliothek C5 bricht mit den Konventionen und nennt ihre array-basierte Liste ArrayList.
In Java finden wir ein Interface List mit Implementierungen ArrayList und LinkedList. In Java gibt es auch viele Third-Party-Collection-Implementierungen die bei den Typnamen ein sehr konsequentes Namensschema verfolgen können. Die Collection-Bibliothek fastutil stellt verschiedene Implementierungen für Objekttypen und primitive Typen bereit und die Namen sind dementsprechend noch spezifischer als in der Java-API (z.B. ObjectArrayList and ByteArrayList)
Tatsächlich gibt es oft gute Gründe die Art und Weise der Implementierung nicht im Typ zu beschreiben, nämlich dann, wenn man spätere (bessere) Implementierungen nicht einschränken möchte. In diesem Falle sollte man die Implementierung dennoch beschreiben. Ich bevorzuge in so einem Fall den Präfix Default.
Nun mag der eine oder andere vielleicht Kritik äußern warum DefaultFormDataValue besser sein sollte als ein Suffix FormDataValueImpl. Da gibt es mehrere Argumente:
Defaultsagt aus, was gemeint ist. Es handelt sich um die Default-Implementierung, die jeder im Zweifelsfall verwenden soll.Implsagt aus, dass es sich um eine Implementierung handelt, das wir dahinter eine Standard-Implementierung vermuten ist reine Konvention.Defaultist sprachlich korrekter: Es ist einFormDataValue, was für einer? EinDefault. BeiImpl-Suffix wandeln sich die sprachlichen Rollen: Es ist dann eine Implementierung. Was für eine: Eine Implementierung vonFormDataValue. Wer die Vererbungsbeziehung alsisliest merkt auch schnell, dass sichDefaultFormDataValue is FormDataValuedeutlich weniger holprig liest alsFormDataValueImpl is FormDataValue.Defaultist korrekt, weil es rein-logisch nur einen Default geben kann, für ImplementierungenImplgilt das explizit nicht (davon soll es ja mehrere geben können).Defaultist robuster für zukünftige Implementierungen. Wenn jetzt eine kompakte Implementierung dazu kommt, dann stünde eineFormDataValueImplneben einerFormDataValueCompactImpl(sprachlich ist das erste ein Oberkonzept des zweiten). Im anderen Fall stünde nebenDefaultFormDataValueeinCompactFormDataValue(beide Konzepte sind sprachlich klar getrennt).Defaultdeutet darauf hin, dass es eventuell Non-Default-Implementierungen gibt. Jeder der also in den Implementierungstyp castet wird auf jeden Fall daran erinnert, dass er hier eventuell auf andere Implementierungen prüfen sollte.- Es kann ja vorkommen, dass eine neue Implementierung nachträglich die alte ersetzen soll (z.B. sein
CompactDataValueist der neue Default). Wenn ich eine Umbenennung vonCompactDataValueinDefaultDataValuevornehme weiß jeder was damit intendiert ist,CompactDataValuenachDataValueImplhingegen ist eher kontraintuitiv.
Unabhängig haben wir mit I... und ...Impl natürlich noch das Problem, dass das Namensschema insgesamt fragil ist:
I...funktioniert nur so lange, wie sich die gesamte Community an das Namensschema hält (derzeit gilt das vermutlich nur für .NET)...Implbricht sofort, wenn jemand bewusst mehrere Implementierungen eines Interfaces vorsieht. In dem Fall muss er sich entscheiden ob er eineImplund andere Implementierungen mit anderem Schema wünscht, oder ob er ganz vomImplabgeht. Es gibt definitiv keine Community-Regelungen, wie man solche Konflikte auflöst oder gar konsistent bleibt.
Etwas ausführlicher haben sich auch andere bereits zu dem Thema geäußert (1,2,3).