Правильный дизайн для Java-интерфейсов в отношении исключений

Я пытаюсь понять, как правильно определять интерфейсы в Java в отношении того, как исключения обрабатываются на языке (и времени выполнения).

ПРИМЕЧАНИЕ. Возможно, этот вопрос уже задан и получил адекватный ответ, но мой поиск SO (и других сайтов) не выявил ничего, что непосредственно касалось проектного вопроса и компромиссов, которые я представляю. Я с радостью удалю этот вопрос, если можно найти аналогичный ответ, который отвечает на мой вопрос. Я заранее извиняюсь за давний вопрос, но я хотел как можно более четко заявить о проблеме. Я также знаю об спорах вокруг отмеченных исключений, но поскольку я не в состоянии изменить язык и все еще работать в нем, Я хотел бы выбрать подход (ы), который вызовет наименьшую боль в долгосрочной перспективе.

Повторяющаяся проблема, с которой я сталкиваюсь, - это когда я пытаюсь определить свои собственные интерфейсы. Я столкнулся с выбором того, как указать, какие (если есть) исключения допускаются к членам интерфейса. Беда в том, что у меня нет способа узнать, какие исключения имеют смысл, поскольку я не знаю заранее, какие могут быть все возможные реализации для моего интерфейса. Для примера предположим, что у нас есть следующий интерфейс:

// purely hypothetical interface meant to illustrate the example...
public interface SegmentPartition {
    // option 1
    bool hasSegment(Segment s);
    // option 2
    void addSegment(Segment s) throws Exception;
    // option 3
    bool tryAddSegment(Segment s) throws TimeoutException, IOException, ParseException, XmlException, SQLException;
    // option 4
    void optimizeSegments() throws SegmentPartitionException;
}

Какие типы исключений должны объявлять этот интерфейс заранее? Возможные варианты:

  • не объявлять никаких исключений, реализации могут только бросать RuntimeException
  • объявить каждый метод как метать Exception
  • попытайтесь предвидеть все возможные типы исключений, которые имеют смысл для этого интерфейса и объявляют их как бросающиеся исключения.
  • создайте свой собственный тип исключения (например, SegmentException) и потребуйте, чтобы он был добавлен с внутренним исключением, если это необходимо для дополнительных подробностей.

Есть проблемы с каждым из этих подходов, и это не совсем ясно, каковы будут все компромиссы, я упомянул несколько для каждого случая. В идеале я не хочу, чтобы исключения были исключены из реализации, но это не всегда практическое ограничение.

Для параметра 1 возникает проблема, что многие типы исключений, которые могут быть выбраны, не выводятся из RuntimeException (например, IOException или TimeoutException). Я мог бы, конечно, добавить эти конкретные исключения в интерфейс, а затем разрешить другие RuntimeExceptions, но как насчет пользовательских исключений? Это кажется плохим вариантом.

С опцией 2 мы получим интерфейс, который, похоже, может привести к каким-либо исключениям (что, я полагаю, истинно) и не дает указания разработчикам о том, что на самом деле нужно выбросить. Интерфейс уже не является самостоятельным документированием. Хуже того, каждый сайт вызова этого интерфейса должен либо быть объявлен как бросающий Exception, либо обернуть каждый вызов методам этого интерфейса в try{} catch(Exception), либо создать блок, который ловит исключение, и пытается выяснить, была ли она реализована в результате реализации интерфейса или что-то еще в этом блоке. Тьфу. В качестве альтернативы, код, который использует этот интерфейс, может поймать конкретные исключения, которые могут быть реализованы реализацией, но затем он нарушает цель начала интерфейса (в том, что он требует, чтобы вызывающий пользователь знал тип выполнения реализации). На практике это вариант, наиболее близкий к тому, что большинство других языков, которые я знаю, относятся к исключениям (С++, С#, Scala и т.д.).

С опцией 3 я должен заранее предвидеть потенциальные реализации, которые могут быть определены для моего интерфейса, и объявлять исключения, наиболее подходящие для них. Поэтому, если я ожидаю, что мой интерфейс может быть реализован через удаленное, ненадежное соединение, я бы добавил TimeoutException, если я ожидаю, что он может быть реализован с использованием внешнего хранилища, я бы включил IOException и т.д. Я почти наверняка ошибаюсь... это означает, что я собираюсь постоянно выводить интерфейс в качестве новой поверхности парадигм реализации (что, разумеется, нарушает все существующие реализации и требует, чтобы они объявляли нечувствительные исключения как бросаемые). Еще одна проблема с этим подходом заключается в том, что результаты в уродливом, повторяющемся коде... особенно для больших интерфейсов со многими методами (да, иногда это необходимо), которые затем необходимо повторять в каждой точке реализации.

С опцией 4Я должен создать свой собственный тип исключения вместе со всем багажом, который сопровождает это (сериализуемость, вложенность и внутренние исключения, подходящая иерархия и т.д.). Кажется, что здесь неизбежно происходит то, что вы получаете один тип исключения для каждого типа интерфейса: InterfaceFoo + InterfaceFooException. Помимо раздувания кода это влечет за собой настоящую проблему: тип исключения ничего не значит... это всего лишь тип, используемый для объединения всех условий ошибки для конкретного интерфейса. В некоторых случаях, возможно, вы можете создать несколько подклассов типа исключения, чтобы указать конкретные режимы отказа, но даже это сомнительно.

Итак, что здесь делать, в общем? Я понимаю, что не может быть сурового и сухого правила, но мне интересно, что делают опытные разработчики Java. p >

Спасибо.

Ответ 1

Имея throws Exception (это это проверенное исключение) в интерфейсе означает, что вы знаете, при каком условии должно быть выбрано такое проверенное исключение. Это означает, что интерфейс уже имеет некоторые знания о его реализации. Этого не должно быть. Поэтому я бы пошел против любого throws Exception в интерфейсе. Если интерфейс выбрасывает проверочный Exception, я бы предложил упростить его, оставив логику исключения в реализации (ей).

Ответ 2

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

Если интерфейс будет использоваться внутренне, т.е. в базе данных, одним из возможных способов является бросить те исключения, которые вы конкретно будете обрабатывать.

Если это публичная библиотека, т.е. ExecutorService, может потребоваться глубокий анализ.

Пример - JavaDoc из ExecutorService:

 /**
 * ...
 * @return the result returned by one of the tasks
 * @throws InterruptedException if interrupted while waiting
 * @throws TimeoutException if the given timeout elapses before
 *         any task successfully completes
 * @throws ExecutionException if no task successfully completes
 * @throws RejectedExecutionException if tasks cannot be scheduled
 *         for execution
 */
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
                long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException;

Все эти исключения зависят от домена и могут принимать решения, основанные на их общем значении. Таким образом, вы можете догадываться, что они означают в контексте метода invokeAny, данного только для описания этого метода.

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

Еще несколько пунктов:

  • Java-библиотеки исключают выполнение сценариев выполнения, которые могут быть выбраны, поэтому они прекрасно подходят для их использования.
  • Таким образом, реализация классов может вызывать исключение среды выполнения для конкретного домена, если вы забыли один из случаев.
  • Можно документировать, чтобы обернуть необъявленные исключения в одно из исключений возвращаемого типа, т.е. в ExecutionException - так оно и было для java.util.concurrent.

Ответ 3

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

Элегантность - это вопрос мнения. Однако мне кажется самым элегантным, чтобы я выбрал пользовательское SegmentException для добавления (если это возможно) и попытаться свести к минимуму возможность исключения (-ов) вообще.