Прощавай, else: покращення якості коду за допомогою лінійних конструкцій

Програмування — це коли ти розв’язуєш одну проблему і створюєш дві нові

Привіт, я Руслан Дмитракович — розробник програмного забезпечення та підприємець. Кар’єру в ІТ розпочав у 1994 році. Спеціалізуюсь на керуванні складністю в розробці та працюю архітектором.

Кожен розробник стикався з легасі-кодом — заплутаним, складним для розуміння та модифікації. Іронія в тому, що майже кожен з нас у якийсь момент створює такий код сам. Чому так відбувається?

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

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

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

У програмуванні поганим тоном вважається використання конструкцію GOTO. Попри її наявність у багатьох мовах програмування, покоління розробників фактично «скасували» GOTO, і в коді цей оператор зустрічається дуже рідко. Сьогодні я запропоную вам зробити те ж саме зі словом ELSE, тобто відмовитися від його використання, і показати, як при мінімальних зусиллях отримати значний прогрес у боротьбі за якість коду.

Принцип коду без else

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

Послідовність інструкцій та перевірок

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

Приклад:

def process_order(order):
    if not order.is_valid():
        return "Invalid order"
      
    if order.is_paid():
        return "Order already paid"
    
    payment_result = process_payment(order)
    if not payment_result.is_successful():
        return f"Payment failed: {payment_result.error_message}"
    
    send_confirmation_email(order)
    return "Order processed successfully"

Використання раннього виходу

Замість else-блоків використовуються return, throw exception або break для виходу з функції чи блоку при певних умовах.

Приклад:

function saveOrderToDB($order, $db) {
    try {
        $db->beginTransaction();
        
        // Збереження інформації про замовлення
        $orderId = $order->save($db);
        if (!$orderId) {
            throw new Exception("Failed to save order");
        }
        
        // Оновлення кількості товару на складі
        if (!$order->updateInventory($db)) {
            throw new Exception("Failed to update inventory");
        }
        
        // Створення запису про оплату
        if (!$order->createPaymentRecord($db)) {
            throw new Exception("Failed to create payment record");
        }
        
        $db->commit();
        return $orderId;
    } catch (Exception $e) {
        $db->rollBack();
        return false;
    }
}

Тернарний оператор

Для простих умовних присвоєнь використовується тернарний оператор замість if-else.

const status = user.isLoggedIn() 
  ? "active" 
  : "inactive";

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

let status;
if (user.isLoggedIn()) {
  status = "active";
} else {
  status = "inactive";
}

Тепер порівняємо метрики двох фрагментів і зробимо висновки:

Метрика

Тернарний оператор

Конструкція if-else

Кількість знаків

57

93

Кількість знаків без пробілів

50

73

Кількість слів

7

15

Кількість рядків

3

6

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

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

Тим, хто пише на Go, з тернарним оператором не пощастило. Розробники мови дотримувались іншої думки і не створили тернарний оператор (але, мабуть, оскільки в операторі присутнє Go, реалізували GoTO :-) ). Тому інші розробники на Go, яким подобається тернарний оператор, пропонують робити так:

func Ternary(statement bool, a, b interface{}) interface{} {
    if statement {
        return a
    }
    return b
}

var res = Ternary(val > 0, "positive", "negative")

Виділення логіки в окремі функції

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

public double getDiscountPercentage(User user, Product product) {
    if (user.isPremium() && product.isOnSale()) {
        return 0.2;
    }
    if (user.isPremium() || product.isOnSale()) {
        return 0.1;
    }
    return 0;
}

// Використання:
double discount = 0.0;
if (isBlackFriday) {
    discount = getDiscountPercentage(currentUser, selectedProduct);
}
System.out.println("Discount percentage: " + discount);

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

Когнітивне навантаження та лінійність

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

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

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

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

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

Є ще один неочевидний, але дуже важливий наслідок використання else. Більшість програмістів використовують правило: «працює — не чіпай». Тому коли розробник стикається з кодом, у який потрібно внести зміни, він, швидше за все, ще більше його ускладнить. Додасть нову логіку замість того, щоб робити покращення, змінюючи наявний код.

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

Наслідки застосування цього методу:

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

Висновки

Підхід до написання коду без використання else є простим для запам’ятовування та застосування, але при цьому дає значний ефект.

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

Застосування цього методу вимагає певної дисципліни та зміни звичного способу мислення. Однак описані вище результати варті цих зусиль.

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

Простий принцип — уникнення «шкідливого слова» else — може стати потужним інструментом у вашому арсеналі для боротьби зі складністю. І на відміну від довгого списку правил, його легко запам’ятати і застосовувати щодня.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось18
До обраногоВ обраному6
LinkedIn

Найкращі коментарі пропустити

Чи можна попрохати шановного пана автора навести статистично достовірне підтвердження ефективності відмови від else у if else.

Наприклад, через півроку після відмови від else ми наполовину скоротили команду QA, бо багів стало вдвічі менше.

Если что-то такое всерьез обсуждать, то надо сначала рассказать какие «фатальные недостатки» вы видите в общепринятом принципе «single function exit point», который по сути предлагает делать ровно наоборот, и описан/рекомендован в куче книг (вот примеры аргументации за — wiki.c2.com/...​leFunctionExitPointPlease , и против — wiki.c2.com/?SingleFunctionExitPoint). Без его упоминания вся статья является слишком поверхностной.

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

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

Я вважаю що когнітивну складність бізнес-логіки треба зменшувати іншим шляхом — DDD, щоб код і моделі читались як DSL-опис. Багато логіки можна реструктуризувати і спростити, перетворивши алгоритм + вхідні дані на моделі.

Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Тим, хто пише на Go, з тернарним оператором не пощастило. Розробники мови дотримувались іншої думки і не створили тернарний оператор

Причина відмови від тернарного оператору в Go є на сайті мови:

The reason ?: is absent from Go is that the language’s designers had seen the operation used too often to create impenetrably complex expressions. The if-else form, although longer, is unquestionably clearer. A language needs only one conditional control flow construct.

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

«used too often to create impenetrably complex expressions» . Це так само відноситься до if як і до тернарного оператора. Я згадав у статті GOTO саме для того, щоб показати штучність цього аргументу. GOTO чомусь реалізували, хоча «репутація» у нього значно гірша.
На мою думку причина такого рішення поза технічною сферою. Але пояснення з боку розробників: «мова наша, що хочемо то і робимо» виглядало би нечемно.

Java 17+ approach:

var status = OrderStatus.fromValue(order.status());
return switch (status) {
case PAID -> ...;
case UNPAID -> ...;
case ERROR -> ...;
default -> ...;
}

UPD: як ці теги тут працюють це п**ц

Та як воно працює блін :)

System.out.println("А тут норм");
<br>var status = OrderStatus.fromValue(order.status());<br>return switch (status) {<br>case PAID -> ...;<br>case UNPAID -> ...;<br>case ERROR -> ...;<br>default -> ...;<br>}

<p>(Exploding head)</p>

Ти мабуть суті не вловив, буває :)

Так з Kotlin частково підтягнули оператор when, але досі не такий, трохи простіший. З іншого боку в Kotlin досі нема try multi catch

Rust — найкраще щєплення від бранчінга коду )

?

там все треба перевіряти на None/ Err (якщо не чітити з expect(), шо не трушно). і проста інструкція на 5 рядків може перетворитись на «кілометровий фарш» якщо не розбити все правильно по маленькім функціям

```if err ! = nil {
return err
}
```

простітє :)

```if err ! = nil {
return err
}
```

if nil != err .....

чьорт, такої кнопкі в мене щє нема...

Кто-то может поревьюить этот код ? мне самому лень, слишком много коментов писать сюда придется

а зараз вже по руках не б’ють за десять return-ів із функції?

Дивлячись яка мова програмування... Для xQuery це норма.

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

«Є ще один неочевидний, але дуже важливий наслідок використання else. Більшість програмістів використовують правило: „працює — не чіпай“.»
...
«Отже, код з else з більшою ймовірністю буде „розбухати“ та створювати конструкцію, у якій важко розібратися. Прибираючи else, ви зменшуєте потенційну можливість ускладнювати код.»

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

«Додатково зверну увагу на форматування коду тернарного оператора. Вираз можна написати в один рядок, і тоді його сприйняття значно погіршиться.»
...
«Але автоформатер вашого середовища розробки виправить вас і розіб’є такий код на декілька рядків.»

Автор зазначає, що тернарний оператор може бути нечитабельним в одному рядку, але автоформатер це виправить. Тобто у два чи більш рядків як if-else?

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

Що логічно! Бо найчастіше зміни це необхідність доповнити код, а не видалити.

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

Ви нічого не запропонували замість else, коли потрібно саме багато розгалужень.

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

Розробники оперують функціями, классами, об’єктами. Майже завжди чимость значно більшим, ніж малесенький блок коду з умовами чи без. Особливо зараз, коли такі «чанки» у 90% випадків генеруються копайлотом або gpt-4о. Зараз не 2016 рік і навіть не 2020-й.

Далі мої кометарі за темою.

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

2. Коли функція має кілька точок виходу, легко забути про необхідність звільнення ресурсів або виконання необхідних дій перед кожним return чи throw. І це не тільки про С++.

3. Використання else є найбільш природним та ефективним способом вирішення задачі. Чого про це забули? «Піди купи білий хліб, якщо не має, купи батон». Просто. Інтуітивно-зрозуміло.
Давай, спробуй без «якщо немає».

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

легко забути про необхідність звільнення ресурсів або виконання необхідних дій перед кожним return чи throw. І це не тільки про С++.

В С++ так ніхто не робить — там кожен ресурс обертають в клас, котрий звільняє його в деструкторі, за принципом RAII. В Пітоні для цього є контексти з with. Про інші мови — не знаю, де таке є проблемою, окрім старого С (що майже не змінювався з 80х).

Та ширше ж дивись ) RAII — це оптимізація та зменьшення коду. RAII придумали умні люди.
Якщо торкатися RAII, тоді мова про безпосередньо деструктор. Ось там і є if-else, у деяких випадках багато if-else в залежностід від того, як саме потрібно звільнювати ресурс.

RAII — це те, що дозволяє не слідкувати за виділеними ресурсами і не боятися ставити return та кидати ексепшни. Це не оптимізація, а основна відмінність між С та С++. І також — між С++ та Джавою й Пітоном www.dietmar-kuehl.de/...​exceptions.html#faq-17.12

