Статьи

Мой вариант использования для проверенных исключений

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

Однако динамические языки, а также статически типизированные языки, такие как C #, не требуют немедленной обработки исключений.

Опасности бросков оговорок

Существуют некоторые технические проблемы с проверенными исключениями: иногда создается недостижимый код, когда реализации не генерируют исключения, определенные их интерфейсами (Миско Хевери приводит пример ByteArrayOutputStream). Другие проблемы методологические: проверенные исключения приводят к потере попытки / catch блоки, которые распространяются и начинают быстро игнорироваться программистами. Более того, нет восстановления из многих исключений (по крайней мере, для текущего потока выполнения), поэтому создание каждого отдельного фрагмента кода, указывающего, как обрабатывать ошибку, представляется излишним.

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

Примечание о распределенных вычислениях

Широко распространено мнение, что невозможно прозрачно распространить парадигму локальных вычислений (сделанную из вызовов функций и методов) на область распределенных вычислений. Удаленный вызов процедур и удаленный вызов методов являются полезными абстракциями, но они не могут представить интерфейс, идентичный интерфейсу локальных методов. Известная статья «Заметка о распределенных вычислениях» объясняет основные проблемы распределения, которые не могут быть решены автоматически:

  • задержка при удаленных вызовах на несколько порядков выше, чем при локальных.
  • Доступ к памяти в низкоуровневых языках, таких как C, проблематичен, поскольку память не может использоваться совместно с удаленными процессами. Вы не можете передать указатель на сервер, хотя в случае виртуальных машин виртуальные прокси более или менее работают.
  • Параллельность есть везде, и нет способа обеспечить синхронизацию автоматически с синхронизированными и распространенными шаблонами многопоточности, если вы не примете конкретные алгоритмы.
  • Возможны частичные сбои . Локальный сбой обычно приводит к прекращению всего процесса; Распределение вычислений означает, что не только сбои более вероятны из-за ненадежных каналов, но они также охватывают только часть системы, и оставшиеся в живых части должны продолжать работать правильно и справляться с этим (на самом деле работа распределения часто маскирует сбои отдельных пользователей). компоненты). Вы не хотите, чтобы ваш главный сервер базы данных вышел из строя, потому что он не смог обновить ведомое устройство.

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

  • сделать локальную парадигму похожей на распределенную; это решение будет без необходимости создавать трудности, такие как проверка сбоев, которые никогда не произойдут локально в течение вашей жизни. Поймать исключения RemoteException во всех вызовах методов не смешно.
  • сделайте распределенную парадигму похожей на локальную, отслеживая режимы частых сбоев и проблемы, указанные выше. Что произойдет в этих случаях, будет неопределенным.

Проверенные исключения входят

Некоторые объекты скрывают сетевое взаимодействие, содержа ссылки на сокеты и потоки TCP, отправляя пакеты UDP или совершая HTTP-вызовы. Реализации типа RMI делают это автоматически, но сущность не меняется.

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

Я нашел этот подход полезным при соединении локальных и удаленных объектов:

  • объекты, которые работают с сетью, выдают проверенные исключения . Эти исключения являются частью интерфейса так же, как и тип возвращаемого значения.
  • Объекты моста имеют дело с частичным отказом , синхронизацией и безопасностью; они пытаются замаскировать сбои или проблемы с производительностью и обеспечивают основное правильное поведение.
  • Объектам, работающим через мосты, не нужно заботиться о сети , и их можно тестировать изолированно.

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

Я видел аналогичную идею для «формальной» проверки в день DDD в прошлом году Джакомо Тесио , где доменные модели использовали проверенные исключения для выражения сбоев. Идея заключается в том, что такого рода сбои (распределенные или соответствующие ошибки в бизнес-сфере) слишком важны, чтобы их можно было оставить только для тестирования покрытия. Предпочтение состоит в том, чтобы пожертвовать гибкостью кода, чтобы гарантировать, что архитектура не испорчена объектами, вызывающими сервер в локальных вычислениях (например, сравнение для алгоритма сортировки или фильтрации результатов).

Пример кода

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

public interface RemoteDocumentsSearcher {

    QueryResult query(Query query)
            throws NoResponseException;

}

Внутренне исключения Java-сокетов обернуты в новые, представляющие режимы сбоев, которыми интересуется приложение. В только подражает типы , которыми вы владеете , может применяться также для исключения: Я предпочитаю , чтобы установить свою собственную иерархию, изготовленную из классов , где я могу добавить методы и поле.

try {
            return reader.readLine();
        } catch (SocketException e) {
            throw new ConnectionClosedException("Connection closed while reading.");
        } catch (IOException e) {
            throw new ConnectionClosedException("Connection closed while reading.");
        }

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

        QueryResult result = new QueryResult();
        if (valid()) {
            try {
                QueryResult additionalResult = searcher.query(this);
                result = result.merge(additionalResult);
            } catch (NoResponseException e) {
                // exclude not responding neighbors from the search
            }
        }
        return result;

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

public interface DocumentsSearcher {
        public abstract QueryResult searchDocument(Query query);

}