Недавно проводил собеседование. На тестовое задание вместе выбрали сделать консольный калькулятор. Суть работы в том, что вы вводите первый операнд, операцию и второй операнд.
> calculator.exe 1 + 2После чего должен показаться результат.
Мы сели вместе за компьютер и начали конечно же с тестов. Постепенно вырисовывалась логика приложения. Мы решили, что у нас будет объект AgrumentParser, который принимает массив аргументов командной строки в конструкторе и проверяете их. Потом у этого класса можно узнать значение первого операнда, операцию, которую он уже преобразовал в enum, и второй операнд.
1: public enum OperationType
2: {
3: None = 0,
4: Add,
5: Min,
6: Plus,
7: Div
8: }
9:
10: public class ArgumentParser
11: {
12: private double firstOperand;
13: private double secondOperand;
14: private OperationType operationType;
15:
16: public ArgumentParser(string[] args)
17: {
18: // заполняем переменные firstOperand, secondOperand и operationType из args
19: }
20:
21: public double FirstOperand
22: {
23: get { return firstOperand; }
24: }
25:
26: public OperationType OperationType
27: {
28: get { return operationType; }
29: }
30:
31: public double SecondOperand
32: {
33: get { return secondOperand; }
34: }
35: }
К тому же мы завели очень интересный спор. Суть его вот в чем - если пользователь ввел аргументы в неверном формате (например, 1 плюс 2), как вести себя классу, который "оборачивает" эти аргументы? Если аргументы неверные, то как можно обратиться к полю FirstOperand? Должны мы выбрасывать исключение или же добавить поле IsArgumentValid в класс ArgumentParser?
Здесь нам виделось 2 основных подхода:
- Т.к. состояние объекта после передачи ему неверных аргументов будет не определено, то сам объект должен выбрасывать исключение. Соответственно его клиент, должен ловить это исключение и обрабатывать ошибку.
- После того, как создан класс ArgumentParser из параметров командной строки, мы должны узнать у него на сколько эти параметры верны, т.е. вызвать у него свойство IsArgumentValid.
В принципе, оба подхода вполне приемлемы для такого маленького приложения. Но если задумать о разработке модулей в общем. Как нужно строить логику своего приложения? Неужели опираясь на виды исключительных ситуаций?
В данном примере это абсолютно оправдано:
1: using (var adapter = new DataAccessAdapter(false))
2: {
3: try
4: {
5: adapter.SaveEntity(entity, false, false);
6: }
7: catch
8: {
9: adapter.Rollback();
10: throw;
11: }
12: adapter.Commit();
13: }
А что если ваш модуль при обращении к нему может выбрасывать свои определнные исключения? Как работать с такой "исключительной" логикой?
Так:
1: Customer customer = CustomerFactory.Create();
2: if (customer.IsLoaded)
3: {
4: // обычная логика
5: }
6: else
7: {
8: // альтернативная логика
9: }
или так:
1: try
2: {
3: Customer customer = CustomerFactory.Create();
4: // обычная логика
5: }
6: catch(NotLoadedException)
7: {
8: // альтернативная логика
9: }
Возвращаясь к примеру с консольным калькулятором.
Первый вариант:
1: var parser = new ArgumentParser(args);
2: if (parser.IsArgumentValid)
3: {
4: // работаем с аргументами и производим операцию выбранную пользователем
5: }
6: else
7: {
8: // показываем пользователю формат работы с нашим приложением
9: }
Второй вариант:
1: try
2: {
3: var parser = new ArgumentParser(args);
4: // работаем с аргументами и производим операцию выбранную пользователем
5: }
6: catch(ArgumentException)
7: {
8: // показываем пользователю формат работы с нашим приложением
9: }
Лично я склоняюсь к первому варианту. Какие будут мысли?
Примечание
Crypto привел отличный аргумент против логики построенной на исключениях.
А я вот больше склоняюсь ко второму варианту. Если посмотреть на эволюцию, то от первого подхода (ErrCode и др.) пришли ко второму.
ОтветитьУдалитьВсе-таки ситуации, когда исключения заметно снижают производительность, редки. Для этих случаев рядом с Parse можно реализовать TryParse.
Интересно узнать твое мнение на этот счет 2 года спустя.
@Idsa
ОтветитьУдалитьМне тоже было интересно перечитать этот пост.
Я думаю, что исключения должны кидаться, когда программа сделала критическую ошибку и дальше работать не может (повреждены данные, нарушена целостность в БД и т.п.). Как было написано в книжке Программист-пракматик: "Мёртвые программы не врут".
Т.е. в случае парсера метод TryParse или возврат объекта в качестве результата выглядит куда более привлекательней.
Я за первый :)
Я за второй :)
ОтветитьУдалитьНесколько аргументов:
1. Исключения нельзя проигнорировать. Ты можешь забыть обработать ErrorCode, и приложение продолжит работать, делая вид, что ошибки нет (отладка в таком случае получится адской). Исключение в этом случае просто убьет приложение (и правильно сделает). К тому же вероятность того, что за время разработки исключение останется без соответствующего обработчика гораздо ниже чем аналогичная ситуация с ErrorCode
2. Исключения позволяют обрабатывать ошибки в конструкторах и в перегруженных операторах
3. Исключения более гибки и функциональны. Мы можем узнать, где и при каких обстоятельствах произошло исключение.
4. Если исключения генерируются редко, думаю, их использование может быть даже эффективнее этих бесконечных проверок ErrorCode
5. Мне кажется, в рамках большого проекта код с исключениями поддерживать проще: они стандартизированы (не так часто нужно создавать свои исключения; а если и нужно, то они наследуются от стандартных), блок try-catch более декларативен (если так можно выразиться).
Если этих аргументов недостаточно, я еще вернусь :)
В целом, считаю, что основным должен быть второй подход. Первый подход в основном применим в коде, в котором может генерироваться действительно много исключений.
Этот комментарий был удален автором.
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалитьДень добрый. Александр, прошел год, мнение не изменилось?
ОтветитьУдалитьКак по мне, то я предпочитаю второй вариант с исключеними, т.о. избавиться от множества проверок + практически сводится к нулю вероятность работы с нарушенной целостностью.
Действительно прошло много времени, перечитал свою статью и понял, что сейчас я уже не 100% на стороне первого варианта.
ОтветитьУдалитьТем не менее я бы не стал часто использовать вариант с исключениями. В данный момент я склоняюсь к способу, который используется в функциях TryParse, когда результат сразу понятен. Еще возможен вариант с созданием NullObject или как вариация InvalidObject через билдер, когда аргументы неверные, зависит от ситуации.
Александр привет. Допустим есть класс треугольник. В конструкторе задаются 3 числа.
ОтветитьУдалитьНапрашивается валидация: все значения больше нуля, сумма двух не может быть меньше третьей стороны.
В данном случае как бы построил эту валидацию? Возможны варианты с билдером, котороый бы принимал параметры эти и выдавал инстанс треугольника, а конструктор сделать приватным к примеру, но в случае не валидных данных InvalidObject/Null Object не всегда можно вернуть, иначе выходит они должны быть наследниками некоего прародителя.
Все же пока меня вариант с exception'ами больше привлекает в связи с простотой дизайна. Хотя не все тут гладко тоже.