Czy dobrą praktyką jest wdrażanie dwóch domyślnych metod Java 8 pod względem siebie?

14

Projektuję interfejs za pomocą dwóch powiązanych metod, podobnych do tego:

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

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

Około połowa implementacji obliczy tylko jedną rzecz, podczas gdy druga połowa może obliczyć więcej.

Czy ma to jakiś precedens w powszechnie używanym kodzie Java 8? Wiem, że Haskell robi podobne rzeczy w niektórych klasach ( Eqna przykład).

Plusem jest to, że muszę pisać znacznie mniej kodu niż gdybym miał dwie abstrakcyjne klasy ( SingleThingComputeri MultipleThingComputer).

Minusem jest to, że pusta implementacja kompiluje się, ale wysadza w czasie działania za pomocą StackOverflowError. Możliwe jest wykrycie wzajemnej rekurencji za pomocą a ThreadLocali podanie ładniejszego błędu, ale to dodaje narzut na kod bez błędów.

Tavian Barnes
źródło
3
Dlaczego miałbyś kiedykolwiek celowo pisać kod z nieskończoną rekurencją? Z tego nie może wynikać nic dobrego.
1
Cóż, nie jest to „gwarantowane”; poprawna implementacja zastąpiłaby jedną z tych metod.
Tavian Barnes,
6
Nie wiesz tego. Klasa lub interfejs musi działać samodzielnie i nie zakładać, że podklasa zrobi to w określony sposób, aby uniknąć poważnych błędów logicznych. W przeciwnym razie jest kruchy i nie może spełnić swoich gwarancji. Zauważ, że nie mówię, że twój interfejs może lub powinien wymusić działanie klasy implementującej throw new Error();lub coś głupiego, tylko że sam interfejs nie powinien mieć kruchej umowy poprzez defaultmetody.
Zgadzam się z @Snowman, to mina. Myślę, że to podręcznikowy „zły kod” w tym sensie, że Jon Skeet znaczy - pamiętaj, że zło nie jest tym samym, co złe , jest niegodziwe / sprytne / kuszące, ale nadal złe :) Polecam youtu.be/lGbQiguuUGc?t=15m26s ( a właściwie cała rozmowa w szerszym kontekście - to bardzo interesujące).
Konrad Morawski
1
To, że każdą funkcję można zdefiniować w kategoriach drugiej, nie oznacza, że ​​można je zdefiniować w kategoriach siebie. To nie lata w żadnym języku. Potrzebujesz interfejsu z 2 abstrakcyjnymi dziedzicznikami, każdy implementuje jeden pod względem drugiego, ale streszcza drugi, więc implementatorzy wybierają stronę, którą chcą zaimplementować, dziedzicząc z poprawnej klasy abstrakcyjnej. Jedna strona musi być zdefiniowana niezależnie od drugiej, inaczej pop trafi na stos . Masz obłąkane wartości domyślne, zakładając, że realizatorzy rozwiążą za ciebie problem, abstractistnieje, aby zmusić ich do rozwiązania problemu.
Jimmy Hoffa,

Odpowiedzi:

16

TL; DR: nie rób tego.

To, co tu pokazujesz, to kruchy kod.

Interfejs to umowa. Mówi: „niezależnie od tego, jaki obiekt otrzymujesz, może wykonać X i Y”. Jak to jest napisane, twój interfejs robi ani X ani Y, ponieważ jest gwarantowane , aby spowodować przepełnienie stosu.

Zgłoszenie błędu lub podklasy oznacza poważny błąd, którego nie należy wychwytywać:

Błąd jest podklasą Throwable, która wskazuje na poważne problemy, których rozsądna aplikacja nie powinna próbować wychwycić.

Ponadto VirtualMachineError , klasa nadrzędna StackOverflowError , mówi:

Wyrzucony w celu wskazania, że ​​wirtualna maszyna Java jest uszkodzona lub zabrakło zasobów niezbędnych do jej dalszego działania.

Twój program nie powinien zajmować się zasobami JVM . To jest zadanie JVM. Wykonanie programu, który powoduje błąd JVM w ramach normalnej pracy, jest złe. Gwarantuje to awarię programu lub zmusza użytkowników tego interfejsu do wychwytywania błędów, których nie powinien się przejmować.


Być może znasz Erica Lipperta z takich przedsięwzięć, jak emerytowany „członek komitetu projektowego w języku C #”. Mówi o cechach językowych popychających ludzi do sukcesu lub porażki: chociaż nie jest to cecha językowa ani część projektowania języka, jego punkt widzenia jest równie ważny, jeśli chodzi o implementację interfejsów lub używanie obiektów.

Pamiętasz w The Princess Bride, kiedy Westley budzi się i zostaje zamknięty w Otchłani Rozpaczy z ochrypłym albinosem i złowrogim, sześciopalczastym mężczyzną, hrabią Rugen? Zasada rozpaczy jest dwojaka. Po pierwsze, że jest to dół, a zatem łatwo wpaść w trudną pracę, z której można się wydostać. Po drugie, że wywołuje rozpacz. Stąd nazwa.

Źródło: C ++ i Pit Of Despair

Posiadanie interfejsu StackOverflowErrordomyślnie wpycha programistów w Pit of Despair i jest złym pomysłem . Zamiast tego popchnij programistów w stronę Pit of Success . Sprawiają, że jest łatwy w użyciu interfejs prawidłowo i bez awarii JVM.

Delegowanie między metodami jest tutaj w porządku. Jednak zależność powinna iść w jedną stronę. Lubię myśleć o delegowaniu metod jak o ukierunkowanym grafie . Każda metoda jest węzłem na wykresie. Za każdym razem, gdy metoda wywołuje inną metodę, narysuj przewagę od metody wywołującej do metody wywoływanej.

Jeśli narysujesz wykres i zauważysz, że jest on cykliczny, będzie to zapach kodu. Jest to potencjał popychania programistów do Pit of Despair. Należy pamiętać, że nie powinno to być kategorycznie zabronione, należy jedynie zachować ostrożność . Algorytmy rekurencyjne będą miały cykle na wykresie połączeń: to dobrze. Dokumentuj to i ostrzegaj programistów. Jeśli nie jest rekurencyjny, spróbuj przerwać ten cykl. Jeśli nie możesz, dowiedz się, jakie dane wejściowe mogą spowodować przepełnienie stosu i albo złagodź je, albo udokumentuj jako ostatni przypadek, jeśli nic więcej nie zadziała.

Społeczność
źródło
Świetna odpowiedź. Ciekawe, dlaczego jest to tak powszechna praktyka w Haskell. Chyba różne kultury programistyczne.
Tavian Barnes,
Nie mam doświadczenia w Haskell i nie mogę mówić, czy i dlaczego jest to bardziej rozpowszechnione w programowaniu funkcjonalnym.
Patrząc na to trochę bardziej, wydaje się, że społeczność Haskell również nie jest z tego zadowolona. Również kompilator nauczył się ostatnio sprawdzać „minimalne pełne definicje”, więc pusta implementacja przynajmniej wygeneruje ostrzeżenie.
Tavian Barnes,
1
Większość funkcjonalnych języków programowania ma funkcję optymalizacji połączeń ogonowych. Przy tej optymalizacji rekurencja ogona nie zniszczyłaby stosu, dlatego takie praktyki mają sens.
Jason Yeo,