Подтвердить что ты не робот

Обработка ошибок без исключений

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

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

Цитата:

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

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

Например, правильное использование:

private void DoSomething(string requiredParameter)
{
if (requiredParameter == null) throw new ArgumentExpcetion("requiredParameter cannot be null");
// Remainder of method body...
}

Неправильное использование:

// Renames item to a name supplied by the user.  Name must begin with an "F".
public void RenameItem(string newName)
{
   // Items must have names that begin with "F"
   if (!newName.StartsWith("F")) throw new RenameException("New name must begin with /"F/"");
   // Remainder of method body...
}

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

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

  • После возврата метода нумерованный результат? RenameResult.Success, RenameResult.TooShort, RenameResult.TooLong, RenameResult.InvalidCharacters и т.д.

  • Использование события в классе контроллера отчитываться перед классом пользовательского интерфейса? Пользовательский интерфейс вызывает контроллера RenameItem, а затем обрабатывает Событие AfterRename, которое контроллер поднимает и который переименовал статус как часть событий args?

  • Контрольный класс напрямую ссылается и вызывает метод из класса пользовательского интерфейса, который обрабатывает ошибку, например. ReportError (текст строки).

  • Что-то еще...?

По сути, я хочу знать, как выполнять сложную проверку в классах, которые могут быть не самим классом Form, и передавать ошибки обратно в класс Form для отображения, - но я не хочу включать обработку исключений, где он должен не используется (хотя это кажется намного проще!)


Основываясь на ответах на вопрос, я чувствую, что мне придется сформулировать проблему в более конкретных формулировках:

UI = Пользовательский интерфейс, BLL = Бизнес-логический уровень (в данном случае просто другой класс)

  • Пользователь вводит значение в пользовательский интерфейс.
  • Пользовательский интерфейс сообщает значение BLL.
  • BLL выполняет рутинную проверку значения.
  • BLL обнаруживает нарушение правил.
  • BLL возвращает нарушение правил в пользовательский интерфейс.
  • UI получает возврат от BLL и сообщает об ошибке пользователю.

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

4b9b3361

Ответ 1

Пример, который вы даете, - это входные данные для проверки UI.

Поэтому хороший подход состоит в том, чтобы отделить валидацию от действия. WinForms имеет встроенную систему проверки, но в принципе она работает в следующих направлениях:

ValidationResult v = ValidateName(string newName);
if (v == ValidationResult.NameOk)
    SetName(newName);
else
    ReportErrorAndAskUserToRetry(...);

Кроме того, вы можете применить проверку в методе SetName, чтобы убедиться, что проверка достоверности была проверена:

public void SetName(string newName)
{
    if (ValidateName(newName) != ValidationResult.NameOk)
        throw new InvalidOperationException("name has not been correctly validated");

    name = newName;
}

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

Чтобы процитировать другой ответ:

Either a member fulfills its contract or it throws an exception. Period.

То, что это пропустило: что такое контракт? В "договоре" вполне разумно указать, что метод возвращает значение статуса. например File.Exists() возвращает код состояния, а не исключение, потому что это его контракт.

Однако ваш пример отличается. В нем вы фактически выполняете два отдельных действия: валидация и хранение. Если SetName может либо вернуть код состояния, либо установить имя, он пытается выполнить две задачи в одном, что означает, что вызывающий абонент никогда не знает, какое поведение он будет выставлять, и должен иметь специальную обработку дела для этих случаев. Однако, если вы разделите SetName на отдельные шаги Validate and Store, то контракт для StoreName может заключаться в том, что вы передаете действительные входы (переданные ValidateName), и он выдает исключение, если этот контракт не выполняется. Поскольку каждый метод затем делает только одно и только одно, контракт очень ясен, и это очевидно, когда нужно исключать исключение.

Ответ 2

Я думаю, что вы неправильно поняли сообщение. Вот отличная цитата, с которой я столкнулся вчера из текущей версии журнала Visual Studio (том 19, № 8).

