Wann werden Generika im Interface-Design verwendet?


11

Ich habe einige Schnittstellen, die von Dritten in Zukunft implementiert werden sollen, und ich stelle selbst eine Basisimplementierung bereit. Ich werde nur ein paar verwenden, um das Beispiel zu zeigen.

Derzeit sind sie definiert als

Artikel:

public interface Item {

    String getId();

    String getName();
}

ItemStack:

public interface ItemStackFactory {

    ItemStack createItemStack(Item item, int quantity);
}

ItemStackContainer:

public interface ItemStackContainer {

    default void add(ItemStack stack) {
        add(stack, 1);
    }

    void add(ItemStack stack, int quantity);
}

Jetzt, Itemund ItemStackFactoryich kann absolut voraussehen, dass Dritte es in Zukunft erweitern müssen. ItemStackContainerkönnte auch in Zukunft erweitert werden, jedoch nicht auf eine Weise, die ich außerhalb meiner bereitgestellten Standardimplementierung vorhersehen kann.

Jetzt versuche ich, diese Bibliothek so robust wie möglich zu gestalten. Dies befindet sich noch im Anfangsstadium (Pre-Pre-Alpha), daher kann dies ein Akt des Over Engineering (YAGNI) sein. Ist dies ein geeigneter Ort, um Generika zu verwenden?

public interface ItemStack<T extends Item> {

    T getItem();

    int getQuantity();
}

Und

public interface ItemStackFactory<T extends ItemStack<I extends Item>> {

    T createItemStack(I item, int quantity);
}

Ich befürchte, dass dies dazu führen könnte, dass Implementierungen und Verwendung schwieriger zu lesen und zu verstehen sind. Ich denke, es ist die Empfehlung, Generika nach Möglichkeit nicht zu verschachteln.


1
Es fühlt sich so an, als würden Sie Implementierungsdetails in beiden Fällen offenlegen. Warum muss der Anrufer mit Stapeln arbeiten und diese kennen / pflegen, anstatt mit einfachen Mengen?
CHao

Daran hatte ich nicht gedacht. Das gibt einen guten Einblick in dieses spezielle Problem. Ich werde sicherstellen, dass die Änderungen an meinem Code vorgenommen werden.
Zymus

Antworten:


9

Sie verwenden Generika in Ihrer Benutzeroberfläche, wenn Ihre Implementierung wahrscheinlich auch generisch ist.

Beispielsweise ist jede Datenstruktur, die beliebige Objekte akzeptieren kann, ein guter Kandidat für eine generische Schnittstelle. Beispiele: List<T>und Dictionary<K,V>.

Jede Situation, in der Sie die Typensicherheit allgemein verbessern möchten, ist ein guter Kandidat für Generika. A List<String>ist eine Liste, die nur mit Zeichenfolgen arbeitet.

Jede Situation, in der Sie SRP allgemein anwenden und typspezifische Methoden vermeiden möchten, ist ein guter Kandidat für Generika. Beispielsweise können ein PersonDocument, ein PlaceDocument und ein ThingDocument durch ein ersetzt werden Document<T>.

Das Abstract Factory-Muster ist ein guter Anwendungsfall für ein Generikum, während eine gewöhnliche Factory-Methode einfach Objekte erstellt, die von einer gemeinsamen, konkreten Schnittstelle erben.


Ich stimme der Idee nicht wirklich zu, dass generische Schnittstellen mit generischen Implementierungen gepaart werden sollten. Das Deklarieren eines generischen Typparameters in einer Schnittstelle und das anschließende Verfeinern oder Instanziieren in verschiedenen Implementierungen ist eine leistungsstarke Technik. Es ist besonders häufig in Scala. Schnittstellen abstrakte Dinge; Klassen spezialisieren sie.
Benjamin Hodgson

@BenjaminHodgson: Das bedeutet nur, dass Sie Implementierungen auf einer generischen Schnittstelle für bestimmte Typen erstellen. Der Gesamtansatz ist immer noch allgemein, wenn auch etwas weniger.
Robert Harvey

Genau. Vielleicht habe ich Ihre Antwort falsch verstanden - Sie schienen von dieser Art von Design abzuraten.
Benjamin Hodgson

@BenjaminHodgson: Würde eine Factory-Methode nicht eher gegen eine konkrete als gegen eine generische Schnittstelle geschrieben ? (obwohl eine abstrakte Fabrik generisch wäre)
Robert Harvey

Ich denke, wir sind uns einig :) Ich würde das Beispiel des OP wahrscheinlich als so etwas wie schreiben class StackFactory { Stack<T> Create<T>(T item, int count); }. Ich kann ein Beispiel dafür schreiben, was ich unter "Verfeinern eines Typparameters in einer Unterklasse" in einer Antwort verstanden habe, wenn Sie mehr Klarheit wünschen, obwohl die Antwort nur tangential mit der ursprünglichen Frage zusammenhängt.
Benjamin Hodgson

8

Ihr Plan, wie Sie Ihre Benutzeroberfläche allgemeiner gestalten können, scheint mir richtig zu sein. Ihre Frage, ob dies eine gute Idee wäre oder nicht, erfordert jedoch eine etwas kompliziertere Antwort.

Im Laufe der Jahre habe ich eine Reihe von Kandidaten für Software Engineering-Positionen für Unternehmen interviewt, für die ich gearbeitet habe, und mir ist klar geworden, dass die Mehrheit der Arbeitssuchenden mit der Verwendung allgemeiner Klassen vertraut ist (z. B. die Standard-Auflistungsklassen,) aber nicht beim Schreiben generischer Klassen. Die meisten haben in ihrer Karriere noch nie eine einzige generische Klasse geschrieben und sehen aus, als wären sie völlig verloren, wenn sie gebeten würden, eine zu schreiben. Wenn Sie also jemandem die Verwendung von Generika aufzwingen, der Ihre Schnittstelle implementieren oder Ihre Basisimplementierungen erweitern möchte, können Sie die Leute abschrecken.

