Leere Schnittstelle, um mehrere Schnittstellen zu kombinieren

20

Angenommen, Sie haben zwei Schnittstellen:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

In einigen Fällen können die implementierenden Objekte nur eines dieser Objekte unterstützen, in vielen Fällen unterstützen die Implementierungen jedoch beide Schnittstellen. Die Leute, die die Schnittstellen benutzen, müssen etwas tun wie:

// can't write to it without explicit casting
Readable myObject = new MyObject();

// can't read from it without explicit casting
Writable myObject = new MyObject();

// tight coupling to actual implementation
MyObject myObject = new MyObject();

Keine dieser Optionen ist besonders praktisch, insbesondere wenn Sie dies als Methodenparameter verwenden möchten.

Eine Lösung wäre, eine Wrapping-Schnittstelle zu deklarieren:

interface TheWholeShabam extends Readable, Writable {}

Dies hat jedoch ein spezifisches Problem: Alle Implementierungen, die Readable und Writable unterstützen, müssen TheWholeShabam implementieren, wenn sie mit Benutzern der Schnittstelle kompatibel sein sollen. Auch wenn es nichts anderes bietet als das garantierte Vorhandensein beider Schnittstellen.

Gibt es eine saubere Lösung für dieses Problem oder sollte ich mich für die Wrapper-Schnittstelle entscheiden?

AKTUALISIEREN

Es ist in der Tat oftmals erforderlich, ein Objekt zu haben, das sowohl lesbar als auch beschreibbar ist, so dass eine einfache Trennung der Bedenken in den Argumenten nicht immer eine saubere Lösung ist.

UPDATE2

(Als Antwort extrahiert, damit es einfacher ist, Kommentare abzugeben.)

UPDATE3

Bitte beachten Sie, dass der primäre Verwendungszweck hierfür nicht Streams sind (obwohl diese ebenfalls unterstützt werden müssen). Streams unterscheiden sehr genau zwischen Input und Output und es gibt eine klare Trennung der Verantwortlichkeiten. Stellen Sie sich stattdessen so etwas wie einen Bytepuffer vor, in dem Sie ein Objekt benötigen, in das Sie schreiben und aus dem Sie lesen können, ein Objekt, dem ein ganz bestimmter Status zugeordnet ist. Diese Objekte existieren, weil sie für einige Dinge wie asynchrone E / A, Codierungen usw. sehr nützlich sind.

UPDATE4

Eines der ersten Dinge, die ich versuchte, war das gleiche wie der unten angegebene Vorschlag (überprüfen Sie die akzeptierte Antwort), aber es erwies sich als zu zerbrechlich.

Angenommen, Sie haben eine Klasse, die den Typ zurückgeben muss:

public <RW extends Readable & Writable> RW getItAll();

Wenn Sie diese Methode aufrufen, wird die generische RW durch die Variable bestimmt, die das Objekt empfängt. Sie benötigen daher eine Möglichkeit, diese Variable zu beschreiben.

MyObject myObject = someInstance.getItAll();

Dies funktioniert, bindet es jedoch erneut an eine Implementierung und kann zur Laufzeit tatsächlich Ausnahmen für Klassencasts auslösen (je nachdem, was zurückgegeben wird).

Wenn Sie außerdem eine Klassenvariable vom Typ RW möchten, müssen Sie die generische Variable auf Klassenebene definieren.

nablex
quelle
5
Die Phrase ist "ganze shebang"
Kevin Cline
Dies ist eine gute Frage, aber ich denke, dass die Verwendung von Readable und 'Writable' als Beispielschnittstellen das Wasser etwas trübt, da es sich normalerweise um unterschiedliche Rollen handelt ...
vaughandroid
@Basueta Während die Benennung vereinfacht wurde, vermitteln lesbar und beschreibbar tatsächlich meinen Verwendungszweck ziemlich gut. In einigen Fällen möchten Sie nur lesen, in einigen Fällen nur schreiben und in überraschend vielen Fällen lesen und schreiben.
Nablex
Ich kann mir keine Zeit vorstellen, in der ich einen einzigen Stream brauchte, der sowohl lesbar als auch beschreibbar ist, und nach den Antworten / Kommentaren anderer Leute zu urteilen, denke ich nicht, dass ich der einzige bin. Ich sage nur, es könnte hilfreicher sein, ein weniger kontroverses Paar von Schnittstellen zu wählen ...
vaughandroid
@Baqueta Hast du etwas mit java.nio * -Paketen zu tun? Wenn Sie sich an Streams halten, ist der Verwendungszweck in der Tat auf Orte beschränkt, an denen Sie ByteArray * Stream verwenden würden.
Nablex

