How to be more confident about the quality of the code of our React applications


My code works, but I think it is implemented incorrectly.

After we've been programming for a while, we reach the conclusion that making our React applications work is not enough to produce good quality software.

We now worry about things like maintainability, readability, and best practices. And that's a good thing and a sign that we're becoming better developers. But when we don't have a concrete grasp of what those concepts mean, and of what really makes software valuable, it's easy to start constantly doubting the quality of our work.

In this article I'm sharing mental models and a strategy we can use to get more confident about the code we make. I Hope this proves to be as useful to other as it is to me.

The content of this article is applicable not only to React applications. And so that we don't get distracted by React technicalities, I'll keep the terms used as general to the context of software development as possible.

We'll start by going over what makes software valuable, so we can understand what good quality software is. After we've got a grasp of what that is, let's talk about an intuitive strategy that doesn't really help improve code quality and why. Then we'll go into some characteristics of poor quality software and how those can be addressed. After that, we'll go over a simple but effective strategy we can use to help us improve the quality of our software. Let's get started.

The characteristics of good quality software

The value of software comes from its correctness and its maintainability. Correctness is about software conforming to the requirements. If a piece of software does what the requirements expect of it, it is correct. Maintainability refers to the software ability to adapt to the future. We can consider that a piece of software is maintainable if, as time goes by, the cost of adding, removing or changing features does not increase.

Given that correctness and maintainability are what make software valuable, we can therefore conclude that good quality software needs to fill the expected requirements at all times, and have a structure which will allow it to adapt to the future.

If you'd like to know why correctness and maintainability are what make software valuable, you can read this article

What makes a code base unmaintainable

Usually it is fairly simple to tell the correctness of our software. We can perform tests, automated and manual, that will give us confidence that the requirements are being filled. However, when dealing with maintainability it tends not to be as straight forward, since we don't have that obvious and immediate feedback as we have with correctness. Therefore, we are sort of left in the uncertainty wondering if our software will be able to adapt to the future or not.

As a rule of thumb, it's easier to find out what will make something fail than it is what will make it succeed. So when we're not sure of the decisions we have to take, just keeping in mind the ones we have to avoid can be a good pretty good strategy. So let's look at one of the most instinctive ways of trying to make our code adapt to the future, and know why we need to resist that urge.

Don't try to predict the future

One of the first instincts that comes when dealing with the uncertainty of the future, is to try to predict it. As such, we'll try to write our code in a way that it is prepared for the changes we believe will happen.

The problem with forecasting is that we as humans are awful at it. If we weren't, software wouldn't change nearly as much as it does. We tend to believe that a feature will be a hit, only to find out later that no one really uses it. As such, if we try to make guesses as to where the software is headed, and the more long term those guesses are the worse, we'll end up with a lot of code that does not contribute to correctness, and probably never will. And code that does not contribute to correctness nor improves maintainability, only makes our lives harder and wastes our time.

Another big problem with the forecasting approach is that it makes it really hard to tell when we're done implementing a feature. Since now we have to imagine all the possible ways that the feature might evolve, it's really easy to start getting paranoid and thinking that we are not writing the necessary code to protect our software from the future. The negative impact this has on the confidence we have on our code is huge! And not to mention how unnecessarily long a feature development can get.

Avoid predicting the future. Let's stick to what we know for sure and not make more code than the necessary to fill requirements we currently have at hand.

Know what makes a code base hard to change

In the spirit of avoiding the path of failure to achieve success, let's look at what generally makes a code base really hard to change, and try to figure out a strategy to deal with that.

  1. Fear of breaking what's already working
  2. Incomprehensible code
  3. Highly coupled code

Fear of breaking what's already working

When making a change to an existing code base, the number one risk we're taking is breaking features that already work. Technically speaking, we have to avoid regressions.

Aside from having a huge negative impact in the morale of the development team, regressions can have a devastating impact on the goals that the software is trying to accomplish. If the goal is a business goal, it might mean a big loss of revenue. A single introduced bug might cause hundreds of clients to lose their trust on the software and stop using it, and that's a risk no one wants to take.

Also, if when we're trying to change or add something new, we're breaking what's already working, when will we be finished with our work? The answer might as well be never.