Я згоден з тобою. Я про щось дещо інше. Дивись. Давай на прикладі.
Є файл і є буфер.
Якщо буфер порожній, ми закриваємо файл.
Якщо буфер містить лише літери латинського алфавіту (A-Z, a-z), записуємо його у файл і зберігаємо.
Якщо буфер містить як літери, так і цифри (A-Z, a-z, 0-9), форматуємо його відповідним чином, а потім зберігаємо. Якщо ж буфер містить недруковані символи, файл потрібно відновити з архівної копії, повернувши його до попереднього стану.

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

Використання if-else у цьому випадку значно ефективніше, ніж тринарні оператори, і тим більше нераціональне застосування throw.

Ота уся фігня в деструктор не кладеться. Вона кладеться в операцію Save() чи Commit(). Чи, ще імовірніше, спочатку аналізується текст, котрий будемо записувати, а потім під його тип обирається відповідний клас файлу.

А деструктор лише закриває файл — незалежно того, його викликало нормальним виходом з функції чи аварійним.

В результаті саме наявність деструктора та RAII дозволяє використовувати multiple returns та throw. Бо нема про що забувати.

2. Коли функція має кілька точок виходу, легко забути про необхідність звільнення ресурсів або виконання необхідних дій перед кожним return чи throw. І це не тільки про С++.

Це поганий приклад

«Піди купи білий хліб, якщо не має, купи батон»

Бо рішення тут КупиБілийХліб() || КупиБатон()

Гм.... Людською мовою це «або»?
«Купи білий хліб або батон» це буде еквівалентно «купи білий хліб, а якщо не буде, то батон»?
Якщо я зустріну батон першим у магазині, я усе-рівно повинен поперше знайти білий хліб?

Гм.... Людською мовою це «або»?

IF-ELSE це типове АБО.

«Купи білий хліб або батон» це буде еквівалентно «купи білий хліб, а якщо не буде, то батон»?

В моєму прикладі використані функції, які повертають тобі або білий хліб, або false/null/undefined, тому це повний еквівалент «купи білий хліб, а якщо не буде, то батон».

Якщо я зустріну батон першим у магазині

В мовах програмування порядок має значення. A || B не те саме, що й B || A

Щось мені приклад з тернанрним оператором не дуже зайшов
Там суто косметичне рішення без уникнення складності — є 2 гілки коду по факту
І якщо ви захочете додати нову умову, то тернарний оператор перетвориться на жах

Чому не можна в вашому прикладі зробити функцію, що первертає статус юзера, в якій зробити один блок if і 2 ретурна
У разі ускладнення логіки, ви або в цю функцію додасте інші умови, або додасте новий рівень вкладених функцій — тут вже від контексту залежить

І ви ж самі потім даєте такий саме приклад — реалізацію логіки окремою функцією

І якщо ви захочете додати нову умову, то тернарний оператор перетвориться на жах

Не перетвориться, якщо робити це з розумом

const
  stateA = boo() ? true  : false,
  stateB = foo() ? true : false
  stateC = stateA && stateB ? true : false
;

а навіщо в вашому прикладі тернарний оператор, якщо там і так результатом буде булево?

мені не потрібно 3 різних статуси — мені потрібно отримати один статус в залежності від трьох різних умов.

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

def get_state_of_some_object(some_object):
  if (first_condition):
    return first_state
  if (second_condition):
    return second_state
  return third_state

current_state = get_state_of_object(some_object)

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

тернарний зручний лише коли значень не більше 2 — чисто скорочення кількості строк.
при більшому значенні вже починаються дикі танці

Отличная статья!
В практике программирования на Go именно такой подход всегда и практикуется. И это одна из вещей, что сразу бросается в глаза при переходе с C++ на Go, благодаря в том числе и этому подходу там код визуально проще выглядит, причём существенно. Это не только помогает с чтением чужого кода, но также и ускоряет написание собственного.

А давайте вообще без ветвлений! Вот!
public double getDiscountPercentage(User user, Product product) {  int mlpr = user.isPremium() + product.isOnSale(); return mlpr * 0.1; }
Обрывные if (if->return) давно используемая практика в С/С++ проектах — имеет как много адептов, так много и противников, хотя все пишут как code-style регламентирует.
Вообще то что предлагает автор это шаг назад лет так на 40, в x86 ассемблере нет конструкции else, как таковой, но там и циклы и перемещения строятся за счет аналогов неких goto. if/else это достижение высокоуровневых языков.
Ну и еще терначи плохо трассируются — так как отладчики так себе ходят по горизонтали.

Вообще то что предлагает автор это шаг назад лет так на 40

Всі сучасні технології побудовані на базі, яку вигадали ще в 50-70х. І дуже цікаво спостерігати, як нове покоління розробників прибігає та вихваляється, що вони щось оригінальне придумали, а тому «оригінальному» сто років в обід вже як. Лінійність виконання роботи — добре, а не погано. Але до цього приходиш тільки з досвідом.

Ну функциональщина. Только вот то все длинное надо было сразу положить под return.

Хз як там if-else, от мені якось ментор розказав за for-else та while-else конструкції і це був просто-таки game changer для певної логіки, яку ми часто використовували в тестах

throw cоздает очень жирный оверхед то что создает стек вызовов для раскрутки наверх, ужасный подход. Прежде чем такое советовать очень рекомендую проверить бенчмарками насколько снижается перформанс с else и exception подходом. А отстутсвие тернарного оператора одновременно и преимущество то что я видел злоупотребление вложенностью ?: ?: и разбирать такие штуки ну его нафиг

throw cоздает очень жирный оверхед

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

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

В статье не про обработку ошибок, а про «замену else»
и єто вот, как отментил Ангуляров — будет очень херово, очень
все эти wind/unwind — єто огромній удар серпом по перфрмансу, особенно в расчетніх задачах (которіх на самом деле множество)

врядли есть идиоты которые в рассчетных задачах кидают ексепшены. а для бизнес задач там хоть каждые 100 мкс бросай ексепшен никто ничего не заметит.

врядли есть идиоты которые в рассчетных задачах кидают ексепшены.

Спасибо, поржал

а для бизнес задач там хоть каждые 100 мкс бросай ексепшен никто ничего не заметит.

Ну вот оказывается я не зря ржал.

Вообще-то, в С else как раз использовался для обработки ошибок.
И С++ хвалился тем, что код стал лучше и кода стало меньше благодаря земене кодов ошибок на исключения.

Вообще-то, в С else как раз использовался для обработки ошибок.
И С++ хвалился тем, что код стал лучше и кода стало меньше благодаря земене кодов ошибок на исключения.

И именно поэтому все базовые рутове вещи до сих по на си, а вся «красота» — на плюсах
Я не спорю, что для обработки ошибок SEH, скажем, куда удобнее чем постоянно проверять getlasterror()/etc
Но блин же не вместо ELSE!!!!
И только там, где єто блин реально надо!

вы же еще учитывайте момент, что 30% девелоперов сейчас прочитают эту статью — и воспримут это не как «еще один забавный вариант», а как блин руководство к действию!
как это было с «не используй гото, это зло!»

в случае С/C++ при экспешионе код должен вернуть системе память free(), дернуть деструкторы объектов, закрыть хэндлы объектов. В случае пхп как привел автор на это можно забить болт то что пыха с окончанием реквеста сама закрывает все хэндлы.В случае долгоживущих сервисов это по-любому вылезет утечкой памяти или ресурсов и сервис потом подохнет от OOM, то что какой-то умник после этой статьи начнет сыпать экспешионами везде

Ну в С и С++ еще бывают арены — когда мапят кусок памяти, пока есть реквест или сессия — из него выделяют блоки подряд, а когда сессия завершилась — переставляют указатель на начало блока. Самый быстрый менеджер памяти. www.gingerbill.org/...​llocation-strategies-002

в случае С/C++ при экспешионе код должен вернуть системе память free(), дернуть деструкторы объектов, закрыть хэндлы объектов.

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

C++ Exceptions for Smaller Firmware — Khalil Estell — CppCon 2024: “Its a shame embedded devs don’t use exceptions... It would make their binaries smaller.”

Link: www.youtube.com/watch?v=bY2FlayomlE

Це що якийсь новий holly war, типу CamEl проти sn_ake або Tab чи 4 space. Тільки тут if else проти if.

Мужик, а ти точно архітект? А то шось такі цікаві тези шо я в шоці.
«Все життя їв тільки виделкою, баста! З сьогодні їм тільки ложкою»

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

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

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

Гівнокод і легасі — це як ентропія в природі. Видається що іноді вони неуникні.

Іноді в разі наприклад складного розрахунку якихось маркетингових знижок це дуже спрощує життя.

Ви їх імперативно розраховуєте? Куди котиться світ... І це ще називається спрощенням життя...

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

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

Дійсно, ранній ретурн це імба. Майже завжди.
І да, мені попадався кусок коду де у великому е-коммерц рітейлері на два екрани йшло всяких else до одного if у розрахунку суми знижок у ордера. Виглядало монструозно і жутко (і писано до нас :)), зате у випадку проблем всі знали в якій функції якого класу треба дебажити.

у великому е-коммерц рітейлері на два екрани йшло всяких else до одного if у розрахунку суми знижок у ордера

Все вірно, воно так і буває. Додайте +5% знижки, але сумарно не більше 15$ на ордера, які зроблені однооким клієнтом у час з першої до третьої години ночі за місцевим часом Папуа Нової Гвінеї.
* якщо в замовленні є шось зелене — збільшити знижку до 7%
** якщо в ордері є шось червоне, зменшити знижку до 4%
*** якщо є і зелене і червоне — відмінити ордер.

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

В бизнес системах есть куча клоунов, которые будут разруливать эту задачу с помощью SOLID, а именно Open-Closed principle и нагораживать IDiscountStrategy вместо простеньких if-else. Эти ублюдки страшнее всего.

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

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

Там все можна побудувати на скорінговій моделі, яка під капотом буде примітивним суматором.

и в итоге состоять это всё будет из серии иф-елс.

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

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

а, так вот оно как оказівается сделано... всего навсего сравнить два значения... ну тогда да, елсе там не нужен

Яким боком цей приклад до елсів?

це імба.

Що таке імба? На якій це мові?

Це геймерський сленг. Імба — imbalanced, круте і потужне значить.

Главное чтобы наш президент не прочитал этот комент.

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

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

Трохи дивно, що майже ніхто не сказав про паттерн заміни умовної логіки поліморфізмом. Криза ООП?

Навіщо прості речі переускладнювати? Можете привести приклад, коли використання поліморфізма суттєво покращує розуміння коду?

Дякую за приклад. Як буде виглядати ваш код, коли тип платіжної системи буде приходити ззовні як параметр?

