[go: up one dir, main page]

DEV Community

Cover image for Refactoring: The Art of Polishing Code
KWAN
KWAN

Posted on

Refactoring: The Art of Polishing Code

Refactoring is the process of improving the internal structure of code without changing its external behavior. This article explores how this practice is fundamental to creating high-quality solutions.

“Refactoring is the process of changing the source code in a way that does not alter its external behavior while improving its internal structure. It is a disciplined technique for cleaning and organizing code, and as a result, minimizing the chances of introducing new bugs.”
— Martin Fowler

From the dictionary, “to polish” means to perfect something rudimentary, rough, or crude; to refine. This definition captures what we seek in refactoring: making changes in the code in pursuit of perfection and beauty.

Developer: A Digital Craftsman

What is a software developer if not a digital craftsman, driven by the quest to create inspiring solutions? We’re not just interested in creating solutions that work, but in doing so with quality. The results don’t always meet expectations, but unlike traditional craftsmen, we can revisit a completed piece and make adjustments at any time.

As stated at the beginning of the article, we seek improvements that bring beauty to our code, and we must remember that beauty resides in simplicity. Therefore, we should strive to simplify our code, as we may not always realize the value this adds.

A simple codebase is easy to understand, whether by another craftsman or by ourselves in a few weeks or months. As Kent Beck says, code should “reveal its intentions.”

Code that is easy to understand can just as easily be modified and evolved, with fewer chances of side effects. Yes, I’m talking about bugs – and I know that caught your attention. Gotcha!

It’s essential to acknowledge the uncomfortable truth that we don’t always get the design right on the first attempt, regardless of our seniority level in software development. The initial version of code might not be the cleanest; depending on circumstances, we’re often more focused on getting it to work and plan to take a more critical look later, evaluating whether we’ve violated any SOLID principles or ignored a design pattern.

There are times when you might recognize the need to modify existing software before adding new features to facilitate the integration of new code.

“Make the change easy, then make the easy change.“
— Kent Beck

Why Tweak What’s Already Working?

Image description

If you’re a software developer who has never asked this question, I must congratulate you—you love what you do and care about doing things well. However, the chance of someone asking this question is high, and I don’t blame them. Understanding the ‘why’ should guide our decisions.

If I had to convince my boss of the importance of refactoring, I’d present the following case study, drawn from the book Clean Architecture, where we can analyze real data from a real company.

The first graph shows the growth of the engineering team, followed by a graph on productivity measured in lines of code. In the second graph, we see that while each release includes a larger team, productivity begins to stagnate.

Image description

Graph 1 – Engineering Team Growth

Image description

Graph 2 – Productivity Over Time

More lines of code were expected with the addition of new developers, right? You might think other factors influenced the outcome, such as a lack of motivation or team dedication, but what was observed was that overtime hours didn’t decrease during this period.

Looking at the cost per line of code graph, we see that costs increased by 40 times between releases one and eight.

Image description

Graph 3 – Cost Per Line of Code Over Time

If you really want to startle the company board, present the next graph, which shows the monthly payroll for development over the same period. What started at several hundred thousand dollars reached around $20 million by the eighth release, with no forecast for change.

Image description

Graph 4 – Monthly Development Payroll Per Release

This is the picture of a system where meeting deadlines was always the priority. Developers were added in hopes of producing more code in less time, with little regard for code quality. Over time, adding new features became harder, as most efforts shifted to fixing issues, leaving little room for significant new functionality.

Agile Methodologies

The concept of agile is often misunderstood, with many people equating agility with speed. Setting tight deadlines to appear ‘agile’ is far from the reality. When applied correctly, the agile method can create the impression of rapid progress, as if developers are accelerating to meet delivery targets.

Where agility truly makes a difference is in responsiveness to changes, decision-making, and project course corrections when needed.

But as not everything is rosy, working with agile methods can bring the following challenges:

  • Requirements can change, bringing new perspectives to the solution.
  • Starting a sprint without clearly defined requirements – which is often part of the framework’s essence – allows for earlier failures, enabling adjustments to be made sooner.

