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

Имена в кода

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

Проблем

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

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

static double calc(double a, int t, boolean f) {
double d = 4.99;
if (a >= 50) {
d = 0;
}
if (t == 2) {
d += 7;
}
if (f) {
d = 0;
}
return a + d;
}
System.out.printf("%.2f €%n", calc(40, 1, false));
System.out.printf("%.2f €%n", calc(40, 2, false));
System.out.printf("%.2f €%n", calc(60, 2, false));
System.out.printf("%.2f €%n", calc(40, 2, true));
Изход
44.99 €
51.99 €
67.00 €
40.00 €

Работи. Сега кажи какво прави. Какво е t? Защо t == 2 добавя 7? Какво е f и защо нулира всичко? Защо точно 50?

Авторът знаеше. Той напусна преди година. Маркетингът иска безплатна доставка от 60 €, а не от 50 €. Колегата търси 50 в проекта и намира 14 места: на едно 50 е прагът за доставката, на друго е най-голямото тегло на пратка в килограми, на трето е броят продукти на страница. Кое да смени?

Логиката тук е вярна. Проблемът е, че кодът не казва на човека, който го чете, какво е мислил авторът.

Идеята

Името казва какво и защо, не как

Компютърът чете calc(a, t, f) без проблем. Човекът не може. А кодът се чете много повече пъти, отколкото се пише: при всяка поправка, при всеки преглед, при всяко търсене на грешка.

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

static double totalWithDelivery(double subtotal, int deliveryType, boolean hasFreeDeliveryCoupon) {
double deliveryFee = 4.99;
if (subtotal >= 50) {
deliveryFee = 0;
}
if (deliveryType == 2) {
deliveryFee += 7;
}
if (hasFreeDeliveryCoupon) {
deliveryFee = 0;
}
return subtotal + deliveryFee;
}
System.out.printf("%.2f €%n", totalWithDelivery(40, 1, false));
System.out.printf("%.2f €%n", totalWithDelivery(40, 2, false));
System.out.printf("%.2f €%n", totalWithDelivery(60, 2, false));
System.out.printf("%.2f €%n", totalWithDelivery(40, 2, true));
Изход
44.99 €
51.99 €
67.00 €
40.00 €

Изходът е същият до последната цифра. Функцията вече се чете като правило: доставката е 4,99 €, безплатна е от 50 €, експресната е със 7 € отгоре, а купонът я прави безплатна.

Кои имена не помагат:

  • име, което казва как, а не какво: doubleValue, list1, tmp;
  • общо име, което пасва на всичко: data, info, value, process, handle, manager;
  • съкращение, което знае само авторът: calc, cnt, dlvFee.

Дължината следва обхвата. i в цикъл от три реда е наред. Параметър на функция, която се вика от десет места, заслужава пълно име.

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

А коментар над функцията, t: 1 = стандартна, 2 = експресна? Помага, докато някой не смени числата и не забрави коментара. Името не може да остарее отделно от кода, защото е част от кода. Кога коментарът помага и кога лъже, е темата на третия урок от модула.

Идеята

Магическо число → константа с име

Имената на променливите вече са на място, но в кода още стоят 4.99, 50, 7 и 2. Число без име в кода се казва магическо число: какво значи, знае само авторът, а когато трябва да се смени, го търсиш навсякъде.

Поправката е константа с име. Стойността се пише веднъж, на едно място, а навсякъде се ползва името:

static final double FREE_DELIVERY_FROM = 50;
static final double STANDARD_DELIVERY_FEE = 4.99;
static final double EXPRESS_SURCHARGE = 7;
static final int STANDARD = 1;
static final int EXPRESS = 2;
static final: полето е на класа (static) и не може да се присвои отново (final). Компилаторът спира всеки опит. Имената на константите са с главни букви и долна черта, UPPER_SNAKE_CASE.

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

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

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

final пази името, не обекта. static final List<String> ZONES не може да сочи друг списък, но в същия списък може да се добавя. За списък, който наистина не се сменя, има List.of(…).

Виж го

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

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

