Interfaccia vuota per combinare più interfacce


20

Supponiamo di avere due interfacce:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

In alcuni casi gli oggetti di implementazione possono supportare solo uno di questi, ma in molti casi le implementazioni supporteranno entrambe le interfacce. Le persone che usano le interfacce dovranno fare qualcosa del tipo:

// 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();

Nessuna di queste opzioni è estremamente conveniente, tanto più se si considera che si desidera questo come parametro del metodo.

Una soluzione sarebbe dichiarare un'interfaccia di wrapping:

interface TheWholeShabam extends Readable, Writable {}

Ma questo ha un problema specifico: tutte le implementazioni che supportano sia Readable che Writable devono implementare TheWholeShabam se vogliono essere compatibili con le persone che usano l'interfaccia. Anche se non offre nulla a parte la presenza garantita di entrambe le interfacce.

Esiste una soluzione pulita a questo problema o devo scegliere l'interfaccia del wrapper?

AGGIORNARE

In effetti è spesso necessario avere un oggetto che sia sia leggibile che scrivibile, quindi semplicemente separare le preoccupazioni negli argomenti non è sempre una soluzione pulita.

UPDATE2

(estratto come risposta, quindi è più facile commentare)

Update3

Si noti che il caso d'uso principale per questo non è flussi (anche se devono essere supportati anche loro). Gli stream fanno una distinzione molto specifica tra input e output e c'è una chiara separazione delle responsabilità. Piuttosto, pensa a qualcosa come un bytebuffer in cui hai bisogno di un oggetto su cui puoi scrivere e leggere, un oggetto a cui sia associato uno stato molto specifico. Questi oggetti esistono perché sono molto utili per alcune cose come I / O asincrono, codifiche, ...

Update4

Una delle prime cose che ho provato è stata la stessa del suggerimento riportato di seguito (controlla la risposta accettata) ma si è rivelata troppo fragile.

Supponiamo di avere una classe che deve restituire il tipo:

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

Se si chiama questo metodo, l'RW generico è determinato dalla variabile che riceve l'oggetto, quindi è necessario un modo per descrivere questo var.

MyObject myObject = someInstance.getItAll();

Ciò funzionerebbe, ma ancora una volta lo lega a un'implementazione e può effettivamente lanciare classcastexceptions in fase di esecuzione (a seconda di cosa viene restituito).

Inoltre, se si desidera una variabile di classe del tipo RW, è necessario definire il generico a livello di classe.


5
La frase è "shebang intero"
kevin cline il

Questa è una buona domanda, ma penso che usare Readable e 'Writable' come interfacce di esempio stia confondendo un po 'le acque dato che di solito hanno ruoli diversi ...
vaughandroid

@Basueta Mentre la denominazione è stata semplificata, leggibile e scrivibile in realtà trasmettono abbastanza bene il mio caso d'uso. In alcuni casi si desidera solo lettura, in alcuni casi solo scrittura e in una quantità sorprendente di casi lettura e scrittura.
nablex,

Non riesco a pensare a un momento in cui ho avuto bisogno di un singolo stream che sia sia leggibile che scrivibile, e a giudicare dalle risposte / commenti degli altri non penso di essere l'unico. Sto solo dicendo che potrebbe essere più utile scegliere una coppia di interfacce meno controversa ...
Vaughandroid,

@Baqueta Qualcosa a che fare con i pacchetti java.nio *? Se ti attieni ai flussi, il caso d'uso è effettivamente limitato ai luoghi in cui utilizzeresti ByteArray * Stream.
nablex,

Risposte:


19

Sì, puoi dichiarare il parametro del metodo come un tipo non specificato che estende sia Readablee Writable:

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

La dichiarazione del metodo sembra terribile, ma usarla è più semplice che dover conoscere l'interfaccia unificata.


2
Preferirei di gran lunga il secondo approccio di Konrad qui: process(Readable readThing, Writable writeThing)e se devi invocarlo usando process(foo, foo).
Joachim Sauer,

1
La sintassi non è corretta <RW extends Readable&Writable>?
VIENI DAL

1
@JoachimSauer: Perché preferiresti un approccio che si rompe facilmente su uno che è semplicemente brutto visivamente? Se chiamo processo (foo, bar) e foo e bar sono diversi, il metodo potrebbe non riuscire.
Michael Shaw,

@MichaelShaw: quello che sto dicendo è che non dovrebbe fallire quando sono oggetti diversi. Perché dovrebbe? In tal caso, direi che processfa più cose diverse e viola il principio della responsabilità singola.
Joachim Sauer,

