A Singapore Government Agency Website

Refactoring

As we add features to a codebase, it grows in complexity. Programming languages provide mechanisms of efficient code reuse, but we have to continually restructure to make use of them as we add new code.

So refactoring, defined as “improving the design of existing code” in Martin Fowler’s book that coined the term, is not a luxurious and optional separate activity, but a continuous maintenance necessary to keep the codebase maintainable as it grows.

One analogy could be that of riding a bike. As you use the bike, it picks up dirt and grime and you have to clean and oil it to keep it going fast, otherwise it gets gummed up and eventually grinds to a halt.

Or in other words, a good team mate uses and leaves the system in a better state, making it easier for others to pick up and continue work later - and that other person could be you!

Go Fast Forever

Two graphs of team speed over time

The team on the left goes faster initially, cutting all the corners and not investing time in things that don’t directly deliver user value. As their codebase becomes a big ball of mud, they find it more and more difficult to add new features. The business is surprised that seemingly easy changes take a long time. Eventually, the prospect of a large refactor or even a complete rewrite looms.

We would rather be the team on the right, able to add new features at a predictable, high pace for the business lifetime of the product - enabling accurate forecasting and release planning.

When to refactor

Refactoring is easier and less disruptive when it’s done continually as we go along, rather than having to stop development to do a big cleanup.

As such, it’s not a separate activity but included in every user story, a non-negotiable part of engineers’ work. It doesn’t normally need to be surfaced as its own item in the backlog unless tech debt has built up to where developers want to spend some extra time on it separately. If the team has taken a loan out on the maintainability of the codebase to meet some deadline, for instance, afterwards it may be necessary to put a refactoring technical task in the backlog for purposes of communication, negotiation and prioritisation with the team. The time spent on this work that doesn’t directly add any user value is an investment in going faster later.

We don’t always know about the need to refactor until we want to add new functionality - so one common situation is to refactor (or “pre-factor”) the code before implementing some change. An example could be adding an

“For each desired change, make the change easy (warning: this may be hard), then make the easy change”

- Kent Beck

Code Smells

What are some signs that we need to refactor? Just feeling pain when trying to change the code is one of them, but here are some typical “code smells“ that indicate something is rotten:

  • Long methods
  • Large files
  • Unused or commented out code
  • Duplicated code, especially business logic
  • Unhelpful naming of classes, variables and methods

Refactoring Techniques

Here are some of the refactoring techniques we can add to our toolbelt for use when improving the design of existing code:

  • Renaming things
  • Extracting variables and constants
  • Extracting methods
  • Moving things
  • Introducing abstractions
  • Deleting (unused) things

Refactoring to Patterns

If we refactor away code smells, what do we refactor towards? We don’t always know the requirements ahead of time as noted above, and we don’t want to do speculative work that may be wasted - but we can use refactoring to introduce patterns into the codebase that make it generally more maintainable.

SOLID Principles

Originally formulated in the context of Object-Oriented Programming but more broadly applicable as well, the SOLID acronym provides a good set of principles to adhere to when structuring code:

  • S: Single Responsibility Principle - code that changes for the same reasons should live together.
  • O: Open-Closed Principle - code should be open for extension, but closed for modification. In other words, you should be able to add new capabilities to the system by adding new objects, rather than changing existing ones.
  • L: Liskov Substitution Principle - the behaviour of subclasses should be compatible, not just their interface signatures.
  • I: Interface Segregation Principle - an API should do one thing, or be split up. This is indicated by forcing consumers to supply or consume data they don’t care about.
  • D: Dependency Inversion Principle - The high-level business logic should not depend on the low-level details of how to implement it, but the other way around.

Automated tests support refactoring

When refactoring, how do we know we’re not changing the behaviour (ie. breaking things)? Having automated tests that give you confidence that the code still works really helps to remove the fear factor of changing a codebase. In fact, an apocryphal saying goes, “if you don’t have tests it’s not refactoring, just moving code around.”

Being afraid to change the code manifests in problems not being fixed, or anti-patterns like only making additive changes (copy-paste and modify), leading to duplication. Having tests can help us to understand the code and how to modify it to our needs.

Practicing Test-Driven Development naturally leads to a trustworthy test suite, but sometimes we want to work on existing code that doesn’t have tests. In this situation, we can try to wrap the code in Characterization Tests to describe and hold the behaviour we care about constant before making changes.

Behavioral tests give freedom to refactor

If our tests are too coupled to the details of the implementation, they will break (fail with errors) when we change those details. If we have unit tests for every method of every class, mocking out collaborators, and we extract out a new object, we will have to change those tests.

Having behavioural tests that verify functionality at an appropriate boundary, say the public API of a component, module or service, allow room to change how that functionality is implemented internally, as long as the external result is the same.

A heuristic for evaluating the amount of coupling in your test suite is the number of times you are surprised to find tests breaking when changing structure and not business logic.

Refactoring tools

The good news is that modern IDEs contain tools that allow us to restructure the code without making mistakes. Using these automated tools where possible increases productivity and makes refactoring easy.

A simple example is renaming a variable with a Rename function instead of manually replacing it or using a search-and-replace feature. Depending on the programming language, the IDE can leverage the compiler to find all the places where this variable is used, taking the burden off the developer and making the change less error-prone.

Good IDEs include automations for all of the refactoring techniques above, as well as the ones mentioned in the Refactoring book. They will often highlight refactoring opportunities in the code and suggest automated techniques to apply.

Chat with me!
Bot Launcher