Theodo apps

Mastering Refactoring: Part 2 - The Developer's Manual

Refactoring is a daily practice for developers, but it’s often viewed as complex. According to stats from my last project, bad refactoring ranked 6th among the top causes for introducing bugs. However, avoiding refactoring isn’t the solution—it's an essential process for maintaining the quality and scalability of a software project. Over time, technical debt builds up, slowing down development, demotivating stakeholders, impacting product stability, and making it harder to scale. Refactoring is a must-have tool to not only prevent the growth of debt but also reduce it in more extreme cases.

I recently attended the Mobilis in Mobile conference and took a course on this topic. I’d like to share some key insights on refactoring and how to master it. This article is the second in a three-part series designed to help you, as a developer, perform refactoring without introducing new bugs.

Understanding the Fundamentals

Refactoring is a key step toward producing cleaner and more maintainable code. To do this effectively, it's important to understand the core principles that define high-quality code and to recognize the warning signs of problematic code—often referred to as code smells. Let's delve into some fundamental theories that will guide you on this path.

Clean Code

To truly understand what makes code high-quality, it's worthwhile to explore Clean Code by Robert C. Martin, also known as Uncle Bob. This must-read book dives into essential development principles across general best practices, testing, comments, functions, naming conventions, and more. Each principle is tied to a code smell, a common indicator of problematic code.

Similarly, Refactoring Guru is a valuable resource that catalogs these code smells.

Design Patterns

Even after you've embraced the best practices from Clean Code, you might encounter complex scenarios where those guidelines aren't enough. This is where design patterns come into play—they're proven solutions to common software design problems. A deep understanding of these patterns is essential for building robust and scalable applications. However, misuse can lead to overcomplicated code and add to technical debt, so it's important to apply them judiciously.

Practical Example:

Now, suppose we want to add new classes Y and Z defined as:

Instead of copying and pasting the doA and doC methods—which would harm maintainability—we'll refactor using the Decorator pattern before implementing these changes.

The 5S Method

Inspired from Toyota's production system, the 5S methodology focuses on optimizing organization and efficiency. When applied to software development, these principles can significantly enhance code quality:

  1. Seiri - Sort: Remove unnecessary or obsolete code from your codebase.
  2. Seiton - Set in Order: Organize your code, files and directories logically, placing components where other developers would expect to find them.
  3. Seiso - Shine: Maintain a consistent coding style to simplify error detection and improve readability.
  4. Seiketsu - Standardize: Establish standards for the first three “S” and share them with your team to ensure everyone is on the same page.
  5. Shitsuke - Sustain: Continuously apply and reinforce these practices through regular code reviews and team collaboration.

SOLID Principles

The SOLID principles are five fundamental guidelines for writing maintainable object-oriented code:

  • S — Single Responsibility Principle: Each class or method should have only one responsibility or purpose, meaning it should have only one reason to change.
  • O — Open/Closed Principle: Your code should be open for extension but closed for modification. This allows you to add new functionality without altering existing, stable code.
  • L — Liskov Substitution Principle: Subclasses should be replaceable for their base classes without affecting the correctness of the program. In essence, derived classes must be substitutable for their base classes.
  • I — Interface Segregation Principle: It's better to have multiple specific interfaces than a single general-purpose one. This reduces unnecessary coupling and makes your code more modular.
  • D — Dependency Inversion Principle: Depend on abstractions rather than concrete implementations. This promotes flexibility, modularity, and easier maintenance.

DRY Principle

