Seven little habits for writing better code

opinion
Sep 3, 20257 mins

Writing good code requires close attention to detail and lots of will power and discipline. Good habits are half the battle.

Bad habits: man sits smoking and drinking at a table...
Credit: Ground Picture/Shutterstock

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.

Nick Hodges

Nick has a BA in classical languages from Carleton College and an MS in information technology management from the Naval Postgraduate School. In his career, he has been a busboy, a cook, a caddie, a telemarketer (for which he apologizes), an office manager, a high school teacher, a naval intelligence officer, a software developer, a product manager, and a software development manager. In addition, he is a former Delphi Product Manager and Delphi R&D Team Manager and the author of Coding in Delphi. He is a passionate Minnesota sports fanโ€”especially the Timberwolvesโ€”as he grew up and went to college in the Land of 10,000 Lakes. He currently lives in West Chester, PA.

More from this author