Имя пользователя:

Пароль:


Список форумов ПРАКТИЧЕСКИЕ ВОПРОСЫ Работа Программирование и IT Просмотров: 745

Элегантное программирование: Обрабатывайте невозможные значения (defensive programming)


Давим клаву за бабло
  #1
Сообщение 29 Dec 2013, 12:26
Ursego Аватара пользователя
СОЗДАТЕЛЬ ТЕМЫ
Canada, Ontario
Город: Toronto
Стаж: 11 лет 6 месяцев 26 дней
Постов: 10707
Лайкнули: 3448 раз
Карма: 33%
СССР: Днепропетровск
Пол: М
Лучше обращаться на: ты
Заход: 20 Nov 2023, 18:00

Конструкции ветвления кода (choose case/switch ) должны обрабатывать все существующие опции. Если этой конструкции передана опция, которую обработать невозможно, необходимо прервать программу аварийно и показать сообщение о технической ошибке.

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

Для этого необходимо сделать две вещи:

1. В конструкции ветвления (switch) должны быть обработаны абсолютно все опции, существующие в системе в настоящий момент. Если какие-то опции не требуют специальной обработки, создайте для них case-пустышку с комментарием "do nothing", "irrelevant" или "not applicable" чтоб читающие код поняли, что так задумано.

2. Добавьте секцию default - её задачей будет просигнализировать об опции, у которой нет обработчика (запустить exception, высветить сообщение об ошибке - смотря как ваша система обрабатывает нештатные ситуации).

Взгляните на два следующих фрагмента. В первом примере я использую 4 статуса клиента фирмы: Active, Pending, Inactive and Deleted, но только статусы Active и Pending обрабатываются в бизнес-логике:

*** Не рекомендуется (статусы Inactive и Pending даже не упомянуты...): ***

switch (custStatus)
{
   case CustStatus.Active:
      [code fragment processing active customers]
      break;
   case CustStatus.Pending:
      [code fragment processing pending customers]
      break;
}

*** Рекомендуется: ***

switch (custStatus)
{
   case CustStatus.Active:
      [code fragment processing active customers]
      break;
   case CustStatus.Pending:
      [code fragment processing pending customers]
      break;
   case CustStatus.Inactive:
   case CustStatus.Deleted:
      // do nothing
      break;
   default:
      throw new Exception ("No case defined for customer status " + custStatus);
}

Так что если новый статус (например, Deceased, не про нас будь сказано...) будет добавлен в бизнес в будущем (даже через много лет!), фрагмент кода сам заставит программистов подумать как поступить. Причём это произойдёт на довольно ранней стадии разработки или юнит-теста! Но даже если ошибка выскочит в работающей системе (продакшене), это лучше, чем жить со скрытым багом (который потенциально может дорого обойтись). Может, новый статус должен быть обработан специальным образом и программисту следует решить как именно (или посоветоваться с бизнес-аналитиком). А если специальной обработки не требуется, то новый статус нужно поместить в секцию с "do nothing".

Никогда не пишите бизнес-код в секции default - она предназначена только для перехвата ошибок!

Описанный способ имеет также дополнительные преимущества:

1. Если требуется найти все места в программе, где используется некое значение (константа или enumerated), то текстовый поиск по системе найдёт ВСЕ фрагменты. Это было бы не так если б бизнес-код был в секциях default (когда название константы не упомянуто) - тогда у Вас был бы хороший шанс кое-что прошляпить...

2. Глядя на конструкцию switch программист видит полную картину, а не её фрагмент, и не должен угадывать при каких обстоятельствах программа идёт в секцию else/default. Как говорится, полуправда - хуже лжи...

Мы обсудили конструкцию switch, но та-же концепция работает и для if-ов. Чаще всего мы должны заменить if на switch чтобы предотвратить чудовищное скопление else if-ов:

*** Не рекомендуется: ***

if (_currEntity == Entity.Car)
{
   this.Retrieve(_carId);
}
else
{
   this.Retrieve(_bikeId);
}

*** Рекомендуется (if заменён на switch): ***

switch (_currEntity)
{
   case Entity.Car:
      this.Retrieve(_carId);
      break;
   case Entity.Bike:
      this.Retrieve(_bikeId);
      break;
   default:
      throw new Exception ("No case defined for entity " + _currEntity);
}

Допустим, в какой-то ситуации может быть два режима и нам нужно действовать по-разному в каждом из них. Само число 2 наводит на мысль, что ситуацию можно прекрасно разрулить с помощью булевой переменной: true - один режим, false - второй, вроде всё прекрасно... Проиллюстрируем ситуацию с помощью класса, экземпляр которого может быть создан как для автомобиля ("автомобильный режим"), так и для мотоцикла ("мотоциклетный режим"). На первый взгляд можно создать логический переключатель _isCarMode и инициализировать его в зависимости от режима, а затем использовать в методах класса следующим образом:

*** Не рекомендуется: ***

if (_isCarMode)
{
   this.Retrieve(_carId);
}
else // Bike Mode
{
   this.Retrieve(_bikeId);
}

Однако лучшим решением в этой ситуации будет создать набор констант (или перечисляемый тип - enum), в котором для каждого режима имеется отдельная константа, и производить явное сравнение - так, как сделано в предыдущем фрагменте "Рекомендуется". Ну, о преимуществах этого подхода вы уже знаете.

Конечно, вся эта красота может быть неприменима в определённых ситуациях. Например, если одна из 20 опций имеет нестандартную обработку, то перечисление оставшихся 19-ти может показаться обременительным и загрязняющим код (просто послать программу в else намного легче), так что нужно думать как поступить в каждом конкретном случае.

Вам есть что сказать по этой теме? Зарегистрируйтесь, и сможете оставлять комментарии