Чи кидання тут виключення є антидіаграмою?


33

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

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

Обговорене рішення використовувалося за такою схемою (ПРИМІТКА: це не власне код, очевидно - спрощено для ілюстративних цілей):

public class Preferences {
    // null values are legal
    private Map<String, String> valuesFromDatabase;

    private static Map<String, String> defaultValues;

    class KeyNotFoundException extends Exception {
    }

    public String getByKey(String key) {
        try {
            return getValueByKey(key);
        } catch (KeyNotFoundException e) {
            String defaultValue = defaultValues.get(key);
            valuesFromDatabase.put(key, defaultvalue);
            return defaultValue;
        }
    }

    private String getValueByKey(String key) throws KeyNotFoundException {
        if (valuesFromDatabase.containsKey(key)) {
            return valuesFromDatabase.get(key);
        } else {
            throw new KeyNotFoundException();
        }
    }
}

Його критикували як анти-модель - зловживання винятками для контролю потоку . KeyNotFoundException- реалізований лише для одного випадку використання - ніколи не буде розглянуто за межами цього класу.

Це, по суті, два способи грати з ним, щоб просто повідомити щось одне одному.

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

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

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

getValueByKeyдоведеться повернути якесь a Tuple<Boolean, String>, bool встановлено на true, якщо ключ вже є, щоб ми могли розрізняти (true, null)і (false, null). ( outПараметр може бути використаний у C #, але це Java).

Це приємніша альтернатива? Так, вам доведеться визначити деякий клас одноразового використання Tuple<Boolean, String>, але тоді ми позбудемося KeyNotFoundException, і такий вид врівноважується. Ми також уникаємо накладних витрат на обробку винятків, хоча це практично не є важливим з точки зору практичної роботи - це не стосується продуктивності, це клієнтське додаток, і це не так, як переваги користувачів будуть виведені мільйони разів за секунду.

Варіант цього підходу може використовувати «Гуаву» Optional<String>(Guava вже використовується у всьому проекті), а не якийсь звичайний Tuple<Boolean, String>, і тоді ми можемо розрізняти Optional.<String>absent()«належне» null. Він все ще відчуває хакерство, проте з легких причин - введення двох рівнів "нікчемності", здається, зловживає концепцією, яка стояла за створенням Optionals в першу чергу.

Іншим варіантом було б чітко перевірити, чи існує ключ (додати boolean containsKey(String key)метод і викликати, getValueByKeyлише якщо ми вже стверджували, що він існує).

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

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

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


1
@ GlenH7 прийнята (і найвища кількість голосів) відповідь підсумовує, що "ви повинні використовувати їх [винятки] у випадках, коли вони спрощують поводження з помилками з меншим захаращенням коду". Напевно, я запитую тут, чи слід вважати альтернативи, які я перераховував, "менш загрожує кодом".
Конрад Моравський

1
Я відкликав свій VTC - ви просите про щось, що стосується, але відрізняється від дубліката, який я запропонував.

3
Я думаю, що питання стає цікавішим, коли getValueByKeyвоно також є публічним.
Док Браун

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

1
Тільки щоб звучати --- це ідеально ідіоматичний Python, і, я думаю, був би кращим способом вирішення цієї проблеми на цій мові. Очевидно, однак, різні мови мають різні умови.
Патрік Коллінз

Відповіді:


75

Так, ваш колега правий: це поганий код. Якщо помилку можна вирішити локально, то її слід негайно поправити. Виняток не слід кидати, а потім негайно обробляти.

Це набагато чистіше, ніж ваша версія ( getValueByKey()метод видалено):

public String getByKey(String key) {
    if (valuesFromDatabase.containsKey(key)) {
        return valuesFromDatabase.get(key);
    } else {
        String defaultValue = defaultValues.get(key);
        valuesFromDatabase.put(key, defaultvalue);
        return defaultValue;
    }
}

Виняток слід кидати лише в тому випадку, якщо ви не знаєте, як вирішити помилку локально.


14
Дякую за відповідь. Для запису я цей колега (оригінал коду мені не сподобався), я просто нейтрально сформулював це запитання, щоб воно не було завантажене :)
Конрад Моравський,

59
Перша ознака божевілля: ти починаєш говорити сам з собою.
Роберт Харві

1
@ BЈовић: ну, в такому випадку я думаю, що це нормально, але що робити, якщо вам потрібні два варіанти - один метод отримання значення без автоматичного створення пропущених ключів, а інший з? Як ви вважаєте, що це найчистіша (і суха) альтернатива?
Doc Brown

3
@RobertHarvey :) хтось ще автор цього коду, фактично окрема людина, я був рецензент
Конрад Моравський