Antworten:

19

Ja, Sie können Ihren Methodenparameter als einen unterbestimmten Typ deklarieren, der sowohl Folgendes umfasst Readableals auch Writable:

public <RW extends Readable & Writable> void process(RW thing);

Die Methodendeklaration sieht schrecklich aus, aber die Verwendung ist einfacher, als über die einheitliche Schnittstelle Bescheid zu wissen.

Kilian Foth
quelle
2
Ich würde Konrads zweiten Ansatz hier sehr bevorzugen: process(Readable readThing, Writable writeThing)und wenn Sie ihn mit aufrufen müssen process(foo, foo).
Joachim Sauer
1
Stimmt die Syntax nicht <RW extends Readable&Writable>?
KOMMEN SIE VOM
1
@JoachimSauer: Warum bevorzugst du einen Ansatz, der sich leicht von einem abhebt, der nur optisch hässlich ist? Wenn ich process (foo, bar) aufrufe und foo und bar unterschiedlich sind, schlägt die Methode möglicherweise fehl.
Michael Shaw
@MichaelShaw: Was ich sage ist, dass es nicht scheitern sollte, wenn es sich um verschiedene Objekte handelt. Warum sollte es Wenn dies der Fall ist, würde ich argumentieren, dass processdies mehrere verschiedene Dinge tut und gegen das Prinzip der einfachen Verantwortung verstößt.
Joachim Sauer
@ JoachimSauer: Warum sollte es nicht scheitern? für (i = 0; j <100; i ++) ist eine Schleife nicht so nützlich wie für (i = 0; i <100; i ++). Die for-Schleife liest und schreibt in dieselbe Variable und verstößt dabei nicht gegen die SRP.
Michael Shaw
12

Wenn es einen Ort , wo Sie brauchen , myObjectda beide ein Readableund WritableSie können:

  • Diesen Ort umgestalten? Lesen und Schreiben sind zwei verschiedene Dinge. Wenn eine Methode beides tut, folgt sie möglicherweise nicht dem Prinzip der Einzelverantwortung.

  • Übergeben Sie myObjectzweimal als Readableund als Writable(zwei Argumente). Was kümmert es die Methode, ob es sich um dasselbe Objekt handelt oder nicht?

