When and how to refactor your code

Katarzyna Abratkiewicz

What is refactoring?

Refactoring is a process of changing an application’s code and its structure without affecting functionality. Its goal is to bring the code up to date with appropriate standards and patterns, making the code simpler, more generic and less repetitive.

Well-designed projects can help developers understand and maintain the code more easily, resulting in fewer bugs and faster development. New team members need less time to familiarise themselves with the product and can follow the existing structure with fewer problems.

Generic solutions may take more time to write and test initially, but, for larger projects, their ultimate benefits quickly pay dividends.

When to begin

It’s worth listening to customers’ and developers’ feedback.

Customers might complain about poor performance, the length of time it takes to deliver new functionality or loads of bugs. Developers might feel like they can hardly work on the code without noting sub-optimal design, unnecessary complexity and bugs hidden in the code.

If there is no way to add or change functionality easily or without knock-on changes, then the code quickly fills with workarounds and hacks, making the problems only grow. Problems that appear different might have the same core cause: badly designed or neglected code, lack of proper structure or obsolete technologies.

How to approach it

Once the core problem is discovered, finding the proper solution seems like an easy task. For example, if developers notice the need to copy-paste large chunks of code, possibly there are missing extensions or generic solutions that would allow easy reuse of the code.

Here is a list of other helpful questions that might help identify whether it’s time for some refactoring:

  • Does the code contain a lot of repetition?
  • Is the code easy to understand and maintain?
  • How good is performance?
  • Is the code resistant to bugs?
  • How easy is it to add new functionality?
  • Is the code for different purposes mixed up? For example, business logic, UI and data access don’t have clear separation.
  • Is the project monolithic (that is, one big project) and missing clear layers?

There might be many different reasons, requiring less or more time and involvement to solve. Depending on how deeply the problem exists, your strategy may differ.

Tips

  • With refactoring that involves many files in many areas it’s probably worth thinking about spending some additional time to prepare a good refactoring plan. This can help prevent the cascade effect of fixes, where changing one thing requires changes to something else, which means changing more things, and so on.
  • To reduce potential conflicts among team members, split refactoring into many smaller tasks that can be quickly tested and merged back to your main development branch.
  • It’s also good to separate compiler-safe work that doesn’t require testing (like renaming or formatting) from changes that might cause bugs and require testing.

Benefits

At first glance refactoring might look like a waste of time – there are no evident changes to show to the customers. But refactoring isn’t a feature, it’s an investment in features.

A well-designed, up-to-date project allows developers to write code much quicker with fewer bugs, resulting in less time taken to add new functionality and happier stakeholders/customers. The effect is real, but is not always known to those who decide where developer time is spent.

The final product is not the only beneficiary of the refactoring changes. They also have an impact on the people who create the application. Working on an easy-to-understand codebase with clean dependencies can motivate developers and encourage others to work on the project.

Case study: OCC MarketPlace

In the MarketPlace project we spotted the signs that refactoring was necessary. The code was becoming hard to understand and maintain and was opened to bugs. We had an architectural problem – we clearly missed a business logic layer. Business logic was spread out across the whole project; some of it was in models, controllers or repositories.

We wanted to correct that but we were also aware that in such a mature program (MarketPlace is 10 years old), changing the architecture would be a very difficult and time-consuming task.

Finally, we saw an opportunity – a requirement to add a new API to our existing functionality. At that point, we had to stop letting the bad design grow and try to face the problem.

Onion Architecture

The first step was to find an architecture that matched our needs and purposes – we chose Onion Architecture.

Diagram illustrating the layers of Onion Architecture and their dependencies

This architecture, as the name suggests, has onion-like layers that force references to be directed inward. Inner layers cannot reference outer layers, maintaining clean separation throughout the code. This also prevents potential bugs caused by mixing layers.

With this design, every layer has its own responsibility and is not supposed to do any work from another layer. For example, the Models layer is a light project with nothing but models that can be used by any other layer, but itself cannot reference any of the other layers. The UI layer is responsible only for UI-related stuff. The business logic that used to be in controllers is moved to its own dedicated layer.

Lessons

We started refactoring two years ago and there is still plenty of work to do. As we were refactoring the first module (a term we use for a collection of related features), we noticed a cascade effect of fixes required in other modules too – forcing us to change a lot more than we initially planned.

With this experience, we changed strategy. Because we knew that moving an entire module to the new architecture had this cascading effect on other modules, we embraced that and instead broke down the changes required into smaller tasks, which would cut across many modules, but could be developed and tested independently.

The effort we put into changing the architecture, despite not being finished yet, has already started to pay off. The updated code becomes clearer, simplified and more generic. As a side effect, it also improved performance, as we reduced the amount of data taken from the database, decreased the number of unnecessary database calls, and removed repetitive code.

Summary

Every project needs an individual approach to refactoring, but the common aspect is that sooner or later it needs to be done. It’s worth periodically updating the libraries, removing unused or obsolete code, considering generic solutions or thinking about redesign if there are any problems noted by team members.

For new applications, the future of the product is not always clear, so mistakes in the design easily happen. Spending time on generic solutions early on could be a waste of time. But once the product becomes established it’s worth starting to refactor before the problems get bigger. If a codebase is neglected for years, it will be harder and harder to find people who want, or are able, to work on the obsolete technologies and code.