Names in code
In short: Good names in code say what a thing is and why it exists: constants instead of magic numbers, booleans phrased as questions and the words of the business instead of abbreviations.
Problem
The function nobody dares to touch
In the code of an online shop there is a function that every order goes through:
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.00It works. Now say what it does. What is t? Why does t == 2 add 7? What is f, and why does it reset everything? Why exactly 50?
The author knew. He left a year ago. Marketing wants free delivery from €60, not from €50. Your colleague searches the project for 50 and finds 14 places: in one, 50 is the delivery threshold; in another, it is the largest parcel weight in kilograms; in a third, it is the number of products on a page. Which one should change?
The logic here is right. The problem is that the code does not tell the person reading it what the author was thinking.
The idea
The name says what and why, not how
The computer reads Calc(a, t, f) with no trouble. A person can't. And code is read many more times than it is written: at every fix, at every review, every time someone looks for a bug.
A good name answers the questions you would otherwise ask the author: what is this, why is it here, how is it used. This is called an intention-revealing name. Here is the same function, with only the names changed:
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.00The output is the same down to the last digit. The function now reads like a rule: delivery costs €4.99, it is free from €50, express delivery costs €7 more, and the coupon makes it free.
Names that don't help:
- a name that says how, not what:
decimalValue,list1,tmp; - a general name that fits anything:
data,info,value,process,handle,manager; - an abbreviation only the author knows:
Calc,cnt,dlvFee.
The length follows the scope. i in a three-line loop is fine. A parameter of a function called from ten places deserves a full name.
More precisely, if you are curious
What about a comment above the function, t: 1 = standard, 2 = express? It helps until someone changes the numbers and forgets the comment. A name can't go out of date separately from the code, because it is part of the code. When a comment helps and when it lies is the topic of the third lesson in this module.
The idea
Magic number → named constant
The names of the variables are in place now, but 4.99, 50, 7 and 2 are still in the code. A number without a name in the code is called a magic number: only the author knows what it means, and when it has to change, you search for it everywhere.
The fix is a named constant. You write the value once, in one place, and use the name everywhere else:
const decimal FreeDeliveryFrom = 50m;const decimal StandardDeliveryFee = 4.99m;const decimal ExpressSurcharge = 7m;const int Standard = 1;const int Express = 2;const: the value is known at compile time and can't change. The compiler stops every attempt. In C#, constants use PascalCase (FreeDeliveryFrom), like methods and properties, not capital letters.Marketing wants €60: you change one line, and every place that asks "where does free delivery start" sees the new value. And the 50s that are weights stay untouched, because they are not FreeDeliveryFrom.
More precisely, if you are curious
Not every number is magic. 0 for "no fee", 1 in i + 1 and 100 in a percentage are clear from where they are. A number is magic when it carries a business rule (a threshold, a fee, a limit, a status code) or may change. A constant named after its value (FIFTY = 50) doesn't help: the name says the role, not the number.
const is only for values known at compile time: numbers, strings, bool. For an object you write static readonly: it is assigned once and doesn't change after that. One more difference: the value of a const is copied into the code that uses it. If another project uses your constant, it sees the new value only when it is compiled again.See it
The same calculation, read through the names
The names and the constants together:
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.00Watch every number and every letter from the first version get a meaning as the calculation goes:
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}");Step by step, in words
- The old call was Calc(40, 2, false). Now it says what it calculates, and 2 is Express.
- The parameters: a, t, f become subtotal, deliveryType, hasFreeDeliveryCoupon. The values are the same.
- "d = 4.99m" becomes "deliveryFee = StandardDeliveryFee". The same 4.99, but now you know what it is: the standard delivery fee.
- 50 is FreeDeliveryFrom: the threshold for free delivery. 40 is below it, so the delivery is not free.
- "t == 2" was a riddle. deliveryType == Express is a sentence: express delivery adds ExpressSurcharge.
- "if (f)" told you nothing. hasFreeDeliveryCoupon is a question, and the answer is false: no coupon.
- The order plus the delivery. The program prints €51.99.
- Back and forth: the same values, calculation and output as Calc(40, 2, false). Only the names changed.
All three versions print the same thing. The computer sees no difference. The difference is for the person who changes the rule tomorrow. One place still says nothing: false in the call. We get to it in the next lesson.
Try it
Predict the output
An order of exactly €50 with standard delivery, and an order of €49.90 with express delivery. Read the rule through the names. What does the program print?
Console.WriteLine($"€{TotalWithDelivery(50, Standard, false):F2}");Console.WriteLine($"€{TotalWithDelivery(49.90m, Express, false):F2}");The first one: subtotal >= FreeDeliveryFrom is true at exactly 50, so delivery is 0 and the total is €50.00. The second one is below the threshold: €4.99 for delivery and €7 for express, so 49.90 + 11.99 = €61.89.
Try it
Fill in the blank
The threshold for free delivery is a constant now. What goes in the blank?
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;
}The comparison uses the name of the constant, not the number. The threshold is in one place, and the line reads like the rule: "if the sum is at least the free delivery threshold".
The idea
A boolean is a question
A boolean answers yes or no, so its name is a question: isPaid, hasCoupon, canShip. Then the if reads like a sentence:
decimal subtotal = 62.40m;bool isPaid = true;bool qualifiesForFreeDelivery = subtotal >= FreeDeliveryFrom;
if (isPaid && qualifiesForFreeDelivery){ Console.WriteLine("We ship with no delivery fee.");}We ship with no delivery fee.What doesn't work:
flag,status,check,ok: they don't say what the question is;- a negative in the name:
isNotPaid. Thenif (!isNotPaid)is a double negative, and everyone trips over it. WriteisPaid, and put the negation in the check when you need it.
The same goes for the other names. A class and a variable are nouns (Order, deliveryFee). A function does something, so it is a verb (SendInvoice). A function named like a question (IsInStock) promises that it only asks. In the next lesson you will see what happens when it breaks that promise.
The idea
The words of the shop in the code
You don't invent the best names. They are the words of the people the program is for: parcel, courier, cash on delivery, refund, invoice. When the code says parcel, courier, refund, the programmer and the person in the warehouse talk about the same thing. These are domain names.
- One word for one concept. If the customer is
customer, it doesn't becomeclientin the next file andbuyerin the database. If reading isfind…, it doesn't becomeget…andfetch…elsewhere. A different word makes the reader look for a different thing. - One word doesn't mean two things. If
totalis the sum with delivery, the sum without it has its own name,subtotal. - Technical words are for technical things.
queue,cache,parserare fine when that is the thing itself.Manager,ProcessorandHelperusually mean "I don't know what exactly it does".
Try it
Find the bug
Accounting complains: an invoice for €50 without VAT comes out at €110. VAT is 20%, so it should be €60.
static decimal Vat(decimal price){ return price * 1.20m;}
static void PrintInvoice(decimal net){ decimal total = net + Vat(net); Console.WriteLine($"Net: €{net:F2}"); Console.WriteLine($"Total with VAT: €{total:F2}");}PrintInvoice(50);Net: €50.00Total with VAT: €110.00Which line is the bug on?
Pick the line with the bug.
The bug is the name. Vat promises the tax, but returns the price with the tax. The author of PrintInvoice trusted the name and added "the tax" to the price, so the price was counted twice. Each function on its own calculates exactly what is written in it. Only the name lies. The fix is a function whose name and body say the same thing:
const decimal VatRate = 0.20m;
static decimal VatAmount(decimal price){ return price * VatRate;}With it, the invoice is €60.00.
The idea
How your language writes names
A good name in one language looks odd in another. Each language has its own convention, and code that follows it reads faster, because the shape of the name tells you what it is.
- classes, methods, properties and constants:
PascalCase(Order,TotalWithDelivery,FreeDeliveryFrom); - local variables and parameters:
camelCase(deliveryFee,subtotal); - private fields:
_camelCase(_stock); - interfaces:
Iin front (IPaymentMethod); - the value of an object is read through a property:
order.Subtotal, not a methodGetSubtotal(). A property is a noun, and a method is a verb.
More precisely, if you are curious
The same name in the four languages:
| Java | C# | Python | JavaScript | |
|---|---|---|---|---|
| function | totalWithDelivery |
TotalWithDelivery |
total_with_delivery |
totalWithDelivery |
| variable | deliveryFee |
deliveryFee |
delivery_fee |
deliveryFee |
| constant | FREE_DELIVERY_FROM |
FreeDeliveryFrom |
FREE_DELIVERY_FROM |
FREE_DELIVERY_FROM |
| class | Order |
Order |
Order |
Order |
| boolean | isPaid |
isPaid (property: IsPaid) |
is_paid |
isPaid |
The idea
Rename: the cheapest refactoring
Changing a name without changing the behavior is called Rename. It is the smallest and cheapest refactoring: you don't change a single value or comparison, and the program prints the same thing.
Don't do it by hand with find and replace. The editor knows which d is your variable and which one is the letter in "delivery": in IntelliJ IDEA and PyCharm it is Shift+F6, in VS Code it is F2, in Visual Studio it is Ctrl+R, Ctrl+R. It changes every place at once.
How to refactor in small steps, so that the program works after each one, is the topic of the sixth lesson in this module. Rename is the first step.
Exercise
The courier
Solve the task in your editor.
The courier. This function calculates the price of a parcel:
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.85The rule is this: a parcel up to 2 kg costs €3.50, every kilogram above that is €0.80, and zone 3 is remote, so the price there is 50% higher. Give the function, the parameters and the variable names that say this, and turn the numbers into constants. The output must stay the same:
€3.50€5.90€8.85More tasks
- The boolean. Add a parameter that says whether the parcel is fragile (a €1.20 fee). What will you call it? What will the call look like?
- Your code. Open an old exercise of yours. Find three names you don't understand at first glance today, and change them with the editor's Rename. Run the program: is the output the same?
- The shop's dictionary. Write down ten words the person in the warehouse uses (parcel, courier, zone…) and how you would name them in the code of your language.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Methods · Parameters and return · static: what all objects share · Exceptions
- Continues in: Functions: one job, few parameters, no hidden side effects · Comments that lie and comments that help · Code smells: long method, large class, duplication, primitives instead of types, long if chains · Tests as a safety net
- Tasks on this topic: Clean code and refactoring