1
@Alvaro, використання винятків часто не є проблемою. Використання винятків є проблемою, незалежно від мови.
kdbanman

11

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

Найкращим рішенням (якщо припустити, що ви все ще знаходитесь на Java 7) було б використовувати необов'язково Guava; Я не погоджуюся з тим, що його використання в цьому випадку було б нерозумним. Мені здається, виходячи з розширеного пояснення Гуави Факультативно , що це ідеальний приклад, коли його використовувати. Ви розрізняєте "значення не знайдено" та "значення нуля знайдено".


1
Я не думаю, що це Optionalможе бути нульовим. Optional.of()приймає лише ненульове посилання та Optional.fromNullable()розглядає null як "значення немає".
Навін

1
@Navin він не може мати нульовий характер, але у вас є Optional.absent()у вашому розпорядженні. А значить, Optional.fromNullable(string)буде дорівнює, Optional.<String>absent()якщо stringбуло нульовим, або Optional.of(string)якщо цього не було.
Конрад Моравський

@KonradMorawski Я подумав, що проблема в ОП полягає в тому, що ви не можете розрізняти нульову рядок і неіснуючу рядок, не кидаючи виняток. Optional.absent()охоплює один із таких сценаріїв. Як ви представляєте іншого?
Навін

@Navin з фактичним null.
Конрад Моравський

4
@KonradMorawski: Це звучить як дуже погана ідея. Я навряд чи можу придумати більш чіткий спосіб сказати "цей метод ніколи не повертається null!" ніж оголосити це як повернене Optional<...>. . .
ruakh

8

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

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

Як зазначив Майк у коментарях, з цим виникає проблема; ні Guava, ні Java 8 не Optionalдозволяють зберігати nulls. Таким чином, вам знадобиться прокатати свою власну, яка, хоча і прямолінійна, передбачає неабияку кількість котлоагрегату, тому може бути зайвим для чогось, що буде використано лише один раз всередині. Ви також можете змінити тип карти на Map<String, Optional<String>>, але поводження з Optional<Optional<String>>s стає незручним.

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


1
Ну, насправді це лише Map<String, String>на рівні таблиці бази даних, оскільки valuesстовпець повинен бути одного типу. Однак є шар DAO-обгортки, який сильно набирає кожну пару ключових значень. Але я пропустив це для наочності. (Звичайно, ви могли б мати однорядну таблицю з n стовпцями, але для додавання кожного нового значення потрібно оновити схему таблиці, тому видалення складності, пов'язаної з необхідністю їх розбору, пов'язане з тим, щоб ввести іншу складність в іншому місці).
Конрад Моравський

Гарний момент про кортеж. Справді, що мало (false, "foo")означати?
Конрад Моравський

Принаймні, за бажанням Guava Необов'язково, намагаючись поставити нуль результатів у виняток. Вам доведеться використовувати Optional.absent ().
Майк Партрідж

@MikePartridge Добрий момент. Потворним рішенням було б змінити карту, Map<String, Optional<String>>і тоді ви закінчите Optional<Optional<String>>. Менш заплутаним рішенням було б запустити свій власний необов’язковий, що дозволяє nullабо подібний алгебраїчний тип даних, який забороняє дію, nullале має 3 стани - відсутні, нульові чи наявні. Обидва вони прості у виконанні, хоча кількість котлоагрегатів є високою для того, що буде використовуватися лише всередині країни.
Довал

2
"Оскільки немає ніяких міркувань щодо продуктивності, і це детальна інформація про реалізацію, це в кінцевому рахунку не має значення, яке рішення ви обрали" - За винятком того, що якщо ви використовуєте C # за допомогою винятку, буде дратувати всіх, хто намагається налагодити помилку "Перервати, коли викид буде скинуто "увімкнено для виключень CLR.
Стівен

5

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

Проблема полягає саме в цьому. Але ви вже розмістили рішення:

Варіант цього підходу може використовувати опцію Guava (необов'язково (Guava вже використовується в усьому проекті)) замість якогось спеціального пакету, і тоді ми можемо розмежувати між Optional.absent () та "належним" null . Це все ще відчуває хакерство, проте з легких причин - введення двох рівнів "недійсності", здається, зловживає концепцією, яка стояла за створенням необов'язкових в першу чергу.

Однак не використовуйте null або Optional . Можна і потрібно використовувати Optionalтільки. Для вашого "хакітного" почуття просто використовуйте його вкладене, так що в кінцевому підсумку Optional<Optional<String>>виясняєте, що в базі даних може бути ключ (перший рівень варіантів) і що він може містити заздалегідь задане значення (другий шар опцій).

Такий підхід кращий, ніж використання винятків, і його досить легко зрозуміти, доки Optionalвін для вас не новий.

Також зверніть увагу, що в ньому Optionalє деякі функції комфорту, так що вам не доведеться виконувати всю роботу самостійно. До них належать:

  • static static <T> Optional<T> fromNullable(T nullableReference)для перетворення вхідної бази даних у Optionalтипи
  • abstract T or(T defaultValue) щоб отримати ключове значення внутрішнього шару опціону або (якщо його немає) отримати значення ключа за замовчуванням

1
Optional<Optional<String>>виглядає злим, хоча в ньому є певна принадність, я мушу визнати
:)