Incomprehensible code

If we want to change some feature on our software, but can't reason about the code we're looking at, how effective can we really be? What if we can't even find the code responsible for making a feature work, because it is scattered all around or due to it being really poorly named?

Code that gives no way for a developer to understand how it works as a whole, and/or how its individuals pieces work, is doomed to be unmaintainable.

Highly coupled code

If we have to change 10s of files to make a small change in our software behavior, we have highly coupled code.

The problem with highly coupled code, besides being hard to understand, is that it takes a lot of time to change. Because we have to figure out all the places we'll need to modify, as well as be careful not to inadvertently modify any other features.

As time goes by, if there's not a conscious effort to prevent it, code bases tend to rot. To deal with this, a code base needs to allow us to make small and continuous improvements to the existing code, without compromising the release of new features. If we've got highly coupled code, we won't be able to do those improvements fast enough not to jeopardize the time to market of new features.

Keeping it maintainable

So now that we've seen what generally makes a code base hard to maintain, let's look at how we can address those concerns.

Fear of breaking what's already working

Fear of breaking what's already working can be mitigated by tests. A well-built and taken care suite of tests can save developers from countless hours of anxiety and despair, as well as save the life of the business that depends on the software working correctly.

It's important that the test suite is mainly composed of automated tests that can be quickly executed. This makes it possible for developers to know if they broke something almost immediately. The test suite should also contain some tests that simulate a user and interact with the system as a whole. Manual testing, as part of a test suite, should be kept to a minimum and reserved to situations where the automation might be prohibitively expensive. After executing all the tests of our test suite, we should have gone through all the possible use-cases for our software.

An automated suite of tests allows us to quickly change our software. Therefore, improving maintainability.

Incomprehensible code

Incomprehensible code comes from not being able to understand what a piece of code intends to do. Some reasons that can happen are:

  1. Code organized into huge pieces, that are so big, that it's too much for us to reason about.
  2. Code organized in a way that we can't reason about each piece in isolation.
  3. Code for a single functionality being scattered all around the code base
  4. Misleading names

We can address all the points above if we modularize our code into small independent modules that make sense on their own, and give them a name that reveals what they intend to do.

By keeping our modules small and independent, we avoid our modules being too big for us to understand, as well as allow us to ignore other modules that the current module may interact with. This greatly reduces the cognitive load necessary to understand a code base, because now we can reason about one module at a time.

By grouping our code into modules that we can name in a way that reveal what it intends to do in the context of the software's domain, we are grouping small pieces of code into units that belong together. Otherwise, we wouldn't be able to give our modules meaningful names. This results in code that is easier to understand what it does as a whole, as well as what each single piece does. Also, by following this approach, code that contributes to the same behavior will tend not to be scattered.

Understandable code allows for changes to our software to be made quickly and with confidence. Thus, directly improving its maintainability.

Highly coupled code

When solving the problem of incomprehensible code by organizing our code into small independent modules which make sense on their own, we are already solving the problem of highly coupled code.

Think about it, if we can reason about a piece of code in isolation, that means there is a clear boundary between modules and is easy to understand what each module should do. If we can understand what each module does, and given that it is small, we can safely and quickly refactor it. We can even throw an entire module away and re-write if it needs to be! This kind of modularity allows developers to continuously improve the quality of a code base, without compromising the time to market of new features.

Decoupled code allow us to continuously and quickly improve the quality of our code base. This allows us to renew rotten code that is making our software harder to change as time goes by.

Testing and modular design may not be the only way to keep a code base maintainable. But it certainly is a proven way that works across many kinds of code bases.

Building a strategy to improve code quality

We know that for software to have quality it needs to fill its requirements and it needs to be capable of adapt to future change. We've seen that forecasting is not a good approach to maintainability, and that "Fear of breaking what's working", "Incomprehensible code" and "Highly coupled code" are the main reasons that make a code base unmaintainable. We've also looked at how automated testing and modular design can help us stay away from what makes our code hard to change.

Now, let's organize all of this knowledge into questions that will give us feedback about the quality of our code, and that we can constantly ask ourselves when we are faced with a design decisions or when we've finished implementing or changing a feature.

