¿Tiene sentido siempre "programar en una interfaz" en Java?

9

He visto la discusión en esta pregunta sobre cómo se instanciaría una clase que se implementa desde una interfaz. En mi caso, estoy escribiendo un programa muy pequeño en Java que usa una instancia de TreeMap, y de acuerdo con la opinión de todos allí, debería ser instanciado como:

Map<X> map = new TreeMap<X>();

En mi programa, llamo a la función map.pollFirstEntry(), que no se declara en la Mapinterfaz (y a un par de otras personas que también están presentes en la Mapinterfaz). Me las arreglé para hacer esto enviando a un sitio TreeMap<X>donde llamo este método como:

someEntry = ((TreeMap<X>) map).pollFirstEntry();

Entiendo las ventajas de las pautas de inicialización descritas anteriormente para programas grandes, sin embargo, para un programa muy pequeño donde este objeto no se pasaría a otros métodos, creo que es innecesario. Aún así, estoy escribiendo este código de muestra como parte de una solicitud de empleo, y no quiero que mi código se vea mal ni esté desordenado. ¿Cuál sería la solución más elegante?

EDITAR: Me gustaría señalar que estoy más interesado en las buenas prácticas generales de codificación en lugar de la aplicación de la función específica TreeMap. Como algunas de las respuestas ya han señalado (y he marcado como contestado el primero en hacerlo), se debe usar el nivel de abstracción más alto posible, sin perder la funcionalidad.

jimijazz
fuente
1
Use un TreeMap cuando necesite las funciones. Esa fue probablemente una elección de diseño por una razón específica, por lo que también debería ser parte de la implementación.
@JordanReiter ¿Debo agregar una nueva pregunta con el mismo contenido o hay un mecanismo interno de publicación cruzada?
jimijazz
2
"Entiendo las ventajas de las pautas de inicialización descritas anteriormente para programas grandes" Tener que lanzar por todas partes no es ventajoso sin importar el tamaño del programa
Ben Aaronson

Respuestas:

23

"Programar en una interfaz" no significa "usar la versión más abstracta posible". En ese caso, todos lo usarían Object.

Lo que significa es que debe definir su programa contra la abstracción más baja posible sin perder funcionalidad . Si necesita una TreeMap, deberá definir un contrato con a TreeMap.

Jeroen Vannevel
fuente
2
TreeMap no es una interfaz, es una clase de implementación. Implementa las interfaces Map, SortedMap y NavigableMap. El método descrito es parte de la interfaz NavigableMap . El uso de TreeMap evitaría que la implementación cambie a un ConcurrentSkipListMap (por ejemplo), que es el punto completo de la codificación de una interfaz en lugar de la implementación.
3
@MichaelT: No miré la abstracción exacta que él necesita en este escenario específico, así que lo usé TreeMapcomo ejemplo. "Programa para una interfaz" no debe tomarse literalmente como una interfaz o una clase abstracta: una implementación también puede considerarse una interfaz.
Jeroen Vannevel
1
Aunque la interfaz / métodos públicos de una clase de implementación es técnicamente una 'interfaz', rompe el concepto detrás de LSP y evita la sustitución de una subclase diferente, razón por la cual desea programar en public interfacelugar de 'los métodos públicos de una implementación' .
@JeroenVannevel Estoy de acuerdo en que la programación de una interfaz se puede hacer cuando la interfaz está realmente representada por una clase. Sin embargo, no veo qué beneficio TreeMaphabría tenido SortedMapoNavigableMap
toniedzwiedz
16

Si todavía quieres usar una interfaz, puedes usar

NavigableMap <X, Y> map = new TreeMap<X, Y>();

no es necesario usar siempre una interfaz, pero a menudo hay un punto en el que desea tener una vista más general que le permita reemplazar la implementación (quizás para probar) y esto es fácil si todas las referencias al objeto se abstraen como Tipo de interfaz.


fuente
3

El punto detrás de la codificación de una interfaz en lugar de una implementación es evitar la filtración de detalles de implementación que de otro modo limitarían su programa.

Considere la situación en la que la versión original de su código usó HashMapay lo expuso.

private HashMap foo = new HashMap();
public HashMap getFoo() { return foo; }  // This is bad, don't do this.

Esto significa que cualquier cambio getFoo()es un cambio de API de última hora y haría que las personas que lo usan sean infelices. Si todo lo que está garantizando es que fooes un Mapa, debe devolverlo en su lugar.

private Map foo = new HashMap();
public Map getFoo() { return foo; }