Це не мій, це ChatGPT у відповідь на ваше питання, але це загальний приклад. Я не дуже в JavaScript.
У вигляді чого будуть платіжні системи? Як плагін? Ну пишете для них обгортку, декоратор. В будь якому разі мусить бути інтерфейс.
Гадаю в Angular мають бути готові рішення. Спитайте у ChatGPT
Я думаю, що в нас не зовсім вірно пояснюють принципи ООП на кішках та собаках. Головна перевага ООП мала б пояснюватися на білій кишці й смугастої кішці. Цей приклад значно ближчий до Clean Code.

Можете привести приклад, коли використання поліморфізма суттєво покращує розуміння коду в JavaScript?

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

Приклад: Система оплати
Уявімо, що є система, яка обробляє різні методи оплати: кредитна картка, PayPal і криптовалюта. Кожен метод має свою унікальну реалізацію, але всі вони повинні підтримувати метод processPayment.

Без поліморфізму

function processPayment(paymentMethod, amount) {
  if (paymentMethod.type === 'creditCard') {
    console.log(`Processing credit card payment of $${amount}`);
  } else if (paymentMethod.type === 'paypal') {
    console.log(`Processing PayPal payment of $${amount}`);
  } else if (paymentMethod.type === 'crypto') {
    console.log(`Processing cryptocurrency payment of $${amount}`);
  } else {
    console.error('Unknown payment method');
  }
}

const payment1 = { type: 'creditCard' };
const payment2 = { type: 'paypal' };

processPayment(payment1, 100); // Processing credit card payment of $100
processPayment(payment2, 200); // Processing PayPal payment of $200

Цей код швидко стає важким для підтримки, якщо додається більше типів оплати.

З поліморфізмом
Використовуємо класи (або об’єкти) з однаковим інтерфейсом:

class CreditCardPayment {
  processPayment(amount) {
    console.log(`Processing credit card payment of $${amount}`);
  }
}

class PayPalPayment {
  processPayment(amount) {
    console.log(`Processing PayPal payment of $${amount}`);
  }
}

class CryptoPayment {
  processPayment(amount) {
    console.log(`Processing cryptocurrency payment of $${amount}`);
  }
}

// Функція обробки оплати
function processPayment(paymentMethod, amount) {
  paymentMethod.processPayment(amount);
}

// Використання
const payment1 = new CreditCardPayment();
const payment2 = new PayPalPayment();
const payment3 = new CryptoPayment();

processPayment(payment1, 100); // Processing credit card payment of $100
processPayment(payment2, 200); // Processing PayPal payment of $200
processPayment(payment3, 300); // Processing cryptocurrency payment of $300

Переваги поліморфізму:
Розширюваність: Додавання нових методів оплати не потребує змін у функції processPayment.
Зрозумілість: Логіка для кожного методу оплати інкапсульована в окремому класі.
Зменшення коду: Позбавляємося умовних операторів (if-else).
Таким чином, поліморфізм допомагає зробити код більш зрозумілим, підтримуваним і легким для розширення.

void f(void)
{
  if (logs_enabled) {
    log("Enter to function");
  }

  do_first_step();

  if (logs_enabled) {
    log("First step completed!");
  }

   do _second_step();

  if (logs_enabled) {
    log("Second step completed!");
  }
}

Якщо не розуміння, то чистота.

void f(struct logger * logger)
{
  logger->log("Enter to function");
  ...
}

Гарний приклад, але в ньому функція log може першим рядком містити

if (!logs_enabled) return
що буде аналогічним рішенням, але без ООП.

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

Так, вдалим проектуванням класів можна зменшити кількість умовних операторів. Але це ж зовсім інша тема.

Спробую пояснити, чому я вважаю тернарний оператор простішою конструкцією.

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

public boolean getValue()
{
return parameter A ? (Parameter B ? true : (Parameter D ? true : false)) : (Parameter C ? (Parameter E ? true : false) : false);
}

Реальний код, написаний 20 років тому досить крутим програмістом, співзасновником відомої датскої компанії, яка зараз входить в MS.

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

То ти ще не бачив

void Foo(int x);

bool Bar(int x) {
    if(x > 0)
        return Foo(x), true;
    return false;
}

Bangalor pattern

	Boolean isEnabledDisabled(boolean argument) {
		if( Boolean.valueOf(argument).toString().equalsIgnoreCase("TRUE") ) {
			return Boolean.TRUE;
		} else if( Boolean.valueOf(argument).toString().equalsIgnoreCase("FALSE") ) {
			return Boolean.FALSE;
		} else {
			return null;
		}
	}
Абсолютно ральний код який років ще з 10 років був привселюдно в спадку від індіських вендорів.
Коли з’ясовували от нахіба таке писати, зовсім тупі чи як ? Зясувалось — що індіські компанії платили програмістам зарплатню, за кількість написаних оперетаторів — що нараховувалась після сканінгу софтом. І вони всюди вставляли отакі какаха паттерни, щоб банально більше заробить.
Зараз така практика припенилась більше компанії так не роблять, Індуси перестали цим займатись. Бо код написанний таким стилем, сапорту як такому не підлягає його навіть IDE не можуть індесувати, там реально гігабайти.

Це щоб люди не забували про sequence point ;)

Там в оригіналі булева відповідь?

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

Абсурдність їх вкладання один в одний. Якщо тільки одна умова — то це норм, як на мене.

Там тоже можна файно попастися. Наприклад в PHP ввели синтаксичний сахар типу ??, що є аналогом isset($a) ? $a : othercase.
І я даже в ядрі одного відомого пхп фреймворка виловлював, коли вони замість $a = $b ?? $c, написали $a = isset($b) ?? $c. А потім ми дивувались чого це один хитрий випадок рефандів ніколи не проходить.

Ну тобто така помилка не так наочно очевидна при біглому прочитанні, як хотілося б.

GCC підтримує схожу фігню для С:

x = param ?: default_value;
Я коли вперше побачив то втикав що воно є

Мда, круто... Нічого не сказати...
Знову ж приведу приклад, як це, на мою думку, зроблено адекватно. Це в xQuery 4.0

$value otherwise $fallback
І це можна в ланцюг скласти
$node/(@full-name otherwise @name otherwise @id)
Тут кілометр коду на просту конструкцію замінили.

Так тринарний оператор просто криво написаний сам по собі. Усього то треба.

return A ? (B || D) : (!C && E); 

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

З сішних жартів це опертатор «прямує до 0»

while ( x —> 0 )
{
foo();
}

І чим довша стрілочка тим швидше воно прямує до 0

while ( x ------> 0 )
{
foo();
}

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

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

цей метод форматування має прямий зв’язок з фп. уявіть у вас є функція для чисел Фібоначчі

f 0 = 0
f 1 = 1
f x = f (x-1) + f (x-2)
перші два випадки це частинні випадки. якщо писати це на спп то буде якось так
int f(int x) {
  if (x == 0) return 0;
  if (x == 1) return 1;
  return f(x-1) + f (x-2);
}
на мій погляд ідеальний стиль, використовую усюди де можу. може навіть так писати.
int f(int);
int result_for_case0() { return 0; }
int result_for_case1() { return 1; }
int result_for_others(int x) { return f(x-1) + f(x-2); }
int f(int x) {
  switch (x) {
    case 0: return result_for_case0();
    case 1: return result_for_case1();
    default: return result_for_others(x);
  }
}
це звичайно схематично, але дозволяє не писати коменти взагалі.

Непоганий вкид :)
Я тут підіграю і розповім — що усі три методи — не більше ніж кодстайл який абсолютно нічим не вирізняється один від одного, окрім особистих перепон програміста.
Усі три функції реалізують один і той самий алгоритм, який бере O(N) памє’яті — і витрачає O(2^N) роцессорного часу.
Числа Фібоначі можна вираховувати алгортмом швидкого матрічного перемноження, за O (log N) часу. От є непогана стаття яка пояснює усе
robwilsondev.medium.com/...​ponentiation-d9924545fe54
Там в принципі можна підти далі і перемножувати матриці за допомогою різних методів SIMD -наприклад OpenMP або GPU — OpenCL, CUDA, ROCm і т.д. і отримати взагалі дуже великий буст при великих N за рахунок можливостей сучасного обладнання.
Вкину далі.
Нажаль це не про Haskel — і навітьне про R, але це про С/C++ — та можливо інтерфейси до Python, C#, JavaScript або Java, чи так само Haskel.

Пфффф, плюсисти взагалі рахують це підчас компіляції ;)))

template<int n>
struct fibonacci
{
  static constexpr int value = fibonacci<n-1>::value + fibonacci<n-2>::value;
};
template<>
struct fibonacci<0>
{
  static constexpr int value = 0;
};
template<>
struct fibonacci<1>
{
  static constexpr int value = 1;
};

int main()
{
    fibonacci<42>::value;
    return 0;
}

На іншому форумі плюсистів, довга була дискуссія того — що compile time це звісно добре. але за його допомогою часто реалізують вкрай не оптимальні — наївні алгоритми, що призводить до дуже великого часу компіляції.
Як приклад був strlen — який CRT/glibc робить однією асемблерною інструкцією, а програмісти пишуть рекурсію або цикли, які от жодним чином не ефективні. Моя пропозиція була — дозволити компілятору під час роботи constexr викликати принаймні функції стандарнтої бібліотеки C позаяк вони там є, а массу фуннкцій до яких є інтрісіки — зробити взагалі операторами мови. Ну тобто робити те саме що і інтерпритатори типу Python чи JavaScript і віртуальні машини Java, LLVM і т.д. Там над цим працюють, послабили правила і навіть дозволили ассемблерні вставки в constexpr.

плюсисти взагалі рахують це підчас компіляції ;)))

Ну порахуйте на компіляції для N=300 без втрати значення.

Там же не дарма стоять смайли. І причому тут єтап компіляції якщо це обмеження типу.

Числа Фібоначі можна вираховувати алгортмом швидкого матрічного перемноження, за O (log N) часу. От є непогана стаття яка пояснює усе

максимальное число, которое может хранитm дот нетовский дециамл это 79,228,162,514,264,337,593,543,950,335.
141е число фибоначи это 81055900096023504197206408605, оно больше чем макс дециамал
следовально, для хранения всех практически доступных чисел фибоначи нужен массив из 140 децимал по 16 байт = 2240 байт, сложность O(const)
Этого достаточно для практического применения

Підкажіть будь ласка, який тег для коду? Щось < code > нічого не дає.

і все?

class CreditCardPayment {
  processPayment(amount) {
    console.log(`Processing credit card payment of $${amount}`);
  }
}
чомусь тільки так

Чудова стаття

public double getDiscountPercentage(User user, Product product) {
    if (user.isPremium() && product.isOnSale()) {
        return 0.2;
    }
    if (user.isPremium() || product.isOnSale()) {
        return 0.1;
    }
    return 0;
}

я б так написала, це відповідає правилам?

