Обработка исключений: контракт против исключительного подхода

Я знаю два подхода к обработке исключений, давайте посмотрим на них.

  1. Контрактный подход.

    Когда метод не выполняет то, что он должен делать в заголовке метода, он генерирует исключение. Таким образом, метод «обещает», что выполнит операцию, и, если по какой-то причине не удалось выполнить эту операцию, он сгенерирует исключение.

  2. Исключительный подход.

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

Давайте использовать оба подхода в разных случаях:

У нас есть класс Customer с методом OrderProduct.

контрактный подход:

class Customer
{
     public void OrderProduct(Product product)
     {
           if ((m_credit - product.Price) < 0)
                  throw new NoCreditException("Not enough credit!");
           // do stuff 
     }
}

исключительный подход:

class Customer
{
     public bool OrderProduct(Product product)
     {
          if ((m_credit - product.Price) < 0)
                   return false;
          // do stuff
          return true;
     }
}

if !(customer.OrderProduct(product))
            Console.WriteLine("Not enough credit!");
else
   // go on with your life

Здесь я предпочитаю исключительный подход, поскольку это не является действительно исключительным, если у клиента нет денег, если он не выиграл в лотерею.

Но вот ситуация, в которой я ошибаюсь по поводу стиля контракта.

Исключительно:

class CarController
{
     // returns null if car creation failed.
     public Car CreateCar(string model)
     {
         // something went wrong, wrong model
         return null;
     }
 }

Когда я вызываю метод под названием CreateCar, я, черт возьми, ожидаю экземпляр Car вместо какого-то паршивого нулевого указателя, который может разрушить мой работающий код на дюжину строк позже. Таким образом, я предпочитаю контракт этому:

class CarController
{
     
     public Car CreateCar(string model)
     {
         // something went wrong, wrong model
         throw new CarModelNotKnownException("Model unkown");

         return new Car();
     }
 }

Какой стиль вы используете? Как вы думаете, какой общий подход к исключениям является лучшим?

Знайте свои исключения!
Знайте свои исключения!
В Java исключение - это событие, возникающее во время выполнения программы, которое нарушает нормальный ход выполнения инструкций программы. Когда...
Управление ответами api для исключений на Symfony с помощью KernelEvents
Управление ответами api для исключений на Symfony с помощью KernelEvents
Много раз при создании api нам нужно возвращать клиентам разные ответы в зависимости от возникшего исключения.
12
0
857
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

Мой обычный подход - использовать контракт для обработки любых ошибок из-за вызова «клиента», то есть из-за внешней ошибки (например, ArgumentNullException).

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

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

Ответ принят как подходящий

Я предпочитаю то, что вы называете «контрактным» подходом. Возвращать пустые значения или другие специальные значения, указывающие на ошибки, необязательно для языка, поддерживающего исключения. Мне кажется, что код намного легче понять, если в нем нет кучи предложений «if (result == NULL)» или «if (result == -1)», смешанных с тем, что может быть очень простой и понятной логикой.

+1 для вашего примера возврата результата null или -1. Вместо того, чтобы сделать код более понятным, возврат null или -1 заставляет автора вызывающего метода знать, как вызываемый метод выбирает сообщить нестандартный результат. Исключением является более чистый контрактный подход, в котором вызывающий может рассматривать вызываемый объект как (возможно) изменяющийся черный ящик. Все, что вызывает вызывающий, должен знать, что он должен будет обработать исключение, а не то, что оно имеет значение null сегодня и Integer.MIN_VALUE завтра.

rajah9 01.10.2013 21:22

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

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

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

TValue GetValue(TKey Key);
bool TryGetValue(TKey Key, ref TValue value);

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

 // In case of failure, set ok false and return default<TValue>.
TValue TryGetResult(ref bool ok, TParam param);
// In case of failure, indicate particular problem in GetKeyErrorInfo
// and return default<TValue>.
TValue TryGetResult(ref GetKeyErrorInfo errorInfo, ref TParam param);

Обратите внимание, что использование в интерфейсе чего-то вроде обычного шаблона TryGetResult сделает интерфейс инвариантным по отношению к типу результата; использование одного из вышеперечисленных шаблонов позволит интерфейсу быть ковариантным по отношению к типу результата. Кроме того, это позволит использовать результат в объявлении var:

  var myThingResult = myThing.TryGetSomeValue(ref ok, whatever);
  if (ok) { do_whatever }

Не совсем стандартный подход, но в некоторых случаях преимущества могут его оправдать.

Другие вопросы по теме