Имена в кода
Накратко: Добрите имена в кода казват какво е нещото и защо съществува: константи вместо магически числа, булеви стойности като въпроси и думите на бизнеса вместо съкращения.
Проблем
Функцията, която никой не смее да пипне
В кода на онлайн магазина има функция, през която минава всяка поръчка:
static decimal Calc(decimal a, int t, bool f){ decimal d = 4.99m; if (a >= 50) { d = 0; } if (t == 2) { d += 7; } if (f) { d = 0; } return a + d;}Console.WriteLine($"{Calc(40, 1, false):F2} €");Console.WriteLine($"{Calc(40, 2, false):F2} €");Console.WriteLine($"{Calc(60, 2, false):F2} €");Console.WriteLine($"{Calc(40, 2, true):F2} €");44.99 €51.99 €67.00 €40.00 €Работи. Сега кажи какво прави. Какво е t? Защо t == 2 добавя 7? Какво е f и защо нулира всичко? Защо точно 50?
Авторът знаеше. Той напусна преди година. Маркетингът иска безплатна доставка от 60 €, а не от 50 €. Колегата търси 50 в проекта и намира 14 места: на едно 50 е прагът за доставката, на друго е най-голямото тегло на пратка в килограми, на трето е броят продукти на страница. Кое да смени?
Логиката тук е вярна. Проблемът е, че кодът не казва на човека, който го чете, какво е мислил авторът.
Идеята
Името казва какво и защо, не как
Компютърът чете Calc(a, t, f) без проблем. Човекът не може. А кодът се чете много повече пъти, отколкото се пише: при всяка поправка, при всеки преглед, при всяко търсене на грешка.
Доброто име отговаря на въпросите, които иначе би задал на автора: какво е това, защо го има, как се ползва. Това се казва име, което разкрива намерението. Ето същата функция, в която са сменени само имената:
static decimal TotalWithDelivery(decimal subtotal, int deliveryType, bool hasFreeDeliveryCoupon){ decimal deliveryFee = 4.99m; if (subtotal >= 50) { deliveryFee = 0; } if (deliveryType == 2) { deliveryFee += 7; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}Console.WriteLine($"{TotalWithDelivery(40, 1, false):F2} €");Console.WriteLine($"{TotalWithDelivery(40, 2, false):F2} €");Console.WriteLine($"{TotalWithDelivery(60, 2, false):F2} €");Console.WriteLine($"{TotalWithDelivery(40, 2, true):F2} €");44.99 €51.99 €67.00 €40.00 €Изходът е същият до последната цифра. Функцията вече се чете като правило: доставката е 4,99 €, безплатна е от 50 €, експресната е със 7 € отгоре, а купонът я прави безплатна.
Кои имена не помагат:
- име, което казва как, а не какво:
decimalValue,list1,tmp; - общо име, което пасва на всичко:
data,info,value,process,handle,manager; - съкращение, което знае само авторът:
Calc,cnt,dlvFee.
Дължината следва обхвата. i в цикъл от три реда е наред. Параметър на функция, която се вика от десет места, заслужава пълно име.
По-точно, за любопитните
А коментар над функцията, t: 1 = стандартна, 2 = експресна? Помага, докато някой не смени числата и не забрави коментара. Името не може да остарее отделно от кода, защото е част от кода. Кога коментарът помага и кога лъже, е темата на третия урок от модула.
Идеята
Магическо число → константа с име
Имената на променливите вече са на място, но в кода още стоят 4.99, 50, 7 и 2. Число без име в кода се казва магическо число: какво значи, знае само авторът, а когато трябва да се смени, го търсиш навсякъде.
Поправката е константа с име. Стойността се пише веднъж, на едно място, а навсякъде се ползва името:
const decimal FreeDeliveryFrom = 50m;const decimal StandardDeliveryFee = 4.99m;const decimal ExpressSurcharge = 7m;const int Standard = 1;const int Express = 2;const: стойността е известна при компилиране и не може да се смени. Компилаторът спира всеки опит. В C# константите са с PascalCase (FreeDeliveryFrom), като методите и свойствата, а не с главни букви.Маркетингът иска 60 €: сменяш един ред, и всяко място, което пита „от колко е безплатна доставката“, вижда новата стойност. А 50-те, които са тегло, никой не пипа, защото не са FreeDeliveryFrom.
По-точно, за любопитните
Не всяко число е магическо. 0 за „без такса“, 1 в i + 1 и 100 в процент се разбират от мястото си. Магическо е числото, което носи правило от бизнеса (праг, такса, лимит, код на статус) или може да се смени. Константа, кръстена на стойността си (FIFTY = 50), не помага: името казва ролята, не числото.
const е само за стойности, известни при компилиране: числа, низове, bool. За обект се пише static readonly: присвоява се веднъж и после не се сменя. Още една разлика: стойността на const се копира в кода, който я ползва. Ако друг проект ползва твоята константа, той вижда новата стойност чак когато се компилира наново.Виж го
Същата сметка, прочетена през имената
Имената и константите заедно:
const decimal FreeDeliveryFrom = 50m;const decimal StandardDeliveryFee = 4.99m;const decimal ExpressSurcharge = 7m;const int Standard = 1;const int Express = 2;
static decimal TotalWithDelivery(decimal subtotal, int deliveryType, bool hasFreeDeliveryCoupon){ decimal deliveryFee = StandardDeliveryFee; if (subtotal >= FreeDeliveryFrom) { deliveryFee = 0; } if (deliveryType == Express) { deliveryFee += ExpressSurcharge; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}Console.WriteLine($"{TotalWithDelivery(40, Standard, false):F2} €");Console.WriteLine($"{TotalWithDelivery(40, Express, false):F2} €");Console.WriteLine($"{TotalWithDelivery(60, Express, false):F2} €");Console.WriteLine($"{TotalWithDelivery(40, Express, true):F2} €");44.99 €51.99 €67.00 €40.00 €Виж как всяко число и всяка буква от първата версия получава смисъл, докато сметката върви:
const decimal FreeDeliveryFrom = 50m;const decimal StandardDeliveryFee = 4.99m;const decimal ExpressSurcharge = 7m;const int Standard = 1;const int Express = 2;
static decimal TotalWithDelivery(decimal subtotal, int deliveryType, bool hasFreeDeliveryCoupon){ decimal deliveryFee = StandardDeliveryFee; if (subtotal >= FreeDeliveryFrom) { deliveryFee = 0; } if (deliveryType == Express) { deliveryFee += ExpressSurcharge; } if (hasFreeDeliveryCoupon) { deliveryFee = 0; } return subtotal + deliveryFee;}
Console.WriteLine($"{TotalWithDelivery(40, Express, false):F2} €");Стъпка по стъпка, с думи
- Старото извикване беше Calc(40, 2, false). Сега казва какво смята, а 2 е Express.
- Параметрите: a, t, f стават subtotal, deliveryType, hasFreeDeliveryCoupon. Стойностите са същите.
- „d = 4.99m“ става „deliveryFee = StandardDeliveryFee“. Същото 4.99, но вече знаеш какво е: таксата за стандартна доставка.
- 50 е FreeDeliveryFrom: прагът за безплатна доставка. 40 е под него, значи доставката не е безплатна.
- „t == 2“ беше загадка. deliveryType == Express е изречение: експресната доставка добавя ExpressSurcharge.
- „if (f)“ не казваше нищо. hasFreeDeliveryCoupon е въпрос, а отговорът е false: купон няма.
- Поръчката плюс доставката. Програмата печата 51.99 €.
- Назад и пак напред: същите стойности, сметка и изход като Calc(40, 2, false). Сменени са само имената.
Трите версии печатат едно и също. Компютърът не вижда разлика. Разликата е за човека, който утре ще смени правилото. Остана едно място, което още не казва нищо: false в извикването. До него ще стигнем в следващия урок.
Пробвай
Предвиди изхода
Поръчка точно за 50 € със стандартна доставка и поръчка за 49,90 € с експресна. Прочети правилото през имената. Какво печата програмата?
Console.WriteLine($"{TotalWithDelivery(50, Standard, false):F2} €");Console.WriteLine($"{TotalWithDelivery(49.90m, Express, false):F2} €");Първата: subtotal >= FreeDeliveryFrom е вярно при точно 50, затова доставката е 0 и сумата е 50,00 €. Втората е под прага: 4,99 € доставка и 7 € за експресната, общо 49,90 + 11,99 = 61,89 €.
Пробвай
Попълни
Прагът за безплатна доставка вече е константа. Какво пише на празното място?
static decimal TotalWithDelivery(decimal subtotal, int deliveryType, bool hasFreeDeliveryCoupon)
{
decimal deliveryFee = StandardDeliveryFee;
if (subtotal >= )
{
deliveryFee = 0;
}
if (deliveryType == Express)
{
deliveryFee += ExpressSurcharge;
}
if (hasFreeDeliveryCoupon)
{
deliveryFee = 0;
}
return subtotal + deliveryFee;
}Сравнението ползва името на константата, а не числото. Така прагът е на едно място, а редът се чете като правилото: „ако сумата е поне прага за безплатна доставка“.
Идеята
Булевата стойност е въпрос
Булевата стойност отговаря с да или не, затова името ѝ е въпрос: isPaid, hasCoupon, canShip. Тогава if се чете като изречение:
decimal subtotal = 62.40m;bool isPaid = true;bool qualifiesForFreeDelivery = subtotal >= FreeDeliveryFrom;
if (isPaid && qualifiesForFreeDelivery){ Console.WriteLine("Изпращаме без такса за доставка.");}Изпращаме без такса за доставка.Какво не работи:
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 €.
static decimal Vat(decimal price){ return price * 1.20m;}
static void PrintInvoice(decimal net){ decimal total = net + Vat(net); Console.WriteLine($"Нето: {net:F2} €"); Console.WriteLine($"Общо с ДДС: {total:F2} €");}PrintInvoice(50);Нето: 50.00 €Общо с ДДС: 110.00 €На кой ред е грешката?
Избери реда с грешката.
Грешката е в името. Vat обещава данъка, а връща цената с данъка. Авторът на PrintInvoice е повярвал на името и е добавил „данъка“ към цената, тоест е броил цената два пъти. Двете функции поотделно смятат точно каквото пише в тях. Лъже само името. Поправката е функция, чието име и съдържание казват едно и също:
const decimal VatRate = 0.20m;
static decimal VatAmount(decimal price){ return price * VatRate;}С нея фактурата е 60,00 €.
Идеята
Как пише имената твоят език
Добро име на един език изглежда странно на друг. Всеки език има своя уговорка, а кодът, който я спазва, се чете по-бързо, защото по вида на името личи какво е то.
- класове, методи, свойства и константи:
PascalCase(Order,TotalWithDelivery,FreeDeliveryFrom); - локални променливи и параметри:
camelCase(deliveryFee,subtotal); - частни полета:
_camelCase(_stock); - интерфейси:
Iотпред (IPaymentMethod); - стойност на обект се чете през свойство:
order.Subtotal, а не методGetSubtotal(). Свойството е съществително, а методът е глагол.
По-точно, за любопитните
Едно и също име на четирите езика:
| 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. Той сменя всяко място наведнъж.
Как се прави refactoring на малки стъпки, така че програмата да работи след всяка, е темата на шестия урок от модула. Rename е първата стъпка.
Упражнение
Куриерът
Реши задачата в редактора си.
Куриерът. Тази функция смята цената на пратка:
static decimal F(decimal w, int z){ decimal p = 3.50m; if (w > 2) { p += (w - 2) * 0.80m; } if (z == 3) { p *= 1.5m; } return p;}Console.WriteLine($"{F(1.5m, 1):F2} €");Console.WriteLine($"{F(5, 1):F2} €");Console.WriteLine($"{F(5, 3):F2} €");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