(or 
  (when (and user.isPremium() product.isOnSale()) 0.2)
  (when (or  user.isPremium() product.isOnSale()) 0.1) 
  0)

Ці ваши Ліспи Clojure з полським записом
Kotlin

    fun getDiscountPercentage(user: User, product: Product): Double
        =   when {
                user.premium && product.onSale -> 0.2
                user.premium || product.onSale -> 0.1
                else -> 0.0
            }

Важлива не відсутність else, а саме причина його не використовувати.

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

Коментатори які у якості аргументу проти тернарного оператора використовують вираз з декількох тернарних операторів допускають туж саму помилку.

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

Якщо аналізувати ваш приклад, то за своєю складністю він буде аналогом if else. Штучний приклад у статті можна розбити на три незалежні частини. І це головна його перевага.

Якщо аналізувати ваш приклад, то за своєю складністю він буде аналогом if else. Штучний приклад у статті можна розбити на три незалежні частини. І це головна його перевага.

це ж жарт в дусі а ваша мова так може

звісно тут має бути написано нормально

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

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

Чи можна попрохати шановного пана автора навести статистично достовірне підтвердження ефективності відмови від else у if else.

Наприклад, через півроку після відмови від else ми наполовину скоротили команду QA, бо багів стало вдвічі менше.

О ну це знову в повторення класки в Дейкстуру, матоди математичного доведення вірності алгоритму повністю провалились в індустрії. youtu.be/E9-scyUdmeI?t=2036
Індустрія використовує науковий метод — тестування по нашому. Від модульних тестів до End to end testing E2E.
Тут відправлю до дядька Боба, Роберта Мартіна та книги Clear Architecture.
Те що наводить автор це насправді про тип мислення. Він теж має місце, той же принцип Fail Fast нікуди не подівся. Тобто щоби читати программу людиною — її логіка має бути розділенна на прості фрагменти кожний з яких відпвідає одному простому шагу алгорттма обробки данних. І далі йдеться про шляхи досягнення цього.
У бідь якогому разі спагетті код, де усе між собою переплутано і доволі чітко показує спутаний і не системинй потік мислення прогаміста який це писав (зазвичай початківця, хоча є люди які мають і десятки років досвіду але пишуть абсолютне спагетті на костилях і як попало), а також брак алгоритмичної підготовки — не любить ніхто.
І там що multiple return, що single — різниці не буде ніякої.
BTW Стан любого програмного проекту можна оцінити по двом ключовим факторам — прогнати статичний аналіз санітайзерами, типу SonarQube, СppCheck і т.п. і подивитись на спосіб яким команда описує вимоги. Спагетті код з величезним техдептом і якісь технічні специфікації або взагалі фрагменти коду у вимогах — знайте проект в повній дупі, це експерементальне програмування (ляп ляп і в продакшн). Такий підход це чистий random — невідтворюванність результатів. Ніхто не може гарантувати не достатньо довгий час підтримки, не що більш важливо — попадання в строки та в бюджет. Тим не меше бурний розвиток індустрії призвів до того що 80% усіх команд з розробки софта використовують саме експерементальне програмування. CMM level 1 en.wikipedia.org/...​Capability_Maturity_Model

СppCheck

Покажи лише чи його вже хтось проганяв на минуломі тижні

Ну є і таке, що CI ранить автоматом статичний аналіз, але менеджер примусив відкрутити гейт щоб білд проходив і вийти в реліз. Звісно реліз скінчився постійними хотфіксами і з рештою зміною вендора, навіть погодились на команду України замість поляків. При чому приводили «експерта» і потім який сказав, що воно галіматію показує і взагалі не треба цим займатись — головне якнайшвидше фіксити баги і деплоїти на прод хутчіше. Я не шуткую — усього місяці три тому була така розмова, доводилось мало не лекцію читати про : «Що таке хорошо — що таке погано. XP Кент Бек та космічні кораблі на просторах великого театру » (www.youtube.com/watch?v=RKww4qZpoLw). Менеджериня яка давила (з сусідньої тіми) — аж з себе виходила.
Хоча і у Українців буває. Якось тімлід одного стріму сказав команді — що можна тести не писати, знову таки під тиском менеджмента. Потім сидів сам по ночах та вихідних і фіксив баги, бо після абдейту на нову версію фреймверка — усе к бісу розвалиось числьні регрессії. Щоправда коли мене додали я почав лаятись на код ревью. І потім тімлід усім сказав, що хто не буде писати тести, буде сам фіксити баги в безкоштовні овертайми. І народ почав писати тести.
Тут така сама классика — Фредерік Брукс, Міфічна людиногодина.

Я описав тільки один з прийомів. Було б добре якби так все просто вирішувалось. Якщо з досвіду: прийшов на проект який був у критичному стані — бізнес був дуже незадоволений станом розробки (критичні прод баги та проблеми з продуктивністю). За 2 роки системної роботи проектом, зі стану «Шеф, фсьо пропало!» проект перейшов у режим коли фічі виходили за графіком узгодженим з бізнесом, критичних прод багів не було, а загальна кількість багів зменшилась. І кількість else по коду теж зменшилась :) Звісно це заслуга всієї команди, але як архітектор я теж свій внесок зробив.

бізнес був дуже незадоволений станом розробки

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

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

не треба нас скорочувати :(

QA-ї після скорочення не опустили руки. Трішки попрацювали над собою і стали PM-ми, PO-ми та Architec-ми)

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

Ох же ці сноби, хоча, бачу, пан на с++ і SQL пише, він звик до уродливого синтаксису і мазохізму

Закликаю пана до взаємоввічливості. Аргументовано оцінюватись і обговорюватись мають думки та ідеї, а не люди.

Такий комент ніби реальний Дональд Трамп написав :D (без образ, просто кумедно)

Ото підтримую. Як бачу метрики повязані з кількістю символів у лістингу, так прямо олдскули зводить, перфоленту ж треба єкономити, перфокарти складати ніде. То вже куди менше когнітивного навантаження у таких пердлах:
С: while( *dst++ = *src++ ) ;
або на котлін тож можна: var i = 5; i = ++i + ++i;
До речі, а скільки буде і?

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

Короткий код != читаємий код != код який легше підтримувати. Треба дивитися по ситуації, який підхід і які мовні фічі роблять код більш читаємим і зрозумілим в кожній конкретній ситуації, і залишити код в цьому стилі, а не так, як пан пропонує ресьорч робити як та чи інша мовна конструкція зменшує кількість багів чи економить час тестерів, і писати код в старому стилі поки не буде доведено що новий стиль зменшує кількість багів, навіть якщо очевидно що новий стиль більш читабельний і зрозумілий. А в прикладах в статті очевидно що парочка if з return читаєміша ніж парочка вкладенних if — else.

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

early exit — відомий, дуже «дешевий» трюк щоб запустити процесс «очищення» і покращити культуру / швидкість розробки, наравнє з статичними аналізаторами і базовою інструментацією мєтріками. Але це все в комплексі треба робити, тоді і результат буде видно.

Його можна поміряти з величезною похибкою через Developer satisfaction survey і cycle/lead time. (Але воно все рівно віламі по водє, нажаль, дуже контєкстно залежно і biased).

P.S. Але докопатись з вимогами дати статистику — свята справа, розумію )

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

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

наравнє

пан докопався до мови — очікувано ) відчуває свою інтелектуальну перевагу?

Вибачте, будь ласка, мабуть я неправильно сформулював свої думки.

Мені направду дуже подобається r2u.org.ua. Хотілося б щоб якнайбільше українців якнайчастіше цими чудовими словниками користувались.

Саме так, для того щоб було добре потрібні комплексні заходи. Це лише одна з частин загальної картини.

Класичний холівар, multiple return та exception based logic. Це ж як про таби та пробіли.
Є цілі книги написані які кажуть — чому ці код стали погані і навпаки купа народу якраз за це. А Go lang взагалі позбавився ексепшенів як механізму, як Python зробив коли код не в кодстайлі Гвідо, щоб взагалі була помилка компіляції.
У Kotlin дуже здорово пропрацювали із оператором присвоювання на томість. Тобто можна використовувати той самий if else у функціональному стилі.
Часто програмісти створюють нові мови програмування, не через нові парадигми які підвищують продуктивність праці — а фактично щоби реалізувати свій код стайл і свій поток мислення — як єдино вірний. При цьому як кажуть фундаментальні принципи не міняються із часів FORTRAN.

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

Це ще більше класичний холівар. Бо наприклад процесси мислення жінок і чловіків навіть на базових рівнях еволюцією зроблені по різному. Скажімо жінкам дуже складно з парлельним паркуванням, натомісь чоловіки шукають молоко в холодильнику хвилину — коли жінка його там знайде миттєво. І часто ми один одного бісим цим, хоча так було еволюційно задумнно для виконання різних функцій. Так краще для розвитку, ніж наприклад у черв’яків.
Також само існують різні школи з програмування і різні підходи і т.д..
В тих же розробках компиліторів. Американська школа — LALR граматики, євпропейська — LL граматики. Яка вінрна та усяка, у кожної свої плюси і свої мінуси.

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

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

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

О, нарешті адекватні ідеї на форумі %)) Буквально позавчора розповідав студентам про це.

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

Детальніше тут:
refactoring.guru/...​tional-with-guard-clauses

Увеличение количества проверок:
Повторение проверок в разных местах кода, что может привести к избыточности и усложнению логики.

Проблемы с читаемостью длинных функций:
Длинные функции с множеством ранних выходов становятся трудными для восприятия и требуют больше усилий для анализа.

Утрата контекста вложенности:
Логика условий может быть «рассеяна» по коду, из-за чего теряется интуитивное понимание последовательности.

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

Риск избыточной линейности:
Линейная структура может перегрузить код проверками, сделать его менее структурированным и менее читаемым.

Необходимость дополнительных комментариев:
Лишённый вложенности код часто требует пояснительных комментариев, что противоречит принципу самодокументируемости.

Необходимость переворачивания условий:
Использование отрицаний (if not) может ухудшить читаемость, особенно при сложных условиях.

Не всегда подходит для больших команд:
Новый подход может быть непонятен всей команде, особенно если её участники привыкли к традиционным конструкциям.

Меньшая выразительность кода:
В некоторых случаях else логически завершает ветку условий и делает намерение кода более очевидным.

Ограниченная применимость:
Отказ от else не всегда оправдан и может быть неуместным в случаях, где вложенность отражает естественную логику задачи.

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

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