Esto le brinda la flexibilidad de cambiar cómo funcionan las cosas dentro de su código. Te das cuenta de que en foorealidad debe ser un Mapa que devuelva las cosas en un orden particular.

private NavigableMap foo = new TreeMap();
public Map getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Y eso no rompe nada para el resto del código.

Más tarde, puede fortalecer el contrato que da la clase sin romper nada tampoco.

private NavigableMap foo = new TreeMap();
public NavigableMap getFoo() { return foo; }
private void doBar() { ... foo.lastEntry(); ... }

Esto profundiza en el principio de sustitución de Liskov

La sustituibilidad es un principio en la programación orientada a objetos. Establece que, en un programa de computadora, si S es un subtipo de T, entonces los objetos de tipo T pueden reemplazarse por objetos de tipo S (es decir, los objetos de tipo S pueden sustituir a objetos de tipo T) sin alterar ninguno de los deseados propiedades de ese programa (corrección, tarea realizada, etc.).

Dado que NavigableMap es un subtipo de Mapa, esta sustitución se puede hacer sin alterar el programa.

Exponer los tipos de implementación hace que sea difícil cambiar la forma en que el programa funciona internamente cuando se necesita hacer un cambio. Este es un proceso doloroso y muchas veces crea soluciones desagradables que solo sirven para infligir más dolor al codificador más tarde (te estoy mirando a un codificador anterior que seguía barajando datos entre un LinkedHashMap y un TreeMap por alguna razón - confía en mí, siempre que yo veo tu nombre en svn la culpa me preocupa).

Se podría todavía quieren evitar fugas tipos de implementación. Por ejemplo, es posible que desee implementar ConcurrentSkipListMap en su lugar debido a algunas características de rendimiento o simplemente en java.util.concurrent.ConcurrentSkipListMaplugar de java.util.TreeMapen las declaraciones de importación o lo que sea.


fuente
1

De acuerdo con las otras respuestas, debe usar la clase (o interfaz) más genérica de la que realmente necesita algo, en este caso TreeMap (o como alguien sugirió NavigableMap). Pero me gustaría agregar que esto es ciertamente mejor que lanzarlo a todas partes, lo que sería un olor mucho más grande en cualquier caso. Consulte /programming/4167304/why-should-casting-be-avoided por algunos motivos.

Sebastiaan van den Broek
fuente
1

Se trata de comunicar su intención de cómo se debe usar el objeto. Por ejemplo, si su método espera un Mapobjeto con un orden de iteración predecible :

private Map<String, String> processOrderedMap(LinkedHashMap<String, String> input) {
    // ...
}

Luego, si absolutamente necesita decirles a las personas que llaman sobre el método anterior que también devuelve un Mapobjeto con un orden de iteración predecible , porque existe tal expectativa por alguna razón:

private LinkedHashMap<String,String> processOrderedMap(LinkedHashMap<String,String> input) {
    // ...
}

Por supuesto, las personas que llaman aún pueden tratar el objeto de retorno Mapcomo tal, pero eso está más allá del alcance de su método:

private Map<String, String> output = processOrderedMap(input);

Dando un paso atrás

El consejo general de codificación a una interfaz es (generalmente) aplicable, porque generalmente es la interfaz la que proporciona la garantía de lo que el objeto debería poder realizar, también conocido como el contrato . Muchos principiantes comienzan HashMap<K, V> map = new HashMap<>()y se les aconseja declarar eso como a Map, porque a HashMapno ofrece nada más de lo que Mapse supone que debe hacer. A partir de esto, podrán comprender (con suerte) por qué sus métodos deberían tomar en Maplugar de a HashMap, y esto les permite darse cuenta de la característica de herencia en OOP.

Para citar solo una línea de la entrada de Wikipedia del principio favorito de todos relacionado con este tema:

Es una relación semántica más que simplemente sintáctica porque tiene la intención de garantizar la interoperabilidad semántica de los tipos en una jerarquía ...

En otras palabras, usar una Mapdeclaración no es porque tenga sentido 'sintácticamente', sino que las invocaciones sobre el objeto solo deberían importarle que sea un tipo de Map.

Código más limpio

Encuentro que esto me permite escribir código más limpio a veces también, especialmente cuando se trata de pruebas unitarias. Crear una HashMapentrada de prueba de solo lectura requiere más de una línea (excluyendo el uso de la inicialización de doble llave), cuando puedo reemplazarla fácilmente con Collections.singletonMap().

hjk
fuente