Skip to content

Instantly share code, notes, and snippets.

@rikukissa
Last active September 19, 2019 07:04
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rikukissa/0dd8c7df3681c139c0f92cee3a0d5466 to your computer and use it in GitHub Desktop.
Save rikukissa/0dd8c7df3681c139c0f92cee3a0d5466 to your computer and use it in GitHub Desktop.
Collection of code simplification and refactoring tips #best practices #refactoring #code review

logo


Collection of code simplification and refactoring tips

Want to write clean code? Well here's your chance. The following set of examples are practices that I've come across during my times as a programmer and that I believe in. At least at the moment. Please feel free to add comments, criticise my thinking and contribute to everyone's knowledge. I'm trying to find a better platform for these, but for now a gist should do. I'm doing this solely to document, analyse and share my own thinking, which oftentimes is quite unstructured. These days I'm not too fussed about the syntactical structures of the code, but would rather concentrate on painting the largest guidelines and deciding the direction I want my programming style to move towards.

Contents:

Further reading:

@rikukissa
Copy link
Author

Oh, thanks a lot for your feedback and for taking time to read through some of it :)

@rikukissa
Copy link
Author

rikukissa commented Oct 4, 2018

How many levels of module dependency hierarchy is okay?

๐Ÿ‘ Avoid creating dependency hierarchies deeper than 4 levels. Aim for 2-3.

Few times during my career I've come across projects with +10 levels of dependant modules. Most of them have been Java projects.
The issue is, code like that is extremely tiring to follow. I remember writing down the path I had taken whilst reading the code just so I won't forget how I ended up to the current module I'm reading. Not good.

I feel like the best way to approach this would be to try and describe the whole process from start to finish on the main level:


function getCake() {
  oven = getPreheatOven()
  pan = getGreasedPan()
  mixture = mixIngredients(getEggs(), getFlour())
  putMixtureIntoPan(pan, mixture)
  return bakeForMinutes(40, oven, pan)
}

As the alternative (bad) approach I see functions that instead of being written to flow from top to bottom,
are written to flow deeper and deeper the module tree

function getCake() {
  return getBakedCake(40)
}
function getBakedCake(minutes) {
  oven = getPreheatOven()
  pan = getPanWithMixture()
  return bakeForMinutes(minutes, oven, pan)
}
function getPanWithMixture() {
  pan = getGreasedPan()
  mixture = mixIngredients(getEggs(), getFlour())
  return putMixtureIntoPan(pan, mixture)
}

... and so on. The reader would have to go through 3 different files just to know what ingredients are used.

@rikukissa
Copy link
Author

rikukissa commented Oct 10, 2018

How to solve cyclic dependencies between modules

AKA "why am I getting undefined when I import something from my module




Hey! A classic situation with a very generic and bad example (TODO). You've written few modules and soon notice that they depend on each other for one reason or another. Albeit being a **very strong indicator of bad design**, this is a common situation and one that's easy to solve:



user.ts

import { calculatePoints } from './points'
  
interface User {
   createdAt: number; // in milliseconds
   level: number;
}

export function updateUserPoints(user) {
  return {
    ...user,
    points: calculatePoints(user) // dependency to points.ts
  };
}

export function getUserRegisteredInYears(user) {
  return Math.floor((Date.now() - user.age) / 1000 / 3600 / 24 / 365); // Please, don't to this in real life
}

points.ts

import getUserAge from './user'
  
export function calculatePoints(user) {
  return getUserRegisteredInYears(user) * 3; // dependency to user.ts
}

There are few reasons why this happened in the first place:

  • Responsibilities of each module are too loosely defined
  • There are more concepts in play that you first imagined

And few solutions:

Responsibilities of each module are too loosely defined

Is there really a need for a point module? If not, the user module could look like this:

interface User {
   createdAt: number; // in milliseconds
   level: number;
}

function calculatePoints(user) {
  return getUserRegisteredInYears(user) * 3;
}

export function updateUserPoints(user) {
  return {
    ...user,
    points: calculatePoints(user)
  };
}

export function getUserRegisteredInYears(user) {
  return getYears(Date.now() - user.age);
}

However, if you answered "yes", could it be that the points module knows a bit too much about the internals of the user module? Could we, for example, turn the signature of calculatePoints into being:

- export function calculatePoints(user) {
+ export function calculatePoints(userAgeInYears) {

which would simplify the function to:

export function calculatePoints(userAgeInYears) {
  return userAgeInYears * 3;
}

There are more concepts in play from what you first imagined

In this very simple example, the only thing that the getUserRegisteredInYears function really provides us with is the conversion from milliseconds since the creation of the user to years. Could we add another module called time.ts? That way we could drop the getUserRegisteredInYears function altogether and refer to this new module from our points module:

points.ts

import getYears from './time'
  
export function calculatePoints(user) {
  return getYears(Date.now() - user.createdAt) * 3;
}

@rikukissa
Copy link
Author

rikukissa commented Oct 10, 2018

Stop repeating yourself!

You repeating yourself

๐Ÿ‘Ž

interface User {
 userCreatedAt: number;
 userPoints: number;
}

๐Ÿ‘

interface User {
 createdAt: number;
 points: number;
}

Repeating the word user creates extra fuzz around the words that are actually relevant, without adding any value. The reader should be able to infer the context even when it's few lines above.


๐Ÿ‘Ž

it('should add two numbers together', ...)
it('should fail if divider is zero', ...)

๐Ÿ‘

it('adds two numbers together', ...)
it('fails if divider is zero', ...)

By rephrasing the test case description, we can keep it as descriptive while making it more concise. Someone could even argue, that a test not only verifies that your code should do something, but that it actually does the thing it was written for.

Don't add unneeded context

@rikukissa
Copy link
Author

rikukissa commented Oct 10, 2018

Always name your constants

Got this idea while writing the bit about cyclic dependencies. You might have come across time calculation like this before:

// Returns true if token is older than 3 days
export function hasTokenExpired(token) {
  return (Date.now - token.createdAt) > 86400000 * 3;
}

The comment is pretty spot on, but we can do better:

export function hasTokenExpired(token) {
  const day = 24 * 3600 * 1000;
  return (Date.now - token.createdAt) > day * 3;
}

Not sure if you disagree, but to me it's much easier to read that something's greater than day * 3 (probably means 3 days) than that something's greater than 86400000 (possibly the mass of the Earth)

Great quote from clean-code-javascript:

We will read more code than we will ever write. It's important that the code we do write is readable and searchable. By not naming variables that end up being meaningful for understanding our program, we hurt our readers. Make your names searchable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment