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

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

  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();
     }
 }

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

25.08.2008 16:52:23
5 ОТВЕТОВ
РЕШЕНИЕ

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

6
25.08.2008 17:01:07
+1 для вашего примера возврата нулевого или -1 результата. Вместо того, чтобы сделать код более понятным, возвращаемое значение null или -1 заставляет автора вызывающего метода узнать, как вызываемый метод выбирает сообщить о нестандартном результате. Исключением является более чистый, контрактный подход, при котором инициатор может рассматривать вызванный как (возможно) изменяющийся черный ящик. Все, кто знает, должен знать, что он должен обработать исключение, а не то, что сегодня он равен нулю, а завтра Integer.MIN_VALUE.
rajah9 1.10.2013 17:22:49

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

0
25.08.2008 16:59:43

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

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

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

1
25.08.2008 17:00:45

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

0
25.08.2008 18:13:44

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

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

TValue GetValue (клавиша TKey);
bool TryGetValue (ключ TKey, значение ref TValue);

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

 // В случае сбоя установить ok false и вернуть значение по умолчанию <TValue>.
TValue TryGetResult (ref bool ok, параметр TParam);
// В случае сбоя укажите конкретную проблему в GetKeyErrorInfo
// и вернуть значение по умолчанию <TValue>.
TValue TryGetResult (ref GetKeyErrorInfo errorInfo, ref TParam param);

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

  var myThingResult = myThing.TryGetSomeValue (ref ok, что угодно);
  if (ok) {do_whwhat}

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

0
22.09.2011 15:43:02