DEV Community

Pan Chasinga
Pan Chasinga

Posted on

How I Refactor My Code

constructor guy

Refactoring code is very fundamental to any developer’s work. Yet I have come across relatively few resources that talk in-depth about this.

This blog post happened after this morning when I refactored my JavaScript code. It lasted just less than thirty minutes, but had me excited enough to return to writing here on Medium.

Let’s begin our story of the great refactor!

First, I had these two fetch functions littered everywhere in my codebase with slightly different names that I wanted to refactor into a single module of reusable functions. Here are just two of them:

async function postLoginData(data) {
  const loginUrl = `${apiBaseUrl}/login`;
  let response = await fetch(loginUrl, {
    method: "POST",
    mode: "cors",
    cache: "no-cache",
    credentials: "same-origin",
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
    redirect: "follow",
    referrer: "no-referrer",
    body: JSON.stringify(data),
  });
  return response;
}

// Get the user's data based on user id.
async function getUser(userId) {
  const userUrl = `${apiBaseUrl}/users/${userId}`;
  let response = await fetch(userUrl, {
    method: "GET",
    mode: "cors",
    cache: "no-cache",
    credentials: "same-origin",
    headers: {
      "Content-Type": "application/json; charset=utf-8",
    },
    redirect: "follow",
    referrer: "no-referrer",
  });
  return response;
}
Enter fullscreen mode Exit fullscreen mode

I’m not an extreme DRY advocate, but this felt cumbersome. Each function does very little from what could be achieved with just fetch over which it wraps. Apart from the encapsulating the endpoint URLs and the method property, these two look exactly alike and should be made reusable throughout the codebase.

Function should be pure when possible

My first and foremost criteria for a function is it should be refactored to be pure when possible. Purity means reusablity. If it needs to change any shared state, it might be a candidate for a method. This keeps functions easy to test and reusable. Functions with name like postLoginData violates this. Here are a few ways to refactor it without thinking about the implementation:

  • user.login()
  • login(user)
  • post(loginUrl, user)

The above list was ordered from least generality to highly reusable. Actually, the first two share the same level of generality. Only the last one is reusable, and that’s what I was going for.

Now, you could see how my two functions are quite offending. Sometimes, you wear different hats and prioritize different things. It’s okay to rush through to get something working as long as we occasionally clean up things.

How to Justify a Refactor

To decide if something should be refactored, I think of the intent and the worthiness of creating a function for it.

For example, a function which “POST” and another which “GET” data have fundamentally different intents, regardless of only a small difference in the implementation. The intentions are clearly distinguished enough to justify creating two functions.

However, wrapping an arbitrary URL inside a function, for instance, a login API endpoint, and then naming a function postLoginData does not add much value to a function, considering its decreased generality. The URL, apart from being a one-liner string, should be a “story” of the caller. Consider an artist with oil paints, a palette, and brushes. What the artist wants to paint should be the artist’s story. The palette and collections of paints and brushes should provide variants to support the subject. Can you imagine a set of paints for painting oceanic scenes? That is sensible. Now how about one for painting a ship. Not so easy. The subject is just to specific to be encapsulated.

Without further ado, here is the first refactor attempt:

const baseConfig = {
  mode: "cors",
  cache: "no-cache",
  credentials: "same-origin",
  headers: {
    "Content-Type": "application/json; charset=utf-8", 
  },
  redirect: "follow",
  referrer: "no-referrer",
};

// Configurable POST with predefined config
async function post(uri, data, config = {}) {
  config = Object.assign({
    method: "POST",
    body: JSON.stringify(data),
    ...baseConfig,
  }, config);
  return await fetch(uri, config)
}

// Configurable GET with predefined config
async function get(uri, config = {}) {
  config = Object.assign({
    method: "GET",
    ...baseConfig,
  }, config);
  return await fetch(uri, config);
}

export {get, post};
Enter fullscreen mode Exit fullscreen mode

Now this looks much cleaner with the repeated configuration object properties refactored into a constant baseConfig. Also, I added an optional parameterconfig to each function to make it configurable from outside. Object.assign is used to merged the custom config with the baseConfig (you could use spread operator too).

We can also see the object spreading in action. At this point, I was pretty pleased, but with spare time I decided to see if I could pull off something more. Here’s the final attempt:

const baseConfig = {
  mode: "cors",
  cache: "no-cache",
  credentials: "same-origin",
  headers: {
    "Content-Type": "application/json; charset=utf-8",
  },
  redirect: "follow",
  referrer: "no-referrer",
};

const send = (method, payload) => (
  async function(uri, config) {
    // Create an array of source config objects to be merged.
    let sources = [config];
    if (method === "POST") {
      sources.push({ body: JSON.stringify(payload) });
    }
    config = Object.assign({
      method: method,
      ...baseConfig,
    }, ...sources);

    return await fetch(uri, config);
  }
);

const get = (uri, config = {}) => (
  send("GET")(uri, config)
);


const post = (uri, data, config = {}) => (
  send("POST", data)(uri, config)
);

export {get, post};
Enter fullscreen mode Exit fullscreen mode

I personally like this version best because the get and post functions are very thin wrappers over the newly created send function (which isn’t exported because I wanted to keep it private). This makes the latter the single point of debugging if bugs persist later on, which they will.

Refactoring is a tricky business, not because it’s hard, but because it takes deeper design thinking and there is no absolute right or wrong. Make no mistake that you won’t get it right for everyone. Refactoring code to be reusable can surprisingly turn some people off, especially when the tradeoffs are much greater than the gain. Therefore balance is something to strive for. There are other factors for instance naming conventions and function parameters which can help with accessibility and should always be though hard about. However, ultimately, keep in mind that you should refactor for yourself first, since you are more likely to interact with the code you write.

Originally posted here

Top comments (0)