Чи слід фіксувати помилки на конструкторах, що викидають винятки?


15

Я будував додаток протягом декількох місяців, і я зрозумів, що з'явилася закономірність:

logger.error(ERROR_MSG);
throw new Exception(ERROR_MSG);

Або при лові:

try { 
    // ...block that can throw something
} catch (Exception e) {
    logger.error(ERROR_MSG, e);
    throw new MyException(ERROR_MSG, e);
}

Отже, кожного разу, коли я кидав або ловив виняток, я б реєстрував це. Насправді це був майже весь журнал, який я робив у програмі (окрім чого для ініціалізації додатків).

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

Отже, це антизразка? Якщо так, то чому?


Що робити, якщо ви серіалізуєте та десеріалізуєте виняток? Це ввійде помилка?
апокаліпсис

Відповіді:


18

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

Можливо, ви не завжди хочете реєструвати всі екземпляри даного винятку (можливо, це відбувається під час перевірки вхідних даних, журнал яких може бути як об'ємним, так і нецікавим).

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

Нарешті, що робити, якщо винятки сталися під час реєстрації іншого винятку? Ви б це ввійшли? Він стає безладним ...

В основному ваші варіанти:

  • Ловіть, записуйте та (пере) кидайте, як ви дали у своєму прикладі
  • Створіть клас ExceptionHelper для обох, але Helpers має запах коду, і я б не рекомендував цього.
  • Перемістіть обробку винятків у виловленні на більш високий рівень
  • Розгляньте AOP для більш складного рішення наскрізних проблем, таких як ведення журналів і обробка виключень (але набагато складніше, ніж просто наявність цих двох рядків у ваших блоках вилову;))

+1 за "об'ємний та нецікавий". Що таке AOP?
Tulains Córdova

@ user61852 Програмування, орієнтоване на аспекти (я додав посилання). Це питання показує один приклад ж / б / т АОП і лісозаготівель в Java: stackoverflow.com/questions/15746676/logging-with-aop-in-spring
Dan1701

11

Отже, як програміст, я уникаю повторення [...]

Тут існує небезпека, коли поняття "don't repeat yourself"сприймається занадто серйозно до того, як воно стає запахом.


2
Тепер, як я маю вибирати правильну відповідь, коли всі добрі і будують один на одного? Відмінне пояснення того, як DRY може стати проблемою, якщо я скористаюся фанатичним підходом до цього.
Бруно Брант

1
Це дійсно хороший удар в DRY, я повинен зізнатися, що я DRYholic. Тепер я подумаю двічі, коли я думаю про переміщення цих 5 рядків коду кудись ще заради DRY.
SZT

@SZaman Спочатку я був дуже схожий. Зі свого боку, я думаю, що для тих із нас, хто занадто сильно схиляється над стороною усунення надмірності, більше, ніж тих, хто, скажімо, записує функцію 500 рядків за допомогою копіювання та вставлення і навіть не думає про її рефакторинг. Головне, що потрібно пам’ятати IMO, це те, що кожного разу, коли ви виділяєте невелике дублювання, ви децентралізуєте код і перенаправляєте залежність в інше місце. Це може бути або добра, або погана річ. Це дає вам центральний контроль змінити поведінку, але поділитися цією поведінкою також може почати вас кусати ...

@SZaman Якщо ви хочете внести зміни на кшталт "Тільки ця функція потребує цього, інші, хто використовує цю центральну функцію, не роблять". У будь-якому випадку це балансуючий акт, як я його бачу - те, що важко зробити ідеально! Але іноді трохи дублювання може допомогти зробити ваш код більш незалежним і відокремленим. І якщо ви протестуєте фрагмент коду, і він працює дуже добре, навіть якщо він дублює основну логіку тут і там, можливо, буде мало причин колись змінитися. Тим часом те, що залежить від цілого ряду зовнішніх речей, знаходить набагато більше зовнішніх причин, які доведеться змінити.

6

Для відлуння @ Dan1701

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

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

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

Так що в цьому випадку я думаю, що "SRP" козиряє "DRY".


1
[...]"SRP" trumps "DRY"- Я вважаю, що ця цитата майже ідеально підсумовує її.

Як сказав @Eke ... Це те обґрунтування, яке я шукав.
Бруно Брант

+1 для вказівки на те, що зміна контексту ведення журналу буде входити так, ніби клас винятку є початком або записом журналу, який не є.
Tulains Córdova

2

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

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

Підсумовуючи це, ваш код присвоїв право та обов'язок за часткове поводження з винятком, тим самим порушуючи SRP.
DRY не вступає в це.


1

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

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

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

Однак хороший підхід використовувати виключення як засіб для позначення рівня програми, в якому з’явилася помилка.

Розглянемо наступний напівпсевдо-код:

interface ICache<T, U>
{
    T GetValueByKey(U key); // may throw an CacheException
}

class FileCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from FileCache::getvalueByKey. The File could not be opened. Key: " + key);
    }
}

class RedisCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: " + key);
    }
}

class CacheableInt
{
    ICache<int, int> cache;
    ILogger logger;

    public CacheableInt(ICache<int, int> cache, ILogger logger)
    {
        this.cache = cache;
        this.logger = logger;
    }

    public int GetNumber(int key) // may throw service exception
    {
        int result;

        try {
            result = this.cache.GetValueByKey(key);
        } catch (Exception e) {
            this.logger.Error(e);
            throw new ServiceException("CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: " + key);
        }

        return result;
    }
}

class CacheableIntService
{
    CacheableInt cacheableInt;
    ILogger logger;

    CacheableInt(CacheableInt cacheableInt, ILogger logger)
    {
        this.cacheableInt = cacheableInt;
        this.logger = logger;
    }

    int GetNumberAndReturnCode(int key)
    {
        int number;

        try {
            number = this.cacheableInt.GetNumber(key);
        } catch (Exception e) {
            this.logger.Error(e);
            return 500; // error code
        }

        return 200; // ok code
    }
}

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

ERROR: 12:23:27 - Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: 28
ERROR: 12:23:27 - CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: 28

Потім розробник одразу знає, який рівень програмного забезпечення викликав процес переривання, і він має простий спосіб виявити проблему. У цьому випадку це критично важливо, тому що тайм-аут Redis ніколи не повинен відбуватися.

Можливо, інший користувач закликає той самий метод, також отримує 500код, але журнал відображатиме таке:

INFO: 11:11:11- Could not retrieve object from RedisCache::getvalueByKey. Value does not exist for the key 28.
INFO: 11:11:11- CacheableInt::GetNumber failed, because the cache layer could not find any data for the key 28.

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


Підсумок

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


1

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

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


1

Я знаю, що це стара нитка, але я просто натрапив на подібну проблему і вирішив подібне рішення, тому я додам свої 2 копійки.

Я не купую аргумент порушення SRP. Не зовсім все одно. Припустимо, 2 речі: 1. Ви насправді хочете реєструвати винятки під час їх виникнення (на рівні відстеження, щоб мати можливість відтворити потік програми). Це не має нічого спільного з поводженням з винятками. 2. Ви не можете або не будете використовувати AOP для цього - я погоджуюся, що це був би найкращий шлях, але, на жаль, я застряг у мові, яка не пропонує інструментів для цього.

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


0

Це анти-модель.

На мою думку, виклик журналу в конструкторі винятком може бути прикладом наступного: Помилка: Конструктор виконує реальну роботу .

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

Як такий, це також порушило б принцип найменшого здивування .

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.