Як редагувати ланцюжок if-else, якщо заяви відповідають принципам чистого кодексу дядька Боба?


45

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

Я не можу скоротити цю логіку:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

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

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


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

Я вважаю, що це вже інше питання (це можна побачити також, порівнюючи відповіді на ці запитання).

  • Моє запитання - перевірка, щоб перша умова прийняття швидко закінчилася .
  • Зв'язане питання намагається створити всі умови, щоб прийняти , щоб зробити щось. (краще видно в цій відповіді на це питання: https://softwareengineering.stackexchange.com/a/122625/96955 )

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

38
У цьому коді нічого поганого. Це дуже легко читати і легко дотримуватися. Все, що ви зробите для подальшого скорочення, додасть непрямості і ускладнить розуміння.
17 з 26

20
Ваш код добре. Поставте свою енергію, що залишилася, в щось більш продуктивне, ніж намагатися скоротити її далі.
Роберт Харві

5
Якщо це дійсно лише 4 умови, це добре. Якщо це дійсно щось на кшталт 12 або 50, то ви, ймовірно, хочете переробити фактор на більш високому рівні, ніж цей метод.
JimmyJames

9
Залиште свій код точно таким, яким він є. Послухайте те, що завжди говорили ваші батьки: Не довіряйте дядькам, які пропонують дітям солодощі на вулиці. @Harvey Досить смішно, різні спроби "вдосконалити" код зробили його набагато більшим, складнішим і менш читабельним.
gnasher729

Відповіді:


81

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

{
    addAlert(GetConditionCode());
}

і у вас є GetConditionCode () інкапсуляція логіки для перевірки умов. Можливо, також краще використовувати Enum, ніж магічне число.

private AlertCode GetConditionCode() {
    if (CheckCondition1()) return AlertCode.OnFire;
    if (CheckCondition2()) return AlertCode.PlagueOfBees;
    if (CheckCondition3()) return AlertCode.Godzilla;
    if (CheckCondition4()) return AlertCode.ZombieSharkNado;
    return AlertCode.None;
}

2
Якщо можливо інкапсулювати так, як ви описуєте (я підозрюю, що це не може бути, я думаю, що ОП залишає змінні для простоти), він не змінює код, що само по собі добре, але додає ергономічність коду та трохи читабельності + 1
опа

17
За допомогою цих кодів оповіщення я дякую лише один код, який можна повернути одночасно
Josh Part

12
Це також здається ідеальним відповідним для використання оператора перемикання - якщо це доступно мовою ОП.
Френк Хопкінс

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

6
Одне з питань цього повторного втілення полягає в тому, що він змушує функцію addAlertперевіряти на предмет помилкового стану попередження AlertCode.None.
Девід Хаммен

69

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

Будь-яка спроба подальшого "спрощення" дійсно ускладнить справи.

Звичайно, ви можете замінити elseключове слово на таке, returnяк запропонували інші, але це лише питання стилю, а не зміна складності.


Убік:

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

Використовуйте занадто великі методи, і вас накрутили. Використовуйте занадто малі функції, і ви накрутили. Уникайте потрійних виразів, і вас накрутили. Використовуйте потрійні вирази скрізь, і ви накрутили. Зрозумійте, що є місця, які викликають однолінійні функції, і місця, що вимагають 50-лінійних функцій (так, вони існують!). Зрозумійте, що є місця, які вимагають if()заяви, і що є місця, які вимагають ?:оператора. Використовуйте повний арсенал, який у вас є, і намагайтеся завжди використовувати найпридатніший інструмент, який ви зможете знайти. І пам’ятайте, не будьте релігійними навіть щодо цієї поради.


2
Я заперечую, що заміна else ifна внутрішню, що returnсупроводжується простим if(видаленням else), може ускладнити читання коду . Коли код каже else if, я одразу знаю, що код у наступному блоці виконуватиметься лише тоді, коли попередній цього не зробив. Ні мус, ні суєти. Якщо це звичайна, ifто вона може бути виконана чи не може, незалежно від того, виконана попередня. Тепер мені доведеться витратити деяку кількість розумових зусиль, щоб проаналізувати попередній блок, щоб зазначити, що він закінчується символом a return. Я вважаю за краще витратити ці розумові зусилля на аналіз ділової логіки.
CVn

