it-swarm.com.de

Gute oder schlechte Vorgehensweise zum Maskieren von Java Sammlungen mit aussagekräftigen Klassennamen?

In letzter Zeit habe ich mir angewöhnt, Java Sammlungen mit menschenfreundlichen Klassennamen zu "maskieren". Einige einfache Beispiele:

// Facade class that makes code more readable and understandable.
public class WidgetCache extends Map<String, Widget> {
}

Oder:

// If you saw a ArrayList<ArrayList<?>> being passed around in the code, would you
// run away screaming, or would you actually understand what it is and what
// it represents?
public class Changelist extends ArrayList<ArrayList<SomePOJO>> {
}

Ein Kollege wies mich darauf hin, dass dies eine schlechte Praxis ist und Verzögerung/Latenz einführt sowie ein OO Anti-Muster. Ich kann verstehen, dass es ein sehr winziger Grad an Leistungsaufwand, aber ich kann mir nicht vorstellen, dass er überhaupt signifikant ist. Also frage ich: Ist das gut oder schlecht und warum?

46
herpylderp

Verzögerung/Latenz? Ich rufe BS an. Diese Praxis sollte genau null Overhead verursachen. (Edit : In den Kommentaren wurde darauf hingewiesen, dass dies tatsächlich Optimierungen verhindern kann, die von der HotSpot-VM durchgeführt werden. Ich weiß nicht genug über VM Implementierung, um dies zu bestätigen oder abzulehnen. Ich habe meinen Kommentar auf die C++ - Implementierung virtueller Funktionen gestützt.)

Es gibt einen gewissen Code-Overhead. Sie müssen alle Konstruktoren aus der gewünschten Basisklasse erstellen und ihre Parameter weiterleiten.

Ich sehe es auch nicht als Anti-Muster an sich. Ich sehe es jedoch als verpasste Gelegenheit. Anstatt eine Klasse zu erstellen, die die Basisklasse nur zum Umbenennen ableitet, erstellen Sie stattdessen eine Klasse, die die Sammlung enthält und einen Fall bietet -spezifische, verbesserte Schnittstelle? Sollte Ihr Widget-Cache wirklich die vollständige Oberfläche einer Karte bieten? Oder sollte es stattdessen eine spezielle Schnittstelle bieten?

Darüber hinaus funktioniert das Muster bei Sammlungen einfach nicht mit der allgemeinen Regel der Verwendung von Schnittstellen und nicht von Implementierungen zusammen. Das heißt, Sie würden im einfachen Sammlungscode einen HashMap<String, Widget> Erstellen und ihn dann zuweisen zu einer Variablen vom Typ Map<String, Widget>. Ihr WidgetCache kann Map<String, Widget> Nicht erweitern, da dies eine Schnittstelle ist. Es kann keine Schnittstelle sein, die die Basisschnittstelle erweitert, da HashMap<String, Widget> Diese Schnittstelle nicht implementiert und auch keine andere Standardsammlung. Und während Sie es zu einer Klasse machen können, die HashMap<String, Widget> Erweitert, müssen Sie die Variablen als WidgetCache oder Map<String, Widget> Deklarieren, und die erste verliert Ihnen die Flexibilität, eine andere zu ersetzen Sammlung (vielleicht einige Lazys Lazy Loading-Sammlung), während die zweite Art den Punkt der Klasse besiegt.

Einige dieser Kontrapunkte gelten auch für meine vorgeschlagene Fachklasse.

Dies sind alles Punkte, die zu berücksichtigen sind. Es kann die richtige Wahl sein oder auch nicht. In beiden Fällen sind die von Ihrem Kollegen angebotenen Argumente ungültig. Wenn er denkt, dass es ein Anti-Muster ist, sollte er es benennen.

75
Sebastian Redl

Laut IBM Dies ist eigentlich ein Anti-Pattern. Diese 'typedef'-ähnlichen Klassen werden als Pseudo-Typen bezeichnet.

Der Artikel erklärt es viel besser als ich, aber ich werde versuchen, es zusammenzufassen, falls der Link ausfällt:

  • Jeder Code, der ein WidgetCache erwartet, kann einen Map<String, Widget> Nicht verarbeiten.
  • Diese Pseudotypen sind "viral", wenn sie mehrere Pakete verwenden. Sie führen zu Inkompatibilitäten, während der Basistyp (nur eine dumme Map <...>) in allen Fällen in allen Paketen funktioniert hätte.
  • Pseudo-Typen sind oft zu konkret, sie implementieren keine spezifischen Schnittstellen, da ihre Basisklassen nur die generische Version implementieren.

In dem Artikel schlagen sie den folgenden Trick vor, um das Leben ohne Verwendung von Pseudotypen zu erleichtern:

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

Was aufgrund der automatischen Typinferenz funktioniert.

(Ich kam zu dieser Antwort über this verwandte Stapelüberlauffrage)

25
Roy T.

Der Leistungseinbruch würde sich höchstens auf eine vtable-Suche beschränken, die Sie höchstwahrscheinlich bereits durchführen. Das ist kein triftiger Grund, sich dagegen zu stellen.

