Към съдържанието
wannacode

Имена в кода

Накратко: Добрите имена в кода казват какво е нещото и защо съществува: константи вместо магически числа, булеви стойности като въпроси и думите на бизнеса вместо съкращения.

Проблем

Функцията, която никой не смее да пипне

В кода на онлайн магазина има функция, през която минава всяка поръчка:

def calc(a, t, f):
d = 4.99
if a >= 50:
d = 0
if t == 2:
d += 7
if f:
d = 0
return a + d
print(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_fee
print(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 = 50
STANDARD_DELIVERY_FEE = 4.99
EXPRESS_SURCHARGE = 7
STANDARD = 1
EXPRESS = 2
В Python няма константи. Главните букви, UPPER_SNAKE_CASE, са уговорка от PEP 8: „тази стойност не се сменя“. Езикът няма да те спре, но всеки, който чете кода, знае какво значат.

Маркетингът иска 60 €: сменяш един ред, и всяко място, което пита „от колко е безплатна доставката“, вижда новата стойност. А 50-те, които са тегло, никой не пипа, защото не са FREE_DELIVERY_FROM.

По-точно, за любопитните

Не всяко число е магическо. 0 за „без такса“, 1 в i + 1 и 100 в процент се разбират от мястото си. Магическо е числото, което носи правило от бизнеса (праг, такса, лимит, код на статус) или може да се смени. Константа, кръстена на стойността си (FIFTY = 50), не помага: името казва ролята, не числото.

Нищо не спира второ присвояване:

FREE_DELIVERY_FROM = 50
FREE_DELIVERY_FROM = 60
print(FREE_DELIVERY_FROM)
Изход
60

typing.Final (FREE_DELIVERY_FROM: Final = 50) кара инструмент като mypy да се оплаче от второто присвояване. Самата програма пак ще го приеме.

Виж го

Същата сметка, прочетена през имената

Имената и константите заедно:

FREE_DELIVERY_FROM = 50
STANDARD_DELIVERY_FEE = 4.99
EXPRESS_SURCHARGE = 7
STANDARD = 1
EXPRESS = 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, 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 = 50
STANDARD_DELIVERY_FEE = 4.99
EXPRESS_SURCHARGE = 7
STANDARD = 1
EXPRESS = 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} €")
Стъпка по стъпка, с думи
  1. Старото извикване беше calc(40, 2, False). Сега казва какво смята, а 2 е EXPRESS.
  2. Параметрите: a, t, f стават subtotal, delivery_type, has_free_delivery_coupon. Стойностите са същите.
  3. „d = 4.99“ става „delivery_fee = STANDARD_DELIVERY_FEE“. Същото 4.99, но вече знаеш какво е: таксата за стандартна доставка.
  4. 50 е FREE_DELIVERY_FROM: прагът за безплатна доставка. 40 е под него, значи доставката не е безплатна.
  5. „t == 2“ беше загадка. delivery_type == EXPRESS е изречение: експресната доставка добавя EXPRESS_SURCHARGE.
  6. „if f:“ не казваше нищо. has_free_delivery_coupon е въпрос, а отговорът е False: купон няма.
  7. Поръчката плюс доставката. Програмата печата 51.99 €.
  8. Назад и пак напред: същите стойности, сметка и изход като 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} €")
Отговор

Пробвай

Попълни

Прагът за безплатна доставка вече е константа. Какво пише на празното място?

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.40
is_paid = True
qualifies_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 €

На кой ред е грешката?

Избери реда с грешката.

Идеята

Как пише имената твоят език

Добро име на един език изглежда странно на друг. Всеки език има своя уговорка, а кодът, който я спазва, се чете по-бързо, защото по вида на името личи какво е то.

Уговорката е от 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 p
print(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 на редактора. Пусни програмата: изходът същият ли е?
  • Речникът на магазина. Напиши десет думи, с които говори човекът от склада (пратка, куриер, зона…), и как би ги кръстил в кода на своя език.

Тест

Провери се

1. В променлива стои сумата на продуктите след отстъпката, но без доставката. Кое име е най-добро?
2. Какво не е наред с if not is_not_paid:?
3. Безплатната доставка става от 60 € вместо от 50 €. С какво константата помага?
4. get_user(email) намира потребителя, а ако го няма, създава нов и го записва в базата. Какъв е проблемът?
5. Защо Rename е най-евтиният refactoring?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·