Имена в кода
Накратко: Добрите имена в кода казват какво е нещото и защо съществува: константи вместо магически числа, булеви стойности като въпроси и думите на бизнеса вместо съкращения.
Проблем
Функцията, която никой не смее да пипне
В кода на онлайн магазина има функция, през която минава всяка поръчка:
def calc(a, t, f): d = 4.99 if a >= 50: d = 0 if t == 2: d += 7 if f: d = 0 return a + dprint(f"{calc(40, 1, False):.2f} €")print(f"{calc(40, 2, False):.2f} €")print(f"{calc(60, 2, False):.2f} €")print(f"{calc(40, 2, True):.2f} €")44.99 €51.99 €67.00 €40.00 €Работи. Сега кажи какво прави. Какво е t? Защо t == 2 добавя 7? Какво е f и защо нулира всичко? Защо точно 50?
Авторът знаеше. Той напусна преди година. Маркетингът иска безплатна доставка от 60 €, а не от 50 €. Колегата търси 50 в проекта и намира 14 места: на едно 50 е прагът за доставката, на друго е най-голямото тегло на пратка в килограми, на трето е броят продукти на страница. Кое да смени?
Логиката тук е вярна. Проблемът е, че кодът не казва на човека, който го чете, какво е мислил авторът.
Идеята
Името казва какво и защо, не как
Компютърът чете calc(a, t, f) без проблем. Човекът не може. А кодът се чете много повече пъти, отколкото се пише: при всяка поправка, при всеки преглед, при всяко търсене на грешка.
Доброто име отговаря на въпросите, които иначе би задал на автора: какво е това, защо го има, как се ползва. Това се казва име, което разкрива намерението. Ето същата функция, в която са сменени само имената:
def total_with_delivery(subtotal, delivery_type, has_free_delivery_coupon): delivery_fee = 4.99 if subtotal >= 50: delivery_fee = 0 if delivery_type == 2: delivery_fee += 7 if has_free_delivery_coupon: delivery_fee = 0 return subtotal + delivery_feeprint(f"{total_with_delivery(40, 1, False):.2f} €")print(f"{total_with_delivery(40, 2, False):.2f} €")print(f"{total_with_delivery(60, 2, False):.2f} €")print(f"{total_with_delivery(40, 2, True):.2f} €")44.99 €51.99 €67.00 €40.00 €Изходът е същият до последната цифра. Функцията вече се чете като правило: доставката е 4,99 €, безплатна е от 50 €, експресната е със 7 € отгоре, а купонът я прави безплатна.
Кои имена не помагат:
- име, което казва как, а не какво:
float_value,list1,tmp; - общо име, което пасва на всичко:
data,info,value,process,handle,manager; - съкращение, което знае само авторът:
calc,cnt,dlv_fee.
Дължината следва обхвата. i в цикъл от три реда е наред. Параметър на функция, която се вика от десет места, заслужава пълно име.
По-точно, за любопитните
А коментар над функцията, t: 1 = стандартна, 2 = експресна? Помага, докато някой не смени числата и не забрави коментара. Името не може да остарее отделно от кода, защото е част от кода. Кога коментарът помага и кога лъже, е темата на третия урок от модула.
Идеята
Магическо число → константа с име
Имената на променливите вече са на място, но в кода още стоят 4.99, 50, 7 и 2. Число без име в кода се казва магическо число: какво значи, знае само авторът, а когато трябва да се смени, го търсиш навсякъде.
Поправката е константа с име. Стойността се пише веднъж, на едно място, а навсякъде се ползва името:
FREE_DELIVERY_FROM = 50STANDARD_DELIVERY_FEE = 4.99EXPRESS_SURCHARGE = 7STANDARD = 1EXPRESS = 2UPPER_SNAKE_CASE, са уговорка от PEP 8: „тази стойност не се сменя“. Езикът няма да те спре, но всеки, който чете кода, знае какво значат.Маркетингът иска 60 €: сменяш един ред, и всяко място, което пита „от колко е безплатна доставката“, вижда новата стойност. А 50-те, които са тегло, никой не пипа, защото не са FREE_DELIVERY_FROM.
По-точно, за любопитните
Не всяко число е магическо. 0 за „без такса“, 1 в i + 1 и 100 в процент се разбират от мястото си. Магическо е числото, което носи правило от бизнеса (праг, такса, лимит, код на статус) или може да се смени. Константа, кръстена на стойността си (FIFTY = 50), не помага: името казва ролята, не числото.
Нищо не спира второ присвояване:
FREE_DELIVERY_FROM = 50FREE_DELIVERY_FROM = 60print(FREE_DELIVERY_FROM)60typing.Final (FREE_DELIVERY_FROM: Final = 50) кара инструмент като mypy да се оплаче от второто присвояване. Самата програма пак ще го приеме.
Виж го
Същата сметка, прочетена през имената
Имената и константите заедно:
FREE_DELIVERY_FROM = 50STANDARD_DELIVERY_FEE = 4.99EXPRESS_SURCHARGE = 7STANDARD = 1EXPRESS = 2
def total_with_delivery(subtotal, delivery_type, has_free_delivery_coupon): delivery_fee = STANDARD_DELIVERY_FEE if subtotal >= FREE_DELIVERY_FROM: delivery_fee = 0 if delivery_type == EXPRESS: delivery_fee += EXPRESS_SURCHARGE if has_free_delivery_coupon: delivery_fee = 0 return subtotal + delivery_feeprint(f"{total_with_delivery(40, STANDARD, False):.2f} €")print(f"{total_with_delivery(40, EXPRESS, False):.2f} €")print(f"{total_with_delivery(60, EXPRESS, False):.2f} €")print(f"{total_with_delivery(40, EXPRESS, True):.2f} €")44.99 €51.99 €67.00 €40.00 €Виж как всяко число и всяка буква от първата версия получава смисъл, докато сметката върви:
FREE_DELIVERY_FROM = 50STANDARD_DELIVERY_FEE = 4.99EXPRESS_SURCHARGE = 7STANDARD = 1EXPRESS = 2
def total_with_delivery(subtotal, delivery_type, has_free_delivery_coupon): delivery_fee = STANDARD_DELIVERY_FEE if subtotal >= FREE_DELIVERY_FROM: delivery_fee = 0 if delivery_type == EXPRESS: delivery_fee += EXPRESS_SURCHARGE if has_free_delivery_coupon: delivery_fee = 0 return subtotal + delivery_fee
print(f"{total_with_delivery(40, EXPRESS, False):.2f} €")Стъпка по стъпка, с думи
- Старото извикване беше calc(40, 2, False). Сега казва какво смята, а 2 е EXPRESS.
- Параметрите: a, t, f стават subtotal, delivery_type, has_free_delivery_coupon. Стойностите са същите.
- „d = 4.99“ става „delivery_fee = STANDARD_DELIVERY_FEE“. Същото 4.99, но вече знаеш какво е: таксата за стандартна доставка.
- 50 е FREE_DELIVERY_FROM: прагът за безплатна доставка. 40 е под него, значи доставката не е безплатна.
- „t == 2“ беше загадка. delivery_type == EXPRESS е изречение: експресната доставка добавя EXPRESS_SURCHARGE.
- „if f:“ не казваше нищо. has_free_delivery_coupon е въпрос, а отговорът е False: купон няма.
- Поръчката плюс доставката. Програмата печата 51.99 €.
- Назад и пак напред: същите стойности, сметка и изход като calc(40, 2, False). Сменени са само имената.
Трите версии печатат едно и също. Компютърът не вижда разлика. Разликата е за човека, който утре ще смени правилото. Остана едно място, което още не казва нищо: False в извикването. До него ще стигнем в следващия урок.
Пробвай
Предвиди изхода
Поръчка точно за 50 € със стандартна доставка и поръчка за 49,90 € с експресна. Прочети правилото през имената. Какво печата програмата?
print(f"{total_with_delivery(50, STANDARD, False):.2f} €")print(f"{total_with_delivery(49.90, EXPRESS, False):.2f} €")Първата: subtotal >= FREE_DELIVERY_FROM е вярно при точно 50, затова доставката е 0 и сумата е 50,00 €. Втората е под прага: 4,99 € доставка и 7 € за експресната, общо 49,90 + 11,99 = 61,89 €.
Пробвай
Попълни
Прагът за безплатна доставка вече е константа. Какво пише на празното място?
def total_with_delivery(subtotal, delivery_type, has_free_delivery_coupon):
delivery_fee = STANDARD_DELIVERY_FEE
if subtotal >= :
delivery_fee = 0
if delivery_type == EXPRESS:
delivery_fee += EXPRESS_SURCHARGE
if has_free_delivery_coupon:
delivery_fee = 0
return subtotal + delivery_feeСравнението ползва името на константата, а не числото. Така прагът е на едно място, а редът се чете като правилото: „ако сумата е поне прага за безплатна доставка“.
Идеята
Булевата стойност е въпрос
Булевата стойност отговаря с да или не, затова името ѝ е въпрос: is_paid, has_coupon, can_ship. Тогава if се чете като изречение:
subtotal = 62.40is_paid = Truequalifies_for_free_delivery = subtotal >= FREE_DELIVERY_FROM
if is_paid and qualifies_for_free_delivery: print("Изпращаме без такса за доставка.")Изпращаме без такса за доставка.Какво не работи:
flag,status,check,ok: не казват какъв е въпросът;- отрицание в името:
is_not_paid. Тогаваif not is_not_paid:е двойно отрицание, а в него всеки се спъва. Пишиis_paid, а отрицанието в проверката, когато трябва.
Същото важи за останалите имена. Класът и променливата са съществително (Order, delivery_fee). Функцията прави нещо, затова е глагол (send_invoice). Функция с име на въпрос (is_in_stock) обещава, че само пита. В следващия урок ще видиш какво става, когато не спази обещанието.
Идеята
Думите на магазина в кода
Най-добрите имена не ги измисляш ти. Това са думите, с които говорят хората, за които е програмата: пратка, куриер, наложен платеж, връщане, фактура. Когато в кода пише parcel, courier, refund, програмистът и човекът от склада говорят за едно и също. Това са имена от предметната област (domain).
- Една дума за едно понятие. Ако клиентът е
customer, той не ставаclientв съседния файл иbuyerв базата. Ако четенето еfind…, не ставаget…иfetch…на други места. Различна дума кара читателя да търси различно нещо. - Една дума не значи две неща. Ако
totalе сумата с доставката, сумата без нея има свое име,subtotal. - Техническите думи са за техническите неща.
queue,cache,parserса наред, когато това е самото нещо.Manager,ProcessorиHelperобикновено значат „не знам какво точно прави“.
Пробвай
Намери грешката
Счетоводството се оплаква: фактура за 50 € без ДДС излиза 110 €. ДДС-то е 20%, значи трябва да е 60 €.
def vat(price): return price * 1.20
def print_invoice(net): total = net + vat(net) print(f"Нето: {net:.2f} €") print(f"Общо с ДДС: {total:.2f} €")print_invoice(50)Нето: 50.00 €Общо с ДДС: 110.00 €На кой ред е грешката?
Избери реда с грешката.
Грешката е в името. vat обещава данъка, а връща цената с данъка. Авторът на print_invoice е повярвал на името и е добавил „данъка“ към цената, тоест е броил цената два пъти. Двете функции поотделно смятат точно каквото пише в тях. Лъже само името. Поправката е функция, чието име и съдържание казват едно и също:
VAT_RATE = 0.20
def vat_amount(price): return price * VAT_RATEС нея фактурата е 60,00 €.
Идеята
Как пише имената твоят език
Добро име на един език изглежда странно на друг. Всеки език има своя уговорка, а кодът, който я спазва, се чете по-бързо, защото по вида на името личи какво е то.
Уговорката е от PEP 8:
- функции, методи, променливи и модули:
snake_case(total_with_delivery,delivery_fee); - класове:
PascalCase(DeliveryCalculator); - константи на ниво модул:
UPPER_SNAKE_CASE(FREE_DELIVERY_FROM); _отпред (_cache) значи „вътрешно, не го ползвай отвън“. Езикът не го забранява, това е уговорка.
По-точно, за любопитните
Едно и също име на четирите езика:
| 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. Той сменя всяко място наведнъж.
NameError) чак когато се стигне до този ред. Затова след Rename програмата се пуска, а още по-добре тестовете. А име, което идва като текст (getattr(order, "fee"), ключ в JSON), редакторът изобщо не вижда.Как се прави refactoring на малки стъпки, така че програмата да работи след всяка, е темата на шестия урок от модула. Rename е първата стъпка.
Упражнение
Куриерът
Реши задачата в редактора си.
Куриерът. Тази функция смята цената на пратка:
def f(w, z): p = 3.50 if w > 2: p += (w - 2) * 0.80 if z == 3: p *= 1.5 return pprint(f"{f(1.5, 1):.2f} €")print(f"{f(5, 1):.2f} €")print(f"{f(5, 3):.2f} €")3.50 €5.90 €8.85 €Правилото е такова: пратката до 2 кг струва 3,50 €, всеки килограм над тях е 0,80 €, а зона 3 е отдалечена и цената там е с 50% по-висока. Дай на функцията, на параметрите и на променливата имена, които казват това, а числата направи константи. Изходът трябва да остане същият:
3.50 €5.90 €8.85 €Още задачи
- Булевата стойност. Добави параметър, който казва дали пратката е чуплива (такса 1,20 €). Как ще го кръстиш? Как ще изглежда извикването?
- Твоят код. Отвори свое старо упражнение. Намери три имена, които днес не разбираш от пръв поглед, и ги смени с Rename на редактора. Пусни програмата: изходът същият ли е?
- Речникът на магазина. Напиши десет думи, с които говори човекът от склада (пратка, куриер, зона…), и как би ги кръстил в кода на своя език.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Функции · Параметри и return · Общото за всички: атрибути на класа · Изключения
- Продължава в: Функции: една работа, малко параметри, без скрити странични ефекти · Коментари, които лъжат, и такива, които помагат · Миризмите: дълъг метод, голям клас, дублиране, примитиви вместо типове, дълга верига if · Тестовете като предпазна мрежа
- Задачи по темата: Чист код и refactoring