È buona norma implementare due metodi predefiniti Java 8 l'uno rispetto all'altro?


14

Sto progettando un'interfaccia con due metodi correlati, simile a questo:

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

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

Circa la metà delle implementazioni calcolerà solo una cosa, mentre l'altra metà potrebbe calcolare di più.

Questo ha precedenti nel codice Java 8 ampiamente utilizzato? So che Haskell fa cose simili in alcune macchine da scrivere ( Eqper esempio).

Il lato positivo è che devo scrivere significativamente meno codice rispetto a se avessi due classi astratte ( SingleThingComputere MultipleThingComputer).

Il rovescio della medaglia è che un'implementazione vuota si compila ma esplode in fase di esecuzione con a StackOverflowError. È possibile rilevare la ricorsione reciproca con a ThreadLocale dare un errore migliore, ma ciò aggiunge un sovraccarico al codice non difettoso.


3
Perché dovresti mai scrivere deliberatamente codice garantito per avere una ricorsione infinita? Nulla di buono può forse provenire da quello.

1
Beh, non è "garantito"; un'implementazione corretta avrebbe la precedenza su uno di questi metodi.
Tavian Barnes,

6
Non sai che. Una classe o un'interfaccia deve essere autonoma e non assumere che una sottoclasse farà le cose in un certo modo per evitare errori logici importanti. Altrimenti, è fragile e non può essere all'altezza delle sue garanzie. Nota che non sto dicendo che la tua interfaccia può o dovrebbe imporre una classe di esecuzione throw new Error();o qualcosa di stupido, solo che l' interfaccia stessa non dovrebbe avere un contratto fragile attraverso defaultmetodi.

Sono d'accordo con @Snowman, è una mina terrestre. Penso che sia il libro di testo "codice malvagio", nel senso che Jon Skeet significa - nota che il male non è uguale al male , è malvagio / intelligente / allettante, ma ancora sbagliato :) Raccomando youtu.be/lGbQiguuUGc?t=15m26s ( o in effetti, l'intero discorso per un contesto più ampio - è molto interessante).
Konrad Morawski,

1
Solo perché ogni funzione può essere definita in termini di altra non implica che possano essere entrambe definite in termini di reciproca. Non vola in nessuna lingua. Volete un'interfaccia con 2 eredi astratti, ognuno implementa l'uno in termini dell'altro ma ne astrae l'altro così gli implementatori scelgono quale parte vogliono implementare ereditando dalla classe astratta corretta. Un lato deve essere definito indipendentemente dall'altro, altrimenti il ​​pop va in pila . Hai insane impostazioni predefinite presumendo che gli implementatori risolveranno il problema per te, abstractesiste per costringerli a risolverlo.
Jimmy Hoffa,

Risposte:


16

TL; DR: non farlo.

Quello che mostri qui è un codice fragile.

Un'interfaccia è un contratto. Dice "indipendentemente dall'oggetto che ottieni, può fare X e Y." Come è scritto, l'interfaccia non fa X né Y perché è garantito che causi un overflow dello stack.

Il lancio di un errore o di una sottoclasse indica un errore grave che non deve essere rilevato:

Un errore è una sottoclasse di Throwable che indica gravi problemi che un'applicazione ragionevole non dovrebbe tentare di rilevare.

Inoltre, VirtualMachineError , la classe padre di StackOverflowError , dice questo:

Generato per indicare che la Java Virtual Machine è danneggiata o ha esaurito le risorse necessarie per continuare a funzionare.

Il tuo programma non dovrebbe riguardare le risorse JVM . Questo è il lavoro della JVM. Fare un programma che causa un errore JVM come parte del normale funzionamento è male. O garantisce che il programma si arresti in modo anomalo o costringe gli utenti di questa interfaccia a intercettare errori di cui non dovrebbe preoccuparsi.


Potresti conoscere Eric Lippert da sforzi come emerito "membro del comitato di progettazione del linguaggio C #". Parla delle caratteristiche del linguaggio che spingono le persone verso il successo o il fallimento: sebbene questa non sia una caratteristica del linguaggio o parte della progettazione del linguaggio, il suo punto è altrettanto valido quando si tratta di implementare interfacce o usare oggetti.

Ricordi in The Princess Bride quando Westley si sveglia e si ritrova chiuso in The Pit Of Despair con un albino rauco e il sinistro uomo a sei dita, Conte Rugen? L'idea principale di una fossa di disperazione è duplice. Innanzitutto, è una fossa, e quindi facile cadere ma un lavoro difficile da cui uscire. E in secondo luogo, che provoca disperazione. Da qui il nome.

Fonte: C ++ e Pit Of Despair

Avere un'interfaccia lanciata StackOverflowErrordi default spinge gli sviluppatori nel Pozzo della Disperazione ed è una cattiva idea . Spingi invece gli sviluppatori verso il Pit of Success . Lo rendono facile da usare l'interfaccia in modo corretto e senza schiantarsi la JVM.

Delegare tra i metodi va bene qui. Tuttavia, la dipendenza dovrebbe andare in un modo. Mi piace pensare alla delega del metodo come a un grafico diretto . Ogni metodo è un nodo sul grafico. Ogni volta che un metodo chiama un altro metodo, disegna un vantaggio dal metodo chiamante al metodo chiamato.

Se disegni un grafico e noti che è ciclico, è un odore di codice. Questo è un potenziale per spingere gli sviluppatori nel Pozzo della Disperazione. Si noti che non dovrebbe essere categoricamente vietato, solo che si deve usare cautela . In particolare, gli algoritmi ricorsivi avranno dei cicli nel grafico delle chiamate: va bene. Documentalo e avvisa gli sviluppatori. Se non è ricorsivo, prova a interrompere quel ciclo. Se non è possibile, scoprire quali input potrebbero causare un overflow dello stack e mitigarli o documentarlo come ultimo caso se nient'altro funzionerà.


Bella risposta. Sono curioso di sapere perché questa è una pratica così comune in Haskell allora. Diverse culture di sviluppatori immagino.
Tavian Barnes,

Non ho esperienza in Haskell e non posso parlare se o perché questo è più prevalente nella programmazione funzionale.

Analizzandoci un po 'di più, sembra che anche la comunità di Haskell non sia molto contenta. Anche il compilatore ha recentemente imparato a verificare la presenza di "definizioni complete minime", quindi un'implementazione vuota avrebbe almeno generato un avviso.
Tavian Barnes,

1
La maggior parte dei linguaggi di programmazione funzionale ha l'ottimizzazione delle chiamate di coda. Con questa ottimizzazione, la ricorsione della coda non farebbe impazzire, quindi tali pratiche hanno senso.
Jason Yeo,
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.