É uma boa prática implementar dois métodos padrão do Java 8 em termos um do outro?

14

Estou projetando uma interface com dois métodos relacionados, semelhantes a este:

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

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

Cerca de metade das implementações computará apenas uma coisa, enquanto a outra metade poderá computar mais.

Isso tem algum precedente no código Java 8 amplamente utilizado? Eu sei que Haskell faz coisas semelhantes em algumas classes ( Eqpor exemplo).

O lado positivo é que tenho que escrever um código significativamente menor do que se eu tivesse duas classes abstratas ( SingleThingComputere MultipleThingComputer).

A desvantagem é que uma implementação vazia é compilada, mas explode em tempo de execução com a StackOverflowError. É possível detectar a recursão mútua com ae ThreadLocalfornecer um erro melhor, mas isso adiciona uma sobrecarga ao código que não é de buggy.

Tavian Barnes
fonte
3
Por que você escreveria deliberadamente código com garantia de recursão infinita? Nada de bom pode vir disso.
1
Bem, não é "garantido"; uma implementação adequada substituiria um desses métodos.
Tavian Barnes
6
Você não sabe disso. Uma classe ou interface precisa se manter por conta própria e não assumir que uma subclasse fará as coisas de uma certa maneira para evitar grandes erros de lógica. Caso contrário, é frágil e não pode cumprir suas garantias. Observe que não estou dizendo que sua interface pode ou deve impor uma classe de implementação throw new Error();ou algo estúpido, apenas que a própria interface não deve ter um contrato frágil por meio de defaultmétodos.
Concordo com @Snowman, é uma mina terrestre. Eu acho que é o livro "código do mal", no sentido em que Jon Skeet significa - observe que o mal não é o mesmo que o mal , é perverso / inteligente / tentador, mas ainda está errado :) eu recomendo youtu.be/lGbQiguuUGc?t=15m26s ( ou, de fato, toda a conversa para um contexto mais amplo - é muito interessante).
Konrad Morawski
1
Só porque cada função pode ser definida em termos da outra não implica que ambas possam ser definidas em termos uma da outra. Isso não voa em nenhum idioma. Você deseja uma interface com 2 herdeiros abstratos, cada um implementa um em termos do outro, mas abstrai o outro, para que os implementadores escolham qual lado eles desejam implementar herdando da classe abstrata correta. Um lado deve ser definido independentemente do outro, caso contrário, o pop vai para a pilha . Você tem padrões insanos, presumindo que os implementadores resolverão o problema para você, abstractexiste para forçá- los a resolvê-lo.
Jimmy Hoffa

Respostas:

16

TL; DR: não faça isso.

O que você mostra aqui é um código quebradiço.

Uma interface é um contrato. Ele diz "independentemente de qual objeto você obtém, ele pode executar X e Y". Como está escrito, sua interface faz nem X nem Y, pois é garantido para causar um estouro de pilha.

Lançar um erro ou subclasse indica um erro grave que não deve ser capturado:

Um erro é uma subclasse de Throwable que indica problemas sérios que um aplicativo razoável não deve tentar capturar.

Além disso, VirtualMachineError , a classe pai de StackOverflowError , diz o seguinte:

Lançada para indicar que a Java Virtual Machine está com problemas ou ficou sem recursos necessários para continuar operando.

Seu programa não deve se preocupar com os recursos da JVM . Esse é o trabalho da JVM. Fazer um programa que causa um erro da JVM como parte da operação normal é ruim. Ele garante que seu programa falhará ou obriga os usuários dessa interface a capturar erros com os quais não deve se preocupar.


Talvez você conheça Eric Lippert por empreendimentos como "membro emérito do comitê de design de linguagem C #". Ele fala sobre os recursos da linguagem que levam as pessoas ao sucesso ou ao fracasso: embora esse não seja um recurso da linguagem ou faça parte do design da linguagem, seu argumento é igualmente válido quando se trata de implementar interfaces ou usar objetos.

Você se lembra na Princesa Noiva quando Westley acorda e se vê trancado no Poço do Desespero com um albino rouco e o sinistro homem de seis dedos, Conde Rugen? A idéia principal de um poço de desespero é dupla. Primeiro, que é um poço e, portanto, fácil de cair, mas é um trabalho difícil de sair. E segundo, que induz o desespero. Daí o nome.

Fonte: C ++ e o poço do desespero

Ter uma interface ativada StackOverflowErrorpor padrão leva os desenvolvedores ao Pit of Despair e é má idéia . Em vez disso, empurre os desenvolvedores em direção ao poço do sucesso . Torná-lo fácil de usar sua interface corretamente e sem bater a JVM.

Delegar entre os métodos é bom aqui. No entanto, a dependência deve seguir um caminho. Eu gosto de pensar na delegação de método como um gráfico direcionado . Cada método é um nó no gráfico. Cada vez que um método chama outro método, desenhe uma borda do método de chamada para o método chamado.

Se você desenhar um gráfico e perceber que é cíclico, é um cheiro de código. Esse é um potencial para empurrar os desenvolvedores para o poço do desespero. Observe que não deve ser categoricamente proibido, apenas que é preciso ter cuidado . Algoritmos recursivos especificamente terão ciclos no gráfico de chamada: tudo bem. Documente e avise os desenvolvedores. Se não for recursivo, tente interromper esse ciclo. Se não puder, descubra quais entradas podem causar um estouro de pilha e atenue-as ou documente-a como último caso, se nada mais funcionar.

Comunidade
fonte
Ótima resposta. Estou curioso para saber por que essa é uma prática tão comum em Haskell. Diferentes culturas de desenvolvimento, eu acho.
Tavian Barnes
Eu não tenho experiência em Haskell e não consigo falar se ou por que isso é mais prevalente na programação funcional.
Olhando um pouco mais, parece que a comunidade Haskell também não está muito feliz com isso. Além disso, o compilador aprendeu recentemente a verificar "definições completas mínimas", para que uma implementação vazia gere pelo menos um aviso.
perfil completo de Tavian Barnes
1
A maioria das linguagens de programação funcionais possui otimização de chamada de cauda. Com essa otimização, a recursão da cauda não explodiria a pilha; portanto, essas práticas fazem sentido.
Jason Yeo