Die Situation ist häufig genug, dass die meisten statisch typisierten Programmiersprachen eine spezielle Syntax für Aliasing-Typen haben, die normalerweise als typedef bezeichnet wird. Java hat diese wahrscheinlich nicht kopiert, weil es ursprünglich keine parametrisierten Typen hatte. Das Erweitern einer Klasse ist aus den Gründen, die Sebastian in seiner Antwort so gut behandelt hat, nicht ideal, kann es aber sein Eine vernünftige Problemumgehung für die eingeschränkte Syntax von Java.

Typedefs haben eine Reihe von Vorteilen. Sie drücken die Absicht des Programmierers klarer und mit einem besseren Namen auf einer angemesseneren Abstraktionsebene aus. Sie sind einfacher zu Debugging- oder Refactoring-Zwecken zu suchen. Überlegen Sie, ob Sie überall ein WidgetCache finden möchten, anstatt die spezifischen Verwendungen eines Map zu finden. Sie sind einfacher zu ändern, wenn Sie beispielsweise später feststellen, dass Sie stattdessen ein LinkedHashMap oder sogar einen eigenen benutzerdefinierten Container benötigen.

13
Karl Bielefeldt

Ich empfehle Ihnen, wie bereits erwähnt, die Komposition anstelle der Vererbung zu verwenden, damit Sie nur die wirklich benötigten Methoden mit Namen verfügbar machen können, die dem beabsichtigten Anwendungsfall entsprechen. Müssen Benutzer Ihrer Klasse wirklich wissen, dass WidgetCache eine Karte ist? Und damit machen können, was sie wollen? Oder müssen sie nur wissen, dass dies ein Cache für Widgets ist?

Beispielklasse aus meiner Codebasis mit Lösung für ein ähnliches Problem, das Sie beschrieben haben:

public class Translations {

    private Map<Locale, Properties> translations = new HashMap<>();

    public void appendMessage(Locale locale, String code, String message) {
        /* code */
    }

    public void addMessages(Locale locale, Properties messages) {
        /* code */
    }

    public String getMessage(Locale locale, String code) {
        /* code */
    }

    public boolean localeExists(Locale locale) {
        /* code */
    }
}

Sie können sehen, dass es intern "nur eine Karte" ist, aber die öffentliche Oberfläche zeigt dies nicht. Und es hat "programmiererfreundliche" Methoden wie appendMessage(Locale locale, String code, String message) für eine einfachere und aussagekräftigere Möglichkeit, neue Einträge einzufügen. Und Benutzer der Klasse können beispielsweise translations.clear() nicht ausführen, da TranslationsMap nicht erweitert.

Optional können Sie einige der erforderlichen Methoden jederzeit an die intern verwendete Karte delegieren.

11
user11153

Ich sehe dies als Beispiel für eine sinnvolle Abstraktion. Eine gute Abstraktion hat einige Eigenschaften:

  1. Es verbirgt Implementierungsdetails, die für den Code, der es verbraucht, irrelevant sind.

  2. Es ist nur so komplex wie es sein muss.

Durch die Erweiterung wird die gesamte Benutzeroberfläche des übergeordneten Elements verfügbar gemacht, aber in vielen Fällen ist ein Großteil davon möglicherweise besser verborgen. Sie möchten also das tun, was Sebastian Redl vorschlägt, und Komposition gegenüber Vererbung bevorzugen und Fügen Sie eine Instanz des übergeordneten Elements als privates Mitglied Ihrer benutzerdefinierten Klasse hinzu. Jede der für Ihre Abstraktion sinnvollen Schnittstellenmethoden kann (in Ihrem Fall) problemlos an die innere Sammlung delegiert werden.

In Bezug auf eine Auswirkung auf die Leistung ist es immer eine gute Idee, den Code zuerst auf Lesbarkeit zu optimieren. Wenn eine Auswirkung auf die Leistung vermutet wird, profilieren Sie den Code, um die beiden Implementierungen zu vergleichen.

6
Mike Partridge

+1 zu den anderen Antworten hier. Ich werde auch hinzufügen, dass dies von der Domain Driven Design (DDD) -Community als sehr gute Praxis angesehen wird. Sie befürworten, dass Ihre Domain und die Interaktionen mit ihr im Gegensatz zur zugrunde liegenden Datenstruktur eine semantische Domainbedeutung haben sollten. Ein Map<String, Widget> Könnte ein Cache sein, aber es könnte auch etwas anderes sein. Was Sie in meiner nicht so bescheidenen Meinung (IMNSHO) richtig gemacht haben, ist zu modellieren, was die Sammlung darstellt, in diesem Fall ein Cache.

Ich werde eine wichtige Änderung hinzufügen, indem der Domänenklassen-Wrapper um die zugrunde liegende Datenstruktur wahrscheinlich auch andere Mitgliedsvariablen oder Funktionen haben sollte, die ihn wirklich zu einer Domänenklasse mit Interaktionen machen, im Gegensatz zu nur einer Datenstruktur (wenn nur Java hatte Werttypen, wir bekommen sie in Java 10 - Versprechen!)

Es wird interessant sein zu sehen, welche Auswirkungen die Streams von Java 8 auf all dies haben werden. Ich kann mir vorstellen, dass einige öffentliche Schnittstellen es vielleicht vorziehen, sich mit einem Stream von (insert common Java zu befassen primitiv oder String) im Gegensatz zu einem Java Objekt.

4
Martijn Verburg