Это хорошая практика для реализации двух методов Java 8 по умолчанию друг с другом?

14

Я проектирую интерфейс с двумя связанными методами, подобными этому:

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

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

Приблизительно половина реализаций будет когда-либо вычислять только одну вещь, тогда как другая половина может вычислять больше.

Есть ли у этого прецедент в широко используемом коде Java 8? Я знаю, что Haskell делает подобные вещи в некоторых классах типов ( Eqнапример).

Плюс в том, что мне приходится писать значительно меньше кода, чем если бы у меня было два абстрактных класса ( SingleThingComputerи MultipleThingComputer).

Недостатком является то, что пустая реализация компилируется, но взрывается во время выполнения с помощью a StackOverflowError. Можно обнаружить взаимную рекурсию с помощью a ThreadLocalи дать более приятную ошибку, но это добавляет издержки к не глючному коду.

Тавиан Барнс
источник
3
Зачем вам когда-либо намеренно писать код с гарантированной бесконечной рекурсией? Ничего хорошего из этого не получится.
1
Ну, это не "гарантировано"; правильная реализация переопределит один из этих методов.
Тавиан Барнс
6
Ты этого не знаешь. Класс или интерфейс должны стоять самостоятельно и не предполагать, что подкласс будет действовать определенным образом, чтобы избежать серьезных логических ошибок. В противном случае он хрупок и не может оправдать своих гарантий. Обратите внимание, что я не говорю, что ваш интерфейс может или должен принудительно выполнять класс реализации throw new Error();или делать что-то глупое, только то, что сам интерфейс не должен иметь хрупкого контракта через defaultметоды.
Я согласен с @Snowman, это мина. Я думаю, что это учебник "код зла" в том смысле, что означает Джон Скит - обратите внимание, что зло не то же самое, что зло , оно злое / умное / заманчивое, но все же неправильное :) Я рекомендую youtu.be/lGbQiguuUGc?t=15m26s ( или действительно, весь разговор для более широкого контекста - это очень интересно).
Конрад Моравский
1
Тот факт, что каждая функция может быть определена в терминах другой, не означает, что они обе могут быть определены в терминах друг друга. Это не летит ни на одном языке. Вам нужен интерфейс с двумя абстрактными наследниками, каждый из которых реализует один в терминах другого, но абстрагирует другой, поэтому разработчики выбирают, какую сторону они хотят реализовать, наследуя от правильного абстрактного класса. Одна сторона должна быть определена независимо от другой, иначе поп выходит в стек . У вас есть безумные значения по умолчанию, предполагающие, что разработчики решат проблему за вас, abstractсуществует, чтобы заставить их решить ее.
Джимми Хоффа

Ответы:

16

TL; DR: не делай этого.

То, что вы показываете здесь, является хрупким кодом.

Интерфейс - это контракт. Он говорит «независимо от того, какой объект вы получаете, он может делать X и Y». Как написано, ваш интерфейс не делает ни X, ни Y, потому что он гарантированно вызовет переполнение стека.

Создание ошибки или подкласса указывает на серьезную ошибку, которая не должна быть обнаружена:

Ошибка - это подкласс Throwable, который указывает на серьезные проблемы, которые разумное приложение не должно пытаться обнаружить.

Кроме того, VirtualMachineError , родительский класс StackOverflowError , говорит следующее:

Брошенный, чтобы указать, что Виртуальная машина Java сломана или исчерпала ресурсы, необходимые для ее продолжения работы.

Ваша программа не должна быть связана с ресурсами JVM . Это работа JVM. Создание программы, которая вызывает ошибку JVM как часть нормальной работы, плохо. Это либо гарантирует сбой вашей программы, либо вынуждает пользователей этого интерфейса перехватывать ошибки, о которых не следует беспокоиться.


Возможно, вы знакомы с Эриком Липпертом по таким начинаниям, как почетный «член комитета по разработке языка C #». Он говорит о языковых особенностях, подталкивающих людей к успеху или неудаче: хотя это не языковая функция или часть языкового дизайна, его точка зрения одинаково верна, когда речь идет о реализации интерфейсов или использовании объектов.

Вы помните в «Принцессе-невесте», когда Уэстли просыпается и оказывается запертым в «Яме отчаяния» с хриплым альбиносом и зловещим шестигранным мужчиной, графом Ругеном? Основная идея ямы отчаяния двояка. Во-первых, это яма, и поэтому легко попасть в, но трудную работу, чтобы выбраться из нее. И второе, это вызывает отчаяние. Отсюда и название.

Источник: C ++ и Яма Отчаяния

Наличие интерфейса StackOverflowErrorпо умолчанию бросает разработчиков в Pit of Despair, и это плохая идея . Вместо этого подталкивайте разработчиков к Яме Успеха . Сделать это легко , чтобы правильно и без сбоев в JVM использовать интерфейс.

Делегирование между методами здесь хорошо. Однако зависимость должна идти в одном направлении. Мне нравится думать о делегировании методов как о ориентированном графе . Каждый метод является узлом на графике. Каждый раз, когда метод вызывает другой метод, рисуйте грань от вызывающего метода до вызываемого метода.

Если вы рисуете график и замечаете, что он циклический, это - запах кода. Это может подтолкнуть разработчиков в Яму Отчаяния. Обратите внимание, что это не должно быть категорически запрещено, нужно только соблюдать осторожность . В частности, рекурсивные алгоритмы будут иметь циклы в графе вызовов: это нормально. Документируйте это и предупреждайте разработчиков. Если это не рекурсивно, попробуйте разорвать этот цикл. Если вы не можете, выясните, какие входные данные могут вызвать переполнение стека, или либо уменьшите их, либо задокументируйте это как последний случай, если больше ничего не будет работать.

Сообщество
источник
Отличный ответ. Мне любопытно, почему это такая обычная практика в Хаскеле? Я думаю, разные культуры разработчиков.
Тавиан Барнс
У меня нет опыта работы с Haskell, и я не могу сказать, является ли это более распространенным в функциональном программировании.
Рассматривая это немного больше, кажется, сообщество Haskell также не очень доволен этим. Также компилятор недавно научился проверять «минимальные полные определения», поэтому пустая реализация, по крайней мере, выдаст предупреждение.
Тавиан Барнс
1
Большинство функциональных языков программирования имеют функцию оптимизации вызовов. При такой оптимизации хвостовая рекурсия не взорвет стек, поэтому такие практики имеют смысл.
Джейсон Йео