General Programming Patterns

Overview

Okay. One of the core tenets of Galvanizer is a strong methodology around how we write code. The purpose of opinionated programming is to establish a consistent set of tools that everyone on the team can use to tackle issues.

The trickiest part of moving from programming as an individual to programming as a team is the coordination that needs to occur between programmers. If we are to be able to successfully scale, we need to adopt a consistent design language even in the way we write code.

If we all work to solve core problems in the same way, we greatly increase our capacity as a team.

Overarching Principles

Separate your concerns.

Functions should aim to accomplish only one thing. Classes should group functions together to accomplish a single common objective. Code files should concern themselves with only a single objective. Application modules should be dedicated to solving only one specific problem or handling a specific data type. Applications should be focused on a single concern.

Your frontend application should not be concerned with data processing.  Your backend application should not be concerned with presentation.

If your applications were perfectly engineered, the frontend would have zero concern for anything other than displaying data, and the backend would have zero concern for anything other than handling and serving that data.

Keep code readable.

Good code isn’t just performant. People have to be able to read it as well — if that’s other people on your team, or yourself 3 months from now.

Good code should follow in a consistent logic and should be readable from top to bottom without needing to jump around.

Programming is already a complex activity. Don’t make people think more than they have to.

Keep code flat.

The less you have to nest functions, the better. Typescript has a number of features that make it easy to keep your code flat. Here’s a few that I recommend:

Return Early

It is common for a logic flow to be built of a series of nesting if statements, but this can make code hard to read:

function doSomething(foo, bar, baz) {
  let value = "hey"
 
  if (foo) {
    if (bar) {
      value = "new value"
    }
  } else if (baz) {
    value = "baz"
  }
 
  return value
}

How long does it take for you to parse what this function does? A better implementation could look like this:

function doSomething(foo, bar, baz) {
  if (!foo && baz) return "baz"
  if (foo && bar) return "new value"
 
  return "hey"
}

Both examples produce the same results, but it is much easier to track the logic flow in the second example.

Immediately Invoked Function Expressions (IIFEs)

Sometimes you can’t return early because the computation is a smaller part of a larger logic flow. to get around this, you can create an anonymous function and then immediately call it, thus setting the value of your variable to that of the computed expression.

Say you want to set a variable to something that cannot be computed simply. You have a few options:

Option 1: Suck it up.

The first thing you could do is say “Screw it. I’m computing this in one line”. An example is using nested ternary operators to compute a value for future use in your function:

function impossibleToUnderstand(foo: number, bar: number, baz: number) {
  const value = foo ? bar + baz : bar ? foo - baz : baz ? foo + bar : 0
 
  console.log("I am handing in my resignation.")
  console.log(value)
 
  return value
}

Option 2: Multi-line variable computation

Giving up on the whole “computing in one statement concept”, you could split your function into multiple variable declarations and if/else statements:

function betterThanThatOtherThing(foo: number, bar: number, baz: number) {
  let value = 0
 
  if (foo) {
    value = bar + baz
  } else if (bar) {
    value = foo - baz
  } else if (baz) {
    value = foo + bar
  }
 
  console.log("Just another day on the job.")
  console.log(value)
 
  return value
}

Each individual declaration is nice and simple, and this is how most people are used to programming. The problem with this approach is that as it scales, your code gets less and less flat and thus harder to parse. Once this approach reaches scale, your code can become very hard to read — especially when you have multiple of these in a single file.

Option 3: IIFEs

Given the fact that our variable requires some complex computation, we don’t want to handle it in one line. However, we still want to take advantage of early returns to keep our code flat and focused on individual tasks. This is where IIFEs can be very useful:

function probablyTheBest(foo: number, bar: number, baz: number) {
  const value: number = (() => {
    if (foo) return bar + baz
    if (bar) return foo - baz
    if (baz) return foo + bar
    return 0
  })()
 
  console.log("We can return early and it's still clear what's happening.")
  console.log(value)
 
  return value
}

All the logic for computing the variable is self contained, thus reinforcing our separation of concerns, but at the same time, you can actually read what’s going on. Our value variable is a const instead of let, which more clearly communicates that it’s not going to change.

Make your code document itself.

Code can look arbitrary. Take advantage of the fact that you don’t need to pre-compute things to the computer’s level, and can leave things in a format easier for people to read:

Bad example

function wait() {
  return new Promise((resolve) => setTimeout(resolve, 345600000)) // 4 days
}

The end time we’re waiting for is an arbitrary number to a person. What if you want to change it to 5 days? Or 3? You would need to get your calculator out. Instead, you should delegate all computation to the computer. That’s literally the computer’s job.

Better Example

function wait4Days() {
  return new Promise((resolve) => setTimeout(resolve, 1000 * 60 * 60 * 24 * 4))
}

Now, the code documents itself. If you want to change the amount of time the function waits for, just change the last number. The problem with this, however, is it is more difficult for your eyes to scan left to right than it is to scan up and down. Here’s how I would do it:

function wait4Days() {
  const FOUR_DAYS = 1000 * 60 * 60 * 24 * 4
 
  return new Promise((resolve) => setTimeout(resolve, FOUR_DAYS))
}

The more you can make your code read like english, the better.

“An idiot admires complexity, a genius admires simplicity” - Terry Davis