function fitRectangleToSquare(rectangleSize, fitSize){

	if (rectangleSize?.width && rectangleSize?.height && fitSize) {
		let { width, height } = rectangleSize

		if (width > height) {
			var ratio = fitSize/width
		}
		else {
			var ratio = fitSize/height
		}

		return {width: Math.ceil(width * ratio), height: Math.ceil(height * ratio)}
	}
	else {
		return null
	}
}
А ось другий буде таким
function fitRectangleToSquare2(rectangleSize, fitSize){

	if (!rectangleSize?.width || !rectangleSize?.height || !fitSize) return null

	let
		{ width, height } = rectangleSize,
		ratio = (width > height)? fitSize/width : fitSize/height,
		newWidth = Math.ceil(width * ratio),
		newHeight = Math.ceil(height * ratio),
		result = {width: newWidth, height: newHeight}
	;

	return result
}
Хтось скаже фу, кака, як таке можна читати, це ж якийсь асемблер, а не JS! Але в цьому підході є свої позитивні сторони. Наприклад, вам треба повертати оригінальні розміри, якщо прямокутник менший за квадрат.
Треба модифікувати лише один рядок коду
result = (ratio < 1)? {width: newWidth, height: newHeight} : rectangleSize
if (!rectangleSize?.width || !rectangleSize?.height || !fitSize) return null

за такий кондішон я б по руках лупив би.
Перший варіант набагато кращий. От тільки його треба ще розбити, наприклад винести вложений іфелс в функцію `findRatio(w, h)`

А що не так? зі старту перевірили все, без чого далі нема сенсу продовжувати

Я не про саму перевірку, а про те як вона написана. Тут нагромаджено купа синтасичних конструкцій, які значно ускладнюють читання в рази. `!`, conditional chaining, `||` і все це в одній умові.

Це все залежить від звички. Зазвичай той рядок читають раз на пʼятирічку. Навіщо приділяти стільки уваги дрібницям? Фокусуйтеся на головному — на тому, що функція робить.

Фокусуйтеся на головному — на тому, що функція робить.

А ось тут вже починається інша історія: «Як зрозуміти, що робить ф-ція». Мабуть потрібні зрозумілі назви, а з цим теж виникає купа проблем. Тому читабельність коду має бути на першому місці включаючи ось такі кондішени і назви ф-цій. Краще написати 3 іфа, написати лишню функцію але зберегти читабельність. Оті всі оптимізації в 99% випадків не потрібні.

Читабельність — то субʼєктивна характеристика. Люди, які звикли до одного синтаксису, будуть інший вважати поганим за замовчуванням. Люди з різним рівнем та досвідом можуть по-різному оцінювати один й той самий код. Тому «читабельність» в одній групі людей буде одна, а в іншій зовсім інша.

if (risk == "high") { color = "red" } else { color = "green" }
color = (risk == "high")? "red" : "green"
color = risk == "high" && "red" || "green"
Це ідентичний код. З першого погляду другий варіант самий читабельний. Але, третій самий легкомодифікуємий. Я спеціально трохи його «зіпсував», щоб потім трохи поліпшити.
color = (risk == "high" && "red") || (risk == "medium" && "yellow") || "green"
Його можна поліпшити, звісно ж, використавши switch..case, і хтось саме так й зробить і буде вважати кращим варіантом. Але я б вже робив такий варіант:
color = {high: "red", medium: "yellow"}[risk] || "green"
Бо тут можно виконати роботу прямо під час декларації.
let color = if risk == "high" { "red" } else { "green" }

На жаль JS так не вміє :( Така конструкція можлива в xQuery, там можна навіть результат роботи switch..case привласнювати змінній.

На жаль JS так не вміє

const color = risk === 'high' ? 'red' : 'green'
Хіба не те саме? Так, це не світчкейс і виникнуть проблеми якщо умов або значень більше ніж 2.

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

const color = risk === "hight" ? foo() && boo() && goo() : zoo() || yoo()
Така конструкція важкувато читається. Може допомогти додаткова обгортка фукнцією, але це трохи вже занадто, як для такого простого прикладу.

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

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

Самі собі перечите. Для чого стрибати по коду якщо рядок
const ration = findRatio(width, height);
сам за себе все говорить?

Назва функції не пояснює взагалі про яке саме Ratio йде мова. Чому саме find? Де ми його шукати зібралися? Назви таких простих функцій мусять бути більш конкретними ;)

а так?

function is_correct(rectangleSize, fitSize) {
  return !rectangleSize?.width || !rectangleSize?.height || !fitSize
}

function fitRectangleToSquare2(rectangleSize, fitSize) {
  if (!is_correct(rectangleSize, fitSize)) return null
  let
    { width, height } = rectangleSize,
    ratio = (width > height)? fitSize/width : fitSize/height,
    newWidth = Math.ceil(width * ratio),
    newHeight = Math.ceil(height * ratio),
    result = {width: newWidth, height: newHeight}
    ; 
    return result
}

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

let
  noRectWidth = !rectangleSize?.width,
  noRectHeight = !rectangleSize?.height,
  noFitSize = !fitSize
;
if (noRectWidth || noRectHeight || noFitSize) return

Так і отак можна

  function fit_rectangle_to_square(rectangle_size, fit_size) {
       let result = null
       if( !rectangle_size?.width || !rectangle_size?.height || !fit_size) {
		const ratio = (rectangle_size.width - rectangle_size.height) > 0 ? fit_size/rectangle_size.width : fit_size/rectangle_size.height
		result = { width: Math.ceil(rectangle_size.width * ratio), height: Math.ceil(rectangle_size.height * ratio) }
       }
       return result
   }  
Тут сенс такий, що іноді існує більше одного вірного рішення. Це саме код стайл.

Як там кажуть: «Only a Sith deals in absolutes.» Звичайно треба шукати золоту середину і не використовувати крайнощів.

Буквально кожен пункт або дурниця або

не всегда
может быть
может привести

або не може.

Не можу погодитися з усіма твердженнями, оскільки вони залежать від контексту. Мій особистий досвід показує: у 90% випадків, переписавши логіку без else, код стає зрозумілішим.
Я не пропоную повністю відмовлятися від чогось, головне — думати, коли це доцільно, а коли ні.

Чим доросліше стаєш як розробник, тим більше твій код схожий на BASIC :D

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

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

Я вважаю що когнітивну складність бізнес-логіки треба зменшувати іншим шляхом — DDD, щоб код і моделі читались як DSL-опис. Багато логіки можна реструктуризувати і спростити, перетворивши алгоритм + вхідні дані на моделі.

Ви вважаєте між іншим те саме — що і Лінус Торвальдс. Що думати треба про структури данних, а не про код.

В його випадку він правий. Але ядро ОС та якась система автоматизації процесів компанії — продукти різних всесвітів

А яка різниця коли йдеться про алгоритм обробки данних ?
От є данні на вході — от є те що має бути отримано на виході. Уся сутність алгоритму зводиться на тому щоби правильно розмістити данні в пам’яті (в структури, сторонній кеш, файли, базу данних і т.д.) і примінити різні алгоритми — сортувати, фільтрувати, формули разрахункові типу статистики і т.д. і т.п.
І тут це хоч прикладний софт — хоч системний, великої різниці нема.
Де виникає переважна кількість помилок?
Перше це самі вимоги, тобто не вірно зрозуміли формат або вхідних данних — скажімо можна вводити усе що завгодно — а алгоритмом не передбачено що вхідні данні можуть мати не числа. Або невірно зрозуміли формати вихідних — скажімо виводимо тільки якусь усередину статистику, а треба відфільтровувати по конкретним атрибутам, і тестувальники або користувачі сказали — замінити по результатам тестування.
Друге — це не стиковка інтерфейсів, тобто якісь функції чи API різних частин системи, використовуються не вірно позаяк їм подаються якісь не вірні данні на вхід.
Знаючи ці два постулати — можна писати програму будь якого типу і на будь якій мові програмування, та фреймверках.
Коли думаєте про структури данних, ще є «Золоте правило». Виграєш в швидкості — програєш в пам’яті і навпаки.
Ніби усе просто — класика Computer Since, перший курс — другий семестр. Або Дональд Кнут — Мистецтво програмування.

Ну реально ось наступні моменти:
1) В системах управління (до котрих належить ядро ОС) задачі не паралеляться — бо треба управляти фігнею, ця фігня має стан, і цей стан один на всіх. І рішення, що з ним робити, також має приймати хтось один — інакше почнеться карнавал.
2) Системи управління ріалтаймові — ані юзер, ані залізо не буде чекати, доки ядро завершить збирання сміття чи якусь іншу внутрішню задачу.
3) В ядрі обмежені ресурси — ти не можеш підключити ще кілька реплік в клауді коли користувач переглядає відео в 8К і вимкнути їх коли він друкує у Ворді.
І в результаті маєш геть різні підходи до архітектури та коду — настільки різні, що навіть на Джанго мало хто драйвери пише medium.com/...​ing-software-9011fee8bc66

Це підробності, а було про абсолютний загал. Принципової різниці безпосередньо в просецці розробки : драйверу пристрою, Web каналу продажів, або 3D движка для якогось САПР чи просто моделлера Blender з 3D Max, як не дивно нема! Хоча в знаннях якими має обладати програміст подекуди прірва в декілька років досвіду роботи. Бо за кожним чихом до бізнес аналіста не побіжиш, або не будеш читати документацію місяцями коли треба на позавчора.
І там треба знати досконало HTTP наприклад, а там приривання, системні виклики ядра і т.п..
Бререм розробку драйвера. Так само — є інтрефейси ті же протоколи які треба сформувати, щоб щось там записати в порт щоб воно пішло на пристрій і стало чимось керувати типу сервоприводів — це вихідні данні. Відповідно і є данні на вхід які будуть ортимані від когось, наприклад системного виклику ОС — що в свою чергу буде викликатись прикладним процессом і є формат виклику. Є структури — данних аля модуля ядра, блочний чи потоковий інтерфес пристрою, IRQ і т.д. і т.п. залежить від конкретного програмно-аппаратного комплесу.
Далі є підходи к проектуванню — з гори або знизу, тобто від вхідних данних до вихідних, або навпаки від вихідних до вхідних. Bottom up — зазвичай це коли ви або взагалі гадки не маєте як працює пристрій і експерементуєте підганяючі вхідні данні і т.д. або елементарно не маєте вимог і дієте методом — роблю якись прототип показую користувчам, поправляю після того як вони мені скажуть, що їм подобається або ні і що хочуть додати або прибрати. Top Down — це коли ви спочатку збираєте вимоги а далі — діаграми, описи інтерфейсів, шаблони, TDD, тестування і т.д.
Абсолютно усюди ви знайдете те саме, в будь якій формі розробки. І апаратні та інші лімітації є в усіх типах розробки.
Відповідно Bottom UP це фактично завжди — відсутність гарантії результатів, тобто вони можуть бути а їх може і не бути. Та природа бізнесу така — що дуже часто це домінуючий тип проектування в індустрії (якщо взагі документується і робиться проектування). Бо так або данні закриті і на них роялті в мільойни долларів (за що Лінус Торвальдс мав проблеми з NVidea), або банальне клонування методом реверс інженерингу (це улюблене починаючої Microsoft, усі їх початкові продукти — клони популярних рішень на ринку і взагалі це бізнес стратегія аля Китай. Тому і якість часто була теж китайська. В ревес інженерінг системно вмів лише СРСР), або бізнес гадки не має — що він робить і міняє бізнес ідеї раз на день, Bus Factor і т.д. і т.п.

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

