Single Level of Abstraction Principle


There are many well-known principles in software development, such as SOLID or Don’t Repeat Yourself. Such principles offer useful guidance for creating future-proof and human-readable, good quality software.

Here at Outfunnel, we’ve found that applying the Single Level of Abstraction (SLA) principle has contributed a lot to the increase in our code quality. It’s a principle that is quite easy to apply and it offers the benefit of making the code much easier to read and digest.

This article takes a look at the Single Level of Abstraction principle and how to apply it.

What is the Single Level of Abstraction Principle?

The principle was popularized by Uncle Bob in his book Clean Code.

The core logic of the principle is simple: when we write a function, the statements in that function need to be at the same level of abstraction.

Why do we need such a principle, after all? SLA aims to increase the readability of a function. If a function includes statements that are at different levels of abstractions, you need to jump through mental hoops in order to grasp the main point of the function.

The SLA Principle in Action

Let’s see a specific example of how the SLA principle proves useful.

Imagine we have a function that needs to fetch both the properties and contacts from HubSpot CRM:

const axios = require('axios');

const HUBSPOT_API_PROPERTIES_PATH = 'https://api.hubapi.com/crm/v3/properties/contacts';

const HUBSPOT_API_CONTACTS_PATH = 'https://api.hubapi.com/crm/v3/objects/contacts';

async function getProperties () {
    try {
        const response = await axios.get(HUBSPOT_API_PROPERTIES_PATH);

        return response.data.results;
    } catch (err) {
        const statusCode = err.response.status;

        if (statusCode === 500) {
            //retry
        } else if(statusCode === 429) {
            // retry after waiting for a timeout
        } else {
            throw err;
        }
    }
}

async function getNormalizedContacts() {
    const properties = await getProperties();

    const response = await axios.get(HUBSPOT_API_CONTACTS_PATH);

    const contacts = response.data.results;

    return normalizeContacts({contacts: contacts.data, properties});
}

In this example, the function getNormalizedContacts violates the SLA principle. How? The method can be split mentally into three different parts: the first part retrieves the contact properties, the second part retrieves the contacts, and the third part normalizes the responses.

On one hand, getting the properties is nicely separated into a function. On the other hand, obtaining the contacts is not abstracted away at all. Clearly, the second part of the function is at a lower abstraction level than the first one, because it involves making an HTTP request and parsing the response.

The first step of fixing getNormalizedContacts is relatively straightforward - we should just make fetching the contacts a separate function. Doing so means that the function is now nice and tidy - all it cares about is fetching both the properties and contacts and normalizing the responses. All this without knowing the exact details.

const axios = require('axios');

const HUBSPOT_API_PROPERTIES_PATH = 'https://api.hubapi.com/crm/v3/properties/contacts';

const HUBSPOT_API_CONTACTS_PATH = 'https://api.hubapi.com/crm/v3/objects/contacts';

async function getProperties () {
    try {
        const response = await axios.get(HUBSPOT_API_PROPERTIES_PATH);

        return response.data.results;
    } catch (err) {
        const statusCode = err.response.status;

        if (statusCode === 500) {
            //retry
        } else if(statusCode === 429) {
            // retry after waiting for a timeout
        } else {
            throw err;
        }
    }
}

async function getContacts () {
    try {
        const response = await axios.get(HUBSPOT_API_CONTACTS_PATH);

        return response.data.results;
    } catch (err) {
        const statusCode = err.response.status;

        if (statusCode === 500) {
            //retry
        } else if(statusCode === 429) {
            // retry after waiting for a timeout
        } else {
            throw err;
        }
    }
}

async function getNormalizedContacts() {
    const properties = await getProperties();
    const contacts = await getContacts();

    return normalizeContacts({contacts, properties});
}

However, both getProperties and getContacts now violate the SLA principle. The functions consist of making a request over the network, parsing the response, and handling the errors.

Once again, fixing both functions is straightforward - we just need to split the HTTP request, parsing and error handling into three different methods:

const axios = require('axios');

const HUBSPOT_API_PROPERTIES_PATH = 'https://api.hubapi.com/crm/v3/properties/contacts';

const HUBSPOT_API_CONTACTS_PATH = 'https://api.hubapi.com/crm/v3/objects/contacts';

const handleErrors = err => {
    const statusCode = err.response.status;

    if (statusCode === 500) {
        //retry
    } else if(statusCode === 429) {
        // retry after waiting for a timeout
    } else {
        throw err;
    }
}

const parseResponse = response => {
    return response.data.results;
}

async function getProperties () {
    try {
        const response = await axios.get(HUBSPOT_API_PROPERTIES_PATH);

        return parseResponse(response);
    } catch (err) {
        return handleErrors(err);
    }
}

async function getContacts () {
    try {
        const response = await axios.get(HUBSPOT_API_PROPERTIES_PATH);

        return response.data.results;
    } catch (err) {
        return handleErrors(err);
    }
}

async function getNormalizedContacts() {
    const properties = await getProperties();
    const contacts = await getContacts();

    return normalizeContacts({contacts, properties});
}

Finally, we’ve separated the HTTP request logic from the parsing and error handling.

Comparing the first and the last version of the code, we can see a great improvement in readability.

async function getNormalizedContacts() {
    const properties = await getProperties();

    const response = await axios.get(HUBSPOT_API_CONTACTS_PATH);

    const contacts = response.data.results;

    return normalizeContacts({contacts: contacts.data, properties});
}

If somebody happened to read the first version, it would take them quite a lot of effort to get the main goal of the function. Now, it’s significantly easier with the final version:

async function getNormalizedContacts() {
    const properties = await getProperties();
    const contacts = await getContacts();

    return normalizeContacts({contacts, properties});
}

Conclusion

Creating future-proof and readable software requires us to follow certain principles. Single Level of Abstraction (SLA) principle is one that helps us achieve the goal. The point of SLA is to ensure that all statements in a function are at the same level of abstraction.

By applying it at Outfunnel on a daily basis long term we’ve managed to increase readability and decrease the amount of code comments required.