Sie können jedoch versuchen, sowohl generische als auch nicht generische Versionen Ihrer Schnittstellen und Ihrer Basisimplementierungen bereitzustellen, damit Menschen, die keine Generika verwenden, glücklich in ihrer engen, typlastigen Welt leben können, während Menschen, die Generika können die Vorteile ihres breiteren Sprachverständnisses nutzen.


3

Jetzt, Itemund ItemStackFactoryich kann absolut voraussehen, dass Dritte es in Zukunft erweitern müssen.

Da ist deine Entscheidung für dich getroffen. Wenn Sie keine Generika bereitstellen, müssen diese bei Itemjeder Verwendung auf die Implementierung aktualisiert werden:

class MyItem implements Item {
}
MyItem item = new MyItem();
ItemStack is = createItemStack(item, 10);
// They would have to cast here.
MyItem theItem = (MyItem)is.getItem();

Sie sollten zumindest ItemStackgenerisch machen, wie Sie es vorschlagen. Der tiefste Vorteil von Generika ist, dass Sie fast nie etwas umsetzen müssen, und das Anbieten von Code, der Benutzer zum Umsetzen zwingt, ist meiner Meinung nach eine Straftat.


2

Wow ... ich sehe das ganz anders.

Ich kann absolut voraussehen, dass Dritte etwas brauchen

Das ist ein Fehler. Sie sollten keinen Code "für die Zukunft" schreiben.

Außerdem werden Schnittstellen von oben nach unten geschrieben. Sie sehen sich keine Klasse an und sagen: "Hey, diese Klasse könnte eine Schnittstelle vollständig implementieren, und dann kann ich diese Schnittstelle auch woanders verwenden." Das ist ein falscher Ansatz, da nur die Klasse, die eine Schnittstelle verwendet, wirklich weiß, wie die Schnittstelle aussehen soll. Stattdessen sollten Sie denken: "An dieser Stelle benötigt meine Klasse eine Abhängigkeit. Lassen Sie mich eine Schnittstelle für diese Abhängigkeit definieren. Wenn ich mit dem Schreiben dieser Klasse fertig bin, schreibe ich eine Implementierung dieser Abhängigkeit." Ob jemand Ihre Benutzeroberfläche jemals wiederverwenden und Ihre Standardimplementierung durch eine eigene ersetzen wird, ist völlig irrelevant. Sie können nie tun. Dies bedeutet nicht, dass die Schnittstelle unbrauchbar war. Dadurch folgt Ihr Code dem Inversion of Control-Prinzip, wodurch Ihr Code an vielen Stellen sehr erweiterbar ist. "


0

Ich denke, dass die Verwendung von Generika in Ihrer Situation zu komplex ist.

Wenn Sie eine generische Version von Schnittstellen auswählen, zeigt das Design dies an

  1. ItemStackContainer <A> enthält einen einzelnen Stapeltyp, A. (dh ItemStackContainer kann nicht mehrere Stapeltypen enthalten.)
  2. ItemStack ist einem bestimmten Elementtyp zugeordnet. Es gibt also keinen Abstraktionstyp für alle Arten von Stapeln.
  3. Sie müssen für jeden Elementtyp eine bestimmte ItemStackFactory definieren. Es wird als Fabrikmuster als zu fein angesehen.
  4. Schreiben Sie schwierigeren Code wie ItemStack <? erweitert Item> und verringert die Wartbarkeit.

Diese impliziten Annahmen und Einschränkungen sind sehr wichtige Dinge, die sorgfältig entschieden werden müssen. Daher sollten diese vorzeitigen Entwürfe insbesondere im Frühstadium nicht eingeführt werden. Einfaches, leicht verständliches, gemeinsames Design ist erwünscht.


0

Ich würde sagen, es hängt hauptsächlich von dieser Frage ab: Wenn jemand die Funktionalität von Itemund erweitern muss ItemStackFactory,

  • Werden sie nur Methoden überschreiben und Instanzen ihrer Unterklassen genauso wie die Basisklassen verwendet werden, sich nur anders verhalten ?
  • oder werden sie Mitglieder hinzufügen und verwenden die Unterklassen unterschiedlich?

Im ersten Fall sind keine Generika erforderlich, im zweiten Fall wahrscheinlich.


0

Möglicherweise können Sie eine Schnittstellenhierarchie haben, in der Foo eine Schnittstelle und Bar eine andere ist. Wann immer ein Typ beides sein muss, ist es eine FooAndBar.

Um ein Überschneiden zu vermeiden, erstellen Sie den FooAndBar-Typ nur, wenn dies erforderlich ist, und führen Sie eine Liste der Schnittstellen, die niemals etwas erweitern.

Dies ist für die meisten Programmierer viel komfortabler (die meisten mögen ihre Typprüfung oder möchten sich niemals mit statischer Typisierung jeglicher Art befassen). Sehr wenige Leute haben ihre eigene Generika-Klasse geschrieben.

Ebenfalls als Hinweis: Bei Schnittstellen geht es darum, welche möglichen Aktionen ausgeführt werden können. Sie sollten eigentlich Verben sein, keine Substantive.


Dies könnte eine Alternative zur Verwendung von Generika sein, aber in der ursprünglichen Frage wurde gefragt, wann Generika anstelle der von Ihnen vorgeschlagenen verwendet werden sollen. Können Sie Ihre Antwort verbessern, um darauf hinzuweisen, dass Generika niemals erforderlich sind, aber die Verwendung mehrerer Schnittstellen die Antwort auf alle ist?
Jay Elston
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.