Both backend programming and frontend programming require some level of aesthetics. Look at the beautiful examples and develop an urge to seek the ugly places in the codebase and improve them. Not always you’ll have the time or power to do so, but that should not propel you into indifference.
Rules for Young Programmers, XXXI
Do you know what it means for code to be clear? How to repair chaotic code and avoid writing misunderstood code in the future?
The challenge of transforming chaotic code into something more manageable and preventing the creation of misunderstood lines of code is a huge topic that deserves our attention.
It might not be obvious when we write the code ourselves. However, while reading the code, we may find ourselves in a situation where we feel that we are lost in the complexity of functions and variables. The next step is to doubt our ability to understand the code and consider steps we can take to avoid such a situation in the future.
Then, we look for answers on how to avoid incomprehensible code. This article aims to give a glimpse of what those answers could be.
We may not be able to tackle every aspect of this vast topic at once, but we can certainly begin our journey by focusing on some basic principles for creating cleaner code.
What is clear code?
Or, let me ask you first – what makes a good book?
A good book has certain qualities. It’s satisfying to read, it’s an enjoyable experience, and it makes you want to read another page. Or even to read the whole book again.
In the same way, good code can be an enjoyable experience. Most of us have seen a well-written piece of code that, at the end of the day, makes us want to read more tomorrow.
There are certain rules and disciplines to follow if you want to write such code. Good code should follow these rules:
- The written code should follow a certain train of thought and avoid distractions for the reader. Similar logics should be grouped together within code for easy finding.
- No side-effects, pure functions, Single Responsibility Principle.
- Try to make variable, function, and class names self-explanatory.
- Do not repeat yourself (generally).
- Avoid using comments to describe code. Comment on what the code doesn’t say.
- Follow code formatting guidelines and general rules.
In these few points, we can describe what it means for code to be clean.
Below, we will expand on each point to better understand what steps are needed to improve the code.
1. Similar logic should be grouped together within code for easy finding
We read books from top to bottom, don’t we? We can understand the progressing story as a consequence of the actions leading up to this moment. After reading, we can trace back every action that took place to previous events that have been presented to us.
Similarly, we’d expect the same from reading code – that it will lead us through its narrative. It’s a bit easier to do it by consequently putting it in reading order, so that the code, read top to bottom, will make sense to a programmer.
We would like to suggest that these rules can be helpful in maintaining proper order in the code.
- Describe the purpose of the file in a comment at the top of the code.
- Place imports at the beginning of your file and arrange them alphabetically.
- Move types and interfaces below imports.
- Declare functions and variables before using them.
- Group together code elements that are correlated and depend on each other.
/*
* Print user’s welcome messages to the console.
*/
const welcomeMessage = "Hello World!";
interface User {
firstName: string;
lastName: string;
age: number;
}
const adminJohn = {
firstName: "John",
lastName: "Doe",
age: 21,
};
const getUserFullName = (user: User): string => {
const userFullName = `${user.firstName} ${user.lastName}`;
return userFullName;
};
const showWelcomeMessage = (user: User): void => {
const userFullName = getUserFullName(user);
console.log(`${welcomeMessage} I am ${userFullName}.`);
};
showWelcomeMessage(adminJohn);
Such a code arrangement has some qualities to it.
We can understand the code without too much jumping around the file(s) – this helps avoid distractions where the learning process is constantly interrupted by searches that contribute nothing to the overall understanding of the analyzed solution. You’ve probably experienced that yourself, as you read the code example from top to bottom and just got it.
When something unexpected happens, we can quickly think about where to start looking. In this example, it would most likely be a place above the code that failed.
And most importantly, when reading code, programmers usually get a lot of “non-verbal” signs coming just from the code arrangement and develop a feeling of it. Consistency can make a world of difference when it comes to mentalizing the mindset of the people who originally wrote the code, and memorizing common patterns and ways of orienting oneself around the codebase.
Now suppose we mix up the declarations and function calls and scatter them without any logic or reason among several different files. You’d need to read all of them, memorize and understand a lot of information in order to get a mental model of execution of this code. In the example above, that would delay you by a minute or so. In actual production code, that might take a week. Or it may never happen, for that matter.
Often, when writing code, we go back again and again to the file we are currently working on, adding new constraints, logic, changing bits and pieces of functions. Sometimes the rules proposed above are not as clear as in the example above. In such moments, it’s good to ask questions such as this:
Can I reorder this code so that reading it top to bottom will make more sense to someone unfamiliar with it?
2. No side-effects, pure functions, Single Responsibility Principle
These three are tools that will help you better conceptualize functions.
Pure functions
Let’s start with pure functions. A pure function is a function that always returns the same output with the same arguments. If a function is pure, we essentially isolate it from the rest of the program and because of this lack of external dependencies, the function becomes easier to reason about.
Generally, purer functions are simpler to understand and test – what’s especially useful for calculations, aggregations, and pieces of business logic.
Side effects
Now for the second one. A side-effect is a change to the state of a program that happens during the function execution. Side effects might be beneficial – such as printing a message to the terminal or saving a data record somewhere, but the catch is that unknown side effects are hard to find (as you need to read the code line by line) and unexpected side effects are even harder to debug.
When possible, make side effects explicit (ideally by the means of appropriate naming), reduce their number, or push them outside the core of your program (that’s a very imprecise description of one of the foundations of Domain-Driven Design).
Single Responsibility Principle
And thirdly, Robert C. Martin advocates that a function should perform only one responsibility.
But what’s a responsibility in the first place, in the programming context?
A single responsibility can be defined in the terms of change (made to the code) or actors (people that might request such a change). In terms of change, a class should have one, and only one, reason to change. In terms of actors, a module should be responsible to one, and only one, actor.
There’s some interesting discussion on Dan North’s website about the distinction between responsibility (as an insider’s perspective) and purpose (as an outsider’s perspective). Read it if you want to dig deeper into the meaning of the language used there.
In reality, things are not always so clearly divided that we have a single actor to which a module is responsible, or only one reason to change it. However, we still might use this rule as more of a guiding principle.
There are benefits, too. The functions which have one responsibility are simpler to reuse in other cases. They are often easier to conceptualize in the head.
When functions have one responsibility, it is not so hard to find an appropriate name for such a function.
Bringing it all together
Suppose we want to display the message in the console log. The user’s name needs to be capitalized. In addition to that, we need to get information about their favorite albums from different places.
We can consider separating everything and creating functions for each task. Instead of including it in one big function with lots of side tasks, we’ll show you what it can look like.
// Data structures, SRP functions below
interface Album {
userID: number;
name: string;
type: string;
}
interface User {
userID: number;
firstName: string;
lastName: string;
age: number;
}
const adminJohn = {
userID: 1,
firstName: "John",
lastName: "Doe",
age: 21,
};
const allFavoriteAlbums = [
{ userID: 1, name: "Classic Rock", type: "Rock" },
{ userID: 2, name: "90s", type: "Mix" },
{ userID: 1, name: "The best Pop songs", type: "Pop" },
];
// Pure, no side-effects, responsible to API Team
const userFirstNameUpperCase = (user: User): string => {
const userFirstName = user.firstName;
return userFirstName.toUpperCase();
};
// Pure, no side-effects, responsible to API Team
const getUserFavoriteAlbums = (
userID: User["userID"],
allFavoriteAlbums: Album[]
) => {
const userFavoriteAlbums = allFavoriteAlbums.filter(
(album) => album.userID === userID
);
return userFavoriteAlbums.map((album) => album.name);
};
// Non-pure: uses allFavoriteAlbums, and doesn’t return a thing
// Side-effect: console.log (expected – reflected in function name)
// Responsible to the Product Design team
const showUserFavoriteAlbums = (user: User): void => {
const userFirstName = userFirstNameUpperCase(user);
const userFavoriteAlbums = getUserFavoriteAlbums(
user.userID,
allFavoriteAlbums
);
console.log(
`Hello ${userFirstName}, your favorite albums: ${userFavoriteAlbums}`
);
};
showUserFavouriteAlbums(adminJohn);
Here we can see that the first name can be retrieved and converted to uppercase in the userFirstNameUpperCase function. Then, based on the userID, we can get favorite albums for this user by calling the getUserFavoriteAlbums function. Finally, we can call our functions in showUserFavoriteAlbums and write them to the console as a message.
In summary, we have created three separate functions. Each of them has a different reason to change. Yet it’s easy for us to see from the name of the function what we can expect from that function, and then we can easily reuse the first two functions in other places.
The third function, however, gets the job done and is a lot less reusable. We could make it a bit more reusable, but at a cost of more abstraction or more code. If there’s only one format and/or place where the application would display the user’s favorite functions, there’s no need to do so.
On the other hand, if there are multiple places, then we should carefully choose whether we should just copy the code or abstract it away to cover more use cases. This choice often depends on the programmer’s experience.
3. Try to make variable, function, and class names self-explanatory
Variable names can provide us with a lot of information. Without reading the code, we can get a grasp of the variable’s purpose. If a variable is named correctly, it might not even surprise us later on!
Variable names should be short and memorable, but, on the other hand, they should be self-descriptive enough not to require additional commenting and code searches.
Below, we can observe incorrect and correct variable names:
Incorrect | Correct |
let m; | let cardExpireMonth; |
let d; | let appointmentDurationInSeconds; |
const userData = () => {} | const getUserData = () => {} |
const sDate = () => {} | const setDate = () => {} |
The basics are simple:
- Do not use particular abbreviations because only you may know what they mean; or your team, but if someone new comes into your project, it will be difficult for them to understand. (However, that might be mitigated by a project-wide glossary of terms.)
- Avoid cutting words. It will be better if you use “Temporary” instead of “Temp.”
- You can describe what your function will do with simple words at the beginning of the variable name. For example, you can add “get,” “set,” “is” (if your function returns a Boolean value).
- Try to avoid writing the type of a variable or the structure of that variable. For example, try not to use userArray, transactionsList when you have a variable type defined. Because in fact you will repeat the same information about types and you can use these characters for something more meaningful.
- More fundamentally, the variable name should present its responsibility (or purpose, raison d’etre) rather than details of its implementation. Nobody wants to know how sausages are made, as the saying goes.
- Use searchable variable names. You can imagine how difficult it can be to search for the variable “m” or “min” in your code since you will get a lot of matches. All in all, finding the exact variable will be neither quick nor pleasant.
Iwo Herka, our Principal Software Engineer wrote an in-depth review with more tips and recommendations about naming things in progamming. Be sure to check it out if you’re to expand your knowledge on the topic.
Naming conventions in programming – a review of scientific literature
In this article you’ll find theory behind finding good names for programming purposes and a set of practical guidelines that will help you improve your craft. Dive in to discover what the name of your next variable, class or method should be.
4. Do not repeat yourself
New technologies and frameworks have given us a lot of opportunities not to repeat ourselves. We can create functions and components in separate modules. This is a big advantage because we can reuse our code and do not have to write the same code in many different places.
When we write small components with minimal responsibility, it is easy to reuse them in our application. It can be a good way to prevent duplications.
When an effort is made to minimize the number of connections between separate modules, the resulting code is easier to reuse. It looks cleaner, but it is also easier to maintain.
In the future, as the code grows, we may find ourselves in a dilemma. Should we copy the code, or should we change the existing code to fit more use cases?
The obvious benefit of using a single module or function for the same thing is that frequent changes in the future need to be made in one place only. There are some parts of the program where it is worth enforcing this rule, because it makes reasoning about the program easier. For example, we’d like to avoid multiple copies of the same total price calculation when implementing the purchase process in an e-commerce application. So DRY is generally a useful heuristic.
Except when the changes become divergent. And here the single responsibility principle comes into play again. We’d advise against blindly following the DRY rule if we’re making code for two completely different actors, such as “Finance” and “Sales” departments of a company. That might introduce an unintuitive coupling between the two.
How do we create separate components?
- Components and functions should be as small as possible. Smaller parts are easier to reuse.
- The component should have a single responsibility.
- Use structure for files. For example, Atomic Design. It will be helpful to find components and import them into the new functionality. Check it out: Is Atomic Design still relevant in 2023?
5. Avoid using comments to describe code. Comment on what the code doesn’t say
Good and clear code should be self-describing. This means that comments are not needed to describe what this variable is or how the function works. If a comment needs to be added to describe the code at hand, then the basic principles of clear code have probably not been applied.
It is good to stop and think sometimes. Is the variable name clear, or are the functions simple enough to understand? How will I understand this code when I come back to it in a year’s time? How will someone new to the project understand it? Maybe it is a good idea to go back to your code a few times before you deploy it and look at it with a fresh mind and consider whether it is named to the point. Or am I having trouble understanding it and need to dig in my head to find my intentions? Then this is a good signal to improve those parts of the code.
But are the comments bad in general and I should not use them anymore? Absolutely not, try to use comments for those parts that the code is not able to describe. Essentially we can use comments to describe the purpose, context, system constraints on this code and other caveats that one should be aware of when trying to modify the code.
We think that writing code is not like a “quick shot and forget.” It is a process where code should be constantly improved. The first ideas are often not perfect. And probably another look can give us a better idea how to solve the problem, rename variables, or change the logic of functions.
6. Follow code formatting guidelines and general rules
We hope that code formatters are already standard in every modern project, but this is such an important matter that we cannot ignore it in this article.
Code formatters help us format code properly. Everywhere the same indentation, the same rules. Every file looks the same, which can be very helpful for understanding and working quickly with the code.
The most important thing is that the entire team uses the same code formatting rules. Start by discussing with the team what the code should look like. Then implement general rules for your code. It is important to use the same formatting rules consistently throughout the project.
Clear code === fast maintenance in the future
Clear code rule application can often be postponed because the deadline is too close, or there is not enough time to implement the code according to these principles. We can choose shorter and easier ways to accomplish tasks, but in the long run it will cause more effort to maintain that code. In the future, implementing something new or making a small change in the application may take a lot of time to understand the structure of the code, or it may be necessary to change one line of code, but in thousands of files where the same function was used.
In our opinion, taking care of clear code is crucial for developers, and in the long run, it is profitable for the whole project. Here you have been given only a portion of clear code principles. We encourage you to explore this topic and improve yourself in this area.
And if you need a development team that delivers high quality of their work…
Let’s talk!Further reading
- Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin
- The Clean Coder: A Code of Conduct for Professional Programmers by Robert C. Martin
- Tidy First?: A Personal Exercise in Empirical Software Design by Kent Beck
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design by Robert C. Martin
- https://dannorth.net/cupid-for-joyful-coding/ by Daniel Terhorst-North