Чи є кодовим запахом встановити прапор у циклі, щоб використовувати його згодом?


30

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

Приклад:

Map<BigInteger, List<String>> map = handler.getMap();

if(map != null && !map.isEmpty())
{
    for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())
    {
        fillUpList();

        if(list.size() > limit)
        {
            limitFlag = true;
            break;
        }
    }
}
else
{
    logger.info("\n>>>>> \n\t 6.1 NO entries to iterate over (for given FC and target) \n");
}

if(!limitFlag) // Continue only if limitFlag is not set
{
    // Do something
}

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

Чи правий я? Як я можу це видалити?


10
Чому ви вважаєте, що це кодовий запах? які конкретні проблеми ви можете передбачити, роблячи це, що не відбудеться в іншій структурі?
Бен Коттрелл

13
@ gnasher729 Просто з цікавості, який термін ви б використали замість цього?
Бен Коттрелл

11
-1, ваш приклад не має сенсу. entryніде не використовується всередині циклу функцій, і ми можемо лише здогадуватися, що listце таке. Є чи fillUpListповинен заповнити list? Чому він не отримує це як параметр?
Док Браун

13
Я б переглянув ваше використання пробілів та порожніх рядків.
Даніель Жур

11
Немає такого поняття, як запахи коду. "Кодовий запах" - термін, придуманий розробниками програмного забезпечення, які хочуть затримати ніс, коли бачать код, який не відповідає їхнім елітарним стандартам.
Роберт Харві

Відповіді:


70

Немає нічого поганого в тому, щоб використовувати булеве значення за призначенням: записати бінарне розрізнення.

Якби мені сказали перефактурувати цей код, я, певно, ставлю цикл у свій власний метод, щоб призначення + breakперетворилося на a return; тоді вам навіть не потрібна змінна, ви можете просто сказати

if(fill_list_from_map()) {
  ...

6
Насправді запах у його коді - це довга функція, яку потрібно розділити на менші функції. Ваша пропозиція - це шлях.
Бернхард Хіллер

2
Краще словосполучення, яке описує корисну функцію першої частини цього коду, - це виявити, чи буде перевищено обмеження після того, як він накопичить щось із цих відображених елементів. Ми також можемо сміливо припустити, що fillUpList()це якийсь код (який ОП вирішує не ділитися), який фактично використовує значення entryітерації; без цього припущення виглядало б так, що тіло циклу нічого не використовувало з ітерації циклу.
rwong

4
@Kilian: У мене просто одна турбота. Цей метод заповнить список і поверне булевий сигнал, який представляє, що розмір списку перевищує ліміт чи ні, тому ім'я 'fill_list_from_map' не дає зрозуміти, що являє собою повернення Boolean (не вдалося заповнити, a ліміт перевищує тощо). Оскільки повернувся Boolean - це особливий випадок, який не видно з назви функції. Будь-які коментарі? PS: ми також можемо враховувати розділення запитів команд.
Сіддхарт Триха

2
@SiddharthTrikha Ви маєте рацію, і я точно так само хвилювався, коли запропонував цю лінію. Але незрозуміло, який список повинен заповнити код. Якщо це завжди один і той же список, вам не потрібен прапор, ви можете просто перевірити його довжину після цього. Якщо вам потрібно знати, чи перевищило будь-яке індивідуальне заповнення ліміт, вам доведеться якось транспортувати цю інформацію назовні, і IMO принцип поділу команд / запитів не є достатньою причиною для відхилення очевидного шляху: через повернення значення.
Кіліан Фот

6
Дядько Боб каже на сторінці 45 чистого коду : "Функції повинні або робити щось, або відповідати на щось, але не на те і інше. Або ваша функція повинна змінювати стан об'єкта, або повертати деяку інформацію про цей об'єкт. Виконання обох часто призводить до плутанина ».
CJ Dennis

25

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

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


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

1
@SiddharthTrikha: Важко сказати, оскільки я не знаю, що насправді повинен робити код. Якщо ви хочете перевірити, чи карта містить принаймні один елемент, список якого перевищує обмеження, я думаю, ви можете зробити це з одним виразом anyMatch.
ЖакB

2
@SiddharthTrikha: проблему сфери можна легко вирішити, змінивши початковий тест на охоронний пункт, як-от. if(map==null || map.isEmpty()) { logger.info(); return;}Це, однак, буде працювати лише в тому випадку, якщо код, який ми бачимо, є повною частиною функції, а // Do somethingчастина не потрібна у випадку, якщо карта недійсне або порожнє.
Док Браун

14

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

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

"Кодовий запах" - це коли ви не думаєте. Якщо ви дійсно збираєтесь думати про код, тоді зробіть це правильно!

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

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

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


5

Так, це кодовий запах (підказки від усіх, хто це робить).

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

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

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

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

Так само, як і у всіх запахів коду: якщо ви бачите прапор, спробуйте замінити його на while. Якщо ви не можете, додайте додаткові тести на одиницю.


+1 від мене. Це точно кодовий запах, і ви чітко сформулюєте, чому і як з цим впоратися.
Девід Арно

@Ewan: SO as with all code smells: If you see a flag, try to replace it with a whileЧи можете ви детальніше пояснити це на прикладі?
Сіддхарт Триха

2
Наявність декількох точок виходу з циклу може ускладнити міркування, але в цьому випадку це призведе до рефакторингу для того, щоб стан циклу залежав від прапора - це означало б заміну for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())на for (Iterator<Map.Entry<BigInteger, List<String>>> iterator = map.entrySet().iterator(); iterator.hasNext() && !limitFlag; Map.Entry<BigInteger, List<String>> entry = iterator.next()). Це досить незвичайна закономірність, що я мав би більше проблем з її розумінням, ніж порівняно простий перерва.
James_pic