У Web-і навіть є праило одного потока, і тому усі корутини, асинхроніка бо одне немає сенсу без іншого, і йдеться про той самий системиний рівень де конкурується за ввод/вивід через сітьву плату і відповідно network latency та простої процессорів в чеканні IO. Йде бородьба за можливостями наявного обладання обробляти якумога більшу кількість клієнтів якнайшвидше, з мінімальним навантаженням і відповідно ціною володіння обладання, що дорівнює собівартості бізнесу.
Саме тому і функціональщіна часто підходить значно краще, там просто нема блокуючих алгоритмів. За це щоправда треба платити пам’ятю, усі пули потоків, черги, immutable обьєкти, copy on write і т.д. — це все коштує структур данних та пам’яті.
Насправді усе дуже сильно одне з одним пов’язано.
Тут коротше не для форума, тут реально можна книжку писати. Та власне і написано їх вже не мало, щоправда по різних місцях розкидано є сенс зібрати усе в закінений роботі.

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

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

Ну це той самий анекдот про слона в холодильнику. www.anekdot.ru/id/674409 Коротше кажу як є усе одне і те саме.
Був у мене стажор, за освітою електронщік. Полюбляв робити власні проекти на Ardwina. От там в перервах консултьтував, хоча на ассемблері я не писав і з минулої роботи т.д. Усе те саме на спраді.
А те що ви описуєте — буласка приклади Play Framework та Spring Web Flux. А також лінійка усіх Reactive X reactivex.io

Є лише реакція на зовнішні події.

Реакція на зовнішню подію — по суті і є початком алгоритму, обробки цієї події. Є вхід і є вихід. Зовнішня подія — це вхідні данні, на які має бути зрблена обробка, даді має щось статись.
А там воно не так важливо — це преривання в сиситемі сталось від таймера, оконна система прислала евент — що десь мишка клінула, прийшов HTTP запит на серевер і т.п.
І абсюдютно усюди можуть бути кінцеви автомати, користуваціькі сессії, контексти і т.п.

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

Проблема, що алгоритм має буть багатокроковим, і між цими кроками може пролізти будь-що, що змінює сам алгоритм dou.ua/...​cles/telecom-application
В результаті, алгоритму нема — бо, наприклад, тестер натисканням кнопки може генерувати запити на перемикання кодеків швидше, ніж залізо це робить. В результаті замість алгоритму «перемкнути кодек» буде нескінченна ad-hoc зміна налаштувань кодеку без останнього кроку, котрий мав би в нормальному алгоритму підключити звук.

Так тому і науковий метод — тестування тобто. Пропустили вимоги — що запити не можуть приходити одночасно, або занадто часто і т.д. Реально нема таких програмних продуктів як іппаратних для яких відомі абсолютно усі вимоги на етапі проектування.
Коротше я переписую Дядька Боба Clean Architecture в форум.

Пропустили вимоги — що запити не можуть приходити одночасно, або занадто часто і т.д.

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

Тестувати реактивні системи? Ну, як там кажуть, бог на допомогу... ;) Ваші тести будуть разів в 10 більшими за код і все одне не покривати всього.

Не думаю, що у вебі таке полюбляють.

Асинхронщина у вебі рулить! Правда там вона обкладена по несхочу синтаксичним цукром.

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

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

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

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

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

То вже нюанси. Концептуально я сильно не бачу різниці між проакторами та реакторами.

Концептуально — в реакторі потік на кожну задачу (thread == task), а в проакторі один потік робить купу задач вперемішку (multitasking without multithreading). Корутини звуться Half-Sync/Half-Async і знаходяться посередині.

Це особливості реалізації. Якщо взяти вище трохи, то це все реалізація розгалуженого асінхронного виконання задачі без чітко детермінованого алгоритму дій. Situational flow, якщо простіше. Я таке на UI використовую. ;) Тому я знаю те відчуття, коли код працює правильно, але ти не можеш чітко сказати чому. Бо ситуативно може відбуватися багато чого різного, в тебе навіть два однакових виклика, зроблених через проміжок часу можуть давати два різних результати. І це будуть правильні результати в обох випадках.

Ну в реакторі в тебе весь сценарій робиться однією функцією (процедурне програмування), а в проакторі — він розмазаний по 20 колбекам (реактивне програмування).

Знову ж таки, залежить від релізації. Зазвичай реактори всередині побудовані на обробниках подій (event handlers).

Ніби ні, подивіться оригінал патерну www.dre.vanderbilt.edu/...​t/PDF/reactor-siemens.pdf
Там обробник подій лише щоб створити сокет для клієнта.

Йде трійка патернів асинхронного сервера:
* Реактор — це коли кожен таск отримує по потоку, і потоки блокують на командах до периферії
* Проактор — коли одін потік обробляє усе і ніколи не блокує
* Напів-синхронна/Напів-асинхронна — коли кожному таску видається по легковісному потоку (фібру, корутині) що блокується на перемиканні контексту, а під ними живе справжній потік, котрий завідує усим асинхронно.

Оця трійка, ніби, описує усі варіанти, і щось нове додати (чи відняти) важко. А те, що люди називають реактором що підряд — то це черговий патерн, значення котрого поїхало гуляти за час його життя. Ось ще кілька таких medium.com/...​ral-patterns-7733c1225422

Ну так це вимоги ??? І відповідно проектується потім по іншому як системний софт так і прикладний. Системний зазвичай — під те що ви описуєте це Real Time. Прикладний різні паттерни — Command із Cancelable Command, транзакції, Chain of Responsibility і т.д.
Коротше це ще одне преписсання в форум, тільки тепер Едрю Тандербаума та Банди чотирьох.

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

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

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

Реалізуючи DDD ви все одно пишете код. Так що протиставляти DDD і техніки написання коду не можна, повинно бути все в комплексі.
Виділяти окремі функції варто не для того, щоб позбавитись else, а щоб окреслити значну частину коду. І замість купи коду і коментаря типу:
// Наступний блок коду робить те і те
... кілька десятків рядків коду

викликати функцію із зрозумілою назвою.

Реалізуючи DDD ви все одно пишете код.

Моцне твердження, важко з ним не погодитись )

Так що протиставляти DDD і техніки написання коду не можна

...а далі вже не так моцно — DDD по суті і є техніка написання коду, одна з. І пропонований тобою «кенселінг елсів» — це теж техніка, і їх цілком можна порівнювати, поєднувати чи/або протиставляти.

Виділяти окремі функції варто не для того, щоб позбавитись else

Ну, думка і посил твоєї статті, загалом, читається саме так.

// Наступний блок коду робить те і те
... кілька десятків рядків коду

викликати функцію із зрозумілою назвою.

Те, що ти зараз пишеш — це ти відповідаєш на питання «як і навіщо виділяти методи». На цю тему безглуздо дискутувати.

Але відповідаючи на це питання зараз, ти скіпаєш посил своєї статті, який зводиться до «як і навіщо виділяти методи...щоб позбутися елсів». Філ зе діференс.

DDD по суті і є техніка написання коду, одна з.

І мікросервіси також.
Розпишете плюси й мінуси:
* DDD,
* TDD,
* мікросервісів,
* функціонального програмування
* та вилучення else з коду
порівняно одне з одним?

DDD — це концептуальний підхід до розробки програмного забезпечення. Але ваше право вважати DDD просто «технікою написання коду».

Если что-то такое всерьез обсуждать, то надо сначала рассказать какие «фатальные недостатки» вы видите в общепринятом принципе «single function exit point», который по сути предлагает делать ровно наоборот, и описан/рекомендован в куче книг (вот примеры аргументации за — wiki.c2.com/...​leFunctionExitPointPlease , и против — wiki.c2.com/?SingleFunctionExitPoint). Без его упоминания вся статья является слишком поверхностной.

Этому холивару уже лет больше чем нам, он возник точно еще до моего рождения, дискуссии идут от таких языков как PL/1 и Simula 67 и ещё раньше от попытки стандартизовать Algol 68.
И представители одной школы — пошли делать языки программирования и компиляторы без «дефектов синтаксиса». Явные представители Едгер Дейкстера и Никлаус Вирт. Другие стали писать книги про чистый код — которые по сути своей сводятся у тому чтобы программист обходил узкие места в возможностях синтаксиса языков программирования часто приводящий к логическим ошибкам. Яркие представители — Роберт Мартин и Мартин Фаулер.
И там и там на самом деле есть здравый смысл четкие аргументы и плюсы и минусы.

Он был общепринятым до появления деструкторов и сборщиков мусора, которые позволили RAII, который убил сам смысл этого принципа.

Фатальний недолік: більша складність коду. Рахуєте метрики, порівнюєте і нема чого сперечатись.
Зверніть увагу, якою мовою написані приклади за вашим посиланням.
Коли ви програмуєте на C, де самому треба слідкувати за тим, щоб звільнити пам’ять, return посередині коду може призвести до того, що буде текти пам’ять і у вас будуть баги які дуже важко знаходити. У такому випадку один return допомагає полегшити собі життя.

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

більша складність коду

Це дуже субʼєктивно. З мого досвіду, single point of return значно полегшує розуміння коду. Але це також субʼєктивно, я її нікому не навʼязую (окрім колег).

Якщо порівнювати сyclomatic complexity — то вона має бути однаковою в обох випадках.

Рахуєте метрики, порівнюєте

Які саме метрики?

Аргументи «за single function exit point» можна ще якось сприймати в контексті асемблеру чи сішки, де треба було завжди не забути зробити «goto exit» і все почистити. Якщо ж у вас є хоча б С++ або більш високорівнева мова — early return перемагає в суху. Код пишеться так, що безпечно можна вийти в будь-якому рядку. Ніяких «goto exit». Все саме має закриватися і чиститися, де б ти return не написав. Окрім меншої когнітивної складності це дає ще й більшу безпечність коду. Одні плюси, ніяких мінусів.

чи сішки, де треба було завжди не забути зробити «goto exit»

Скільки сішних сорсів переглядав (ембедед в основному) ні разу не бачив такого О_о
Всі наче break юзають;)

