Не прымушайце мяне думаць: ня ўсе пераменныя хочуць мяняцца, а функцыі функцыянаваць

Не прымушайце мяне думаць: ня ўсе пераменныя хочуць мяняцца, а функцыі функцыянаваць

Некалі чытаў выдатную кнігу Стыва Крага «Don't Make Me Think» пра юзабіліці вэб-інтэрфейсаў. Але назва, на мой погляд, апісвае значна шырэйшы прынцып. Добры код — гэта таксама «не прымушай мяне думаць». Кожны раз, калі чытаючы код трэба спыніцца і задумацца: «а для чаго гэта?», «а якое значэньне ў гэтай пераменнай?», «а чаму функцыя робіць тое, чаго я ад яе не чакаў?» — гэта кошт. Кошт канцэнтрацыі, часу, памяці. І выплочваецца ён штодня — і тым, хто пісаў, і тымі, хто будзе гэта потым падтрымліваць. Мне самому прытрымлівацца падыходаў гігіены коду дапамагае моцна засеўшы выраз, знойдзены нейдзе ў сеціве: пішы код так, быццам падтрымліваць яго будзе маньяк зь нестабільнай псіхікай, які ведае дзе ты жывеш.

За гады працы з рознымі мовамі і рознымі камандамі мой досьвед склаўся ў звод не надта складаных правілаў. Ніводнае зь іх не зьяўляецца адкрыцьцём — усё гэта ёсьць у кнігах, стандартах ды бэстпраксіс. Але адна справа прачытаць, і зусім іншая — зразумець, на чым усё трымаецца. Бо ня маючы выявы цалкам, можна недаацаніць той ці іньшы пункт, палічыўшы яго неістотным і закласьці пад сьцены нетрывалы падмурак.

Дазволь коду расказаць аб сабе

Часам дзіву даюся, колькі ў сьвеце распрацоўшчыкаў, выкарыстоўваючых асэмблер. Праўда яны на ім не пішуць, ды і наогул у вочы ніколі не бачылі. Прагрэс сягнуў настолькі далёка, што мовы праграмаваньня зараз могуць ні ў чым сабе не адмаўляць і спадзявацца на іньшыя аб'ёмы дадзеных, большыя чым тыя, ў якія даводзілася ў даўнія часы ўціснуць цела праграмы. Трэба было эканоміць на ўсім, у тым ліку на назвах пераменных. І вось гэты патэрн працягвае жыць, справа яго не забытая.

Клясічны прыклад:

$d = new DateTime();
$r = getUserById($id);
$po = processOrder($r, $d);

Тры радкі, тры загадкі. $d — гэта дата? Дырэктар? Доўг? $rresult? row? resource? Для аўтара гэта відавочна — у момант напісаньня. Праз месяц нават аўтар будзе чытаць гэта як першы раз.

$nowDateTime = new DateTime();
$foundUser = getUserById($id);
$processedOrder = processOrder($user, $now);

Радкоў ня стала больш. Зьявілася разуменьне без намаганьня.

Пра пераменныя ёсьць яшчэ адзін момант, пра часта забываюцца: назва павінна апісваць тое, чым зьяўляецца значэньне, а не тое, якога яно тыпу. userArray ды isString — дрэнныя назвы. activeUsers ды isEmailValid — добрыя. Тып бачны з кантэксту ці падказкі IDE, а сэнс — не.

Тое ж самае датычыцца ўласьцівасьцяў класаў і аргументаў функцый. Кожная назва — гэта маленькі кавалачак дакументацыі. Збіраць яго ў галаве пры кожным прачытаньні — марнаваць рэсурс, чаго можна было б пазьбегнуць.

Добры тэст для назвы: ці можна зразумець прызначэньне без чытаньня прысваеньня і сумежнага кода? Калі трэба глядзець вакол — назва дрэнная.

Ёсьць і яшчэ адзін практычны аргумент. Поўныя, сэнсавыя назвы дазваляюць шукаць па праекту. $userId — гэта канкрэтна, $currentUserId - яшчэ лепш. А $id разьмешчаны ва ўсіх файлах, і пошук па ім выдасьць дзясяткі нерэлявантных вынікаў.

Аргументы функцый: калі меней гэта болей

На нейкім этапе прафесійнага шляху даволі часта распрацоўшчыкі адкрываюць для сябе "магічны антыпатэрн" - перадаваць у функцыі цэлыя аб'екты «на ўсялякі выпадак». Маўляў, спатрэбіцца — возьмем. Выглядае зручна. Насамрэч — паступова такія сутарэньні класаў робяць усю канструкцыю крохкай.

// Функцыя атрымоўвае цэлы аб'ект, хоць выкарыстоўвае толькі адно поле
function formatUserGreeting(User $user): string {
    return "Hello, " . $user->getName();
}

