Programmieren gegen Interfaces



  • Hallo zusammen,

    ich wollte mal nach gängigen Meinungen bzw. nach Praxistauglichkeit hören. Meine Frage dreht sich zwar um Java, aber da es um Prinzipien geht, dürfte es auch für andere Sprachen gelten.

    Folgender Fall:
    Wir haben eine Datenklasse, die in eine Datenbank persistiert wird. Alle Instanzen können komfortabel mit einer API-Funktion aus der Datenbank gelesen werden und werden als Map herausgereicht.
    Um das mal ganz simpel zu schreiben

    public class Daten
    {
    	private String id;
    	private int x;
    	private double y;
    	// ...
    
    	// Methoden für diese Daten
    
    	public static Map<String, Daten> getAllAsMap(Database db)
    	{
    		Map<String, Daten> map = ...;
    
    		readFromDB(db, map);
    
    		return map;
    	}
    }
    

    So sieht das aus, bzw. sollte.

    Mein Punkt bzw. meine Frage ist jetzt der "..." Teil.

    Es gibt in (Java) ja mehrere Implementierungen einer Key-Value-Association:
    - HashMaps, die nach außen hin quasi keine sinnvolle Ordnung oder Sortierung haben
    - TreeMaps, die nach einem gewissen Comparator sortiert sind
    - und wir haben eine hauseigene sogenannte OrderedMap, wo die Ordnung die Einfügereihenfolge ist (in etwa, intern hat das Ding mehrere Listen und HashMaps um den Map Zugriff zu providen, aber um nicht bei jedem Zugriff linear suchen zu müssen)

    Bisher sah solch eine Datenbank Funktion, wie oben gezeigt, so aus, dass sie intern eine OrderedMap genutzt hat, diese aber auch aber auch als Rückgabetyp deklariert hat:

    public static OrderedMap<String, Daten> getAllAsMap(){}
    

    Dies wiederspricht meiner Intuition was Entwicklung betrifft.
    - Alle Aufrufer sind nun direkt abhängig von dieser konkreten Unterklasse von Map.
    - Das Dependency-Inversion Prinzip von SOLID ist verletzt
    - Wollen wir nun dafür sorgen, dass die entsprechende Map nur lesbar ist (wie es bei solch einer Operation ja der Fall sein sollte), können wir nicht Collections.unmodifiableMap nutzen

    Ich hatte vor Weihnachten diese Datenbank API dahingehend geändert, dass ich die Interfaces auf eine Map als Rückgabetyp umgestellt habe, um flexibler zu sein (intern blieb alles gleich, gerade von der Ordnung der Daten), vor allem da mir aufgefallen ist, dass sämtlicher aufrufende Code keine speziellen Methoden der OrderedMap braucht/nutzt.

    Nun habe ich gesehen, ein Kollege hat das alles wieder zurückgedreht. Sein Argument war, dass der Aufrufer dann über die interne Ordnung/Sortierung Bescheid weiss, was er bei einer Hash-,Tree- oder nur Map nicht wüsste.

    Habt ihr da vielleicht einen Gedanken zu?

    Viele Grüße,
    Skym0sh0³



  • Hallo Skym0sh0,

    Skym0sh0 schrieb:

    Nun habe ich gesehen, ein Kollege hat das alles wieder zurückgedreht. Sein Argument war, dass der Aufrufer dann über die interne Ordnung/Sortierung Bescheid weiss, was er bei einer Hash-,Tree- oder nur Map nicht wüsste.

    Nur ein paar Gedankengänge die mir spontan kommen.

    1. Wenn der Entwickler nicht weiß wie eine Hash-/Tree-Map aufgebaut ist und nur seine eigene Klassen verwenden möchte, um exakt zu wissen was passiert würde ich an dieser Stelle wohl empfehlen sich in die verfügbare Bibliothek einzuarbeiten. Es kann ja nicht sein, dass für "Standardprobleme" immer eigene Lösungen erstellt werden. (Neue Mitarbeiter tun sich dann eh schwer sich in die "Sonderlösungen" einzuarbeiten.)

    2. Ich sehe den Vorteil, dass wenn nur das Interface "Map" zurück gegeben wird, dass man als API Aufrufer noch unterscheiden kann, ob ich die Daten sortiert benötige. Mit der "OrderedMap" werde ich sie immer sortiert vorliegen haben. Zum einen kostet das vermutlich in manchen Fällen einfach unnötig Speicher und Performance. (Dieser Gedanke ist nur Relevant, wenn das eine Rolle spielen sollte.)

    3. Ich mag Interface Definitionen, so kann die interne Implementierung schön geändert werden, ohne größeren Änderungsaufwand vom Rest. Was passiert den wenn in der neuen Java Version eine tolle schnelle "OrderedMap" Implementierung erscheint und ihr noch irgendwelche Spezialfunktionen von eurer Implementierung aufruft? (Was wohl aktuell nicht der Fall ist.)

    Viele Grüße



  • Danke für deine Antworten jb 🙂

    jb schrieb:

    1. Wenn der Entwickler nicht weiß wie eine Hash-/Tree-Map aufgebaut ist und nur seine eigene Klassen verwenden möchte, um exakt zu wissen was passiert würde ich an dieser Stelle wohl empfehlen sich in die verfügbare Bibliothek einzuarbeiten. Es kann ja nicht sein, dass für "Standardprobleme" immer eigene Lösungen erstellt werden. (Neue Mitarbeiter tun sich dann eh schwer sich in die "Sonderlösungen" einzuarbeiten.)
      Das denke ich auch. Auf der einen Seite haben wir hier im Haus viele eigene Lösungen, was irgendwo cool ist (man kann mal selsbt ne Map Implementierung schreiben, sehr itneressant) und gibt Unabhängigkeit gegenüber Extern. Zum anderen ist es eben mies, weil es kostet, oft nicht annähernd so gut ist, wie etwas "Externes".

    Einen Punkt sehe ich jedoch: das Wissen hier in der Firma ist, naja, etwas durchwachsen. Ich habe den Eindruck, viele haben eben diese OrderedMap mal gesehen und dachten sich "Cool, so kann ich per Schlüssel auf Values zugreifen" kennen umgekehrt aber nicht die Standard Java Maps.

    jb schrieb:

    1. Ich sehe den Vorteil, dass wenn nur das Interface "Map" zurück gegeben wird, dass man als API Aufrufer noch unterscheiden kann, ob ich die Daten sortiert benötige. Mit der "OrderedMap" werde ich sie immer sortiert vorliegen haben. Zum einen kostet das vermutlich in manchen Fällen einfach unnötig Speicher und Performance. (Dieser Gedanke ist nur Relevant, wenn das eine Rolle spielen sollte.)

    An der Stelle möchte ich grad nochmal einwerfen, dass ich zwischen Ordnung und Sortierung unterscheide. Als Sortierung betrachte ich z.B. so etwas wie Comparable, was nach einem Kriterium in eine (mathematische) Ordnung gebracht werden kann: 1 < 2 < 3 ... (sowas macht ja z.b. die java.util.TreeMap)
    Die OrderdMap jedoch kennt nur eine Ordnung oder, besser genannt, Reihenfolge. Und die wäre die Reihenfolge des Einfügens der Daten.

    Beispiel dazu:
    Ich schiebe die folgenden Key-Value-Paare in die Map: <5, X>, <3, Y> und <10, Z>
    Dann wäre die Reihenfolge der TreeMap intern: <3, Y>, <5, X> und <10, Z>
    Bei der HashMap mit der HashFunktion h(x) = x % 7: <3, Y>, <10, Z>, <5, X>
    Und bei der OrderedMap: <5, X>, <3, Y>, <10, Z>

    Ich als Nutzer würde mir die Daten aus der Datenbank holen und (sofern ich eine gewisse Ordnung oder Reihenfolge brauche!) diese dann ordnen/sortieren.

    jb schrieb:

    1. Ich mag Interface Definitionen, so kann die interne Implementierung schön geändert werden, ohne größeren Änderungsaufwand vom Rest. Was passiert den wenn in der neuen Java Version eine tolle schnelle "OrderedMap" Implementierung erscheint und ihr noch irgendwelche Spezialfunktionen von eurer Implementierung aufruft? (Was wohl aktuell nicht der Fall ist.)

    Ja, genaud as ist auch mein Punkt. Alleine die Map unmodifiable zu machen, geht ja schon nicht. D.h. der Kollege hat das gemacht und auch geschafft. Hat dafür aber eine neue Klasse UnmodifiableOrderedMap (die von der OrderedMap erbt) gebaut und alle modifizierenden Operationen überschrieben, so dass sie Exceptions werfen. Kostenpunkt 400 Zeilen.



  • Skym0sh0 schrieb:

    An der Stelle möchte ich grad nochmal einwerfen, dass ich zwischen Ordnung und Sortierung unterscheide. [...]
    Beispiel dazu:
    Ich schiebe die folgenden Key-Value-Paare in die Map: <5, X>, <3, Y> und <10, Z>
    Dann wäre die Reihenfolge der TreeMap intern: <3, Y>, <5, X> und <10, Z>
    Bei der HashMap mit der HashFunktion h(x) = x % 7: <3, Y>, <10, Z>, <5, X>
    Und bei der OrderedMap: <5, X>, <3, Y>, <10, Z>

    Ich als Nutzer würde mir die Daten aus der Datenbank holen und (sofern ich eine gewisse Ordnung oder Reihenfolge brauche!) diese dann ordnen/sortieren.

    Mein die API kann es ja anbieten die Daten sortiert oder geordnet zurückzugeben, allerdings werden die Daten in der DB sehr wahrscheinlich ungeordnet und nur nach dem Primärschlüssel sortiert abgelegt werden. Also ich sehe das so wie Du. (Dann könnte auch die API die passende Klasse dafür verwenden, je nach Anforderung - dann muss ein "Neuling" der eure API verwendet sich keine tieferen Gedanken machen welcher Container der geeignetste ist.)

    Skym0sh0 schrieb:

    Ja, genaud as ist auch mein Punkt. Alleine die Map unmodifiable zu machen, geht ja schon nicht. D.h. der Kollege hat das gemacht und auch geschafft. Hat dafür aber eine neue Klasse UnmodifiableOrderedMap (die von der OrderedMap erbt) gebaut und alle modifizierenden Operationen überschrieben, so dass sie Exceptions werfen. Kostenpunkt 400 Zeilen.

    Das hat er nicht gemacht - oder?!? Warum erbt OrderedMap nicht von UnmodifiableOrderedMap würde doch mehr Sinn machen, oder? (Ich hab zwar grad Urlaub aber sinnvoll finde ich das nicht.) Dann können schon keine ungültige Aufrufe durchgeführt werden.

    class OrderedMap {
    public:
      virtual void modify() { /* todo */ }
    };
    
    class UnmodifiableOrderedMap : public OrderedMap {
    public:
      virtual void modify() { std::cerr << "Pech gehabt, Du darfst das nicht aufrufen!"; }
    }
    


  • Der Fairness muss man dazu sagen, dass die java Standard-Bibliothek es nicht wirklich anders macht.

    Eine unmodifiableList ist nichts anderes als eine List-Implementierung, die als Wrapper um eine andere List gelegt ist, wobei die modifizierenden Operationen eine Exception werfen.

    Und ja es wäre schöner wenn das schon durch intelligente Typhierarchien unterstützt würde.



  • Das Interface mit der OrderedMap sagt, dass Du die Daten in einer bestimmten Reihenfolge bekommst und Du Dich darauf verlassen kannst (woher die Reihenfolge auch immer kommt). Ändere ich das Interface, weiß der Aufrufer nicht mehr, in welcher Reihenfolge die Sätze kommen. Es ist also ein anderes Interface.

    Wahrscheinlich werden da Daten mit einer "order by"-Klausel gelesen. Und wenn sie dann als Map geliefert werden, ist die Klausel hinfällig. Sie kann vom Anwender nicht mehr genutzt werden, da das Interface die Sortierung verwirft.

    Wobei ich mich frage, warum die Daten aus der Datenbank als Map zurück kommen. Ich habe immer eine Datenzugriffsschicht, die aus der Datenbank fachliche Objekte liefert und nicht irgendwelche key-value-Paare. Aber das ist eine andere Designentscheidung.



  • Genau das war auch sein Argument.

    Aber widerspricht das nicht genau dem Dependency-Inversion Prinzip?
    Ich mein, wenn man ne TreeMap (mit einem bestimmten Comparator) als einfache Map rausreicht gehen doch auch diese Informationen verloren o_O



  • Skym0sh0 schrieb:

    Der Fairness muss man dazu sagen, dass die java Standard-Bibliothek es nicht wirklich anders macht.

    Eine unmodifiableList ist nichts anderes als eine List-Implementierung, die als Wrapper um eine andere List gelegt ist, wobei die modifizierenden Operationen eine Exception werfen.

    Ja, sie erbt von AbstractList oder was auch immer, nicht von einer konkreten Implementierung. Die konkrete Implementierung wird intern gewrappt. Nach deiner Beschreibung würde ein Upcast reichen um aus ImmutableOrderedMap (nicht Unmodifable bitte) eine OrderedMap zu machen.

    Ansonsten verstehe ich die generelle Argumentation gerade nicht:

    Sein Argument war, dass der Aufrufer dann über die interne Ordnung/Sortierung Bescheid weiss, was er bei einer Hash-,Tree- oder nur Map nicht wüsste.

    Ist denn dein Map-Interface nicht viel genereller als OrderedMap? was sagt das Map-Interface denn mehr über die Order der Daten aus als OrderedMap? Ich kann OrderedMap doch immer Upcasten auf Map, oder?

    PS:
    Ich persönlich mag bei sowas eine eigene abstrakte Klasse die Zugriff auf Daten aus der DB ermöglicht. Das Problem ist ja, dass wenn du fix eine OrderedMap zurück gibst du die Datenbankdaten ja zB gar nicht mehr Streamen kannst oder dergleichen. Deshalb sollte da ein abstraktes Interface auf etwas "map" oder "list" ähnliches verwendet werden. Welches Collection Interface sich da am besten anbietet weiß ich nicht, da ich dazu zu lange kein Java gemacht habe.

    Aber viele operationen einer Map braucht man ja bei einem "Datenbank ResultSet" eh nicht. Ich hab ja nur Readonly Zugriff, etc. Dieses ResultSet kann aber dennoch eine Funktion "asOrderedMap()" mit O(N) anbieten um die Daten als Map zu liefern, falls man sort oder dergleichen darauf anwenden will.

    Lange rede kurzer sinn: ein Interface ist hier schon richtig. uU ist aber Map das falsche.



  • Shade Of Mine schrieb:

    Skym0sh0 schrieb:

    Der Fairness muss man dazu sagen, dass die java Standard-Bibliothek es nicht wirklich anders macht.

    Eine unmodifiableList ist nichts anderes als eine List-Implementierung, die als Wrapper um eine andere List gelegt ist, wobei die modifizierenden Operationen eine Exception werfen.

    Ja, sie erbt von AbstractList oder was auch immer, nicht von einer konkreten Implementierung. Die konkrete Implementierung wird intern gewrappt. Nach deiner Beschreibung würde ein Upcast reichen um aus ImmutableOrderedMap (nicht Unmodifable bitte) eine OrderedMap zu machen.

    AbstractList ist eine abstrakte Implementierung des List Intmerfaces, bei dem man zum Nutzen die size und die get Methoden selbst implementieren muss. Implementiert man zusätzlich noch add und set kann die Liste auch verändert werden, d.h. es kann reingeschrieben werden. Und eine UnmodifiableList ist nichts anderes wo, die set und add Methode eben eine Exception wirft. Alle anderen Methoden delegieren weiter.

    Und doch, es geht um Unmodifiable und nicht Immutable. Jedenfalls um mit der Namensgebung konsistent zu bleiben.
    Wobei, jetzt wo ich grad mal dran denke: Wo wäre denn Unterschied zwischen einer Immutable[Datenstruktur] und einer Unmodifiable[Datenstruktur]?
    Immutable kenn ich (bisher) nur, dass es keine Möglichkeit gibt ein Objekt nach der Initialisierung zu ändern, also beispielsweise vom Interface her. Unmodifiable umgekehrt, lässt vom Interface her Änderungen zu, aber unterbidnet diese dann zur Laufzeit (fehleranfällig :().

    Shade Of Mine schrieb:

    Ansonsten verstehe ich die generelle Argumentation gerade nicht:

    Sein Argument war, dass der Aufrufer dann über die interne Ordnung/Sortierung Bescheid weiss, was er bei einer Hash-,Tree- oder nur Map nicht wüsste.

    Ist denn dein Map-Interface nicht viel genereller als OrderedMap? was sagt das Map-Interface denn mehr über die Order der Daten aus als OrderedMap? Ich kann OrderedMap doch immer Upcasten auf Map, oder?

    public class OrderedMap<K, V> implements Map<K, V>, Iterable<K>, Serializable
    {
        // ...
    }
    

    Das ist im groben die Signatur der Klasse.

    Also, um auf deine Fragen zu antworten: Ja, das Map-Interface ist genereller (oder anders ausgedrückt, kann weniger) und die OrderedMap ist auch implizit upcastbar zur Map.

    Als ich die Refaktorisierungen angefangen habe, war eine meienr Hauptmotivationen eher die OrderedMap zu entfernen, da sie bei uns im Haus inflationär eingesetzt. Die meisten Kollegen sind BWL'er, die mal nebenbei programmieren gelernt haben und hauptsächlich auf die Inhaltliche Seite fokussiert sind. Das führt halt zu Situationen wie:
    "Mh, ich muss diese Daten mit denen verknüpfen, damit ich von A auf X zugreifen kann. Ja, das kann ich mit 5 Arrays hier in der Funktion schnell selbst schreiben."
    [5000 Zeilen später]
    "Ja, das geht jetzt"
    Senior Developer kommt rein: "Hier mach das doch mit der OrderedMap"
    "Oh, cool, wusste ich nicht, super einfach"
    => Folge: OrderedMap ist die SilverBullet für alles

    Und 90% des Codes durch den ich mich dann geforstet habe, der war nicht abhängig vom Interface dieser OrderedMap und auch nicht von der Sortierung/Ordnung.

    Shade Of Mine schrieb:

    PS:
    Ich persönlich mag bei sowas eine eigene abstrakte Klasse die Zugriff auf Daten aus der DB ermöglicht. Das Problem ist ja, dass wenn du fix eine OrderedMap zurück gibst du die Datenbankdaten ja zB gar nicht mehr Streamen kannst oder dergleichen. Deshalb sollte da ein abstraktes Interface auf etwas "map" oder "list" ähnliches verwendet werden. Welches Collection Interface sich da am besten anbietet weiß ich nicht, da ich dazu zu lange kein Java gemacht habe.

    Aber viele operationen einer Map braucht man ja bei einem "Datenbank ResultSet" eh nicht. Ich hab ja nur Readonly Zugriff, etc. Dieses ResultSet kann aber dennoch eine Funktion "asOrderedMap()" mit O(N) anbieten um die Daten als Map zu liefern, falls man sort oder dergleichen darauf anwenden will.

    Lange rede kurzer sinn: ein Interface ist hier schon richtig. uU ist aber Map das falsche.

    Jaaaaa, das ist natürlich etwas ganz anderes. Und das würde ich auch gerne sehen. Stichwort: DAO Pattern
    Aber Programmieren geht ja ganz einfach indem man schnell mal in ner OnClick Methode eines Buttons ein 5 seitiges SQL zusammen schraubt und dessen ResultSet dann als HTML String wieder an den Brwoser leitet.
    Okay, ich war nicht ganz fair, SQL ist bei uns durch eine hausinterne Entwicklung abgekapselt. Man konkatteniert keine Strings mit SQL Code, sondern steckt sich Objekthierarchien zusammmen, die per toString() SQL Code erzeugen.



  • Also erstmal vorneweg: ja prinzipiell ist das natürlich richtig, dass deine Typen so generell wie möglich sein sollten. Aber ich hab erstmal ein paar Fragen an eure Firma:

    und wir haben eine hauseigene sogenannte OrderedMap, wo die Ordnung die Einfügereihenfolge ist (in etwa, intern hat das Ding mehrere Listen und HashMaps um den Map Zugriff zu providen, aber um nicht bei jedem Zugriff linear suchen zu müssen)

    Aha also quasi genau wie eine LinkedHashMap aus der Standardbibiliothek nur wahrscheinlich schlechter implementiert. Wieso?

    Nun habe ich gesehen, ein Kollege hat das alles wieder zurückgedreht.

    Macht ihr keine Code Reviews wo sowas eher zur Sprache käme?

    Alle Instanzen können komfortabel mit einer API-Funktion aus der Datenbank gelesen werden und werden als Map herausgereicht

    Das hört sich nach dem gröbsten Unfug an. Warum hat euer Data Model überhaupt irgendwelche Kenntnisse über irgendwelche Datenbanken?? Habt ihr kein Data Access Layer oder Repository zwischen dem Data Model/Service Layer und der Datenbank?
    Warum benutzt ihr hier überhaupt Maps anstatt (geordnete) Lists? Wenn ich einen Record by Foreign Key benötige, lade ich ihn sowieso direkt (einzeln) aus der Datenbank und hol mir nicht ne ganze Liste.

    Jaaaaa, das ist natürlich etwas ganz anderes. Und das würde ich auch gerne sehen. Stichwort: DAO Pattern
    Aber Programmieren geht ja ganz einfach indem man schnell mal in ner OnClick Methode eines Buttons ein 5 seitiges SQL zusammen schraubt und dessen ResultSet dann als HTML String wieder an den Brwoser leitet.
    Okay, ich war nicht ganz fair, SQL ist bei uns durch eine hausinterne Entwicklung abgekapselt. Man konkatteniert keine Strings mit SQL Code, sondern steckt sich Objekthierarchien zusammmen, die per toString() SQL Code erzeugen.

    Ersetz das mal durch jooq oder such dir ne neue Firma

    Ansonsten verstehe ich die generelle Argumentation gerade nicht:
    Zitat:

    Sein Argument war, dass der Aufrufer dann über die interne Ordnung/Sortierung Bescheid weiss, was er bei einer Hash-,Tree- oder nur Map nicht wüsste.

    Ist denn dein Map-Interface nicht viel genereller als OrderedMap? was sagt das Map-Interface denn mehr über die Order der Daten aus als OrderedMap? Ich kann OrderedMap doch immer Upcasten auf Map, oder?

    Prinzipiell ist die Argumentation vom Kollegen hier schon korrekt, auch wenn sie der Daumenregel aus meinem ersten Satz widerspricht. Falls der aufrufende Code davon ausgeht dass die Map nunmal nach XYZ geordnet ist, dann muss der Typ das auch wiederspiegeln. Wenn der Typ zu generell ist und sich die Implementierung ändert (z.B. keine OrderedMap mehr zurückgibt), aber die Businesslogik nunmal darauf vertraut dass die Map geordnet ist, ist der Code nun plötzlich broken, auch wenn er kompiliert. Die Daumenregel muss also erweitert werden: so generell wie möglich, aber so spezifisch wie nötig.

    Vorschläge:

    • Führ Code Reviews in eurem Team ein um solche Sachen diskutieren zu können
    • Stell sicher dass eure Businesslogik getestet ist bevor du refactorst
    • Entfern die bekloppten Utility Methoden die dein Data Model an die Datenbank ketten
    • Stattdessen implementier den Datenbankzugriff über ein DAL, z.B. mittels DAOs
    • Trenne die Operationen getDataByID() und getAllDataOrderedByXYZ(), wobei die letztere eine List liefert keine Map
    • Wo eine geordnete Map gebraucht wird, versuch durch LinkedHashMap (oder guava oder apache commons collections) zu ersetzen statt selbstgebrödelte Implementierungen.


  • Skym0sh0 schrieb:

    Nun habe ich gesehen, ein Kollege hat das alles wieder zurückgedreht.

    Also muss die Frage vom Chef entschieden werden.



  • volkard schrieb:

    Skym0sh0 schrieb:

    Nun habe ich gesehen, ein Kollege hat das alles wieder zurückgedreht.

    Also muss die Frage vom Chef entschieden werden.

    Naja, der Kollege ist der Senior Developer und ich nur der dumme Student. Ergo beuge ich mich.


Anmelden zum Antworten