@James_pic моя Java трохи іржава, але якщо ми використовуємо карти, я б використав колектор, щоб підбити підсумки та відфільтрувати їх після обмеження. Однак, як я кажу, приклад "не такий поганий" запах коду - це загальне правило, яке попереджає вас про потенційну проблему. Не священний закон, якого завжди потрібно дотримуватися
Еван

1
Ви не маєте на увазі "кий", а не "черга"?
psmears

0

Просто використовуйте інше ім'я, ніж limitFlag, яке говорить про те, що ви насправді перевіряєте. І чому ви щось записуєте, коли карта відсутня або порожня? limtFlag буде помилковим, все, що вам цікаво. Цикл просто добре, якщо карта порожня, тому не потрібно перевіряти це.


0

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

Ви повинні перемістити логіку циклу for в метод fillUpList, щоб він перервався, якщо досягнуто обмеження. Потім відразу перевіряйте розмір списку.

Якщо це порушує ваш код, то чому?


0

Спочатку загальний випадок: використання прапора для перевірки того, чи відповідає якийсь елемент колекції певній умові, не рідкість. Але модель, яку я бачив найчастіше для вирішення цього питання, - це перемістити чек на додатковий метод і безпосередньо повернутися з нього (як Кіліан Фот, описаний у своїй відповіді ):

private <T> boolean checkCollection(Collection<T> collection)
{
    for (T element : collection)
        if (checkElement(element))
            return true;
    return false;
}

Оскільки у Java 8 існує більш стислий спосіб використання Stream.anyMatch(…):

collection.stream().anyMatch(this::checkElement);

У вашому випадку це, мабуть, буде виглядати приблизно так (припускаючи list == entry.getValue()ваше запитання):

map.values().stream().anyMatch(list -> list.size() > limit);

Проблема у вашому конкретному прикладі - додатковий дзвінок на fillUpList(). Відповідь багато залежить від того, що цей метод повинен зробити.

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

Тому я припускаю, що фактичний код передає струм entryметоду.

