5 Lessons I Learned in My First 2 Years of Programming

Things I didn’t find in any tutorial

Published on
May 20, 2021

Read time
7 min read

Introduction

Things I didn’t find in any tutorial

I have now been paid to write code for over two years. In that time, my views about programming have changed a lot.

For me, the most important lessons weren’t about learning the intricacies of a particular language or framework. Instead, my biggest mental shifts have been more about understanding programming in general, working with other people, and making something that can be maintained long after you stop working on it.

The examples in this article are all written in JavaScript, but I hope they’re simple enough that they’d be easy to understand no matter your background.

You’re writing code for people, not computers

When I started out, it seemed like coding was all about writing instructions for computers to interpret. Good code felt like code that got the job done. But consider the following example:

for (let i = 0; i < 100; )
  console.log((++i % 3 ? "" : "Fizz") + (i % 5 ? "" : "Buzz") || i);

You’ll probably recognise that this is an implementation of the classic FizzBuzz problem. It works, but it’s not immediately obvious how the code works. How about something like this?

const { log } = console;

for (let i = 0; i < 100; i++) {
  if (i % 15 === 0) log("FizzBuzz");
  else if (i % 3 === 0) log("Fizz");
  else if (i % 5 === 0) log("Buzz");
  else log(i);
}

The first example might be cleverer, but — to me — the second is much easier to read, understand and maintain. In most circumstances, our goal is the latter. Of course, this example is pretty contrived, but as the requirements of our code become more complex, the need for clarity only becomes more important.

Good code is, therefore, code written for other people. We have more in common with traditional writers than we might think!

People disagree about what makes code readable

Unfortunately, what constitutes readable code can be divisive.

In my current job, we use type conversion shorthands like + for a number and !! for a boolean. But in my previous work, I had learned to avoid these in favour of the Number and Boolean constructors. I have also witnessed debates about async/await versus then/catch (an issue I have strong opinions about)!

While a lot of style issues can be solved with your favourite code formatter, it’s important to know that code readability practices change depending on context. In general, we should aspire to write code that is readable by the other people that are working on it, based on their experience and preferences.

Here are a couple of more specific thoughts about code readability.

1. Cognitive complexity

One readability concept I have found useful is cognitive complexity. I encountered it using the code quality software Sonarcloud, and it attempts to measure how difficult a unit of code is to understand — including all possible paths.

Each new layer of nesting and each new possible path adds an increasing amount of cognitive complexity. This includes for and while loops, if statements, the ternary operator, and the optional chaining operator.

As a quick example, this isPrime function has a cognitive complexity of 4:

const isPrime = (num) => {
  // + 1
  if (num <= 1) {
    return false;
  }
  // + 1
  for (let i = 2; i < num; i++) {
    // + 2
    if (num % i === 0) {
      return false;
    }
  }
  return true;
};

It’s easy to encounter files that score extremely high on the cognitive complexity scale, with lots of nesting and lots of possible paths. Reducing this is one way to improve readability that your co-workers are likely to agree on but that won’t get picked up by your formatter.

2. Comments

A lot of educational content aimed at beginner programmers emphasises the importance of good comments. But in my experience, relying too heavily on comments is actually bad for maintainability.

There are two main reasons for this. First, comments can easily become displaced from the code that they explain. Over time, as a project changes, lines of code will inevitably move around. But because comments are not necessary for the code to run, it’s very easy to leave them in the wrong place. This can lead to more confusion, as the comment may end up next to code that it explains incorrectly.

The second, more important reason is that often, commented code could be rewritten so that what the code does can be easily understood from the code itself — mainly through better structure and better variable names.

Here’s a simple example:

const productCodes = [
  // t-shirts
  "c0c0c72b-cb60-4ff6-ac7c-623cc4497c9d",
  "169ce45b-03b9-4161-a9e3-aa319210c827",
  // hoodies
  "bd04d570-009f-4a4d-b8f5-58e2be0d44a2",
  "8067be8d-b302-457b-bd97-e297d3383118",
];

I have encountered something similar to the above in a production code base. Even though we need all the product codes in one array, we can still store them separately, allowing us to remove the comments:

const tshirts = [
  "c0c0c72b-cb60-4ff6-ac7c-623cc4497c9d",
  "169ce45b-03b9-4161-a9e3-aa319210c827",
];

