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

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

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

    Когда метод не делает то, что он говорит, что он будет делать в заголовке метода, он выдает исключение. Таким образом, метод "promises", что он выполнит операцию, и если он по какой-то причине сработает, он выдаст исключение.

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

    Бросьте исключения только тогда, когда произойдет что-то действительно странное. Вы не должны использовать исключения, когда можете разрешить ситуацию с помощью обычного потока управления (операторы 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();
     }
 }

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

Ответ 1

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

Ответ 2

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

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

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

Ответ 3

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

Ответ 5

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

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

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

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

 // 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 }

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