🌌 Архітектура без дір — С.І.Д. із гордістю майстра закреслює «дірку» в структурі вузла — комбінацію на кшталт «заблокований, але без коду блокування», яку недбалий тип пропустив би мовчки. Дивись, каже він, як гарно це лагодиться: правильно дібраний enum робить таку дірку фізично неможливою, а не просто малоймовірною.
Типова початківська помилка проєктування — описувати стан через набір
незалежних полів Option/bool, коли насправді ці поля жорстко
взаємопов'язані. Розгляньмо struct Node { is_compromised: bool, compromised_by: Option<String>, compromised_at: Option<u64> }. Формально
ця структура допускає чотири комбінації полів is_compromised і
наявності compromised_by: скомпрометований вузол без вказаного нападника,
нескомпрометований вузол із заповненим compromised_by, розбіжність між
compromised_by і compromised_at (одне є, інше None) — жодна з цих
комбінацій не має сенсу в предметній області, але тип-система нічим не
заважає їх створити. Кожне місце коду, що читає ці поля, змушене або
довіряти («такого не буває на практиці»), або писати захисні перевірки
рантайму на комбінації, які взагалі не повинні існувати.
Принцип «зроби некоректний стан непредставним» (make illegal states
unrepresentable), сформульований у спільноті функціонального програмування
(Yaron Minsky) і природно застосовний у Rust завдяки алгебраїчним типам
даних, пропонує інший підхід: замість набору незалежних полів — один
enum, кожен варіант якого несе рівно ті дані, які осмислені саме для
цього стану. enum NodeState { Clean, Compromised { by: String, at: u64 } }. Тепер компілятор фізично не дозволить створити «скомпрометований, але
без нападника» — такого варіанта просто не існує в типі. Некоректний стан
не забороняється перевіркою — він не має способу бути записаним.
Цей підхід систематично зменшує кількість if/assert у кодовій базі,
призначених для перевірки «внутрішньоузгодженості» даних: якщо тип
структурований правильно, така перевірка стає зайвою — вона гарантована
конструкцією типу, а не логікою функції, що з ним працює. Це і зменшує
кількість місць, де можна помилитися (немовби забути одну з перевірок
у якомусь віддаленому куточку коду), і робить кожен match над таким
enum самодокументованим описом усіх реально можливих станів системи.
Практичний прийом виявлення таких «дір» у наявному коді — шукати поля,
які завжди заповнені/порожні разом (кореляція Option-полів), і
булеві прапорці, що фактично дублюють інформацію, яку вже несе інше
поле (наприклад, окремий is_compromised: bool поряд з Option<String>
компрометатора — сам факт Some/None уже й є цим прапорцем, тож
зберігати обидва — джерело розбіжності). Рефакторинг зазвичай іде за
схемою: знайти групу корельованих Option/bool-полів → визначити
список реально можливих станів (не комбінацій полів!) предметної
області → створити один enum з відповідними варіантами, кожен із
власним набором релевантних даних → замінити всі місця читання старих
полів на match над новим enum (компілятор вкаже кожне таке місце,
якщо старі поля прибрати одразу).
Варто зауважити межу застосовності: цей принцип найкраще працює, коли
стани справді взаємовиключні (вузол не може бути одночасно «чистим» і
«скомпрометованим»). Якщо ж атрибути предметної області справді
незалежні один від одного (наприклад, is_online: bool і
firmware_version: String не корелюють між собою), об'єднувати їх у
штучний enum немає сенсу — комбінаторний вибух варіантів лише
ускладнить код без жодної реальної переваги; принцип застосовується
саме до полів, чия залежність одне від одного і є джерелом
потенційно некоректного стану.
Переглянути класичну статтю/розділ спільноти Rust про «making illegal states unrepresentable» і переробити приклад struct із необов'язковими (Option) полями на enum із варіантами, що несуть лише релевантні для кожного стану дані.
Наведіть приклад struct із двома bool/Option полями, комбінація яких створює логічно неможливий стан, і покажіть, як enum усуває саму можливість такої комбінації.
is_compromised: false, compromised_by: Some("C1D") — та сама суперечлива комбінація зі структурного прикладу, тільки справжня. С.І.Д. запевняє, що це не він — і не кіт. Після переходу на enum NodeState такий запис стане непредставним, і тому, хто лишає підписи на мінус третьому, доведеться придумати новий спосіб.Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.
cargo run-- йдуть програмі (cargo run -- staff.log).cargocargo buildtarget/debug/; --release — з оптимізаціями у target/release/.💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.