break це як для звільнення ресурсів?

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

Є полотно на дві сторінки вкладених іфів. Серед цих іфів 75% еррор кейсів. І по ходу цього полотна виділяються ресурси — наприклад, аллокуються та розраховуються структури даних, далі іфи перевіряють їх поля на валідність, і якщо все ок — то виділяють та рахують наступну структуру на базі тих, що вже є.

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

Інший варіант — вкладені цикли. break 2 до С не завезли. Коли десь глибоко вкладена помилка — можна лише зробити return, але він не звільнить ресурси. А коли таких вкладених помилок багато й на різних рівнях чи в різних циклах — то треба той самий goto cleanup.

Є полотно на дві сторінки вкладених іфів. Серед цих іфів 75% еррор кейсів.

Ось тут вже треба зупинитись і подивитись — даю 99%, що вже порушено Single Responsibility і бажано цю функцію розбити на 3-5-6 менших функцій.
Те ж саме стосується, якщо рівень вкладеності if-ів більше 3-4 (якщо це всередині класу чи структури, то цей додаткий рівень вкладеності не рахуємо)

Інший варіант — вкладені цикли. break 2 до С не завезли. Коли десь глибоко вкладена помилка — можна лише зробити return, але він не звільнить ресурси

Треба завести змінну — наскрізний прапорець помилки і перевіряти його у стратегічних точках. Відповідно, всі цикли мають додаткову умову «&& err_flag == 0» (наприклад).
Все одно він буде потрібен в кінці, бо як інакше знати, чи ти вийшов із циклу штатно чи через помилку?

перевіряти його у стратегічних точках.

ну т.е. у нас получается так

DoSomeWork()
if LastError != 0 set Flag = true;
if not Flag { DoOtherWork2. if LastError != 0 set Flag = true; }
if not Flag { DoOtherWork3. if LastError != 0 set Flag = true; }
if not Flag { DoOtherWork4. if LastError != 0 set Flag = true; }
if not Flag { DoOtherWork5. if LastError != 0 set Flag = true; }
if not Flag { DoOtherWork6. if LastError != 0 set Flag = true; }

вместо

DoSomeWork()
if LastError != 0 goto Finish
DoSomeWork2()
if LastError != 0 goto Finish
DoSomeWork3()
if LastError != 0 goto Finish
DoSomeWork4()
if LastError != 0 goto Finish
DoSomeWork5()
if LastError != 0 goto Finish
Finish:

DoSomeWork()
if LastError != 0 goto Finish
DoSomeWork2()
if LastError != 0 goto Finish
DoSomeWork3()
if LastError != 0 goto Finish
DoSomeWork4()
if LastError != 0 goto Finish
DoSomeWork5()
if LastError != 0 goto Finish
Finish:

Ну замість цього можна юзати:

DoSomeWork()
if lastError==0{
DoSomeWork2()
If lasterror ==0{
...
}
}
/*finish code*/
Але так, якщо там кілька десятків іфів то вже краще goto...
Та й за умови що там завичай код засраний “ifdef...endif” goto то вже наймешна проблема)

Ну замість цього можна юзати:

какая , говорите, будет вложенность скобочек и сложность поддержки всего єтого, когда надо будет добавить/убрать какой-то код в середине?

чому не можна зробити

DoSomeWork

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

DoSomeWork() || 
DoSomeWork2() ||
...
DoSomeWorkN()
чому не можна зробити

1. тому ©
потому что не всё мы пишем сами. некоторые вещи нам предоставляют либы, и они, б***ь, не булевые, а войдовые
2. А акк внутре будет выглядеть этот сам доворк? то же амое неебическо булево выражение?
3. как поступать с циклами, кейсами и прочим? выносить в отдельные функции?
4. и что получится? куча нечитаемой и неподдерживаемой херни, «зато ни одного елзе»?

1. так зробіть булеву обгортку на войдові функції — от проблема
2-3 — я вам написав варіант як обійти купу if та goto, використовуючі булеву логіку. вирішувати абстрактні задачі, які ви тільки що вигадали без реального прикладу коду — ну так ви так можете нескінчено вигадувати (це як в тому анекдоті «ти за мене, чи за медведя»)
4. залежить від програмиста — купа if з goto для мене є менш читабільним і зручним ніж один «рядок» з булевим ||

2-3 — я вам написав варіант як обійти купу if та goto

путём написания кучи оберток на войд методами? :)
да, текст от этого станет читабильнее и понятнее, согласен

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

Что, простите? последовательность стейтментов (как это сделано в большистве проограмного кода) — это «абстрактная задача»?????

Треба завести змінну — наскрізний прапорець помилки і перевіряти його у стратегічних точках.

Б’є по перформансу.

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

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

В старих DirectX SDK таких прикладів була купа, хоча й плюси

Була. Більше того, у DirectX SDK ще й чисто сішний інтерфейс був, щоб все по хардкору! Але якщо дивитися на щось сучасніше, то там вже були CComPtr чи якась інша обгортка і коду було менше і все чистіше.

Так, сам загортав це у CComPtr коли позичав код з СДК, дуже дратувало

А де аплікативні функтори?

import Control.Applicative (liftA2)

sumMaybes :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int
sumMaybes mx my mz = do
  x <- mx
  y <- my
  z <- mz
  return (x + y + z)

printResult :: Maybe Int -> IO ()
printResult (Just result) = putStrLn $ "  The sum is: " ++ show result
printResult Nothing       = putStrLn "  One or more values are missing, result is Nothing."

main :: IO ()
main = do
  let a = Just 10
      b = Just 20
      c = Just 30
      d = Nothing

  let result1 = sumMaybes a b c
  let result2 = sumMaybes a b d
  
  putStrLn "Result 1:"
  printResult result1
  
  putStrLn "Result 2:"
  printResult result2

Ось, без жодної умови :)

Result 1:
  The sum is: 60
Result 2:
  One or more values are missing, result is Nothing.

x <- mx

 def flatMap[A, B](right: A => Option[B]): Option[B] =   if (left.isEmtpy) None else right(left.get) 

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

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

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

Ну це вже пішло від того анекдоту як покласти слона в холодильник в три операції — Відкрити холодильника, засунути туди слона, закрити холодильника. www.anekdot.ru/id/674409

Це просто імперативний стиль замінений на функціональний. А так абсолютно те саме.

Ну... як на мене це більше переваги do-нотації, як синтаксичного цукру над монадами. У LISP її немає.

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

Можете привести пример, который демонстрирует ваше утверждение на практике?

Я не вижу здесь примера.

Можете пример кода (с else и без else), и ассемблерные листинги или результаты бенчмарков, которые бы я мог воспроизвести сам?

В статті про те як технологія передбачень працює.
А код — просто перевірте час на виконання if-else з часом, коли є ранній вихід через throw, чи з часом коли логіка в окремій функції...
— на java:
public class FunctionTiming {     public static void main(String[] args) {         long startTime = System.currentTimeMillis();         for (int i = 0; i < 1000000; i++) {             if (i % 2 == 0) {                 Math.pow(i, 2);             } else {                 Math.acos(i);             }         }         System.out.println("if-else time: " + (System.currentTimeMillis() - startTime));         for (int i = 0; i < 1000000; i++) {             try {                 if (i % 2 == 0) {                     throw new Exception();                 }                 Math.pow(i, 2);             } catch (Exception e) {                 Math.acos(i);             }         }         System.out.println("try-catch time: " + (System.currentTimeMillis() - startTime));         for (int i = 0; i < 1000000; i++) {             checkI(i);         }         System.out.println("function time: " + (System.currentTimeMillis() - startTime));     }     public static double checkI(int i) {         if (i % 2 == 0) {            return Math.pow(i, 2);         }         return Math.acos(i);     } }

А взагалі

Замість else-блоків використовуються return, throw exception або break

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

А код — просто перевірте час на виконання if-else з часом, коли є ранній вихід через throw, чи з часом коли логіка в окремій функції...

Це взагалі що за порівняння буде? Котлет з монетками? Я вам й без виконання можу назвати самий швидкий варіант — if-else. Тому що немає додаткових витрат на створення виключення чи виклик функції. Але, ви приводите Java-code, яка не вміє в оптимізації під конкретні архітектури процесорів або сет інструкцій по типу AVX. Байткод, який буде згенеровано для if-else, може не відповідати взагалі тому, що в реальності виконає процесор.

Сенкс, запустил.

Результат:

% java bench.java
if-else time: 6
try-catch time: 454
function time: 459

Просто переходом с if/else на if/return код стал медленнее в 76 раз. С одной стороны, збс, теория подтверждена, с другой стороны, вас этот результат не смущает? Не заставил подумать, может быть что-то не так в методологии теста и нужно его подправить, чтоб получить более реалистичный и объяснимый результат.

Например, ре-инициализировать startTime перед каждым тестом, а не только один раз в начале программы, и тогда результат будет немного более объяснимым:

public class FunctionTiming {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            if (i % 2 == 0) {
                Math.pow(i, 2);
            } else {
                Math.acos(i);
            }
        }
        System.out.println("if-else time: " + (System.currentTimeMillis() - startTime));
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            try {
                if (i % 2 == 0) {
                    throw new Exception();
                }
                Math.pow(i, 2);
            } catch (Exception e) {
                Math.acos(i);
            }
        }
        System.out.println("try-catch time: " + (System.currentTimeMillis() - startTime));
        startTime = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
            checkI(i);
        }
        System.out.println("function time: " + (System.currentTimeMillis() - startTime));
    }
    public static double checkI(int i) {
        if (i % 2 == 0) {
            return Math.pow(i, 2);
        }
        return Math.acos(i);
    }
}


% java bench.java
if-else time: 6
try-catch time: 449
function time: 5

В качестве домашнего задания увеличить кол-во итераций, так как из-за низкого разрешения таймера при таком маленьком кол-ве итераций вы нормальные результаты не получите.

Кому цікаво, зробив аналогічну перевірку швидкості виконання на JavaScript:

let startTime = new Date();
for (let i = 0; i < 1000000; i++) {
    if (i % 2 == 0) {
        Math.pow(i, 2);
    } else {
        Math.acos(i);
    }
}
console.log("if-else time: " + (new Date() - startTime));

startTime = new Date()
for (let i = 0; i < 1000000; i++) {
    try {
        if (i % 2 == 0) {
            throw new Error();
        }
        Math.pow(i, 2);
    } catch (error) {
        Math.acos(i);
    }
}
console.log("try-catch time: " + (new Date() - startTime));

startTime = new Date()
for (let i = 0; i < 1000000; i++) {
    checkI(i);
}
console.log("function time: " + (new Date() - startTime));

function checkI (i) {
    if (i % 2 == 0) {
        return Math.pow(i, 2);
    }
    return Math.acos(i);
}

