zur Übersicht
12. Okt. 2016

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 ein FormDataValue, was für einer? Ein Default. Bei Impl-Suffix wandeln sich die sprachlichen Rollen: Es ist dann eine Implementierung. Was für eine: Eine Implementierung von FormDataValue. Wer die Vererbungsbeziehung als is liest merkt auch schnell, dass sich DefaultFormDataValue is FormDataValue deutlich weniger holprig liest als FormDataValueImpl is FormDataValue.
  • Default ist korrekt, weil es rein-logisch nur einen Default geben kann, für Implementierungen Impl 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 eine FormDataValueImpl neben einer FormDataValueCompactImpl (sprachlich ist das erste ein Oberkonzept des zweiten). Im anderen Fall stünde neben DefaultFormDataValue ein CompactFormDataValue (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 von CompactDataValue in DefaultDataValue vornehme weiß jeder was damit intendiert ist, CompactDataValue nach DataValueImpl 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 eine Impl und andere Implementierungen mit anderem Schema wünscht, oder ob er ganz vom Impl 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).