@JoachimSauer: Perché non dovrebbe fallire? per (i = 0; j <100; i ++) non è utile un ciclo come per (i = 0; i <100; i ++). Il ciclo for legge e scrive nella stessa variabile e non viola SRP.
Michael Shaw,

12

Se esiste un luogo in cui è necessario myObjectsia come a Readableche Writableè possibile:

  • Refactor quel posto? Leggere e scrivere sono due cose diverse. Se un metodo fa entrambe le cose, forse non segue il principio della singola responsabilità.

  • Passa myObjectdue volte, come a Readablee come Writable(due argomenti). Cosa importa al metodo se è o non è lo stesso oggetto?


1
Questo potrebbe funzionare quando lo usi come argomento e sono preoccupazioni separate. Tuttavia a volte vuoi davvero un oggetto che sia leggibile e scrivibile allo stesso tempo (per lo stesso motivo per cui vuoi usare ByteArrayOutputStream per esempio)
nablex

Come mai? I flussi di output scrivono, come indica il nome: sono i flussi di input che possono leggere. Lo stesso in C # - c'è un StreamWritervs. StreamReader(e molti altri)
Konrad Morawski il

Si scrive su un ByteArrayOutputStream per raggiungere i byte (toByteArray ()). Ciò equivale a scrivere + leggere. La funzionalità effettiva dietro le interfacce è molto simile ma in un modo più generico. A volte vorrai solo leggere o scrivere, a volte vorrai entrambi. Un altro esempio è ByteBuffer.
nablex,

2
A quel secondo punto indietreggiai un po ', ma dopo un momento di riflessione non sembra proprio una cattiva idea. Non solo stai separando la lettura e la scrittura, stai facendo una funzione più flessibile e riducendo la quantità di stato di input che muta.
Phoshi,

2
@Phoshi Il problema è che le preoccupazioni non sono sempre separate. A volte vuoi un oggetto in grado sia di leggere che di scrivere e vuoi la garanzia che sia lo stesso oggetto (es. ByteArrayOutputStream, ByteBuffer, ...)
nablex,

4

Nessuna delle risposte attualmente risolve la situazione quando non hai bisogno di una lettura o di una scrittura ma entrambe . Hai bisogno di garanzie che quando scrivi su A, puoi leggere quei dati indietro da A, non scrivere su A e leggere da B e spero solo che siano effettivamente lo stesso oggetto. I casi d'uso sono numerosi, ad esempio ovunque si utilizzi un ByteBuffer.

Ad ogni modo, ho quasi finito il modulo su cui sto lavorando e attualmente ho optato per l'interfaccia wrapper:

interface Container extends Readable, Writable {}

Ora puoi almeno fare:

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

Le mie implementazioni di container (attualmente 3) implementano tutte Container rispetto alle interfacce separate, ma se qualcuno dovesse dimenticarlo in un'implementazione, IOUtils fornisce un metodo di utilità:

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

So che questa non è la soluzione ottimale, ma al momento è ancora la migliore via d'uscita perché Container è ancora un caso d'uso abbastanza grande.


1
Penso che sia assolutamente perfetto. Meglio di alcuni degli altri approcci proposti in altre risposte.
Tom Anderson,

0

Data un'istanza MyObject generica, dovrai sempre sapere se supporta le letture o le scritture. Quindi avresti un codice come:

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

Nel caso semplice, non penso che tu possa migliorare su questo. Ma se readThisReadablevuole scrivere il Leggibile su un altro file dopo averlo letto, diventa imbarazzante.

Quindi probabilmente andrei con questo:

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

Prendendo questo come parametro, readThisReadableora readThisWholeShabam, è possibile gestire qualsiasi classe che implementa TheWholeShabam, non solo MyObject. E può scriverlo se è scrivibile e non scriverlo se non lo è. (Abbiamo un vero "polimorfismo".)

Quindi il primo set di codice diventa:

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

E puoi salvare una riga qui avendo readThisWholeShebam () fare il controllo di leggibilità.

Ciò significa che il nostro precedente solo leggibile deve implementare isWriteable () (restituendo false ) e write () (non fare nulla), ma ora può andare in tutti i tipi di posti in cui non poteva andare prima e tutto il codice che gestisce TheWholeShabam gli oggetti lo gestiranno senza ulteriori sforzi da parte nostra.

Un'altra cosa: se riesci a gestire una chiamata a read () in una classe che non legge e una chiamata a write () in una classe che non scrive senza cestinare qualcosa, puoi saltare isReadable () e isWriteable () metodi. Questo sarebbe il modo più elegante per gestirlo - se funziona.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.