In these cases, refactoring is a useful and necessary tool, given the agile methodology’s characteristic of seeking small, incremental deliveries and admitting mistakes that must be promptly corrected.

Want to learn more about the agile world? It’s worth reading some excellent articles on applying the agile manifesto to developing an agile mindset and on the functioning of a Scrum team.

When to Refactor?

Ideally, refactoring should happen within a reasonable timeframe, not too distant from the code creation, also known as timely refactoring. However, before starting to refactor, certain prerequisites must be met.

If you’re a careful reader, you may have noticed that the word “behavior” appears multiple times throughout the article and might have guessed that it’s a critical element of refactoring. But how can we ensure that code behavior remains unchanged when dealing with projects with a large volume of code, workflows, and scenarios? That’s where testing comes in.

The first requirement before considering refactoring is the presence of unit tests, which aligns with Kent Beck’s first rule of Simple Design. Unit tests are essential for ensuring that the software’s behavior remains consistent, functioning as a contract and providing developers with the confidence to make necessary changes.

Of course, having tests isn’t enough; good test coverage is essential. Coverage rates of 2%, 5%, or 8% are better than nothing but are very low numbers that won’t provide sufficient assurance that everything is working as it should, or that bugs haven’t been introduced.

To ensure the success of the refactoring, all unit tests must continue to pass without modification.

Image description

When Not to Refactor

In theory, all poor code should be refactored, but there are exceptions where we can assess whether the effort is worth it. When dealing with legacy software, for instance, we must determine whether it’s worth refactoring or deprecating it.

For example, you could evaluate whether the language versions and libraries are outdated. The absence of unit tests may indicate poor code quality and a high likelihood of bugs.

In some cases, the effort required to refactor software with significant problems might be equivalent to that of writing new software.

Measuring your Code

Cyclomatic Complexity and Cognitive Complexity are metrics used to determine code quality. Performing this analysis manually is time-consuming, so you should use tools that automate this process. The goal is to provide a basic understanding of these metrics to clarify what they represent.

Cyclomatic Complexity

Cyclomatic Complexity measures how difficult it is to test a unit of code. It’s useful for evaluating the complexity of code segments, such as methods and routines, rather than the codebase as a whole. In short, it measures the maximum number of independent paths within the code.

This value has a direct relationship with the number of test scenarios required to achieve 100% coverage for a method. A high number of test scenarios indicates that the method is complex and challenging to read.

Image description

Cognitive Complexity

Cognitive Complexity measures how difficult it is to understand a unit of code. It assesses the number of breaks in the linear reading flow, weighted by the level of nesting of these breaks.

It focuses on Kent Beck’s second rule of Simple Design: revealing the code’s intent, scoring low when the code is more expressive.

Code Analysis Tools

Beyond our ability to spot problems in code, we have tools that make our lives easier, such as SonarQube, JArchitect, ESLint, and others.

Using SonarQube as an example, it provides an overview of a project’s code status with various indicators.

One interesting metric in SonarQube is technical debt, which estimates the approximate time required to refactor your code.

Image description

Refactoring: The Art of Polishing Code – Final Considerations

Imagine a wise man on top of a hill defining refactoring. He might say, ‘To refactor is to change without changing,’ typical of those who hide knowledge in cryptic phrases and don’t care if you understand them. But I hope this article has helped you enough for the message to make sense: refactoring is about changing the code without altering its essence—its behavior—in pursuit of simplicity for maintainability.

And maybe this concept isn’t entirely unfamiliar. If you’ve ever replaced a magic number with a constant or renamed a variable to something more meaningful, guess what? You’ve refactored your code. Cheers!

Let's connect on social media, follow us on LinkedIn!

Article written by Rodrigo Prata, and originally published at https://kwan.com/blog/refactoring-the-art-of-polishing-code/ on October 18, 2024.

See you in the next article!

Top comments (0)