Чи можете ви пояснити далі, чому це виглядає злом? Це насправді та сама картина, що і List<List<String>>. Звичайно, ви можете (якщо мова дозволяє) зробити новий тип, наприклад, DBConfigKeyякий обгортає Optional<Optional<String>>та робить його більш читабельним, якщо ви хочете цього.
валентери

2
@valenterry Це зовсім інакше. Список списків - законний спосіб зберігання деяких видів даних; необов'язковий варіант необов'язкового - це нетиповий спосіб зазначення двох видів проблем, які можуть мати дані.
Ypnypn

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

1
@valenterry зло в тому сенсі, що означає Джон Скіт; тож не відверто погано, а трохи злий, «розумний код». Я думаю, що це просто дещо бароко, необов'язковий варіант. Не ясно, який рівень факультативності являє собою що. Я мав би подвійний результат, якби зіткнувся з ним у чиєсь коді. Ви можете мати рацію, що це дивно, тому що це незвично, однозначно. Можливо, це не фантазія для програміста функціональної мови, з їхніми монадами та всіма. Хоча я сам би не пішов на це, я все-таки поставив +1 вашій відповіді, оскільки це цікаво
Конрад Моравський,

5

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

Дивлячись на те, як виконується реалізація Properties.getProperty(String)(з Java 7):

Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;

Дійсно, не потрібно «зловживати винятками для контролю потоку», як ви цитували.

Аналогічний, але трохи більш лаконічний підхід до відповіді @ BЈовић також може бути:

public String getByKey(String key) {
    if (!valuesFromDatabase.containsKey(key)) {
        valuesFromDatabase.put(key, defaultValues.get(key));
    }
    return valuesFromDatabase.get(key);
}

4

Хоча я вважаю, що відповідь @ Бовича добре, якщо getValueByKeyце більше ніде не потрібно, я не думаю, що ваше рішення є поганим, якщо ваша програма містить обидва випадки використання:

  • пошук по ключу з автоматичним створенням, якщо ключ заздалегідь не існує, і

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

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

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

Звичайно, є третя альтернатива: створити третій, приватний метод, що повертає Tuple<Boolean, String>, як ви запропонували, і повторно використовуйте цей метод і в, getValueByKeyі в getByKey. Для більш складного випадку, який може бути справді кращою альтернативою, але для такого простого випадку, як показано тут, ІМХО має запах перенапруженості, і я сумніваюся, що код дійсно стає таким ремонтом. Я тут з найвищою відповіддю Карла Білефельдта :

"вам не слід погано користуватися винятками, коли це спрощує ваш код".


3

Optional- правильне рішення. Однак, якщо ви віддаєте перевагу, є альтернатива, яка має менше «двох нульових» відчуттів, розгляньте дозорну.

Визначте приватну статичну рядок keyNotFoundSentinelзі значеннямnew String("") . *

Зараз приватний метод може return keyNotFoundSentinelзамість цього throw new KeyNotFoundException(). Загальнодоступний метод може перевірити це значення

String rval = getValueByKey(key);
if (rval == keyNotFoundSentinel) { // reference equality, not string.equals
    ... // generate default value
} else {
    return rval;
}

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

* Ми навмисно використовуємо, new String("")а не просто, ""щоб не допустити, щоб Java "інтернувала" рядок, що дало б їй таке ж посилання, як і будь-який інший порожній рядок. Оскільки був зроблений цей додатковий крок, ми гарантуємо, що посилання, на яке посилається, keyNotFoundSentinel- це унікальний екземпляр, який нам потрібен, щоб він ніколи не відображався на самій карті.


Це, ІМХО, найкраща відповідь.
fgp

2

Дізнайтеся про рамки, які дізналися з усіх больових моментів Java:

.NET пропонує два куди більш елегантні рішення цієї проблеми, на прикладі:

Останнє дуже просто написати на Java, для першого потрібен допоміжний клас «сильного посилання».


Альтернативою такої Dictionaryсхеми буде зручна ковариація T TryGetValue(TKey, out bool).
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.