Страницы

Поиск по вопросам

четверг, 18 октября 2018 г.

Как грамотно обработать деление на ноль?

Есть унаследованный класс:
public class Division : BinaryOperation { protected override decimal Calculate(decimal a, decimal b) { return a / b; }
public override string Name { get { return "Деление"; } } }
Как внутри него красиво и логично обработать ситуацию деления на ноль? Если обернуть в try-catch, что писать в catch? Допустим, я не знаю какие контролы будут на форме, и вообще, будет ли это использоваться в WinForms или WPF или что-то еще, чтобы куда то выводить текст Exception'а. Возможно есть еще какие-то хорошие варианты кроме try-catch?


Ответ

Сложность в "элегантной" обработке исключений заключается в том, что она сильно зависит от контекста.
Вот, например, в одном приложении, нужно выделить UI контрол и попросить пользователя ввести валидное значение. В другом приложении (если оно серверное), нужно бросить исключение, которое должно пересечь границы приложения тем или иным способом. А в третьем приложении, проверка аргументов происхоидт на более высоком уровне и передача 0 в параметр b является багом и должно привести к крэшу приложения.
Все это приводит к следующим возможностям:
Добавить предусловие в метод Calculate, что b != 0: Contract.Requires(b != 0). В этом случае мы переклаыдваем ответственность за валидацию кода на вызывающую сторону. Мы говорим, что 0 - это невалидное значение, и просим клиента нас не трогать в этом случае. Ничего не делать и пробрасывать DivideByZeroException. Еще одна альтернатива: задокументировать метод Calculate и "сказать", что из него может прилететь это исключение.
В этом случае мы опять же перекладываем ответственность на вызывающий код, который лучше знает, что с ним делать. Теперь клиент может выдать сообщение, отправить нужный ответ клиенту или преобразовать это исключение в какое-то другое.
Добавить новый уровень косвенности. Еще, можно добавить специальный тип исключения, например, CalculationException или OperationException. В этом случае, новый тип исключения будет описывать семейство ошибок, которое может произойти при работе с абстрактной операцией.
Это решение является более расширяемым, поскольку новые наследники могут бросать новые и новые типы исключений, а конкретный тип исключения может содержать более специфичную информацию:
public abstract class OperationException { }
public class DivideByZeroOperationException : OperationException {}
public class Division : BinaryOperation { protected override decimal Calculate(decimal a, decimal b) { if (b == 0) {throw new DivideByZeroOperationException();}
return a / b; } }
Теперь клиент может решить, нужно ли ему обрабатывать базовое исключение или же более специфичное.
Еще одна альтернатива, вернуться к умным кодам возврата и уйти от исклчюений.
Пример:
public enum Error { DivideByZero, YetAnotherError }
// Это такой себе 'Either' тип, который может содержать либо ошибку (код и, // возможно, сообщение), либо валидный результат public class OperationResult { public decimal? Result {get;} public Error? Error {get;}
public static OperationResult Success(decimal result) { }
public static OperationResult Failure(Error error) { // Может быть стоит добавить сообщение или другую контекстную инфомрацию } }
public class Division : BinaryOperation { protected override OperationResult Calculate(decimal a, decimal b) { if (b == 0) {return OperationResult.Error(Error.DivideByZero);} return a / b; }
}
Это более "функциональный" подход, в то время, как предыдущие являются более каноническими в ОО языках, таких как C#.
Главное заключение: не существует вменяемого способа полностью обработать исключение внутри метода Calculate. Информация об ошибке должна быть передана вызывающему коду, поскольку только на более высоком уровне существует достаточно инфомрации (контекста), чтобы обработать эту проблему полноценным образом.

Комментариев нет:

Отправить комментарий