Writing good code requires close attention to detail and lots of will power and discipline. Good habits are half the battle.
Last week, I talked about the relationship between polishing forks and writing good code, and how deep attention to detail is the key to true greatness as a developer.ย Writing that column set me to thinking about how, exactly, one โpolishesโ code to make it worthy of a Michelin star.ย
So, here are seven little habits you can adopt that will set your code apart from the merely good code and make it truly great.
Prefer enumerations over constants
Make meaning explicit, not implied.
You should always prefer enumerations over constant values.ย Not only are enumerations compiler-friendly, but they are more human-readable as well.ย For instance, get a load of this code:
function calculateDiscount(customerId: number): number {
// Look up loyalty tier somehow...
if (customerId === 100) {
return 1;
} else if (customerId === 200) {
return 2;
} else {
return 3; }
}
Itโs not at all clear from the above what 1, 2, and 3 mean. So you think, โOkay, Iโll give them some values,โ and you do something like this:
const DISCOUNT_GOLD = 1;
const DISCOUNT_SILVER = 2;
const DISCOUNT_BRONZE = 3;
function calculateDiscount(customerId: number): number {
if (customerId === 100) {
return DISCOUNT_GOLD;
} else if (customerId === 200) {
return DISCOUNT_SILVER;
} else {
return DISCOUNT_BRONZE;
}
}
Now, Iโll grant that this is better. However, this code still can lead to confusion because the function could be altered to return 45 without defining what 45 means.ย
But if you define an enumeration, then the function must return an enumeration, and thus it becomes impossible to be ambiguous. This code leaves no doubt:
enum DiscountTier {
Gold,
Silver,
Bronze,
}
function calculateDiscount(customerId: number): DiscountTier {
if (customerId === 100) {
return DiscountTier.Gold;
} else if (customerId === 200) {
return DiscountTier.Silver;
} else {
return DiscountTier.Bronze;
}
}
Always use explaining variables. Always.
If a name can save the reader a step, give it.
This particular technique is a pet peeve of mine:
function logPurchase(userId: number, itemId: number): void {
console.log(`User ${userId} purchased item ${itemId}`);
}
logPurchase(getUserId(), getItemId());
Notice how the logPurchase call is made with two function calls as parameters. Argh.ย What are the values of the parameters?ย You have to step into each function to know before logPurchase is even called.ย This is very unfriendly to the developer who has to read this code a year later.ย
Always use an explaining variable. Always make it easy for the future maintainer to step through the code line by line:
const userId = getUserId();
const itemId = getItemId();
logPurchase(userId, itemId);
This is vastly more maintainer and debugger-friendly.ย Remember, that maintainer might very well be you.
Write complete and verbose error messages
An error that explains itself is half fixed.
Nothing is more frustrating than getting an error message like this:
List index out of bounds
Thatโs it. Thatโs the message.ย Which list?ย What index value?ย Who knows?
Given how thorough and reflective code can be these days, how about returning this error message instead:
The list named 'menuItemModifiers' attempted to retrieve index 43, but there are only 42 items in the list
Thatโs vastly more helpful, and not that much more work to write in code.
Donโt be afraid to over-log
Logs are cheap; log insights are priceless.
Along the same lines, donโt be afraid to โoverdoโ your log entries. A complete and detailed log file is vastly more valuable than one that is thin and sparse.ย Log reading tools are designed to slice, dice, and filter, and so donโt be afraid to use logging levels, complete and data-rich log entries, and verbose explanations of what is going on.
If you open it, close it.
Build the habit before you build the logic.
If you create or open something, immediately write the code to destroy or close that thing.ย Sometimes, if you are in a hurry, you might forget. For example:
function readFirstLine(filePath: string): string {
const file = fs.openSync(filePath, "r"); // File handle opened
const buffer = Buffer.alloc(100);
fs.readSync(file, buffer, 0, 100, 0);
// Oops: no close!
// The file handle stays open until the process ends.
return buffer.toString().split("\n")[0];
}
Yeah, thatโs not good.ย Instead, when you write the openSync call, immediately write the tryโฆ finally clause. Immediately.
function readFirstLine(filePath: string): string {
const file = fs.openSync(filePath, "r");
try {
// don't put anything here until the finally clause is written
} finally {
// Always close what you open
fs.closeSync(file);
}
}
Itโs a great habit to get into. If you allocate it, always de-allocate it right away.ย If you write a while loop, make sure there is a way to break out of it before you write any logic.ย Build up the muscle memory for this one.
Donโt store what you can calculate
Derived and stored data ages badly.
Only store raw data, and donโt store data that is calculated.ย For instance, the TotalPrice field below is totally redundant:
CREATE TABLE Orders (
OrderID INT PRIMARY KEY,
Quantity INT NOT NULL,
UnitPrice DECIMAL(10,2) NOT NULL,
TotalPrice DECIMAL(10,2) NOT NULL -- โ Calculated field stored
);
Nothing makes me crazier than a stored field in a database called TotalLessFeesAndTax.ย Store the items, fees, and tax values, and then calculate the rest.ย That way, if one value changes, the computed values never get stale.
Minimize mutability
What canโt be changed wonโt lie to you later.
If something can be made immutable, then make it immutable.ย Few things make for more tedious debugging journeys than a change to something that no one expects to change and that never should be changed in the first place.ย Making that thing impossible to change will nip the problem in the bud.ย
Consider this code:
function moveRight(p: Point, distance: number): void {
// Mutates the original object
p.x += distance;
}
const start: Point = { x: 0, y: 0 };
newPoint = moveRight(start, 5);
The caller of moveRight probably didnโt expect start to change, nor should they.ย If you code defensively:
interface Point {
readonly x: number;
readonly y: number;
}
Now the compiler will find what would otherwise be a very subtle, hard-to-find bug that could drive you crazy later.
Writing good code takes a lot of attention to detail and a lot of will power and discipline.ย Since those traits are in short supply in all of us, a good developer builds good habits and personal coding systems that lead to great code.ย
Build these seven habits, and future you will thank you.


