How to write maintainable code: Straightforward logic
Maintaining code is more complicated than writing it. Before you make any changes, you need to understand what is there already, how it was supposed to work, and how it works now. The difficulty of this task will depend on how the code was structured and if it’s an easy read. In this article, I’ll show you how you can make your code more readable and therefore easier to maintain.
Obvious entry points
When you start a ticket on an application, usually you have some information about where the change or fix should happen. You can make linking that place to the code easier if you consistently use some convention for naming and placing the files. Let’s take a look at a few examples.
Component-based app
In my day job, I build and maintain a few Angular applications that are built with components. Each application has many routes defined, and for each route there is a top-level component that is responsible for this view. The naming convention we use is as follows:
- simple route:
/home-page
—<home-page>
- nested route:
/long/nested-route
—<long-nested-route>
- route with parameter:
/user/:id/edit
—<user-edit>
In this way, if I get the route where the changes should happen, I can immediately find related component files.
Backend
On the backend, we have a few constraints put on the routes we use. The API is loosely inspired by the REST approach, and each endpoint is related to one of the collections that we have in the database. The routes are in a format /collection/operation
—for example:
/users/get
/transactions/get-report
The code that is called by those endpoints is organized in files by the collection: so in one file we have methods related to users' collection and in another we have methods related to transactions. In this way, based on the first part of the API route, I know where to look for the code.
Avoid unnecessary jumps
One of the ways that I often see makes the code complicated is by sending the reader through many unnecessary jumps. When you read code, it’s inevitable that you will need to do some jumps between different parts. This becomes a problem when the jumps are many and unnecessary. Some examples based on what I’ve seen in real-world code:
- a function that does nothing besides creating an alias for another function
function A(param1, param2) {
return B(param1, param2)
}
this can make sense for some refactoring, but it would be good to have it in a temporary situation,
- named callback functions defined away from where they are used:
function success() { /*..success..*/}
function error() { /*..error..*/}
getPromise().then(success, error)
The same thing would be simpler to read if it were:
getPromise().then(() => { /*..success..*/}, () => { /*..error..*/})
- expectations in a test set in variables, away from where they are used:
const expectedObject = { /*..some big object..*/ };
/*..many lines of code..*/
expect(result).toEqual(expectedObject);
Whenever we have a jump in code, we make it slightly more complicated to read. Let’s make sure that this inconvenience is balanced by some benefit—that there is a good reason to do it this way.
Keep it simple
The flow of your code should be straight. It should be easy to draw a chart of how the logic flows through the application and how things depend on each other. There should be no circular paths in your code, unless you are doing some recursion on purpose.
It should be your goal to be always able to explain how user actions or some external events lead to data changes. If you cannot easily tell what is responsible for what, then it’s very likely your code is chaotic and difficult to follow.
Displaying data
Of the simple cases is transforming the data for display. It could be one of many things:
- formatting dates in a user's locale,
- translating into user’s language,
- combining many properties of an object into one, ready to display string, or
- filtering an array based on a search.
Each of these operations should have an easily identifiable input and output format and should be written in a form such that a reader knows what goes in and goes out.
Formatting data for display should leave the original untouched—there is no reason to change the data just because it is displayed in some place.
Changing state
When you change the state of your data, it should be possible to easily identify the following parts of the code:
- source of the change—button that was clicked, the event that occurred in the system, etc.
- the change itself—the code that checks the constraints and updates the state of the data
- saving the changes to the storage—this could be an update to the database in the case of backend code or calling a save API in the case of the frontend
If, for some reason, it’s not easy for you to identify what parts of your code are responsible for some of those points, then reading this code will probably be pretty confusing.
Testability
All the things we discussed here are consistent with writing testable code. Small, well-defined units with clear responsibilities are easy for developers to understand—and easy to test. For me, writing tests is a good exercise that can teach you to write more readable code: just pay attention when testing starts becoming painful and when the code becomes too convoluted to easily work with.
Learn more
If you are interested in hearing from me when I publish some new material about programming or related topics, you can sign up for my mailing list.
Subscribe to my newsletter
Read articles from Marcin Wosinek directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Marcin Wosinek
Marcin Wosinek
I'm JS developer with 13 years of professional experience. I'm always happy to teach my craft.