Имена в кода
Накратко: Добрите имена в кода казват какво е нещото и защо съществува: константи вместо магически числа, булеви стойности като въпроси и думите на бизнеса вместо съкращения.
Проблем
Функцията, която никой не смее да пипне
В кода на онлайн магазина има функция, през която минава всяка поръчка:
function calc(a, t, f) { let d = 4.99; if (a >= 50) { d = 0; } if (t === 2) { d += 7; } if (f) { d = 0; } return a + d;}console.log(`${calc(40, 1, false).toFixed(2)} €`);console.log(`${calc(40, 2, false).toFixed(2)} €`);console.log(`${calc(60, 2, false).toFixed(2)} €`);console.log(`${calc(40, 2, true).toFixed(2)} €`);44.99 €51.99 €67.00 €40.00 €Работи. Сега кажи какво прави. Какво е t? Защо t === 2 добавя 7? Какво е f и защо нулира всичко? Защо точно 50?
Авторът знаеше. Той напусна преди година. Маркетингът иска безплатна доставка от 60 €, а не от 50 €. Колегата търси 50 в проекта и намира 14 места: на едно 50 е прагът за доставката, на друго е най-голямото тегло на пратка в килограми, на трето е броят продукти на страница. Кое да смени?
Логиката тук е вярна. Проблемът е, че кодът не казва на човека, който го чете, какво е мислил авторът.
Идеята
Името казва какво и защо, не как
Компютърът чете calc(a, t, f) без проблем. Човекът не може. А кодът се чете много повече пъти, отколкото се пише: при всяка поправка, при всеки преглед, при всяко търсене на грешка.
Доброто име отговаря на въпросите, които иначе би задал на автора: какво е това, защо го има, как се ползва. Това се казва име, което разкрива намерението. Ето същата функция, в която са сменени само имената:
function totalWithDelivery(subtotal, deliveryType, hasFreeDeliveryCoupon) { let deliveryFee = 4.99; if (subtotal >= 50) { deliveryFee = 0; } if (deliveryType === 2) { deliveryFee += 7; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}console.log(`${totalWithDelivery(40, 1, false).toFixed(2)} €`);console.log(`${totalWithDelivery(40, 2, false).toFixed(2)} €`);console.log(`${totalWithDelivery(60, 2, false).toFixed(2)} €`);console.log(`${totalWithDelivery(40, 2, true).toFixed(2)} €`);44.99 €51.99 €67.00 €40.00 €Изходът е същият до последната цифра. Функцията вече се чете като правило: доставката е 4,99 €, безплатна е от 50 €, експресната е със 7 € отгоре, а купонът я прави безплатна.
Кои имена не помагат:
- име, което казва как, а не какво:
numberValue,list1,tmp; - общо име, което пасва на всичко:
data,info,value,process,handle,manager; - съкращение, което знае само авторът:
calc,cnt,dlvFee.
Дължината следва обхвата. i в цикъл от три реда е наред. Параметър на функция, която се вика от десет места, заслужава пълно име.
По-точно, за любопитните
А коментар над функцията, t: 1 = стандартна, 2 = експресна? Помага, докато някой не смени числата и не забрави коментара. Името не може да остарее отделно от кода, защото е част от кода. Кога коментарът помага и кога лъже, е темата на третия урок от модула.
Идеята
Магическо число → константа с име
Имената на променливите вече са на място, но в кода още стоят 4.99, 50, 7 и 2. Число без име в кода се казва магическо число: какво значи, знае само авторът, а когато трябва да се смени, го търсиш навсякъде.
Поправката е константа с име. Стойността се пише веднъж, на едно място, а навсякъде се ползва името:
const FREE_DELIVERY_FROM = 50;const STANDARD_DELIVERY_FEE = 4.99;const EXPRESS_SURCHARGE = 7;const STANDARD = 1;const EXPRESS = 2;const забранява ново присвояване на името. Главните букви, UPPER_SNAKE_CASE, се ползват за стойности като тези: настройки на програмата, които не зависят от нищо. Иначе const се пише за всяка променлива, която не се присвоява наново, а let само когато стойността се сменя.Маркетингът иска 60 €: сменяш един ред, и всяко място, което пита „от колко е безплатна доставката“, вижда новата стойност. А 50-те, които са тегло, никой не пипа, защото не са FREE_DELIVERY_FROM.
По-точно, за любопитните
Не всяко число е магическо. 0 за „без такса“, 1 в i + 1 и 100 в процент се разбират от мястото си. Магическо е числото, което носи правило от бизнеса (праг, такса, лимит, код на статус) или може да се смени. Константа, кръстена на стойността си (FIFTY = 50), не помага: името казва ролята, не числото.
const пази името, не обекта. Ново присвояване хвърля грешка, но полето на обекта може да се смени:
const FREE_DELIVERY_FROM = 50;try { FREE_DELIVERY_FROM = 60;} catch (error) { console.log(error.message);}
const SHIPPING = { freeFrom: 50 };SHIPPING.freeFrom = 60;console.log(SHIPPING.freeFrom);Assignment to constant variable.60За обект, който наистина не се сменя, има Object.freeze(…).
Виж го
Същата сметка, прочетена през имената
Имената и константите заедно:
const FREE_DELIVERY_FROM = 50;const STANDARD_DELIVERY_FEE = 4.99;const EXPRESS_SURCHARGE = 7;const STANDARD = 1;const EXPRESS = 2;
function totalWithDelivery(subtotal, deliveryType, hasFreeDeliveryCoupon) { let deliveryFee = STANDARD_DELIVERY_FEE; if (subtotal >= FREE_DELIVERY_FROM) { deliveryFee = 0; } if (deliveryType === EXPRESS) { deliveryFee += EXPRESS_SURCHARGE; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}console.log(`${totalWithDelivery(40, STANDARD, false).toFixed(2)} €`);console.log(`${totalWithDelivery(40, EXPRESS, false).toFixed(2)} €`);console.log(`${totalWithDelivery(60, EXPRESS, false).toFixed(2)} €`);console.log(`${totalWithDelivery(40, EXPRESS, true).toFixed(2)} €`);44.99 €51.99 €67.00 €40.00 €Виж как всяко число и всяка буква от първата версия получава смисъл, докато сметката върви:
const FREE_DELIVERY_FROM = 50;const STANDARD_DELIVERY_FEE = 4.99;const EXPRESS_SURCHARGE = 7;const STANDARD = 1;const EXPRESS = 2;
function totalWithDelivery(subtotal, deliveryType, hasFreeDeliveryCoupon) { let deliveryFee = STANDARD_DELIVERY_FEE; if (subtotal >= FREE_DELIVERY_FROM) { deliveryFee = 0; } if (deliveryType === EXPRESS) { deliveryFee += EXPRESS_SURCHARGE; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}
console.log(`${totalWithDelivery(40, EXPRESS, false).toFixed(2)} €`);Стъпка по стъпка, с думи
- Старото извикване беше calc(40, 2, false). Сега казва какво смята, а 2 е EXPRESS.
- Параметрите: a, t, f стават subtotal, deliveryType, hasFreeDeliveryCoupon. Стойностите са същите.
- „d = 4.99“ става „deliveryFee = STANDARD_DELIVERY_FEE“. Същото 4.99, но вече знаеш какво е: таксата за стандартна доставка.
- 50 е FREE_DELIVERY_FROM: прагът за безплатна доставка. 40 е под него, значи доставката не е безплатна.
- „t === 2“ беше загадка. deliveryType === EXPRESS е изречение: експресната доставка добавя EXPRESS_SURCHARGE.
- „if (f)“ не казваше нищо. hasFreeDeliveryCoupon е въпрос, а отговорът е false: купон няма.
- Поръчката плюс доставката. Програмата печата 51.99 €.
- Назад и пак напред: същите стойности, сметка и изход като calc(40, 2, false). Сменени са само имената.
Трите версии печатат едно и също. Компютърът не вижда разлика. Разликата е за човека, който утре ще смени правилото. Остана едно място, което още не казва нищо: false в извикването. До него ще стигнем в следващия урок.
Пробвай
Предвиди изхода
Поръчка точно за 50 € със стандартна доставка и поръчка за 49,90 € с експресна. Прочети правилото през имената. Какво печата програмата?
console.log(`${totalWithDelivery(50, STANDARD, false).toFixed(2)} €`);console.log(`${totalWithDelivery(49.90, EXPRESS, false).toFixed(2)} €`);Първата: subtotal >= FREE_DELIVERY_FROM е вярно при точно 50, затова доставката е 0 и сумата е 50,00 €. Втората е под прага: 4,99 € доставка и 7 € за експресната, общо 49,90 + 11,99 = 61,89 €.
Пробвай
Попълни
Прагът за безплатна доставка вече е константа. Какво пише на празното място?
function totalWithDelivery(subtotal, deliveryType, hasFreeDeliveryCoupon) {
let deliveryFee = STANDARD_DELIVERY_FEE;
if (subtotal >= ) {
deliveryFee = 0;
}
if (deliveryType === EXPRESS) {
deliveryFee += EXPRESS_SURCHARGE;
}
if (hasFreeDeliveryCoupon) {
deliveryFee = 0;
}
return subtotal + deliveryFee;
}Сравнението ползва името на константата, а не числото. Така прагът е на едно място, а редът се чете като правилото: „ако сумата е поне прага за безплатна доставка“.
Идеята
Булевата стойност е въпрос
Булевата стойност отговаря с да или не, затова името ѝ е въпрос: isPaid, hasCoupon, canShip. Тогава if се чете като изречение:
const subtotal = 62.4;const isPaid = true;const qualifiesForFreeDelivery = subtotal >= FREE_DELIVERY_FROM;
if (isPaid && qualifiesForFreeDelivery) { console.log("Изпращаме без такса за доставка.");}Изпращаме без такса за доставка.Какво не работи:
flag,status,check,ok: не казват какъв е въпросът;- отрицание в името:
isNotPaid. Тогаваif (!isNotPaid)е двойно отрицание, а в него всеки се спъва. ПишиisPaid, а отрицанието в проверката, когато трябва.
Същото важи за останалите имена. Класът и променливата са съществително (Order, deliveryFee). Функцията прави нещо, затова е глагол (sendInvoice). Функция с име на въпрос (isInStock) обещава, че само пита. В следващия урок ще видиш какво става, когато не спази обещанието.
Идеята
Думите на магазина в кода
Най-добрите имена не ги измисляш ти. Това са думите, с които говорят хората, за които е програмата: пратка, куриер, наложен платеж, връщане, фактура. Когато в кода пише parcel, courier, refund, програмистът и човекът от склада говорят за едно и също. Това са имена от предметната област (domain).
- Една дума за едно понятие. Ако клиентът е
customer, той не ставаclientв съседния файл иbuyerв базата. Ако четенето еfind…, не ставаget…иfetch…на други места. Различна дума кара читателя да търси различно нещо. - Една дума не значи две неща. Ако
totalе сумата с доставката, сумата без нея има свое име,subtotal. - Техническите думи са за техническите неща.
queue,cache,parserса наред, когато това е самото нещо.Manager,ProcessorиHelperобикновено значат „не знам какво точно прави“.
Пробвай
Намери грешката
Счетоводството се оплаква: фактура за 50 € без ДДС излиза 110 €. ДДС-то е 20%, значи трябва да е 60 €.
function vat(price) { return price * 1.2;}
function printInvoice(net) { const total = net + vat(net); console.log(`Нето: ${net.toFixed(2)} €`); console.log(`Общо с ДДС: ${total.toFixed(2)} €`);}printInvoice(50);Нето: 50.00 €Общо с ДДС: 110.00 €На кой ред е грешката?
Избери реда с грешката.
Грешката е в името. vat обещава данъка, а връща цената с данъка. Авторът на printInvoice е повярвал на името и е добавил „данъка“ към цената, тоест е броил цената два пъти. Двете функции поотделно смятат точно каквото пише в тях. Лъже само името. Поправката е функция, чието име и съдържание казват едно и също:
const VAT_RATE = 0.2;
function vatAmount(price) { return price * VAT_RATE;}С нея фактурата е 60,00 €.
Идеята
Как пише имената твоят език
Добро име на един език изглежда странно на друг. Всеки език има своя уговорка, а кодът, който я спазва, се чете по-бързо, защото по вида на името личи какво е то.
- променливи и функции:
camelCase(totalWithDelivery,deliveryFee); - класове:
PascalCase(DeliveryCalculator); - константи, които са настройка на програмата:
UPPER_SNAKE_CASE(FREE_DELIVERY_FROM); - частни полета на клас:
#отпред (#stock). Това не е уговорка, а част от езика: отвън полето не се вижда.
По-точно, за любопитните
Едно и също име на четирите езика:
| Java | C# | Python | JavaScript | |
|---|---|---|---|---|
| функция | totalWithDelivery |
TotalWithDelivery |
total_with_delivery |
totalWithDelivery |
| променлива | deliveryFee |
deliveryFee |
delivery_fee |
deliveryFee |
| константа | FREE_DELIVERY_FROM |
FreeDeliveryFrom |
FREE_DELIVERY_FROM |
FREE_DELIVERY_FROM |
| клас | Order |
Order |
Order |
Order |
| булева стойност | isPaid |
isPaid (свойство: IsPaid) |
is_paid |
isPaid |
Идеята
Rename: най-евтиният refactoring
Смяната на име без смяна на поведението се казва Rename. Това е най-малкият и най-евтиният refactoring: не сменяш нито една стойност, нито едно сравнение, а програмата печата същото.
Не го прави на ръка с търсене и замяна. Редакторът знае кое d е твоята променлива и кое е буквата в "delivery": в IntelliJ IDEA и PyCharm е Shift+F6, във VS Code е F2, във Visual Studio е Ctrl+R, Ctrl+R. Той сменя всяко място наведнъж.
ReferenceError) чак когато се стигне до този ред. Затова след Rename програмата се пуска, а още по-добре тестовете. А име, което идва като текст (order["fee"], ключ в JSON), редакторът изобщо не вижда.Как се прави refactoring на малки стъпки, така че програмата да работи след всяка, е темата на шестия урок от модула. Rename е първата стъпка.
Упражнение
Куриерът
Реши задачата в редактора си.
Куриерът. Тази функция смята цената на пратка:
function f(w, z) { let p = 3.5; if (w > 2) { p += (w - 2) * 0.8; } if (z === 3) { p *= 1.5; } return p;}console.log(`${f(1.5, 1).toFixed(2)} €`);console.log(`${f(5, 1).toFixed(2)} €`);console.log(`${f(5, 3).toFixed(2)} €`);3.50 €5.90 €8.85 €Правилото е такова: пратката до 2 кг струва 3,50 €, всеки килограм над тях е 0,80 €, а зона 3 е отдалечена и цената там е с 50% по-висока. Дай на функцията, на параметрите и на променливата имена, които казват това, а числата направи константи. Изходът трябва да остане същият:
3.50 €5.90 €8.85 €Още задачи
- Булевата стойност. Добави параметър, който казва дали пратката е чуплива (такса 1,20 €). Как ще го кръстиш? Как ще изглежда извикването?
- Твоят код. Отвори свое старо упражнение. Намери три имена, които днес не разбираш от пръв поглед, и ги смени с Rename на редактора. Пусни програмата: изходът същият ли е?
- Речникът на магазина. Напиши десет думи, с които говори човекът от склада (пратка, куриер, зона…), и как би ги кръстил в кода на своя език.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Функции · Параметри и return · static: общото за всички · Изключения
- Продължава в: Функции: една работа, малко параметри, без скрити странични ефекти · Коментари, които лъжат, и такива, които помагат · Миризмите: дълъг метод, голям клас, дублиране, примитиви вместо типове, дълга верига if · Тестовете като предпазна мрежа
- Задачи по темата: Чист код и refactoring