Либо член выполняет свой контракт, либо лишает его возможности. Период. Нет среднего места. Нет кодов возврата, иногда это не срабатывает, иногда это не так.

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

Ответ 3

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

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

  • Исключения могут быть сериализованы
  • Исключения всегда имеют свойство Message, которое читается человеком и может иметь дополнительные свойства для записи сведений об исключении в машиночитаемой форме.
  • Некоторые из сбоев бизнес-правил могут быть, например, отмечены исключениями - a FormatException, например. Вы можете поймать это исключение и добавить его в список.

Фактически, в этом месяце в журнале MSDN есть статья, в которой упоминается новый класс AggregateException в .NET 4.0, который предназначен для сбора исключений, которые произошли в определенном контексте.


Поскольку вы используете Windows Forms, вы должны использовать встроенные механизмы для проверки: событие Validating и компонент ErrorProvider.

Ответ 4

Я согласен с предложением Хенка.

Традиционно операции "pass/fail" выполнялись как функции с целым числом или возвращаемым типом bool, который указывал бы результат вызова. Однако некоторые противятся этому, заявляя, что "функция или метод должны либо выполнять действие, либо возвращать значение, но не оба". В других словах memeber класса, который возвращает значение, также не должен быть членом класса, который изменяет состояние объекта.

Я нашел лучшее решение для добавления свойства .HasErrors/.IsValid и .Errors в классе, который генерирует ошибки. Первые два свойства позволяют классу клиента тестировать или нет ошибок и, если необходимо, также могут читать свойство .Errors и сообщать о одной или всех ошибках. Затем каждый метод должен знать об этих свойствах и надлежащим образом управлять состоянием ошибки. Эти свойства затем могут быть перенесены в интерфейс IErrorReporting, который могут включать в себя различные классы фасадов уровня бизнес-правил.

Ответ 5

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

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

Я думал долго и упорно, и пришел к этому решению. Выполните следующие действия в классах домена:

  • Добавить объект: ThrowsOnBusinessRule. Значение по умолчанию должно быть истинным для исключения исключений. Установите его в значение false, если вы не хотите его бросать.
  • Добавить личную коллекцию словаря для хранения исключений с ключом как свойство домена, которое имеет нарушение бизнес-правил. (Конечно, вы можете публиковать это публично, если хотите)
  • Добавить метод: ThrowsBusinessRule (string propertyName, Exception e) для обработки вышеуказанной логики
  • Если вы хотите, вы можете реализовать IDataErrorInfo и использовать коллекцию Dictionary. Внедрение IDataErrorInfo является тривиальным с учетом вышеприведенной настройки.

Ответ 6

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

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

Исключения существуют, чтобы остановить "плохие вещи", они действуют как защита последней строки от ваших вызовов API, чтобы гарантировать, что ошибки будут остановлены и что может иметь место только правовое поведение. Они являются ошибками на уровне программистов и должны только когда-либо указывать одно из следующих:

  • Произошло что-то катастрофическое и системное, например, нехватка памяти.
  • Программист пошел и запрограммировал что-то неправильно, будь то плохой вызов метода или части незаконного SQL.

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

Ответ 7

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

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

Например.

  1. Проверьте, есть ли запись в БД для обновления. - запись может не существовать
  2. Проверьте поля для обновления. - новые значения могут быть недействительными
  3. Обновите запись базы данных. - БД может вызвать дополнительные ошибки

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

public class Result<T> {
  public T Value { get; }
  public string Error { get; }

  public Result(T value) => Value = value;
  public Result(string error) => Error = error;

  public bool HasError() => Error != null;
  public bool Ok() => !HasError();
}

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

public Result<Record> FindRecord(int id) {
  var record = Records.Find(id);
  if (record == null) return new Result<Record>("Record not found");
  return new Result<Record>(record);
}

public Results<bool> RecordIsValid(Record record) {
  var validator = new Validator<Record>(record);
  if (validator.IsValid()) return new Result<bool>(true);
  return new Result<bool>(validator.ErrorMessages);
}

