¿Es una buena práctica implementar dos métodos predeterminados de Java 8 en términos mutuos?

14

Estoy diseñando una interfaz con dos métodos relacionados, similares a este:

public interface ThingComputer {
    default Thing computeFirstThing() {
        return computeAllThings().get(0);
    }

    default List<Thing> computeAllThings() {
        return ImmutableList.of(computeFirstThing());
    }
}

Alrededor de la mitad de las implementaciones solo calcularán una cosa, mientras que la otra mitad puede calcular más.

¿Tiene esto algún precedente en el código Java 8 ampliamente utilizado? Sé que Haskell hace cosas similares en algunas clases de tipos ( Eqpor ejemplo).

Lo bueno es que tengo que escribir mucho menos código que si tuviera dos clases abstractas ( SingleThingComputery MultipleThingComputer).

La desventaja es que una implementación vacía se compila pero explota en tiempo de ejecución con a StackOverflowError. Es posible detectar la recursividad mutua con a ThreadLocaly dar un error más agradable, pero eso agrega una sobrecarga al código sin errores.

Tavian Barnes
fuente
3
¿Por qué escribirías código deliberadamente para garantizar una recursión infinita? Nada bueno puede venir de eso.
1
Bueno, no está "garantizado"; una implementación adecuada anularía uno de esos métodos.
Tavian Barnes
66
Tu no sabes eso. Una clase o interfaz debe mantenerse por sí misma y no asumir que una subclase hará las cosas de cierta manera para evitar errores lógicos importantes. De lo contrario, es frágil y no puede cumplir con sus garantías. Tenga en cuenta que no estoy diciendo que su interfaz pueda o deba imponer una clase de implementación que haga throw new Error();o algo estúpido, solo que la interfaz en sí no debería tener un contrato frágil a través de defaultmétodos.
Estoy de acuerdo con @Snowman, es una mina terrestre. Creo que es el "código malvado" del libro de texto en el sentido en que Jon Skeet quiere decir: tenga en cuenta que el mal no es lo mismo que malo , es malvado / inteligente / tentador, pero aún así está mal :) Recomiendo youtu.be/lGbQiguuUGc?t=15m26s ( o de hecho, toda la charla para un contexto más amplio, es muy interesante).
Konrad Morawski
1
El hecho de que cada función pueda definirse en términos de la otra no implica que ambas puedan definirse entre sí. Eso no vuela en ningún idioma. Desea una interfaz con 2 herederos abstractos, cada uno implementa uno en términos del otro pero abstrae al otro para que los implementadores elijan qué lado quieren implementar heredando de la clase abstracta correcta. Un lado debe definirse independientemente del otro, de lo contrario, pop irá a la pila . Tiene valores predeterminados locos, suponiendo que los implementadores resolverán el problema por usted, abstractexiste para obligarlos a resolverlo.
Jimmy Hoffa

Respuestas:

16

TL; DR: no hagas esto.

Lo que muestra aquí es código frágil.

Una interfaz es un contrato. Dice "independientemente del objeto que obtenga, puede hacer X e Y". Como está escrito, su interfaz hace ni X ni Y porque está garantizado para causar un desbordamiento de pila.

Lanzar un error o una subclase indica un error grave que no debe detectarse:

Un error es una subclase de Throwable que indica problemas serios que una aplicación razonable no debería intentar detectar.

Además, VirtualMachineError , la clase principal de StackOverflowError , dice esto:

Se arroja para indicar que la máquina virtual Java está rota o se ha quedado sin recursos necesarios para que continúe funcionando.

Su programa no debe preocuparse por los recursos de JVM . Ese es el trabajo de la JVM. Hacer un programa que causa un error de JVM como parte de la operación normal es malo. Garantiza que su programa se bloqueará o obliga a los usuarios de esta interfaz a atrapar errores que no deberían preocuparle.


Es posible que conozca a Eric Lippert por sus esfuerzos como "miembro del comité de diseño del lenguaje C #" emérito. Habla sobre las características del lenguaje que empujan a las personas hacia el éxito o el fracaso: aunque esta no es una característica del lenguaje o parte del diseño del lenguaje, su punto es igualmente válido cuando se trata de implementar interfaces o usar objetos.

¿Recuerdas en The Princess Bride cuando Westley se despierta y se encuentra encerrado en The Pit Of Despair con un ronco albino y el siniestro hombre de seis dedos, el conde Rugen? La idea principal de un pozo de desesperación es doble. Primero, que es un pozo y, por lo tanto, es fácil caer en él, pero es difícil salir del trabajo. Y segundo, que induce a la desesperación. De ahí el nombre.

Fuente: C ++ y el pozo de la desesperación

Tener una interfaz que arroja StackOverflowErrorpor defecto empuja a los desarrolladores al Pozo de la desesperación y es una mala idea . En cambio, empuje a los desarrolladores hacia el Foso del Éxito . Que sea fácil de usar interfaz de forma correcta y sin que se caiga la JVM.

Delegar entre los métodos está bien aquí. Sin embargo, la dependencia debería ir en un sentido. Me gusta pensar en la delegación de métodos como un gráfico dirigido . Cada método es un nodo en el gráfico. Cada vez que un método llama a otro método, dibuje una ventaja desde el método de llamada al método llamado.

Si dibuja un gráfico y nota que es cíclico, es un olor a código. Ese es un potencial para empujar a los desarrolladores al Pozo de la Desesperación. Tenga en cuenta que no se debe prohibir categóricamente, solo que se debe tener precaución . Los algoritmos recursivos específicamente tendrán ciclos en el gráfico de llamadas: eso está bien. Documente y advierta a los desarrolladores. Si no es recursivo, intente romper ese ciclo. Si no puede, averigüe qué entradas pueden causar un desbordamiento de la pila y mitíguelas o documente como último caso si nada más funcionará.

Comunidad
fuente
Gran respuesta. Tengo curiosidad por qué esta es una práctica tan común en Haskell entonces. Diferentes culturas de desarrollo, supongo.
Tavian Barnes
No tengo experiencia en Haskell y no puedo hablar si o por qué esto es más frecuente en la programación funcional.
Si lo analizamos un poco más, parece que la comunidad de Haskell tampoco está realmente contenta con eso. Además, el compilador aprendió recientemente a buscar "definiciones completas mínimas", por lo que una implementación vacía generaría al menos una advertencia.
Tavian Barnes
1
La mayoría de los lenguajes de programación funcionales tienen optimización de llamada de cola. Con esta optimización, la recursión de cola no volaría la pila, por lo que tales prácticas tienen sentido.
Jason Yeo