Как бороться с интерфейсами, которые могут выбрасывать что угодно? - PullRequest
4 голосов
/ 29 января 2012

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

interface DataStream
{
  Data ReadNext();
}

class DeserializingFileDataStream : DataStream
{
  //this one does file operations + serialization,
  //so can throw eg IOException, SerializationException, InvalidOperationException, ...
}

class NetworkDataStream : DataStream
{
  //get data over tcp
  //so throws IOException, SocketException
}

class HardwareDeviceDataStream : DataStream
{
  //read from a custom hardware device implemented in unmanaged code
  //so throws mainly custom exceptions
}

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

Я уже пробовал пару решений, но ни одно из них меня не особо порадовало, поэтому возникает вопрос: есть ли какая-то распространенная практика / шаблон для обработки подобных ситуаций? Пожалуйста, будьте конкретны и не говорите мне «смотреть на ELMAH». Вот некоторые вещи, которые я использовал в прошлом:

  • catch( Exception ) ну, очевидно, в чем проблемы
  • TryAndCatchDataStream( Action what, Action<Exception> errHandler ) метод, состоящий из попытки, сопровождаемой перехватом для любого исключения, представляющего интерес из любой реализации. Это означает, что он должен обновляться, когда реализации меняются или добавляются / удаляются.
  • оставил комментарий к интерфейсу, в котором говорилось: «должен только генерировать DataStreamExceptions», и во всех реализациях следите за тем, чтобы этому правилу следовало перехватывать что-либо интересное и заключать его в DataStreamException, а затем выбрасывать. Не так уж и плохо, но добавляет шум к каждой реализации.

Затем у меня есть второй, более общий вопрос об обработке исключений (не думаю, что для этого нужно создавать отдельный пост): у меня есть несколько случаев, когда метод A вызывает B, который вызывает C, который вызывает D, а D выбрасывает SomeException все же вызывающий объект A ловит исключение. Разве это не признак того, что с кодом что-то не так? Поскольку для того, чтобы сделать это, вызывающий объект A должен знать, что A в конце концов вызовет D. Если только документы A не могут вызвать исключение SomeException; но в обоих случаях это означает, что при обновлении простой детали реализации A (т. е. заставить его вызывать что-то, кроме D), это изменение будет видно пользователям A.

Ответы [ 5 ]

4 голосов
/ 29 января 2012

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

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

Что касается вашего второго вопроса, A должен задокументировать любое исключение, которое может всплыть из него, включая исключения, выброшенные зависимыми объектами. B должен задокументировать исключения, которые он выдает, чтобы A мог знать о них и т. Д. И т. Д.

Если вы измените A, чтобы сделать что-то, что вызывает изменение в выданных исключениях, то вам придется изменить документацию. С точки зрения вызывающих приложений все, что он знает, это А. может делать все, и вызывающему приложению все равно, является ли это B, C или D. Ответственен за то, что сообщается вызывающему.

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

3 голосов
/ 29 января 2012

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

Если вы настаиваете на интерфейсе, вы должны подождать до следующей версии CLR, где интерфейсы могут иметь базовую реализацию.Есть видеофильм Вэнса Моррисона , рассказывающий о такой функции, но я никогда не слышал об этом снова.

Если вы не можете дождаться CLR vNext, у вас есть другой вариант создания класса-оболочки (давайте назовем его DataStreamReader), который принимает интерфейс IDataStream, который также реализует IDataStream, но выполняет обтекание по вашему желанию.Если вы позаботитесь о том, чтобы в вашем коде, в котором вы читали только экземпляры DataStreamReader, все было в порядке.

Конструкция вынуждает любого наследника реализовывать метод ReadNextImpl, где ваши потребители реализуют свою логику.Вы вызываете как прежде ReadNext и получаете с текущей простой реализацией всегда DataStreamException.Это самое простое решение, которое я мог придумать.

   class Data { }
    public class DataStreamException : Exception
    {
        public DataStreamException(string message, Exception inner)
            : base(message, inner)
        {   }
    }

    abstract class DataStream
    {
        protected abstract Data ReadNextImpl();
        public Data ReadNext()
        {
            try
            {
                return ReadNextImpl();
            }
            catch (Exception ex)
            {
                throw new DataStreamException("Could not read from stream. See inner exception for details.", ex);
            }
        }
    }

    class DeserializingFileDataStream : DataStream
    {
        protected override Data ReadNextImpl()
        {
            throw new NotImplementedException();
        }

    }

    class NetworkDataStream : DataStream
    {
        protected override Data ReadNextImpl()
        {
            throw new Exception();
        }
    }
1 голос
/ 01 февраля 2012

По сути, код, который перехватывает исключение, должен знать несколько вещей:

  1. Что случилось?
  2. Требует ли условие каких-либо конкретных действий?
  3. После выполнения вышеуказанного действия, если оно есть, исключительное условие считается выполненным.
  4. В каком состоянии будут находиться применимые объекты?

К сожалению, многие языки и фреймворки, включая C ++, Java и .net, пытаются использовать один довольно неуклюжий вид информации (тип брошенного объекта исключения) для передачи всех трех фрагментов информации, хотя на практике они ' часто во многом ортогональны. Особенно примечательная трудность такой конструкции состоит в том, что возможно одновременное возникновение нескольких исключительных условий, так что обработчики, применимые к ЛЮБОМУ из них, должны выполняться, но исключение должно продолжаться вверх по стеку, пока не будут запущены обработчики, применимые ко ВСЕМ из них.

Несмотря на ограничения типового дизайна, с ним приходится работать. При разработке собственных новых классов / интерфейсов и исключений, которые вытекают из них, следует попытаться использовать разные типы исключений для ответа на вопрос № 3- № 4 выше, поскольку эти вопросы, скорее всего, будут интересны вызывающему абоненту. Кроме того, я бы предположил, что для классов может быть желательно иметь переменную состояния или флаг, который может быть признан недействительным, если исключение, выброшенное из метода, может оставить класс в недопустимом состоянии, возможно, с использованием шаблона, например:

  ... before doing anything that might cause object to enter invalid state even temporarily ...
  if (state != MyClassState.HappyIdle) throw new StateCorruptException(....);
  state = MyClassState.UnhappyOrBusy;
  ... manipulate state in a fashion that will end up with a valid state when complete ...
  state = MyClassState.HappyIdle;

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

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

0 голосов
/ 29 января 2012

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

То, что часто делают Java-программисты (после того, как их научили не использовать throws Exception), - ваш третий вариант. Этот третий вариант имеет смысл для исключений, которые вы не ожидаете, что ваш вызывающий код знает, как обрабатывать - если вы хотите, чтобы вызывающий код обрабатывал их (скажем, повторить три раза в случае IOException или NetworkException), вам лучше придерживаться к тому, что люди уже знают.

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

0 голосов
/ 29 января 2012

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

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

Не совсем уверен, если это хорошая идея или нет. Просто подумал, что предложу идею.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...