Konrad Morawski
quelle
1
Dies könnte funktionieren, wenn Sie es als Argument verwenden und es sich um separate Probleme handelt. Manchmal möchten Sie jedoch wirklich ein Objekt, das gleichzeitig lesbar und beschreibbar ist (aus dem gleichen Grund, aus dem Sie beispielsweise
ByteArrayOutputStream
Woher? Ausgabestreams schreiben, wie der Name schon sagt - es sind die Eingabestreams, die gelesen werden können. Gleiches in C # - es gibt einen StreamWritervs. StreamReader(und viele andere)
Konrad Morawski
Sie schreiben in einen ByteArrayOutputStream, um die Bytes zu ermitteln (toByteArray ()). Dies entspricht Schreiben + Lesen. Die eigentliche Funktionalität hinter den Schnittstellen ist ähnlich, jedoch allgemeiner. Manchmal möchten Sie nur lesen oder schreiben, manchmal möchten Sie beides. Ein weiteres Beispiel ist ByteBuffer.
Nablex
2
An diesem zweiten Punkt zuckte ich ein wenig zurück, aber nach einem Moment des Nachdenkens scheint das keine schlechte Idee zu sein. Sie trennen nicht nur das Lesen und Schreiben, Sie machen eine flexiblere Funktion und reduzieren die Menge an Eingabezuständen, die es mutiert.
Phoshi
2
@Phoshi Das Problem ist, dass die Anliegen nicht immer getrennt sind. Manchmal möchten Sie ein Objekt, das sowohl lesen als auch schreiben kann, und Sie möchten die Garantie, dass es dasselbe Objekt ist (z. B. ByteArrayOutputStream, ByteBuffer, ...)
nablex
4

Keine der Antworten behandelt derzeit die Situation, in der Sie keine lesbaren oder beschreibbaren, sondern beide benötigen . Sie benötigen Garantien, dass Sie beim Schreiben nach A diese Daten von A zurücklesen können, nicht nach A und von B lesen und nur hoffen, dass sie tatsächlich dasselbe Objekt sind. Anwendungsfälle sind reichlich vorhanden, beispielsweise überall dort, wo Sie einen ByteBuffer verwenden würden.

Wie auch immer, ich bin fast fertig mit dem Modul, an dem ich arbeite, und habe mich derzeit für die Wrapper-Oberfläche entschieden:

interface Container extends Readable, Writable {}

Jetzt können Sie zumindest Folgendes tun:

Container container = IOUtils.newContainer();
container.write("something".getBytes());
System.out.println(IOUtils.toString(container));

Meine eigenen Implementierungen von container (derzeit 3) implementieren alle Container im Gegensatz zu den separaten Schnittstellen. Sollte dies jedoch jemand in einer Implementierung vergessen, stellt IOUtils eine Dienstprogrammmethode bereit:

Readable myReadable = ...;
// assuming myReadable is also Writable you can do this:
Container container = IOUtils.toByteContainer(myReadable);

Ich weiß, dass dies nicht die optimale Lösung ist, aber es ist im Moment immer noch der beste Ausweg, da Container immer noch ein ziemlich großer Anwendungsfall ist.

nablex
quelle
1
Ich finde das absolut in Ordnung. Besser als einige der anderen in anderen Antworten vorgeschlagenen Ansätze.
Tom Anderson
0

Bei einer generischen MyObject-Instanz müssen Sie immer wissen, ob sie Lese- oder Schreibvorgänge unterstützt. Sie hätten also Code wie:

if (myObject instanceof Readable)  {
    Readable  r = (Readable) myObject;
    readThisReadable( r );
}

Im einfachen Fall glaube ich nicht, dass Sie dies verbessern können. Aber wenn readThisReadablewill schreiben die Lesbar auf eine andere Datei , nachdem sie lesen, wird es peinlich.

Also würde ich wahrscheinlich so weitermachen:

interface TheWholeShabam  {
    public boolean  isReadable();
    public boolean  isWriteable();
    public void     read();
    public void     write();
}

Wird dies als Parameter verwendet, readThisReadablekann jetzt readThisWholeShabamjede Klasse, die TheWholeShabam implementiert, verarbeitet werden, nicht nur MyObject. Und es kann es schreiben, wenn es beschreibbar ist und nicht, wenn es nicht beschreibbar ist. (Wir haben echten "Polymorphismus".)

Der erste Satz Code wird also:

TheWholeShabam  myObject = ...;
if (myObject.isReadable()
    readThisWholeShebam( myObject );

Und Sie können hier eine Zeile speichern, indem Sie readThisWholeShebam () die Lesbarkeitsprüfung durchführen lassen.

Dies bedeutet, dass unser früherer Readable-only-Server isWriteable () implementieren muss ( false zurückgeben ) und write () (nichts tun), aber jetzt kann er alle Arten von Stellen durchlaufen, die vorher nicht möglich waren, und den gesamten Code, der TheWholeShabam verarbeitet die objekte werden sich ohne weiteren aufwand unserer seite darum kümmern.

Eine andere Sache: Wenn Sie mit einem Aufruf von read () in einer Klasse, die nicht liest, und einem Aufruf von write () in einer Klasse, die nicht schreibt, ohne etwas in den Papierkorb zu werfen, fertig sind, können Sie isReadable () und isWriteable überspringen () Methoden. Dies wäre die eleganteste Art, damit umzugehen - wenn es funktioniert.

RalphChapin
quelle