1
Я знаю, це невелика річ, але принаймні для мене else ifутворює одну смислову одиницю. (Це не обов'язково єдиний блок для компілятора, але це нормально.) ...; return; } if (...Немає; не кажучи вже про те, якщо він розкинувся на кілька рядків. Це те, що я насправді повинен подивитися, щоб побачити, що це робить, замість того, щоб мати можливість взяти це безпосередньо, просто побачивши пару ключових слів else if.
CVn

@ MichaelKjörling Full Ack. Я б віддав перевагу else ifконструкції, тим більше, що її ланцюгова форма - це така відома модель. Однак код форми if(...) return ...;також є добре відомим зразком, тому я б не засуджував це повністю. Я вважаю це справді незначним питанням: логіка потоку управління однакова в обох випадках, і один детальний погляд на if(...) { ...; return; }сходи скаже мені, що це дійсно еквівалентно else ifдрабині. Я бачу структуру одного терміна, випливаю з його значення, усвідомлюю, що це повторюється скрізь, і я знаю, що відбувається.
cmaster

Походячи з JavaScript / node.js, деякі користуються кодом "пояс і підтяжки", використовуючи і else if і, і return . наприкладelse if(...) { return alert();}
user949300

1
"І пам'ятайте, не будьте релігійними навіть щодо цієї поради". +1
Слова, як Джаред

22

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

Поставте свої умови в об’єкти і поставте їх у список

foreach(var condition in Conditions.OrderBy(i=>i.OrderToRunIn))
{
    if(condition.EvaluatesToTrue())
    {
        addAlert(condition.Alert);
        break;
    }
}

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

void RunConditionalAction(ConditionalActionSet conditions)
{
    foreach(var condition in conditions.OrderBy(i=>i.OrderToRunIn))
    {
        if(condition.EvaluatesToTrue())
        {
            RunConditionalAction(condition);
            break;
        }
    }
}

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

Але ваш приклад дійсно є шаблон

Поширеним випадком використання цього шаблону буде валідація. Замість :

bool IsValid()
{
    if(condition1 == false)
    {
        throw new ValidationException("condition1 is wrong!");
    }
    elseif(condition2 == false)
    {
    ....

}

Стає

[MustHaveCondition1]
[MustHaveCondition2]
public myObject()
{
    [MustMatchRegExCondition("xyz")]
    public string myProperty {get;set;}
    public bool IsValid()
    {
        conditions = getConditionsFromReflection()
        //loop through conditions
    }
}

27
Це лише переміщує if...elseсходи в побудову Conditionsсписку. Чистий приріст є негативним, оскільки побудова Conditionsзабирає стільки ж коду, скільки і код ОП, але додаткове опосередкування має затрати на читабельність. Я б напевно віддав перевагу чистому коду сходів.
cmaster

3
@cmaster так, я думаю, я точно сказав, що "тоді налаштування для об'єкта буде настільки ж складною, як і оригінал, якщо заява £
Ewan

7
Це менш читабельно, ніж оригінал. Для того, щоб з'ясувати, яка умова насправді перевіряється, вам потрібно перекопатися в якійсь іншій частині коду. Це додає непотрібного рівня непрямості, що ускладнює розуміння коду.
17 з 26

8
Перетворити ланцюжок if if .. if if .. else .. на таблицю предикатів та дій має сенс, але лише для значно більших прикладів. Таблиця додає певної складності та опосередкованості, тому для амортизації цієї концептуальної накладних витрат вам достатньо записів. Так, для 4 пар предикатів / дій, зберігайте простий оригінальний код, але якщо у вас було 100, обов'язково перейдіть з таблицею. Точка перехрестя знаходиться десь посередині. @cmaster, таблиця може бути статично ініціалізована, тому додаткові накладні витрати для додавання присудкової / дії дії - це один рядок, який просто називає їх: важко зробити краще.
Стівен К. Сталь

2
Читання НЕ особисте. Це обов'язок перед громадськістю програмування. Це суб'єктивно. Саме тому важливо приходити в подібні місця та слухати, що громадськість програмування має про це сказати. Особисто я вважаю цей приклад незавершеним. Покажіть мені, як conditionsбудується ... АРГ! Не анотації-атрибути! Чому бог? Ов, мої очі!
candied_orange

7

Подумайте про використання return;після того, як одна умова була успішною, це заощадить всі ви else. Ви навіть можете мати можливість return addAlert(1)безпосередньо, якщо цей метод має повернене значення.


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

5

Я бачив подібні конструкції, які вважаються чистішими:

switch(true) {
    case cond1(): 
        statement1; break;
    case cond2():
        statement2; break;
    case cond3():
        statement3; break;
    // .. etc
}

Тернар з правильним інтервалом також може бути акуратною альтернативою:

cond1() ? statement1 :
cond2() ? statement2 :
cond3() ? statement3 : (null);

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


1
тернар акуратний
Еван

6
@Ewan налагодження зламаного "глибоко рекурсивного потрійця" може бути непотрібним болем.
dfri

5
на екрані це виглядає охайно.
Еван

Гм, яка мова дозволяє використовувати функції з caseмітками?
undercat

1
@undercat - це дійсний ECMAScript / JavaScript
afaik

1

Як варіант відповіді @ Евана, ви можете створити ланцюжок (замість "плоского списку") таких умов:

abstract class Condition {
  private static final  Condition LAST = new Condition(){
     public void alertOrPropagate(DisplayInterface display){
        // do nothing;
     }
  }
  private Condition next = Last;

  public Condition setNext(Condition next){
    this.next = next;
    return this; // fluent API
  }

  public void alertOrPropagate(DisplayInterface display){
     if(isConditionMeet()){
         display.alert(getMessage());
     } else {
       next.alertOrPropagate(display);
     }
  }
  protected abstract boolean isConditionMeet();
  protected abstract String getMessage();  
}

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

Саме тут він переважає над підходом "плоского списку", коли вам потрібно здійснити "пропуск" у циклі, який застосовує умови.

Ви просто налаштуєте ланцюг умов:

Condition c1 = new Condition1().setNext(
  new Condition2().setNext(
   new Condition3()
 )
);

І почніть оцінювання з простого дзвінка:

c1.alertOrPropagate(display);

Так, це називається " Шаблон відповідальності"
Макс

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

0

Перш за все, оригінальний код не страшний IMO. Це досить зрозуміло, і в ньому немає нічого поганого.

Тоді якщо вам це не подобається, вибудовуючи ідею @ Евана використовувати список, але видаляючи його дещо неприродний foreach breakзразок:

public class conditions
{
    private List<Condition> cList;
    private int position;

    public Condition Head
    {
        get { return cList[position];}
    }

    public bool Next()
    {
        return (position++ < cList.Count);
    }
}


while not conditions.head.check() {
  conditions.next()
}
conditions.head.alert()

Тепер адаптуйте це на своїй обраній мові, зробіть кожен елемент списку предметом, кортежем, будь-яким, і ви добре.

EDIT: схоже, це не так зрозуміло, як я думав, тому дозвольте мені пояснити далі. conditions- упорядкований список якогось роду; head- це поточний елемент, який досліджується - на початку це перший елемент списку, і щоразу, коли next()він називається, він стає наступним; check()і alert()є checkConditionX()та addAlert(X)з ОП.


1
(Не спростував, але) Я не можу слідувати цьому. Що таке голова ?
Бель-Софі

@Belle Я відредагував відповідь, щоб пояснити далі. Це та сама ідея, що і у Евана, але while notзамість цього foreach break.
Ніко

Блискуча еволюція геніальної ідеї
Еван

0

У питанні відсутня певна специфіка. Якщо умови такі:

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

або якщо вміст у addAlertскладніший, можливим кращим рішенням у скажімо c # буде:

//in some central spot
IEnumerable<Tuple<Func<bool>, int>> Conditions = new ... {
  Tuple.Create(CheckCondition1, 1),
  Tuple.Create(CheckCondition2, 2),
  ...
}

//at the original place
var matchingCondition = Conditions.Where(c=>c.Item1()).FirstOrDefault();
if(matchingCondition != null) 
  addAlert(matchingCondition.Item2)

Кортежі не такі гарні в c # <8, але вибрані для зручності.

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


0

Найкращий спосіб зменшити складність цикломатики у випадках, коли вас багато, if->then statements- використовувати словник або список (залежно від мови) для зберігання ключового значення (якщо значення заяви або деяке значення), а потім значення / функція.

Наприклад, замість (C #):

if (i > 10) { return "Two"; }
else if (i > 8) { return "Four" }
else if (i > 4) { return "Eight" }
return "Ten";  //etc etc say anything after 3 or 4 values

Я можу просто

var results = new Dictionary<int, string>
{
  { 10, "Two" },
  { 8, "Four"},
  { 4, "Eight"},
  { 0, "Ten"},
}

foreach(var key in results.Keys)
{
  if (i > results[key]) return results.Values[key];
}

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

var results = new Dictionary<Func<int, bool>, Func<int, string>>
{
  { (i) => return i > 10; ,
    (i) => return i.ToString() },
  // etc
};

foreach(var key in results.Keys)
{ 
  if (key(i)) return results.Values[key](i);
}

0

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

Я не можу скоротити цю логіку:

if (checkCondition()) {addAlert(1);}
else if (checkCondition2()) {addAlert(2);}
else if (checkCondition3()) {addAlert(3);}
else if (checkCondition4()) {addAlert(4);}

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

if (is_an_apple()) {
  addAlert(1);
}
else if (is_a_banana()) {
  addAlert(2);
}
else if (is_a_cat()) {
  addAlert(3);
}
else if (is_a_dog()) {
  addAlert(4);
}

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


0

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

EG: checkCondition1()стане evaluateCondition1(), на якому він буде перевіряти, чи було виконано попередню умову; якщо так, то воно кешує якесь значення, яке слід отримати getConditionNumber().

checkCondition2()стане evaluateCondition2(), на якому він би перевірив, чи були виконані попередні умови. Якщо попередня умова не була виконана, вона перевіряє сценарій умови 2, кешуючи значення, яке слід отримати getConditionNumber(). І так далі.

clearConditions();
evaluateCondition1();
evaluateCondition2();
evaluateCondition3();
evaluateCondition4();
if (anyCondition()) { addAlert(getConditionNumber()); }

Редагувати:

Ось як потрібно перевірити дорогі умови для здійснення цього підходу.

bool evaluateCondition34() {
    if (!anyCondition() && A && B && C) {
        conditionNumber = 5693;
        return true;
    }
    return false;
}

...

bool evaluateCondition76() {
    if (!anyCondition() && !B && C && D) {
        conditionNumber = 7658;
        return true;
    }
    return false;
}

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

clearConditions();
evaluateCondition10();
evaluateCondition9();
evaluateCondition8();
evaluateCondition7();
...
evaluateCondition34();
...
evaluateCondition76();

if (anyCondition()) { addAlert(getConditionNumber()); }

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


Мені не подобається ця пропозиція - вона приховує логіку тестування всередині декількох функцій. Це може ускладнити підтримку коду, якщо, наприклад, вам потрібно було змінити порядок і зробити номер 3 перед №2.
Лоуренс

Ні. Ви можете перевірити, чи оцінювались попередні умови, якщо anyCondition() != false.
Емерсон Кардосо

1
Гаразд, я бачу, до чого ти потрапляєш. Однак, якщо (скажімо,) умови 2 та 3 оцінюються true, ОП не хоче, щоб умова 3 оцінювалася.
Лоуренс

Я мав на увазі те, що ви можете перевірити anyCondition() != falseфункції evaluateConditionXX(). Це можливо здійснити. Якщо підхід використання внутрішнього стану не бажаний, я розумію, але аргумент про те, що це не працює, не вірно.
Емерсон Кардозу

1
Так, моє заперечення полягає в тому, що воно ненадійно приховує логіку тестування, а не те, що вона не може працювати. У Вашій відповіді (параграф 3) чек на виконання умови 1 розміщується всередині eval ... 2 (). Але якщо він переключає умови 1 і 2 на верхньому рівні (через зміни вимог клієнта тощо), вам доведеться перейти в eval ... 2 (), щоб зняти чек на умову 1, а також перейти в eval. ..1 (), щоб додати чек на стан 2. Це може привести до роботи, але це може легко призвести до проблем із технічним обслуговуванням.
Лоуренс

0

Будь-яке більше двох "інших" пропозицій змушує читача коду пройти через увесь ланцюг, щоб знайти цікавий. Використовуйте такий метод, як: void AlertUponCondition (умова умови) {switch (condition) {case Condition.Con1: ... break; випадок Умова.Кон2: ... перерва; тощо ...} Де "Умова" є належним перерахуванням. Якщо потрібно, поверніть булінг або значення. Назвіть це так: AlertOnCondition (GetCondition ());

Це дійсно не може бути простішим, І це швидше, ніж ланцюг if-else, коли ви перевищите кілька випадків.


0

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

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

Поліморфізм, можливо, вам більше підходить.

Будьте підозрілі до коду з довгими методами, що містять довгі або складні конструкції if-then. Ви часто хочете, щоб там було класне дерево за допомогою деяких віртуальних методів.

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