На першы погляд — нармальна. Але цяпер гэтую функцыю немагчыма выкарыстаць без аб'екту User. Нельга патэставаць з простым радком. Нельга перанесьці ў іншы кантэкст, дзе User недаступны. З часам такіх залежнасьцяў назапашваецца дастаткова, каб зрабіць нешта простае ператварылася ў заданьне.

function formatUserGreeting(string $userName): string {
    return "Hello, " . $userName;
}

Тэстуецца трывіяльна. Перавыкарыстаньне — ніводнай перашкоды. Функцыя стала дастаткова ізаляванай ад вонкавага сьвету.

Прынцып просты: перадавайце толькі тое, что функцыі насамрэч патрэбна. Не «увесь аб'ект, раптам спатрэбіцца», а канкрэтныя значэньні, якія яна выкарыстоўвае. Гэта дысцыплінуе і да выяўленьня залішняй залежнасьці — калі для простай аперацыі ты збіраешся перадаць пяць розных аб'ектаў, магчыма, функцыя робіць занадта шмат.

Ніхто не любіць нечаканыя зьмены

Адна з найбольш непрыкметных крыніц багаў — мутацыя аргументаў функцыі знутры. Асабліва гэта актуальна для спасылкавых тыпаў: масіваў, аб'ектаў.

function applyDiscount(array $items, float $discount): array {
    foreach ($items as &$item) {
        $item['price'] *= (1 - $discount);
    }
    return $items;
}

$cart = getCartItems();
$discounted = applyDiscount($cart, 0.1);
// Сюрпрыз: $cart таксама зьмяніўся

Калькулятар зьніжкі змяніў кошык. Гэта не тое, чаго хто-небудзь чакаў, і гэта не відаць з назвы функцыі.

Непасрэдная мутацыя аргументаў разбурае прадказальнасьць. Пасьля выкліку функцыі нельга быць упэўненым у стане перададзеных значэньняў — давядзецца заглядаць унутр або трымаць у галаве: «а тая функцыя не псуе свой аргумент?». Гэта інфармацыя, што трэба трымаць у галаве падчас працы, якая расьце разам з кодам. Прычым такога хутчэй за ўсё будзе нашмат больш, і вось ужо думаеш не рэалізацыю новага функцыяналу, а аб тым, як ненаўмысна не зачапіць адну зь гэтых мыліц.

Рашэньне звычайна простае: рабіце копію і вяртайце вынік.

function applyDiscount(array $items, float $discount): array {
    $result = [];
    foreach ($items as $item) {
        $item['price'] *= (1 - $discount);
        $result[] = $item;
    }
    return $result;
}

Выключэньне — метады клясу, якія наўмысна зьмяняюць стан бягучага інстансу. Там мутацыя і ёсьць тое, што ад метаду чакаеш.

Магічныя канстанты і месцы іх рассяленьня

Магічная канстанта — гэта лічба ці радок, якія зьяўляюцца ў кодзе без тлумачэньня. Нараджаюцца яны звычайна не ад злога намеру, а таму, что ў момант напісаньня кантэкст відавочны — вы толькі-толькі абмяркоўвалі гэтую логіку, ўсё дасяжна ў галаве.

if ($user->getAge() >= 18) { ... }
if ($response->getStatus() === 2) { ... }
if (strlen($token) === 42) { ... }

Тры розныя праблемы: 18 — гэта ўзрост паўналецьця? У якой краіне? У якім кантэксьце? 2 — гэта які статус? Актыўны? Зацьверджаны? 42 — чаму менавіта 42?

const LEGAL_AGE = 18;
const STATUS_APPROVED = 2;
const TOKEN_LENGTH = 42;

if ($user->getAge() >= LEGAL_AGE) { ... }
if ($response->getStatus() === STATUS_APPROVED) { ... }
if (strlen($token) === TOKEN_LENGTH) { ... }

Рэакцыя на канстанту і без яе — розная. «Чаму 18?» запускае пошук. «Чаму LEGAL_AGE?» — у большасьці выпадкаў пытаньня нават не ўзьнікае.

Дадатковы бонус — зьмяніць значэньне ў адным месцы, а не шукаць па ўсёй кодавай базе, дзе яшчэ сустракаецца гэтае 18 ці 42.

Неаптымальная "аптымізацыя"

Ёсьць асобны від кода, які прыходзіць на код-рэвю пад сьцягам «элегантнасьці» ці «аптымізацыі»: блёк абгорнуты ў блёк, вызаў нанізаны на вызаў, прамежкавыя вынікі нідзе не захоўваюцца, усё ў адзін — ну, або тры, але надзвычай шчыльных — радкі. Быццам месца на экране каштуе грошай.

// "Аптымізаваны" варыянт
return array_map(
    fn($u) => $u->getName(),
    array_filter(
        $repo->findAll(),
        fn($u) => $u->isActive() && in_array($u->getRole(), getRolesWithAccess(getCurrentContext()))
    )
);

Калі гэта працуе — выглядае імпазантна. Калі не працуе — тут пачынаецца сапраўдны квэст.

