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 Team-intern gegen Interface-Namenskonventionen wie z.B. IFormDataValue
entschieden.”
Da fühle ich mich dann etwas missverstanden. Beim Naming gibt es nunmal 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 casted, 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 Impplementierung gibt, nutzt (z.B. in dem er auf den Implementierugnstyp 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 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 was er für Eigenschaften 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 Implementatierungen für Objekttypen und primitive Typen bereit und die Namen sind dementsprechend noch spezifischer als in der Java-API (e.g. 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 an einem Suffix FormDataValueImpl
. Da gibt es mehrere Argumente:
Default
sagt aus, was gemeint ist. Es handelt sich um die Default-Implementierung, die jeder im Zweifelsfall verwenden soll.Impl
sagt aus, dass es sich um eine Implementierung handelt, das wir dahinter eine Standard-Implementierung vermuten ist reine Konvention.Default
ist 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 alsis
liest merkt auch schnell, dass sichDefaultFormDataValue is FormDataValue
deutlich weniger holprig liest alsFormDataValueImpl is FormDataValue
.Default
ist korrekt, weil es rein-logisch nur einen Default geben kann, für ImplementierungenImpl
gilt das explizit nicht (davon soll es ja mehrere geben können).Default
ist robuster für zukünftige Implementierungen. Wenn jetzt eine kompakte Implementierung dazu kommt, dann stünde eineFormDataValueImpl
neben einerFormDataValueCompactImpl
(sprachlich ist das erste ein Oberkonzept des zweiten). Im anderen Fall stünde nebenDefaultFormDataValue
einCompactFormDataValue
(beide Konzepte sind sprachlich klar getrennt).Default
deutet 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
CompactDataValue
ist der neue default). Wenn ich eine Umbenennung vonCompactDataValue
inDefaultDataValue
vornehme weiß jeder was damit intendiert ist,CompactDataValue
nachDataValueImpl
hingegen 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)...Impl
bricht sofort, wenn jemand bewusst mehrere Implementierungen eines Interfaces vorsieht. In dem Fall muss er sich entscheiden ob er eineImpl
und andere Implementierungen mit anderem Schema wünscht, oder ob er ganz vomImpl
abgeht. 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).