Interfaz vacía para combinar múltiples interfaces.

20

Supongamos que tiene dos interfaces:

interface Readable {
    public void read();
}

interface Writable {
    public void write();
}

En algunos casos, los objetos de implementación solo pueden soportar uno de estos, pero en muchos casos las implementaciones admitirán ambas interfaces. Las personas que usan las interfaces tendrán que hacer algo como:

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

Ninguna de estas opciones es terriblemente conveniente, aún más cuando se considera que desea esto como un parámetro de método.

Una solución sería declarar una interfaz de ajuste:

interface TheWholeShabam extends Readable, Writable {}

Pero esto tiene un problema específico: todas las implementaciones que admiten tanto Readable como Writable tienen que implementar TheWholeShabam si quieren ser compatibles con las personas que usan la interfaz. Aunque no ofrece nada aparte de la presencia garantizada de ambas interfaces.

¿Existe una solución limpia para este problema o debería elegir la interfaz de contenedor?

ACTUALIZAR

De hecho, a menudo es necesario tener un objeto que sea legible y escribible, por lo que simplemente separar las preocupaciones en los argumentos no siempre es una solución limpia.

ACTUALIZACIÓN2

(extraído como respuesta para que sea más fácil comentarlo)

ACTUALIZACIÓN3

Tenga en cuenta que el caso de uso principal para esto no son las transmisiones (aunque también deben ser compatibles). Las transmisiones hacen una distinción muy específica entre entrada y salida y existe una clara separación de responsabilidades. Por el contrario, piense en algo así como un bytebuffer donde necesita un objeto en el que pueda escribir y leer, un objeto que tiene un estado muy específico adjunto. Estos objetos existen porque son muy útiles para algunas cosas como E / S asíncronas, codificaciones, ...

ACTUALIZACIÓN4

Una de las primeras cosas que probé fue la misma que la sugerencia dada a continuación (verifique la respuesta aceptada) pero resultó ser demasiado frágil.

Supongamos que tiene una clase que debe devolver el tipo:

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

Si llama a este método, el RW genérico está determinado por la variable que recibe el objeto, por lo que necesita una forma de describir esta var.

MyObject myObject = someInstance.getItAll();

Esto funcionaría, pero una vez más lo vincula a una implementación y, en realidad, puede arrojar ideas de clase en tiempo de ejecución (dependiendo de lo que se devuelva).

Además, si desea una variable de clase del tipo RW, debe definir el genérico a nivel de clase.

nablex
fuente
55
La frase es "todo shebang"
Kevin Cline
Esta es una buena pregunta, pero creo que usando legible y 'Escribir' como sus interfaces de ejemplo está enturbiando las aguas un poco, ya que son por lo general los diferentes papeles ...
vaughandroid
@Basueta Si bien la denominación se ha simplificado, la lectura y la escritura pueden transmitir mi caso de uso bastante bien. En algunos casos, solo desea leer, en algunos casos solo escribir y en una sorprendente cantidad de casos leer y escribir.
nablex
No puedo pensar en un momento en que haya necesitado una sola transmisión que sea legible y escribible, y a juzgar por las respuestas / comentarios de otras personas, no creo que sea el único. Solo digo que podría ser más útil elegir un par de interfaces menos controvertidas ...
vaughandroid
@Baqueta ¿Tiene algo que ver con los paquetes java.nio *? Si se atiene a las transmisiones, el caso de uso se limita a los lugares donde usaría ByteArray * Stream.
nablex

Respuestas:

19

Sí, se puede declarar el parámetro de método como un tipo subespecificada que se extiende tanto Readabley Writable:

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

La declaración del método parece terrible, pero usarla es más fácil que tener que saber acerca de la interfaz unificada.

Kilian Foth
fuente
2
Prefiero mucho el segundo enfoque de Konrads aquí: process(Readable readThing, Writable writeThing)y si debe invocarlo usando process(foo, foo).
Joachim Sauer el
1
¿No es la sintaxis correcta <RW extends Readable&Writable>?
VENIDO DEL
1
@JoachimSauer: ¿Por qué preferirías un enfoque que se rompa fácilmente sobre uno que sea simplemente feo visualmente? Si llamo proceso (foo, bar) y foo y bar son diferentes, el método podría fallar.
Michael Shaw
@MichaelShaw: lo que digo es que no debería fallar cuando son objetos diferentes. ¿Por qué debería hacerlo? Si lo hace, entonces diría que processhace varias cosas diferentes y viola el principio de responsabilidad única.
Joachim Sauer
@JoachimSauer: ¿Por qué no fallaría? for (i = 0; j <100; i ++) no es un bucle tan útil como for (i = 0; i <100; i ++). El bucle for lee y escribe en la misma variable, y no viola el SRP al hacerlo.
Michael Shaw
12

