DEV Community

loading...

The Single Responsibility Principal

Maximilian
Software engineer based in London & Winchester. I write about tech, code and my journey through the tech industry.
・3 min read

One of my favourite software design patterns is the Single Responsibility Principal. By default, it keeps code readable, maintainable, testable, predictable and efficient.

The fundemental problem with code is human involvement. A computer will just do what it's told. The solution, therefore, has to be code that is easily readable by people. Every developer or engineer will have their own opinions on what this means to them (and you'll probably see/hear the term DRY a lot), but fundementally, I think it all comes down to this pattern, which is applicable to all languages, all frameworks and all types of software.

What you on about?

The single responsibility principal is simple: every file, every function and every line of code does one thing (a Mandelbrot Set of single responsibilities, if you will). For example, let's say we need to create a function that gets a particular group of users from a database, checks how many tokens they have, divides their tokens by 3 (n), sends a request to an API that will plant (n) trees in their name, then remove their tokens and update their planted tree count in the database. (We'll ignore the scaling issues of multiple API queries and a plethora of other flaws in this example!)

async function plantTreesForTeamCarbon() {
    try {
        const users = db.query(`
            SELECT * FROM users
            WHERE team = 'carbon';
        `;)

        for(user of users) {
            if (user.tokens >= 3) {
                    const res = await fetch('https://planttreesinmynameyoufilthyanimal.com', {
                        method: 'post',
                        body:    JSON.stringify({
                            username: user.username,
                            numberOfTrees: Math.floor(user.tokens / 3),
                        }),
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': process.env.API_KEY,
                        },
                    });

                    db.query(`
                        UPDATE users
                        SET
                            tokens = ${user.tokens % 3},
                            trees_planted = ${user.trees_planted + (Math.floor(user.tokens / 3))}
                        WHERE user_id = ${user.user_id};
                    `;);
            }
        }
    } catch (err) {
        console.error(err);
    }
}
Enter fullscreen mode Exit fullscreen mode

Isn't that horrible?

Now although this isn't a particularly large function, it's already tricky to read through and work out what's going on, and that's without talking about how poorly it would perform or how difficult it would be to unit test. If you multiply this way of writing code by an entire application, it becomes a nightmare. So let's now refactor this using the single responsibility principal in an effort to make the code easier to read, easier to maintain, more dynamic and so much more easily testable:

async function plantTreesForTeam(team) {
    const users = await getUsersFromTeam(team);

    for (user of users) {
        const { treesToPlant, remainingTokens } = calculateNumberOfTrees(
            user.tokens
        );
        if (!treesToPlant) continue;

        const plantTrees = await plantTreesForUser(user.user_id, treesToPlant);

        await Promise.all([
            db.updateUserTokens(user.user_id, user.tokens),
            db.updateUserTrees(user.user_id, remainingTokens),
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

With the single responsibility principal applied, we have abstracted a lot of code away from this function and turned it into a 'controller function'. If someone came to this code fresh and needed to know exactly what the logic was behind the getUsersFromTeam() function, for example, they can click through to view it, but the important part is they don't have to in order to understand what it does. It's very clear just from the name and context what it does, and that's the point! So from the top down, we can see how each line / function has a single purpose and doesn't care about anything outside of it:

  1. plantTreesForTeam() plants trees for a team.
  2. getUsersFromTeam() gets users from a team.
  3. calculateNumberOfTrees() calculates the number of trees to plant based on token quantity.
  4. if (!treesToPlant) continue; breaks the loop if there are no trees to plant.
  5. plantTreesForUser() plants trees for a single user.
  6. Then we have a Promise.all() that updates our database asynchronously, but inside that, db.updateUserTokens() updates a user's token count, and db.updateUserTrees() updates a user's planted tree count.

Each function is dynamic, reusable, easily unit testable and easily error handleable. We can take this one step further in Javascript or Typescript and have each function return an [err, success] array to make error handling even simpler, but I'll save that design pattern for another post.

Conclusion

With people being the weak link in the chain of software development, it's fair to argue that the most important way we write code is in a human-readable and maintainable format. The single responsibility principal is a great and simple way of achieving this as it forces you to think in a way that simplifies the architecture of your application. Arguably, it can lead to more lines of code and longer names, but it also removes the need for commenting in most cases, makes testing and error handling simple and ultimately, makes for a cleaner code base.

Comment below!

Have you used this design pattern before? What do you think of it? Do you have any design patterns you prefer to follow that achieve similar results? Let me know in the comments below!

NB: Thanks to Josh for showing me this pattern!

Discussion (0)