Спробуйте / Ловити / Вхід / Знову - Анти шаблон?


19

Я бачу декілька постів, де важливість обробки винятку в центральному місці чи на межі процесу підкреслювалася як хороша практика, а не засмічення кожного блоку коду навколо try / catch. Я твердо переконаний, що більшість з нас розуміє важливість цього, проте я бачу, що люди все ще закінчуються анти-схемою catch-log-rethrow, головним чином тому, що для полегшення усунення несправностей під час будь-якого винятку вони хочуть записати більше конкретної інформації (наприклад: параметри методу пройшов), і спосіб полягає в обгортанні методу try / catch / log / rethrow.

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        logger.log("error occured while number 1 = {num1} and number 2 = {num2}"); 
        throw;
    }
}

Чи є правильний спосіб досягти цього, зберігаючи при цьому добру практику винятків? Я чув про такі рамки AOP, як PostSharp, але хотів би знати, чи є якісь недоліки чи основні витрати на продуктивність, пов'язані з цими рамками AOP.

Спасибі!


6
Існує величезна різниця між загортанням кожного методу в спробі / ловити, реєструвати винятки та дозволяти коду чугувати. І спробуйте / зловити та скинути виняток з додатковою інформацією. По-перше, це жахлива практика. По-друге, це прекрасний спосіб поліпшити ваш досвід налагодження.
Ейфорія

Я кажу, спробуйте / ловити кожен метод і просто увійдіть у блок блоку лову та повторне скидання - це нормально робити?
rahulaga_dev

2
Як вказував Амон. Якщо у вашій мові є стеки-сліди, входити в кожний улов безглуздо. Але обробку винятку та додавання додаткової інформації є хорошою практикою.
Ейфорія

1
Дивіться відповідь @ Liath. Будь-яка відповідь, яку я дав би, дуже віддзеркалює його: виловлюйте винятки якомога раніше, і якщо все, що ви можете зробити на цьому етапі, введіть корисну інформацію, тоді зробіть це та повторно киньте. Розглядати це як антипатерн - це безглуздо, на мій погляд.
Девід Арно

1
Liath: додано невеликий фрагмент коду. Я використовую c #
rahulaga_dev

Відповіді:


19

Проблема - не локальний блок лову, проблема - журнал та повторне скидання . Або обробляйте виняток, або обшивайте його новим винятком, який додає додатковий контекст і кине його. Інакше ви зіткнетеся з декількома повторюваними записами журналу за одним винятком.

Ідея тут - розширити можливість налагодження програми.

Приклад №1: Обробіть це

try
{
    doSomething();
}
catch (Exception e)
{
    log.Info("Couldn't do something", e);
    doSomethingElse();
}

Якщо ви обробляєте виняток, ви можете легко зменшити важливість запису журналу винятків, і немає ніяких причин переробляти цей виняток в ланцюзі. Це вже вирішено.

Поводження з винятком може включати в себе інформування користувачів про те, що сталася проблема, реєстрацію події або просто її ігнорування.

ПРИМІТКА. Якщо ви навмисно ігноруєте виняток, рекомендую надати коментар у пустому пункті вилову, який чітко вказує чому. Це дає майбутнім обслуговуючим особам знати, що це не помилка чи ліниве програмування. Приклад:

try
{
    context.DrawLine(x1,y1, x2,y2);
}
catch (OutOfMemoryException)
{
    // WinForms throws OutOfMemory if the figure you are attempting to
    // draw takes up less than one pixel (true story)
}

Приклад №2: Додайте додатковий контекст і киньте

try
{
    doSomething(line);
}
catch (Exception e)
{
    throw new MyApplicationException(filename, line, e);
}