Si hay un lugar donde necesita myObjecttanto como a Readabley Writablepuede:

  • ¿Refactorizar ese lugar? Leer y escribir son dos cosas diferentes. Si un método hace ambas cosas, tal vez no siga el principio de responsabilidad única.

  • Pase myObjectdos veces, como ay Readablecomo a Writable(dos argumentos). ¿Qué le importa al método si es o no el mismo objeto?

Konrad Morawski
fuente
1
Esto podría funcionar cuando lo usa como argumento y son preocupaciones separadas. Sin embargo, a veces realmente desea un objeto que sea legible y escribible al mismo tiempo (por la misma razón que desea usar ByteArrayOutputStream, por ejemplo)
nablex
¿Cómo? Las secuencias de salida escriben, como su nombre lo indica, son las secuencias de entrada las que pueden leer. Lo mismo en C # - hay un StreamWritervs. StreamReader(y muchos otros)
Konrad Morawski
Usted escribe en un ByteArrayOutputStream para obtener los bytes (toByteArray ()). Esto es equivalente a escribir + leer. La funcionalidad real detrás de las interfaces es muy parecida pero de manera más genérica. A veces solo querrás leer o escribir, a veces querrás ambos. Otro ejemplo es ByteBuffer.
nablex
2
Retrocedí un poco en ese segundo punto, pero después de pensarlo un momento, realmente no parece una mala idea. No solo está separando la lectura y la escritura, está haciendo una función más flexible y reduciendo la cantidad de estado de entrada que muta.
Phoshi
2
@Phoshi El problema es que las preocupaciones no siempre están separadas. A veces, desea un objeto que pueda leer y escribir y desea la garantía de que sea el mismo objeto (por ejemplo, ByteArrayOutputStream, ByteBuffer, ...)
nablex
4

Ninguna de las respuestas actualmente aborda la situación cuando no necesita una lectura o escritura pero ambas . Necesita garantías de que al escribir en A, puede leer esos datos desde A, no escribir en A y leer desde B y solo esperar que sean realmente el mismo objeto. Los casos de uso son abundantes, por ejemplo, en todas partes usarías un ByteBuffer.

De todos modos, casi he terminado el módulo en el que estoy trabajando y actualmente he optado por la interfaz del contenedor:

interface Container extends Readable, Writable {}

Ahora al menos puedes hacer:

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

Mis propias implementaciones de contenedor (actualmente 3) implementan Container en lugar de las interfaces separadas, pero si alguien olvida esto en una implementación, IOUtils proporciona un método de utilidad:

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

Sé que esta no es la solución óptima, pero sigue siendo la mejor salida en este momento porque Container sigue siendo un caso de uso bastante grande.

nablex
fuente
1
Creo que esto está absolutamente bien. Mejor que algunos de los otros enfoques propuestos en otras respuestas.
Tom Anderson
0

Dada una instancia genérica de MyObject, siempre necesitará saber si admite lecturas o escrituras. Entonces tendrías un código como:

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

En el caso simple, no creo que pueda mejorar esto. Pero si readThisReadablequiere escribir el Leíble en otro archivo después de leerlo, se vuelve incómodo.

Entonces probablemente iría con esto:

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

Tomando esto como un parámetro, readThisReadableahora readThisWholeShabampuede manejar cualquier clase que implemente TheWholeShabam, no solo MyObject. Y puede escribirlo si se puede escribir y no escribirlo si no lo es. (Tenemos un verdadero "polimorfismo").

Entonces el primer conjunto de código se convierte en:

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

Y puede guardar una línea aquí haciendo que ReadThisWholeShebam () realice la verificación de legibilidad.

Esto significa que nuestro anterior solo legible tiene que implementar isWriteable () (devolviendo falso ) y escribir () (sin hacer nada), pero ahora puede ir a todo tipo de lugares a los que no podía ir antes y todo el código que maneja TheWholeShabam los objetos se ocuparán de ello sin ningún esfuerzo adicional de nuestra parte.

Otra cosa: si puede manejar una llamada a read () en una clase que no lee y una llamada a write () en una clase que no escribe sin destruir algo, puede omitir isReadable () y isWriteable () métodos. Esta sería la forma más elegante de manejarlo, si funciona.

RalphChapin
fuente