The Importance Of Abstraction in JS


JavaScript is no longer what it used to be. The Web has evolved in an extraordinary way. Yes, you know what I’m talking about. You sense it too.

Every day new libraries are published, new ideas are introduced, and new API’s come into the world.

If you’re like me, and I believe you are, you always want your application to be ahead of its time.

The bigger your application, the more difficult it is to migrate to newer stuff. How can we always stay one step ahead? How can we minimize the difficulty of this transition?

Let me equip you with one important tip — abstraction.


Abstraction is a process whereby you hide implementation details from the developer and instead only provide the functionality


Put simply, the developer will have the information on what the object does, not how it does it.

Let’s examine two examples from our world.


#Example One — Lodash

For this example, we will take the library that most of us use — Lodash.


If you are using Lodash, I assume all your code is smeared with imports like the following:


import { fromIndex } from 'loadsh';
...
Lodash usage


The problem with the above code is that when we use it, we tie our application to Lodash. But what if, at some point, we want to change Lodash with something like Ramda, or even better, with the native JS API’s? This won’t be an easy task.

To overcome the problem, we can create an abstraction.


// array.utils.js

import { findIndex } from 'lodash';

export function findIndex(array, predicate) {

if ( Array.prototype.findIndex ) { return array.findIndex(predicate) }

return findIndex(array, predicate); }

Array utils

With this abstraction, we gain three important things:

  1. We have one place with our single source of truth.
  2. We can check for the support of native API’s and use them.
  3. We can swap the implementation without breaking anything.

import { findIndex } from 'ramda';

export function findIndex(array, predicate) {

if ( Array.prototype.findIndex ) { return array.findIndex(predicate) }

return findIndex(predicate, array); }

Array utils with Ramda


#Example Two— HTTP

Every application leverages a third-party HTTP library to deal with XHR requests. Let’s say you’re working with the axios library and are using it like this:


import axios from 'axios';

class TodosService {

get() { return axios.get('/todos'); } }

Todos service — axios

Again, our application is tied to a specific implementation. What if we want to use a different library or swap and use the native fetch API? What if, even worse, a new version of axios is released with breaking changes in the API? we won’t be covered.

To overcome this problem, we can create an abstraction that will encapsulate the real implementation.


import axios from 'axios';

class HTTP {

  get(url, options) { return axios.get(url, options);   }

... }

HTTP service

With this abstraction, we gain three important things:

  1. The implementation is in one place, allowing us to easily address breaking changes.
  2. We can swap a library without breaking anything.
  3. We can add things like logging, or global HTTP headers in one place. (if the library doesn’t support these)

import axios from 'axios';

class HTTP {

get(url, options) { // If dev mode console.log(HTTP GET - ${url} with ${options.params}) return axios.get(url, options); }

... }

Add logging

Summary

In this article we saw the problems we face when we tie our application to a specific implementation, and we learned how to solve these issues with abstractions. You do not have create abstractions for everything. Use your judgment to decide it’s necessary in your particular case.




0 comments