суббота, 29 июня 2013 г.

Принцип самурая или почему надо радоваться преждевременным ошибкам

Делаем игру с командой и часто инспектируем код друг друга.
Натолкнулся вот на это:(ссылка)


  1.   void Awake ()
  2.   {
  3.     saveDirPath = Application.dataPath + "/Levels/";
  4.     
  5.     if (buttonSave != null) {
  6.       UIEventListener.Get (buttonSave.gameObject).onClick += OnButtonSave_Click;
  7.     }
  8.     if (buttonLoad != null) {
  9.       UIEventListener.Get (buttonLoad.gameObject).onClick += OnButtonLoad_Click;
  10.     }
  11.     if (buttonInputDialogOk != null) {
  12.       UIEventListener.Get (buttonInputDialogOk.gameObject).onClick += OnButtonInputDialogOk_Click;
  13.     }
  14.     if (buttonInputDialogCancel != null) {
  15.       UIEventListener.Get (buttonInputDialogCancel.gameObject).onClick += OnButtonInputDialogCancel_Click;
  16.     }
  17.     if (buttonselectLevelDialogClose != null) {
  18.       UIEventListener.Get (buttonselectLevelDialogClose.gameObject).onClick += OnButtonselectLevelDialogClose_Click;
  19.     }
  20.   }
Коротко: есть кнопки, и при запуске подписываются обработчики. Все хорошо, код абсолютно безопасен. Но он плох. Почему?



Все очень просто. Если кто-нибудь забудет указать связь кнопки с кодом, то это значение будет равно null => подписка обработчиков не будет работать => обработчик никогда не вызовется.  То есть как-бы все хорошо, все работает, но по факту кнопки не работают, ничего не работает.

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

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


Как поступить в этом примере?
Первое, что приходит в голову - просто убрать проверки на null. Поскольку это делается в awake(то есть конструктор-методе Unity),

этот способ применим, потому что этот код вызовется сразу же, упадет c null-pointer-ом, мы сразу о нем узнаем. Но в общем случае, это родственник предыдущего кода.

Более общим и правильным решением является старый assert(ссылка), знакомый всем составителям задач:




  1.   void Awake ()
  2.   {
  3.     saveDirPath = Application.dataPath + "/Levels/";
  4.     
  5.     Tools.Assert (buttonSave != null, "buttonSave is not assigned");
  6.     UIEventListener.Get (buttonSave.gameObject).onClick += OnButtonSave_Click;
  7.   
  8.     Tools.Assert (buttonLoad != null, "buttonLoad is not assigned");
  9.     UIEventListener.Get (buttonLoad.gameObject).onClick += OnButtonLoad_Click;
  10.  
  11.     Tools.Assert (buttonInputDialogOk != null, "buttonInputDialogOk is not assigned");
  12.     UIEventListener.Get (buttonInputDialogOk.gameObject).onClick += OnButtonInputDialogOk_Click;
  13.  
  14.     Tools.Assert (buttonInputDialogCancel != null, "buttonImputDialogCancel is not assigned");
  15.     UIEventListener.Get (buttonInputDialogCancel.gameObject).onClick += OnButtonInputDialogCancel_Click;
  16.  
  17.     Tools.Assert (buttonselectLevelDialogClose != null, "buttonselectLevelDialogClose is not assigned");
  18.     UIEventListener.Get (buttonselectLevelDialogClose.gameObject).onClick += OnButtonselectLevelDialogClose_Click;
  19.   }
  20.  
  21. //in Tools class
  22.   public static void Assert (bool condition, string message)
  23.   {
  24. #if UNITY_EDITOR
  25.     if (!condition) {
  26.       throw new UnityException (message);
  27.     }
  28. #endif
  29.   }
Значит ли это, что надо везде придерживаться предусловий и постусловий? И да, и нет. Конечно, можно начинать любой метод с кучи проверок на случай ядерной войны. Можно поступить лучше: для каждого метода должно быть объявлено некое соглашение - контракт, на основании которого он работает. Если по контракту обработка неправильного поведения происходит на уровне интерфейсов, тогда функция, у которой core обязанности, никогда не примет невалидные данные.

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

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