// if-else time: 10
// try-catch time: 1970
// function time: 4

Ще виміряв швидкість варіанта з виключенням без передачі значення (щоб дізнатись що саме впливає на швидкість: саме виключення чи створення об’єкта помилки для передачі у вигляді значення виключення):

startTime = new Date()
for (let i = 0; i < 1000000; i++) {
    try {
        if (i % 2 == 0) {
            throw null;
        }
        Math.pow(i, 2);
    } catch {
        Math.acos(i);
    }
}
console.log("try-catch (no exception value) time: " + (new Date() - startTime));

// try-catch (no exception value) time: 1643

Також спробував збільшити число ітерацій в 10 разів для точнішого результату:

// if-else time: 27
// try-catch time: 18925
// function time: 13
// try-catch (no exception value) time: 16856

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

Це означає, що такий підхід точно суттєво не нашкодить швидкості виконання коду.

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

if (i % 2 == 0) { Math.pow(i, 2); continue};
Math.acos(i);

en.algorithmica.org/...​pc/pipelining/branchless а можна і так! не зрозуміло що ти хочеш довести

Я «перекладаю» ваш коментар так: продуктивність важливіша за зрозумілість коду.
Але по перше стаття не про продуктивність заліза.
У переважній більшості випадків бізнесу важливіша продуктивність програміста, а не продуктивність його коду. Грубо кажучи час програміста коштує набагато більше за час серверу. Описаний підхід дозволяє збільшити якість роботи програміста і його продуктивність.

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

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

Але якщо брати продуктивність команди розробки, особливо в перспективі «довгого» проекту, то описаний підхід повністю виправданий. Це перевірено на особистому досвіді.

Ще один класичний холівар. Дуже часто похідний код після того як його оптимізували з точки зору перформансу — далі вже людина читає дуже погано. Умовно коли ви побачите який небудь itoa із наївним алгоритмом в якому робиться операція ділення, то він буде ну дуже ясний і зрозумілий самому не підготовленому програмісту. Але коли побачите той же самий itoa на алгоритмі branched lut — то це буде ну жодним чином не зрозуміло.
Тільки різниця в 1000 разів по швидко дії.

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

stackoverflow.com/...​lated-to-c-coding-standar

Це безперечно, про це власне пишуть, тойже Томас Кайт (він же дядько Том колишній СTO Oracle, зараз на пенсії). Ше дужче «Техника оптимизации программ» покійного Мищьха він же Кріс Касперскі.
Та такі речі як конвертація чисел в строку і назад, при глобальному розповсюдженні текствих форматів передачі данних. HTML, XML, CSS, JSON, YAML, CSV і т.д. — це якараз і будуть ті 95%.
Відповідно прийде хтось такий модний на проект, побачить там дикий код Branched LUT github.com/miloyip/itoa-benchmark прибире, бо він виглядатиме криво вставить тутди штатну функцію і все, перформанс деградує в 20 разів як с куста.

Та хз, бо зараз проци стали швидшими за пам’ять — і лукап тепер дуже спірний порівняно з обрахунком влоб. Усі поради з 90х варто переміряти на своєму реальному залізі.

Глянь бенчмарк. SSE2 метод справді обганяє branchlut аж на 1%. Звісно для тих же Big Tech — це ноормальна така різниця. Та ассемблерну вставку в код взгалі станом на сьогодні можна буде вставити в код, вилючно якщо ти сам CTO. Відібють руки і назовуть параноідальним шизофреніком. Ну і в дечому матимуть рацію — це платформо залежне, на ARM вже не працює.

SSE2 сто років в обід скоро буде... Хоча знову ж таки, очукувати прискорення виконання алгоритма при компіляції під якийсь сучасний набір векторних інструкцій можна лише тоді, коли задача відповідна. Звичайний код вони не прискорять. Ефективніше боротися за краще попадання в кеш, там можна дійсно отримати буст в два рази на рівному місці. Але, знову ж таки, з сучасними процами, де за branch prediction вже відповідають нейронки, це треба ледь не емпірічним шляхом шукати...

Там просто достатньо SSE інструкцій. AVX512 ХЗ. Якщо робити тими же інтрісіками з GCC або Clang — воно може і AVX згенерує. Це вже тонкості та офтоп.
Сенс тільки такий — що це буде от жодним чином не Clean Code після таких варварських оптимізації, якщо звісно не робити рефакторинг по уму — виносити усе в окремі бібліотеки і т.д.
І це типовий холівар. Ще коли Річі та Томпсон переписували частину кода Unix на C (відпочатку усе було написано на ассемблері). Потім до оприлюднення UNIX — їх команда побила рефакторинг з розділення загального і платформо-специфічного один від одного по модулям і т.д.

що це буде от жодним чином не Clean Code після таких варварських оптимізації

/
Коли ми кажемо «оптимізація», то можна про «Clean Code» одразу забувати. Нічого з цим не поробиш...

Це не холівар. Є бізнес і є нефункціональні вимоги. І є три які бізнес завжди буде казати : зробіть безкоштовно, якісно і на вчора.

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

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

Бізнес є дуже дуже різний і у нього є дуже дуже різні пріорітети. От тут Антон пояснив, нема чого додати youtu.be/E9-scyUdmeI?t=2161
З певного рівня навантаження — реально роблять реплатформінги, це рівнем по менше ніж Big Tech. У тих як бачимо реально до асемблера доходить, власної підтримки проектів компіляторів типу GCC, та розробки власних мов програмування. Бо це мільйонні заощадження на бізнесі.

О цікаво ? Це як конструкція if elise закладена на рівні процесора ?
Візьмемо Intel — от є команди CMP, JZ та JNZ. (У ARM — CMP та LR, BL які по суті те саме)
Щось мені підказує — що в нас тут є команда яка виставляє флаг кері, а по суті є операцією мінус і дві команди умовних GOTO які переводять instruction pointer на вказану адресу пам’яті в залежності від стану керрі флага, тобто 0 або 1.
Є ще команда RET, це вже і справді щось схоже на return в C — вона виймає instruction ponter з адреси яка була покладена в стек при в ході в функцію. Туди де були покладені ще і параметри функції відповідно до узгоди про виклики. Навідміну від ЯВУ — усі питання по управляння стеком визовів і т.д. — це питання програміста.
Є ще команди ексепшенів, та ЯВУ використовують механізми насправді — або не досконалий setjump/longjump, або stack unwinding.
В цілому же так будь який сучасний ЯВУ — насправді і дійсно не так вже і далеко відстоїть від ассемблера. Якщо не брати до уваги купи бібліотек мало не по усіх випадки життя і т.д.
Але нема там таких зручних штук як if else — switch, for і т.д.

Та там про branch prediction мова йшла. Коли проц намагається вгадати куди само буде виконано перехід та починає або завчасно дані запрошувати, або, як було в деяких процах, виконувати взагалі альтернативну гілку паралельно, бо конвейер дозволяв. На рівні інструкцій там нічого явно схожого на if-else немає. Branch prediction дозволяє суттєво «прокачати» продуктивність проців, бо пенальті за кеш-промах доволі таки великий, конвейер почистити, наново дані та інструкції в кеші запхати, тощо.

Так в C++ навіть ввели в стандарт те що було раніше компілятор специфічними інтрісіками — тобто likely і unlikely, хоч це і такий собі спосіб оптимізації високоінкнвого коду.
В цілому компілятори ну давно вже зроблять, в простих звісно випадках, що з multiple return що else, що батарею if else if else що навіть зі switch приблизно один і той самий код після оптимізації. З ексепшенами звісно ні, це доволі дорогий механізм.
Тобто питання чисто хорліварне — а який код стайл краще ?

З ексепшенами звісно ні, це доволі дорогий механізм.

В современных и с исключениями разбираются весьма дёшево: вся обработка исключений
1) табличная — код размотки по адресу ищет, где обработчик;
2) обработчик в самой функции выносится как минимум из основного тела функции, а дальше вообще в так называемые cold кодовые секции. В результате уменьшение толщины кода нормального пути и его больше влезает в кэш.
Если случилось исключение, то обработка в результате чуть дороже, но без него — заметно дешевле.
Всё это можно увидеть сассемблировав современным компилятором (например, GCC от 10-го и новее).

Жаль только, что это не смогли сочетать с SEH. Вот такого механизма не хватает в юниксах.

А ось з цього місця попросимо докладніше, в якого саме процессора

if-else закладена на базовому рівні процесора

технологія передбачення = костиль щоб якось не дуже просідала побчислювальна потужність ) гуглити по branch prediction unit)

Це скоріше імпрувмент. Ніхто через це не перечіпляється.

бо покращується читання за рахунок швидкодії

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

Конструкція if-else закладена на базовому рівні процесора (технологія передбачення).

«Передбачення» як branch prediction не повʼязане з самим фактом наявности умовних переходів в системі команд.

Дякую за статтю! Що ви мали на увазі під

Спеціалізуюсь на керуванні складністю в розробці

? Керування складністю є головним імперативом розробки програмного забезпечення (по Макконнеллу) як такої, чи не так?

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

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

Є конкретна книжка A Philosophy of Software Design by John Ousterhout.

Написання коду та архітектура це тільки частина цього питання. Зверніть увагу, автор не дає чіткого визначення що таке складність. Так само ви не знайдете чіткого визначення у Мартіна що таке «чистий код».
Автор приводить багато характеристик складності, але це все наслідки. Причина знаходиться в нетехнічній площині — обмеженності когнітивних функцій людини.

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

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

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

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

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

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

когнітивні здібності, або рівень розумової сили

Ну це як порівнювати потужність ноутбуків за кількістю оперативки.

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

Унікальність способу побудови мережева асоціацій мабуть і є причиною неповторності ментальної моделі навколишнього світу у кожної людини.

В цьому є сенс. Якщо відкинути геймінг, то по перформансу основний затик, зазвичай, оперативка.

І якщо відкинути компіляцію та МЛ.

Ну це актуально лише для програмістів і то не для всіх.

А для кого актуальна оперативка? Для секретарок? Чи для CAD-дизайнерів?

Я не впевнений що без 16Гб зараз умовна вінда без лагів працювати буде. Актуально для будь кого хто 30+ вкладок на хромі відкритими тримає :)

Відповідно когнітивні здібності, або рівень розумової сили людини — це здатність утримувати в голові певну кількість одиниць інформації

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

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

А розмір оперативної пам’яті у всіх однаковий :)

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

Агатангел Кримський свого часу скаржився знайомим, що ніколи нічого не забував.

Звісно я спростив опис.
Вказавши на більш вагому різницю, аніж пам’ять.

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

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

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

Підписатись на коментарі