The DRY (Don't Repeat Yourself) principle focuses on minimizing code duplication. The idea is that every piece of knowledge or functionality should have a single representation within your codebase. This not only enhances maintainability but also reduces the risk of errors and simplifies future modifications. However, it's important to note that in certain architectures, like microservices, a controlled amount of duplication might be acceptable to avoid tight coupling between services.

KISS Principle

The KISS (Keep It Simple, Stupid) principle focuses on simplicity in your code. Simple code is not only easier to understand but also easier to test and maintain. Essentially, KISS resumes the core objective of refactoring: transforming your code to make it clearer and more accessible to everyone who works with it.

The Methodology

Armed with these fundamental principles, you're now better equipped to identify bad code. For every code smell you encounter, there's at least one refactoring technique designed to address it.

Common Pitfalls to Avoid

Before you start refactoring, be cautious of these common mistakes that can derail your efforts:

  • Ignoring Technical Debt
  • Attempting Large Refactorings in Isolation
  • Refactoring Without Team Consensus
  • Mixing Feature Development and Refactoring in a Single Commit, this makes code reviews more complicated and increases the risk of introducing bugs.
  • Combining Multiple Refactorings in One Commit, keep your changes atomic to simplify reviews and reduce cognitive load.
  • Refactoring Untested Code, Without  tests, you can't guarantee that your changes haven't broken the refactored functionality.

Key Takeaways

For a successful refactoring:

  • Functional Invariance: The program's behavior should not change from the user's perspective (or the resource consumer in the case of backend services).
  • Improved readability: After refactoring, your code should be more readable, and better organized.
  • Simplified Future Development: The refactored code should simplify upcoming functonnalities.

For each commit during the refactoring process:

  • Atomic Changes:  Each commit should represent a single action.
  • Test Coverage: Changes must be covered by tests.
  • Stability: All tests should pass before finalizing the commit.

Applying Methods While Adhering to Core Principles

To execute effective refactoring, it's essential to follow the key takeaways we've discussed. Begin by identifying the specific code smell using the fundamentals, and then apply the appropriate refactoring technique to address it. Detailed explanations of these techniques can be found in Martin Fowler's book Refactoring or on the Refactoring Guru website.

Practical Example:

Imagine you've discovered a method that's over 100 lines long, with complex conditional logic that makes it difficult to read and maintain.

Here's a step-by-step approach to tackle it:

  1. Ensure Test Coverage: First, verify that this method is covered by unit tests. If it's not, write the necessary tests and commit them separately. This ensures you can detect any unintended changes in functionality during the refactoring process.
  2. Select the Right Refactoring Technique: For a too long body function, the Extract Method technique is often recommended. This involves breaking the function down into smaller sub-methods.
  3. Refactor Incrementally: Start extracting portions of the code into new methods one at a time. This incremental approach minimizes the risk of introducing errors.
  4. Run Tests After Each Change: After each modification, run your unit tests. If all tests pass, you can confidently commit the change. If any tests fail, address the issues before proceeding.
  5. Simplify Complex Conditionals: Once the method is broken down, focus on simplifying complicated conditional statements. The Decompose Conditional technique helps clarify if statements.
  6. Repeat Testing and Committing: At every stable point in your refactoring—such as after simplifying a conditional—run your tests again. If everything checks out, commit your changes before moving on to the next section.

By adhering to this structured methodology, you'll gradually enhance the readability and maintainability of your code without introducing new bugs. Validating each step with tests ensures that the application's behavior remains consistent from the user's perspective, while also making future development easier.

Use the force of your IDE

Modern IDEs come equipped with a suite of automated refactoring tools—such as method extraction, renaming, and code restructuring. By using these features, you can streamline your refactoring process, save time, and significantly reduce the risk of introducing new errors into your codebase.

The Mikado Method

When facing refactorings too complex for standard techniques, the Mikado Method offers an effective approach. Imagine each Mikado stick as a segment of your codebase. Attempting to change a deep stick can cause the entire stack to collapse—leading to bugs and broken functionality.

Here's how to apply the Mikado Method:

  1. Clearly Define what you aim to achieve with your refactoring.
  2. Implement the Change and Observe Failures
  3. Identify Obstacles: Determine what's causing issues and what needs to be modified beforehand to make your change work.
  4. Revert and Plan: Set aside your changes (using stash or a separate branch), then restart from step 1, targeting the newly identified obstacle.

By resolving dependencies one at a time—recursively—you can achieve your refactoring goal without disrupting other parts of your codebase. This method makes the large-scale refactoring process safer. Creating a Mikado Graph can help you visualize and keep track of all the dependencies involved.

Practical Example:

To bring the Mikado Method to life, let's draw an analogy with Han Solo and Chewbacca, who are looking to upgrade the Millennium Falcon for a quicker escape from the Empire.

Day 1: Installing the New Hyperdrive

Han decides to install a new hyperdrive to boost the ship's speed. Excited about the upgrade, he removes the old unit and fits the new one he just bought. However, when he tries to connect it, he realizes the existing wiring isn't compatible with the new hardware.

Acknowledging the issue, Han reinstalls the old hyperdrive to keep the Millennium Falcon operational. He understands that before upgrading the hyperdrive, he needs to address the wiring incompatibility.

Day 2: Updating the Wiring

The next day, Chewbacca takes on the task of installing new, compatible wiring. Then they run diagnostics through the ship's computer but encounter another problem: the power supply requires reprogramming to support the updated wiring. Not wanting to leave the ship in an unstable state, Chewbacca reinstalls the old wiring to ensure they're ready for immediate departure if needed.

Day 3: Reprogramming and Finalizing the Upgrade

On the third day, Han and Chewbacca reprogram the power supply to accommodate the new wiring. With this working, they reinstall the new wiring and fit the new hyperdrive. This time, all systems are OK. The Millennium Falcon is now faster and ready to run away from the Empire at any moment.

Alternative Techniques

While we've discussed proven refactoring strategies, there are other methods available—such as using Proxy or Facade patterns to isolate the code you're refactoring. However, these approaches can introduce additional complexity and potentially bring new bugs into your codebase. Because they're more permissive, there's a risk of letting errors slip through. It's generally advisable to stick with well-established techniques that prioritize stability and clarity.

What's Next?

Now that we've identified the roots of technical debt in the first article and explored how to refactor without introducing new bugs in this one, the next challenge is finding the time to put it into practice. While small refactorings can be smoothly integrated into your daily tasks, larger ones demand careful planning and transparent communication with your team or clients. In the upcoming and final article of this series, we'll delve into how to demonstrate to your team that investing in code quality benefits everyone.

Additional Resources

Développeur mobile ?

Rejoins nos équipes