Додавання додаткового контексту (наприклад, номер рядка та ім'я файлу в коді розбору) може допомогти розширити можливість налагодження вхідних файлів - якщо припустити, що проблема є. Це начебто окремий випадок, тому повторне обговорення виключення у "ApplicationException" просто для ребрендингу не допоможе вам налагодити. Обов’язково додайте додаткову інформацію.

Приклад №3: Не робіть нічого за винятком

try
{
    doSomething();
}
finally
{
   // cleanup resources but let the exception percolate
}

У цьому остаточному випадку ви просто дозволяєте винятку вийти, не торкаючись його. Обробник винятків на самому зовнішньому шарі може обробляти журнал. Ця finallyстаття використовується для забезпечення очищення будь-яких ресурсів, необхідних вашому методу, але це не місце для реєстрації того, що викид було викинуто.


Мені сподобалось " Проблема не локальний блок лову, проблема - журнал та повторне скидання ". Я думаю, що це має ідеальний сенс для забезпечення більш чистого ведення журналів. Але в кінцевому підсумку це також означає бути в порядку, якщо спробувати / ловити розповсюджуються на всі методи, чи не так? Я думаю, що має бути певна настанова для того, щоб цю практику дотримуватися розумно, а не застосовувати кожен метод.
rahulaga_dev

Я дав настанову у своїй відповіді. Це не відповідає на ваші запитання?
Берін Лорич

@rahulaga_dev Я не думаю, що існує настанова / срібна куля, тому вирішіть це питання, оскільки це дуже залежить від контексту. Не може бути загального керівництва, яке вказує вам, де обробляти виняток або коли його повторно скинути. ІМО, єдине керівництво, яке я бачу, - це відкласти журнал / обробку на останній можливий час і уникати входу в код, який може використовуватись повторно, щоб не створювати зайвих залежностей. Користувачів вашого коду не надто розвеселить, якщо ви реєстрували речі (тобто обробляли винятки), не даючи їм можливості обробляти їх по-своєму. Всього два мої центи :)
andreee

7

Я не вірю, що локальний улов - це анти-шаблон, адже якщо я правильно пам’ятаю, він фактично застосовується на Java!

Що для мене ключове при реалізації помилок - це загальна стратегія. Можливо, вам потрібен фільтр, який фіксує всі винятки на кордоні служби, ви можете вручну їх перехопити - обидва є нормальними, доки існує загальна стратегія, яка буде входити у стандарти кодування ваших команд.