static final double FREE_DELIVERY_FROM = 50;
static final double STANDARD_DELIVERY_FEE = 4.99;
static final double EXPRESS_SURCHARGE = 7;
static final int STANDARD = 1;
static final int EXPRESS = 2;
static double totalWithDelivery(double subtotal, int deliveryType, boolean hasFreeDeliveryCoupon) {
double deliveryFee = STANDARD_DELIVERY_FEE;
if (subtotal >= FREE_DELIVERY_FROM) {
deliveryFee = 0;
}
if (deliveryType == EXPRESS) {
deliveryFee += EXPRESS_SURCHARGE;
}
if (hasFreeDeliveryCoupon) {
deliveryFee = 0;
}
return subtotal + deliveryFee;
}
System.out.printf("%.2f €%n", totalWithDelivery(40, STANDARD, false));
System.out.printf("%.2f €%n", totalWithDelivery(40, EXPRESS, false));
System.out.printf("%.2f €%n", totalWithDelivery(60, EXPRESS, false));
System.out.printf("%.2f €%n", totalWithDelivery(40, EXPRESS, true));
Изход
44.99 €
51.99 €
67.00 €
40.00 €

Виж как всяко число и всяка буква от първата версия получава смисъл, докато сметката върви:

static final double FREE_DELIVERY_FROM = 50;
static final double STANDARD_DELIVERY_FEE = 4.99;
static final double EXPRESS_SURCHARGE = 7;
static final int STANDARD = 1;
static final int EXPRESS = 2;
static double totalWithDelivery(double subtotal, int deliveryType, boolean hasFreeDeliveryCoupon) {
double deliveryFee = STANDARD_DELIVERY_FEE;
if (subtotal >= FREE_DELIVERY_FROM) {
deliveryFee = 0;
}
if (deliveryType == EXPRESS) {
deliveryFee += EXPRESS_SURCHARGE;
}
if (hasFreeDeliveryCoupon) {
deliveryFee = 0;
}
return subtotal + deliveryFee;
}
System.out.printf("%.2f €%n", totalWithDelivery(40, EXPRESS, false));
Стъпка по стъпка, с думи
  1. Старото извикване беше calc(40, 2, false). Сега казва какво смята, а 2 е EXPRESS.
  2. Параметрите: a, t, f стават subtotal, deliveryType, hasFreeDeliveryCoupon. Стойностите са същите.
  3. „d = 4.99“ става „deliveryFee = STANDARD_DELIVERY_FEE“. Същото 4.99, но вече знаеш какво е: таксата за стандартна доставка.
  4. 50 е FREE_DELIVERY_FROM: прагът за безплатна доставка. 40 е под него, значи доставката не е безплатна.
  5. „t == 2“ беше загадка. deliveryType == EXPRESS е изречение: експресната доставка добавя EXPRESS_SURCHARGE.
  6. „if (f)“ не казваше нищо. hasFreeDeliveryCoupon е въпрос, а отговорът е false: купон няма.
  7. Поръчката плюс доставката. Програмата печата 51.99 €.
  8. Назад и пак напред: същите стойности, сметка и изход като calc(40, 2, false). Сменени са само имената.

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

Пробвай

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

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

System.out.printf("%.2f €%n", totalWithDelivery(50, STANDARD, false));
System.out.printf("%.2f €%n", totalWithDelivery(49.90, EXPRESS, false));
Отговор

Пробвай

Попълни

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

static double totalWithDelivery(double subtotal, int deliveryType, boolean hasFreeDeliveryCoupon) {
    double 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 се чете като изречение:

double subtotal = 62.40;
boolean isPaid = true;
boolean qualifiesForFreeDelivery = subtotal >= FREE_DELIVERY_FROM;
if (isPaid && qualifiesForFreeDelivery) {
System.out.println("Изпращаме без такса за доставка.");
}
Изход
Изпращаме без такса за доставка.

Какво не работи:

  • 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 double vat(double price) {
return price * 1.20;
}
static void printInvoice(double net) {
double total = net + vat(net);
System.out.printf("Нето: %.2f €%n", net);
System.out.printf("Общо с ДДС: %.2f €%n", total);
}
printInvoice(50);
Изход
Нето: 50.00 €
Общо с ДДС: 110.00 €

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

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

Идеята

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

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

  • класове: PascalCase (DeliveryCalculator, Order);
  • методи, променливи и параметри: camelCase (totalWithDelivery, deliveryFee);
  • константи: UPPER_SNAKE_CASE (FREE_DELIVERY_FROM);
  • достъп до поле: getSubtotal(), а за булева стойност isPaid().
По-точно, за любопитните

Едно и също име на четирите езика:

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 double f(double w, int z) {
double p = 3.50;
if (w > 2) {
p += (w - 2) * 0.80;
}
if (z == 3) {
p *= 1.5;
}
return p;
}
System.out.printf("%.2f €%n", f(1.5, 1));
System.out.printf("%.2f €%n", f(5, 1));
System.out.printf("%.2f €%n", f(5, 3));
Изход
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 за достъпно обучение по програмиране ·