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

Имена в кода

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

Проблем

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

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

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} €");
Стъпка по стъпка, с думи
  1. Старото извикване беше Calc(40, 2, false). Сега казва какво смята, а 2 е Express.
  2. Параметрите: a, t, f стават subtotal, deliveryType, hasFreeDeliveryCoupon. Стойностите са същите.
  3. „d = 4.99m“ става „deliveryFee = StandardDeliveryFee“. Същото 4.99, но вече знаеш какво е: таксата за стандартна доставка.
  4. 50 е FreeDeliveryFrom: прагът за безплатна доставка. 40 е под него, значи доставката не е безплатна.
  5. „t == 2“ беше загадка. deliveryType == Express е изречение: експресната доставка добавя ExpressSurcharge.
  6. „if (f)“ не казваше нищо. hasFreeDeliveryCoupon е въпрос, а отговорът е false: купон няма.
  7. Поръчката плюс доставката. Програмата печата 51.99 €.
  8. Назад и пак напред: същите стойности, сметка и изход като Calc(40, 2, false). Сменени са само имената.

Трите версии печатат едно и също. Компютърът не вижда разлика. Разликата е за човека, който утре ще смени правилото. Остана едно място, което още не казва нищо: false в извикването. До него ще стигнем в следващия урок.

Пробвай

Предвиди изхода

Поръчка точно за 50 € със стандартна доставка и поръчка за 49,90 € с експресна. Прочети правилото през имената. Какво печата програмата?

Console.WriteLine($"{TotalWithDelivery(50, Standard, false):F2} €");
Console.WriteLine($"{TotalWithDelivery(49.90m, Express, false):F2} €");
Отговор

Пробвай

Попълни

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

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 €

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

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

Идеята

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

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

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

Тест

Провери се

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

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