Особисто мені подобається вловлювати помилки всередині функції, коли я можу зробити одне з наступного:

  • Додайте контекстну інформацію (наприклад, про стан об'єктів або що відбувається)
  • Керуйте винятком безпечно (наприклад, метод TryX)
  • Ваша система перетинає межу обслуговування та переходить у зовнішню бібліотеку чи API
  • Ви хочете зловити та повторно скинути виняток іншого типу (можливо, з оригіналом як внутрішнім винятком)
  • Виняток було закинуто як частина фонового функціонування низького значення

Якщо це не один із цих випадків, я не додаю локальний спробу / улов. Якщо це так, залежно від сценарію, я можу обробляти виняток (наприклад, метод TryX, який повертає помилку) або повторно скинути, тому виняток буде оброблятися глобальною стратегією.

Наприклад:

public bool TryConnectToDatabase()
{
  try
  {
    this.ConnectToDatabase(_databaseType); // this method will throw if it fails to connect
    return true;
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    return false;
  }
}

Або приклад переосмислення:

public IDbConnection ConnectToDatabase()
{
  try
  {
    // connect to the database and return the connection, will throw if the connection cannot be made
  }
  catch(Exception ex)
  {
     this.Logger.Error(ex, "There was an error connecting to the database, the databaseType was {0}", _databaseType);
    throw;
  }
}

Тоді ви виявляєте помилку у верхній частині стеку та представляєте приємне користувачеві повідомлення.

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

Ви не згадали, на якій мові працюєте, але як розробник .NET і бачили це занадто багато разів, щоб не згадувати про це.

НЕ ПИШИ:

catch(Exception ex)
{
  throw ex;
}

Використання:

catch(Exception ex)
{
  throw;
}

Перший скидає стежку стека і робить ваш ловець верхнього рівня абсолютно марним!

TLDR

Ловля локально не є антидією, вона часто може бути частиною дизайну і може допомогти додати додатковий контекст до помилки.


3
Який сенс входу в улов, коли той же реєстратор буде використовуватися в обробці винятків верхнього рівня?
Ейфорія

У вас може бути додаткова інформація (наприклад, локальні змінні), до якої у вас не буде доступу у верхній частині стека. Я оновлю приклад для ілюстрації.
Ліат

2
У цьому випадку киньте новий виняток із додатковими даними та внутрішнім винятком.
Ейфорія

2
@Euphoric так, я теж бачив, що зроблено, особисто я не шанувальник, однак це вимагає від вас створити нові типи винятків майже для кожного окремого методу / сценарію, які, на мою думку, є великими накладними витратами. Додавання сюди рядка журналу (і, мабуть, іншого у верхній частині) допомагає проілюструвати потік коду під час діагностики проблем
Ліат

4
Java не змушує вас обробляти виняток, вона змушує вас бути в курсі цього. Ви можете або зловити його і зробити що завгодно, або просто оголосити його як щось, що функція може кинути, і нічого не робити з ним у функції .... Маленький нанизування на інакше досить хороший відповідь!
ньютопський

4

Це дуже залежить від мови. Напр., C ++ не пропонує слідів стеків у повідомленнях про помилки, тому відстеження виключення за допомогою частого перехоплення - журналу - повторного відкидання може бути корисним. На відміну від цього, Java та подібні мови пропонують дуже хороші сліди стека, хоча формат цих слідів стека може бути не дуже налаштованим. Ловля винятків та повторне скидання винятків на цих мовах є абсолютно безглуздим, якщо ви дійсно не можете додати якийсь важливий контекст (наприклад, з'єднання винятку SQL низького рівня з контекстом операції бізнес-логіки).

Будь-яка стратегія поводження з помилками, реалізована за допомогою відображення, майже обов'язково менш ефективна, ніж функціонал, вбудований у мову. Крім того, всеохоплюючий лісозаготівля має неминучі показники продуктивності. Тому вам потрібно збалансувати потік інформації, яку ви отримуєте, проти інших вимог цього програмного забезпечення. Однак, такі рішення, як PostSharp, побудовані на інструментальному рівні на рівні компілятора, як правило, будуть набагато кращими, ніж рефлексія під час виконання.

Я особисто вважаю, що реєструвати все не корисно, оскільки воно включає безліч невідповідної інформації. Тому я скептично ставлюсь до автоматизованих рішень. Враховуючи хороші рамки ведення журналів, можливо, буде достатньо узгодженого керівництва з кодування, яке обговорює, яку інформацію ви хотіли б увійти та як цю інформацію слід форматувати. Потім ви можете додати журнал, де це має значення.

Реєстрація в бізнес-логіці набагато важливіша, ніж увімкнення функцій утиліт А збір слідів стеків реальних звітів про аварійні ситуації (що вимагає ведення журналу лише на верхньому рівні процесу) дозволяє знаходити області коду, де журнал мав би найбільше значення.


4

Коли я бачу try/catch/logв кожному методі, він викликає занепокоєння, що розробники не мали уявлення про те, що може чи не може трапитися в їх застосуванні, вважали найгірше і превентивно реєстрували все скрізь через всі помилки, яких вони очікували.

Це симптом того, що тестування модулів та інтеграції недостатньо, і розробники звикли перебирати багато коду в налагоджувачі і сподіваються, що якось багато журналів дозволить їм розгорнути помилковий код у тестовому середовищі та знайти проблеми, переглянувши колоди.

Код, який викидає винятки, може бути кориснішим за надмірний код, який виловлює і записує винятки. Якщо ви кидаєте виняток із значущим повідомленням, коли метод отримує несподіваний аргумент (і записує його на межі служби), це набагато корисніше, ніж негайно записувати виняток, викинутий як побічний ефект недійсного аргументу, і вгадати, що його спричинило .

Нулі - приклад. Якщо ви отримаєте значення в якості аргументу або результату виклику методу, і воно не повинно бути нульовим, киньте виняток тоді. Не просто записуйте отримані NullReferenceExceptionкинуті п’ять рядків пізніше через нульове значення. У будь-якому випадку ви отримуєте виняток, але один говорить вам щось, а інший змушує вас щось шукати.

Як зазначають інші, винятки найкраще реєструвати на кордоні служби, або коли виняток не буде повторно скинутий, оскільки це обробляється граціозно. Найважливіша різниця - між чим-небудь і нічим. Якщо ваші винятки заносяться в одне місце, до якого легко дістатися, ви знайдете потрібну інформацію, коли вона вам потрібна.


Спасибі Скотту. Точка, яку ви зробили " Якщо ви кидаєте виняток із значущим повідомленням, коли метод отримує несподіваний аргумент (і записуєте його на межі служби) " дійсно вразив і допоміг мені візуалізувати ситуацію, що нависла навколо мене в контексті аргументів методу. Я думаю, що має сенс мати застереження про безпеку та кидати ArgumentException у такому випадку, а не покладатися на деталі аргументу ловлі та реєстрації
rahulaga_dev

Скотт, у мене таке ж почуття. Кожного разу, коли я бачу журнал та ретрансляцію лише для того, щоб записати контекст, я відчуваю, що розробники не можуть контролювати інваріанти класу або не можуть захистити виклик методу. Натомість кожен метод на всьому шляху загортається в аналогічний спробу / catch / log / кидання. І це просто жахливо.
Макс

2

Якщо вам потрібно записати інформацію про контекст, яка вже не є винятком, ви перетворите її в новий виняток і надаєте початковий виняток як InnerException. Таким чином, ви все ще зберегли оригінальний слід стека. Так:

public static bool DoOperation(int num1, int num2)
{
    try
    {
        /* do some work with num1 and num2 */
    }
    catch (Exception ex)
    {
        throw new Exception("error occured while number 1 = {num1} and number 2 = {num2}", ex);
    }
}

Другий параметр для Exceptionконструктора забезпечує внутрішній виняток. Тоді ви можете записати всі винятки в одному місці, і ви все одно отримаєте повний слід стека та інформацію про контекст в одному записі журналу.

Можливо, ви хочете скористатися спеціальним класом виключень, але справа в тому ж.

try / catch / log / rethrow - це безлад, оскільки це призведе до заплутаності журналів - наприклад, що робити, якщо інший виняток трапиться в іншому потоці між тим, як записувати контекстну інформацію та реєструвати фактичне виключення в обробнику верхнього рівня? спробувати / ловити / кинути, але добре, якщо новий виняток додає інформацію до оригіналу.


Що з оригінальним типом винятків? Якщо ми обернемо, її вже немає. Це проблема? Хтось покладався, наприклад, на SqlTimeoutException.
Макс

@Max: Оригінальний тип винятку все ще доступний як внутрішній виняток.
ЖакБ

Це я маю на увазі! Тепер усі в стек викликів, які піймали для SqlException, ніколи не отримають його.
Макс

1

Сам виняток повинен надавати всю інформацію, необхідну для правильного ведення журналу, включаючи повідомлення, код помилки та інше. Тому не повинно ловитись виключення лише для того, щоб повторно скинути його чи викинути інше виключення.

Часто ви бачите шаблон декількох винятків, що потрапляють і перетоплюються, як загальний виняток, наприклад, вилов DatabaseConnectionException, InvalidQueryException та InvalidSQLParameterException та повторне скидання DatabaseException. Незважаючи на це, я заперечую, що всі ці виняткові винятки в першу чергу повинні випливати з DatabaseException, тому повторне скидання не потрібно.

Ви виявите, що видалення непотрібних застережень про випробування (навіть ті, що стосуються виключно ведення журналу) фактично полегшить роботу, а не складніше. Лише місця у вашій програмі, які обробляють виняток, повинні реєструвати винятки, і, якщо все інше не вдасться, обробник винятків для всієї програми для однієї остаточної спроби реєстрації винятку перед вишуканим виходом із програми. Виняток повинен мати повне трасування стека, що вказує точну точку, в яку було викинуто виняток, тому часто не потрібно надавати "контекстну" реєстрацію.

Це сказав, що AOP може бути швидким рішенням для вас, хоча, як правило, тягне загальне уповільнення. Я б закликав вас замість цього видаляти зайві пункти спробу вилову повністю там, де нічого не додається.


1
" Сам виняток повинен надавати всю інформацію, необхідну для правильного ведення журналу, включаючи повідомлення, код помилки та інше". Вони повинні, але на практиці вони не містять нульових посилань як класичний випадок. Я не знаю жодної мови, яка скаже вам змінну, яка викликала її всередині складного виразу, наприклад.
Девід Арно

1
@DavidArno Щоправда, але будь-який контекст, який ви могли б надати, теж не може бути таким конкретним. Інакше ти мав би try { tester.test(); } catch (NullPointerException e) { logger.error("Variable tester was null!"); }. Трасування стека в більшості випадків є достатньою, але, не маючи цього, тип помилки, як правило, достатній.
Ніл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.