These questions have the goal of making us check if our code is correct and of maximizing its capacity of changing in the future. If we can answer yes to all the questions, we can have confidence in the quality of our code. So let's go over what each question means and how we can use testing and modular design to answer each one.

  1. Does the code do what the requirements ask of it?
  2. If a working feature broke, would we quickly know it and be able to easily find out what and where it broke?
  3. If we wanted to add, remove or change a feature, would we know exactly where to look?
  4. Can we rewrite any feature in a timely manner?

Does the code do what the requirements ask of it?

This question assures our software is doing what it is intended to do. If it doesn't do what is expected, it probably won't be contributing to achieve the established goals. So we can't really call it good quality software.

We can answer this question by relentlessly testing our software and making no assumptions that something works without solid proof of it.

If a working feature broke, would we quickly know it and be able to easily find out what and where it broke?

As we've already talked about, if we want software that can adapt to the future, we need to know when something that was already done, stopped working. We need to know it fast, and we need to be able to fix it quickly.

One way to accomplish this is to have a suite of automated tests that are quick to execute, and that when they fail, point us to the piece of code that is failing. By having a suite of automated tests that points us to the piece of code that is failing, we are also designing our code into small independent modules. Otherwise, we won't be able to tell what part of our code stopped working.

If we wanted to add, remove or change a feature, would we know exactly where to look?

When we want to change the behavior of our software, first we need to know where the code responsible for that behavior is. If we can't tell where it is, we can't really change it.

To achieve this, we can organize our code into modules that we can name in such a way that it's obvious what it does in the context of our software. We should do it at an higher and lower level. When done correctly, our names will work out as road signs that will tell us what our code does and where to look when we want to make changes.

Also, if we were able to give our modules meaningful names, it's likely that we succeed at grouping our code such that the reasons for it to change are the same or very closely related. The result is not having to look at wildly scattered code when wanting to change some behavior. Which makes our task of finding out what needs to change much easier.

Can we rewrite any feature in a timely manner?

As we've already talked about, a code base will change a lot during its lifetime. And as it undergoes many changes, it will start to deteriorate. It might be because we had a wrong understanding of the requirements. Maybe the requirements proved out to not be appropriate to achieve the software goal. Or simply there was not enough time to implement the feature in a none sloppy way. Either way, after the mess is done, we need to be capable of cleaning it in a timely manner. Otherwise, the mess will start to spread out and contaminate every part of the code base. The result of that is a code base that can't be changed.

By timely manner I mean an amount of time that is small enough not to compromise the release of new features. I do not mean that any complete rewrite of a feature should be able to be done in one go, but that we should at least be able to do it little by little without compromising the schedule. It might be hard to convince management that we need a refactor sprint, better if we don't even need to ask for it.

Yet again, organizing our code into small independent modules can allow us to answer yes to this question. If our modules are small we can quickly rewrite them. And if they are independent of one another, we won't have to worry much about breaking any other modules. This allows us to continuously and quickly renew our code base.

When to ask the questions

Let's ask these questions when we're done implementing or changing a feature. Let's also ask them whenever we have decision to make that is not so obvious. If after asking these questions we still don't know which path to take, let's get back to the principle of maximizing the capacity to change.

Conclusion

Next time we're programming, let's remember that every line of code should either contribute to our code correctness or to its maintainability. If we implement a feature in a way that makes it harder for us to change it in the future, we are not helping ourselves.

In order to get feedback about the correctness and maintainability of our code, we can ask ourselves these questions:

  1. Does it do what the requirements ask of it?
  2. If a working feature broke, would we quickly know it and be able to easily find out what and where it broke?
  3. If we wanted to add, remove or change a feature, would we know exactly where to look?
  4. Can we rewrite any feature in a timely manner?

If we can answer yes to the questions above and if we keep in mind that our decisions should make our code base easier to change, we can be confident that we have produced good quality code.


As said at the beginning, I didn't go over how to apply these principles in a React app. But if you liked what you read, or have any question, let me know! You can send me a message on twitter at @JooForja.

I hope you liked this article and that it proved useful to you!