Але є ще кілька запитань:

  • Чи списки на карті порожні, перш ніж дійти до цього коду? Якщо так, то чому вже є карта, а не лише список або набір BigIntegerключів? Якщо вони не порожні, навіщо вам заповнювати списки? Коли в списку вже є елементи, чи не в цьому випадку це оновлення чи якесь інше обчислення?
  • Що призводить до того, що список стає більшим за ліміт? Це стан помилок чи очікується, що це трапляється часто? Це викликано невірним введенням?
  • Чи потрібні списки, обчислені до моменту, коли ви досягнете списку, що перевищує обмеження?
  • Що робить частина " Зроби щось "?
  • Ви перезапускаєте заповнення після цієї частини?

Це лише деякі питання, які мені спадали на думку, коли я намагався зрозуміти фрагмент коду. Отже, на мій погляд, це справжній запах коду : Ваш код не чітко повідомляє про наміри.

Це може означати це ("все або нічого", а досягнення межі вказує на помилку):

/**
 * Computes the list of all foo strings for each passed number.
 * 
 * @param numbers the numbers to process. Must not be {@code null}.
 * @return all foo strings for each passed number. Never {@code null}.
 * @throws InvalidArgumentException if any number produces a list that is too long.
 */
public Map<BigInteger, List<String>> computeFoos(Set<BigInteger> numbers)
        throws InvalidArgumentException
{
    if (numbers.isEmpty())
    {
        // Do you actually need to log this here?
        // The caller might know better what to do in this case...
        logger.info("Nothing to compute");
    }
    return numbers.stream().collect(Collectors.toMap(
            number -> number,
            number -> computeListForNumber(number)));
}

private List<String> computeListForNumber(BigInteger number)
        throws InvalidArgumentException
{
    // compute the list and throw an exception if the limit is exceeded.
}

Або це може означати це ("оновлення до першої проблеми"):

/**
 * Refreshes all foo lists after they have become invalid because of bar.
 * 
 * @param map the numbers with all their current values.
 *            The values in this map will be modified.
 *            Must not be {@code null}.
 * @throws InvalidArgumentException if any new foo list would become too long.
 *             Some other lists may have already been updated.
 */
public void updateFoos(Map<BigInteger, List<String>> map)
        throws InvalidArgumentException
{
    map.replaceAll(this::computeUpdatedList);
}

private List<String> computeUpdatedList(
        BigInteger number, List<String> currentValues)
        throws InvalidArgumentException
{
    // compute the new list and throw an exception if the limit is exceeded.
}

Або це ("оновіть усі списки, але зберігайте оригінальний список, якщо він стає занадто великим"):

/**
 * Refreshes all foo lists after they have become invalid because of bar.
 * Lists that would become too large will not be updated.
 * 
 * @param map the numbers with all their current values.
 *            The values in this map will be modified.
 *            Must not be {@code null}.
 * @return {@code true} if all updates have been successful,
 *         {@code false} if one or more elements have been skipped
 *         because the foo list size limit has been reached.
 */
public boolean updateFoos(Map<BigInteger, List<String>> map)
{
    boolean allUpdatesSuccessful = true;
    for (Entry<BigInteger, List<String>> entry : map.entrySet())
    {
        List<String> newList = computeListForNumber(entry.getKey());
        if (newList.size() > limit)
            allUpdatesSuccessful = false;
        else
            entry.setValue(newList);
    }
    return allUpdatesSuccessful;
}

private List<String> computeListForNumber(BigInteger number)
{
    // compute the new list
}

Або навіть наступне ( computeFoos(…)з першого прикладу, але без винятку):

/**
 * Processes the passed numbers. An optimized algorithm will be used if any number
 * produces a foo list of a size that justifies the additional overhead.
 * 
 * @param numbers the numbers to process. Must not be {@code null}.
 */
public void process(Collection<BigInteger> numbers)
{
    Map<BigInteger, List<String>> map = computeFoos(numbers);
    if (isLimitReached(map))
        processLarge(map);
    else
        processSmall(map);
}

private boolean isLimitReached(Map<BigInteger, List<String>> map)
{
    return map.values().stream().anyMatch(list -> list.size() > limit);
}

Або це може означати щось зовсім інше… ;-)

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