public Result<bool> UpdateRecord(Record record) {
  try {
    Records.Update(record);
    Records.Save();
    return new Result<bool>(true);
  }
  catch (DbUpdateException e) {
    new Result<bool>(e.Message);
  }
}

Теперь для нашего агрегатного метода, который связывает все это вместе:

public Result<bool> UpdateRecord(int id, Record record) {
  if (id != record.ID) return new Result<bool>("ID of record cannot be modified");

  var dbRecordResults = FindRecord(id);
  if (dbRecordResults.HasError())
    return new Result<bool>(dbRecordResults.Error);

  var validationResults = RecordIsValid(record);
  if (validationResults.HasError())
    return validationResults;

  var updateResult = UpdateRecord(record);
  return updateResult;
}

Вот Это Да! какой беспорядок!

Мы можем сделать еще один шаг вперед. Мы можем создать подклассы Result<T> чтобы указать конкретные типы ошибок:

public class ValidationError : Result<bool> {
  public ValidationError(string validationError) : base(validationError) {}
}

public class RecordNotFound: Result<Record> {
  public RecordNotFound(int id) : base($"Record not found: ID = {id}") {}
}

public class DbUpdateError : Result<bool> {
  public DbUpdateError(DbUpdateException e) : base(e.Message) {}
}

Затем мы можем проверить на конкретные случаи ошибок:

var result = UpdateRecord(id, record);
if (result is RecordNotFound) return NotFound();
if (result is ValidationError) return UnprocessableEntity(result.Error);
if (result.HasError()) return UnprocessableEntity(result.Error);
return Ok(result.Value);

Однако в приведенном выше примере result is RecordNotFound всегда будет возвращать false, поскольку это Result<Record>, тогда как UpdateRecord(id, record) возвращает Result<bool>.

Некоторые плюсы: * Он в основном работает * Он избегает исключений * Он возвращает приятные сообщения, когда что-то не так * Класс Result<T> может быть настолько сложным, насколько это необходимо. Например, возможно, он может обработать массив сообщений об ошибках в случае сообщений об ошибках проверки. * Подклассы Result<T> могут использоваться, чтобы указать на распространенные ошибки

Минусы: * Есть проблемы с конверсией, где T может быть другим. например. Result<T> и Result<Record> * Методы теперь делают несколько вещей, обработку ошибок и то, что они должны делать * Его чрезвычайно многословные * Агрегатные методы, такие как UpdateRecord(int, Record) теперь должны UpdateRecord(int, Record) к они сами с результатами методов, которые они вызывают.

Теперь использую исключения...

public class ValidationException : Exception {
  public ValidationException(string message) : base(message) {}
}

public class RecordNotFoundException : Exception  {
  public RecordNotFoundException (int id) : base($"Record not found: ID = {id}") {}
}

public class IdMisMatchException : Exception {
  public IdMisMatchException(string message) : base(message) {}
}

public Record FindRecord(int id) {
  var record = Records.Find(id);
  if (record == null) throw new RecordNotFoundException("Record not found");
  return record;
}

public bool RecordIsValid(Record record) {
  var validator = new Validator<Record>(record);
  if (!validator.IsValid()) throw new ValidationException(validator.ErrorMessages)
  return true;
}

public bool UpdateRecord(Record record) {
  Records.Update(record);
  Records.Save();
  return true;
}

public bool UpdateRecord(int id, Record record) {
  if (id != record.ID) throw new IdMisMatchException("ID of record cannot be modified");

  FindRecord(id);
  RecordIsValid(record);
  UpdateRecord(record);
  return true;
}

Тогда в действии контроллера:

try {
  UpdateRecord(id, record)
  return Ok(record);
}
catch (RecordNotFoundException) { return NotFound(); }
// ...

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

Я не уверен, что минусы... люди говорят, что они как GOTO... не совсем уверены, почему это плохо... они также говорят, что производительность плохая... но что с того? Как это соотносится с выполняемыми вызовами БД? Я не уверен, являются ли негативы действительно вескими причинами.