const hoodies = [
  "bd04d570-009f-4a4d-b8f5-58e2be0d44a2",
  "8067be8d-b302-457b-bd97-e297d3383118",
];

const productCodes = [...tshirts, ...hoodies];

This simple change means the code can be understood without comments and if the structure changes, there’s much less risk that we might forget which ids apply to t-shirts and which apply to hoodies.

Programming is all about data

To beginner programmers, the idea of making software can feel very mysterious. It’s easy to imagine the final outcome but much harder to understand how to get there. One of my earliest ‘aha!’ moments was that programming is all about data: fetching it, modifying it, displaying it, and sending it somewhere else.

Of course, boiling it down to these essentials doesn’t mean it is always simple. In essence, making a plane fly is all about lift, drag, thrust, and weight. I know that, but I wouldn’t be much use designing an aircraft!

The challenge of complex software is dealing with a lot of data from a lot of different places that needs to be modified efficiently and at scale — and displaying data brings its own variety of challenges.

That said, looking at programming as a journey undergone by data has helped me break down more complex problems. You’re well on your way to a solution once you can identify what data you need, how to get it, how to change it into the desired form, and where to display it or send it.

Performance doesn’t matter (except when it does)

In an ideal world, we’d have time to make sure all our code ran as quickly as possible. Often however, time spent fine-tuning performance could be better spent doing something else: in business, fixing bugs or creating new features will generally be more valuable than shaving a few hundred milliseconds off a particular function or code block.

There are exceptions to this rule, of course. Some functionality needs to be extremely reliable or is extremely resource-intensive, so every saved millisecond matters. Performance also becomes much more important with scale. If you can save a million users 0.1 seconds of loading time, then if everyone loads once, that equates to 28 hours of overall time saved. Sadly, few of us are working with numbers like this.

Part of the dilemma with performant code is that it can fly in the face of our more important goals: readability and maintainability. Highly optimised code often can be ugly. In my specialism — web development — it can sometimes involve fighting against libraries or frameworks, such as providing escape hatches from React to use regular JavaScript.

It’s easy to bump into problems resulting from premature optimisation. Using React as an example, there are cases when using useMemo or useCallback in fact reduces performance when compared with regular variables and functions.

I have also got into sticky situations by using Promise.all too freely. Take the below function (based roughly on some genuine code I wrote):

async function purgeTestData() {
  await Promise.all([
    makeLotsOfDatabaseChanges(),
    makeLotsOfDifferentDatabaseChanges(),
    doSomethingElseWhichAlsoAffectsTheDatabase(),
  ]);
}

Though this was quicker than performing asynchronous functions one by one, I introduced some unreliable outcomes that slower code avoided. I might have been able to rework the functions so that they could run concurrently, but the time taken to make the three functions run smoothly in parallel wasn’t worth the time saved. So in this situation, it was better to do:

async function purgeTestData() {
  await makeLotsOfDatabaseChanges();
  await makeLotsOfDifferentDatabaseChanges();
  await doSomethingElseWhichAlsoAffectsTheDatabase();
}

Often, the highest level of performance requires the highest level of control. If we wanted to squeeze the maximum theoretical performance out of a website, for example, we’d probably be better off without any libraries or frameworks. Or perhaps we should use Web Assembly to code the entire thing in C++? There’s a good reason why large, complex projects rarely opt for such an approach: It’s too time-consuming to implement and too time-consuming to maintain.

A lot of UI is smoke and mirrors

Finally, a note on the visual side of programming. Something I didn’t fully appreciate as a beginner was that good UI is often about trickery — making things seem faster or more interesting than they normally would.

Take progress bars. Depending on the task, how long it takes can be very difficult to estimate precisely. That’s why we’re so used to not taking estimates of time left too seriously. But what if we didn’t have them? Without a sign telling users that something is happening, people would get bored, they might close their window or restart their computer. So we give them our best guess to help keep them engaged and stop them from doing anything to interrupt our software.

Other examples are placeholder graphics used while we’re fetching data. Or changing state on the client side before that state change has been sent to the API. When you dismiss a notification on a social media site, it looks like the notification has been marked as read immediately. But that’s the client showing you the change before it’s actually hit the server and been stored in the database. A little bit of smoke and mirrors makes the user’s experience feel fast and slick.

© 2024 Bret Cameron