Вы прыходзіце на код-рэвю і павінны ацаніць правільнасьць рэалізацыі алгарытму. Але замест гэтага вы спачатку марнуеце час на тое, каб увогуле разгледзець алгарытм у гэтым нагрувашчваньні. Вы не ацэньваеце — вы расшыфроўваеце. А потым, калі нешта не так, вы нават не можаце дадаць breakpoint (ну ці var_dump/log) у патрэбным месцы, бо патрэбнага месца, па сутнасьці, не існуе — усё злілося ў адзін выраз.

// Той жа вынік, але з прамежкавымі кропкамі
$allUsers = $repo->findAll();
$allowedRoles = getRolesWithAccess(getCurrentContext());
$activeUsersWithAccess = array_filter(
    $allUsers,
    fn($u) => $u->isActive() && in_array($u->getRole(), $allowedRoles)
);
$names = array_map(fn($u) => $u->getName(), $activeUsersWithAccess);
return $names;

Так, радкоў больш. Але цяпер можна паставіць лог пасьля любога кроку. Можна напісаць тэст, які правярае толькі фільтрацыю. Можна зразумець на рэўю, ці правільная логіка, — без таго, каб разматваць клубок у галаве. Ды і наогул гэта можна проста чытаць зьверху ўніз.

Прамежкавая пераменная з добрай назвай — гэта не марнаваньне радкоў. Гэта бясплатная дакументацыя і кропка для адладкі, якой вам можа ніколі не спатрэбіцца — а можа спатрэбіцца роўна ў пятніцу вечарам.

Ня швец, ні жнец і ні на дудзе ігрэц

Пытаньне «колькі радкоў павінна быць у функцыі» — любімая тэма для спрэчак. Хтосьці кажа 20, хтосьці — 50. Я сам схільны трымацца ў межах 30–40 радкоў як арыенціра, але цьвёрдая мяжа тут другасная. Першасная — прынцып адзінай адказнасьці. Мне яшчэ пасуе варыянт - каб зьмяшчалася на адным экране і можна было акінуць позіркам. Аднак экраны зараз сталі настолькі вялікімі...

Калі функцыя робіць тое, пра что кажа яе назва, і толькі гэта — яна, як правіла, і застаецца невялікай. Праблема ўзьнікае ў адваротным напрамку: функцыя патроху расьце, абрастае лагікай, якую зручна дадаць «пакуль ужо тут», і ўрэшце ператвараецца ва ўнівэрсальны комбайн.

function createUser(array $data): User {
    // Валідацыя дадзеных
    // Хэшаваньне паролю
    // Захаваньне ў базу
    // Адпраўка ліста-прывітаньня
    // Лагіраваньне падзеі
    // Абнаўленьне лічыльніка ў статыстыцы
    return $user;
}

Назва кажа «стварыць карыстальніка». Функцыя пры гэтым адпраўляе лісты, перапрацоўвае пароль і абнаўляе статыстыку. Гэта ўжо не createUser, а нешта накшталт doEverythingRelatedToUser.

Шкода тут не толькі эстэтычная. Калі потым трэба стварыць карыстальніка без ліста — давядзецца або абыходзіць логіку, або капіраваць функцыю, або дадаваць флаг. Калі ліст адпраўляецца з памылкай — функцыя стварэньня карыстальніка кідае выключэньне.

Рашэньне — дробнасьць і кампазіцыя. Кожная функцыя робіць адну рэч. Калі трэба зрабіць усё разам — гэта арганізуе ўзровень вышэй, дзе бачны маштаб у агульным.

function createUser(array $data): User { ... }
function hashPassword(string $password): string { ... }
function sendWelcomeEmail(User $user): void { ... }

// На ўзроўні дыспэтчару бізнэс логікі,
// які толькі кіруе паслядоўнасьцю вызаваў:
$user = createUser($validated);
sendWelcomeEmail($user);
logEvent('user.created', $user->getId());

Цяпер кожны крок відны і кіруемы. Можна прапусьціць ліставанье. Можна патэставаць стварэньне карыстальніка без пабочных эфектаў. Можна замяніць логіку адпраўкі ліста, не чапаючы стварэньне.

Падвесьці рыску

Усё апісанае вышэй — гэта не пра правілы дзеля правілаў. Гэта пра тое, каб код можна было чытаць і разумець без дадатковых намаганьняў. Код чытаюць значна часьцей, чым пішуць. Кожны раз, калі спыняешся і думаеш «а навошта тут гэтае $x» або «а чаму гэтая функцыя таксама шле email» — гэта момант, які мог не здарыцца.

Не прымушайце сябе думаць. І тых, хто прыйдзе пасьля — таксама. Пішыце прыгожы код, непрыгожы - не пішыце. І галоўнае - каб гэта прыносіла задавальненье.

Да наступных сустрэч 😎 Падпіскі - каменты - падабайкі - гэта тое, што цешыць вока аўтара 😊