Refactoring C# Code - 4 Essential Techniques Simplified

I've been writing code for 20+ years, and along the way, I've written a ton of code that I needed to go back and refactor. It happens. In fact, it's almost guaranteed if you want to keep a codebase alive as you're evolving it. As a result, I've done a lot of refactoring C# code and want to share with you some simple refactoring techniques which can be especially useful if you're just getting started.


What's In This Article: Techniques for Refactoring C# Code

Remember to check out these platforms:

// FIXME: social media icons coming back soon!


1 - Extract Method

Refactoring code is an important skill for software engineers, as it helps improve the readability and maintainability of our codebase. One such technique that can greatly assist us in achieving this goal is the Extract Method technique. I use this so frequently that almost immediately after I write some code I do this!

The Concept of Extract Method

The Extract Method technique involves taking a block of code within a method and extracting it into a separate method. By doing this, we can improve the clarity and understandability of our code by breaking it down into smaller, more manageable pieces. Don't wait until you have a 2000-line method before you figure out why this is helpful!

To demonstrate this technique, consider the following example in C#:

public class Calculator
{
    public int Add(int a, int b)
    {
        Console.WriteLine($"Adding {a} and {b}...");
        int result = a + b;
        Console.WriteLine("The result is: " + result);
        return result;
    }
}

In the above code, the Add method is responsible for:

  • Printing out the calculation info
  • Performing the addition operation
  • Displaying the result

It's a pretty simple method, but it's doing a handful of things already. By applying the Extract Method technique, we can separate the concerns and create a new method solely responsible for displaying info.

public class Calculator
{
    public int Add(int a, int b)
    {
        DisplayOperationInfo(a, b);
        int sum = a + b;
        DisplaySum(sum);
        return sum;
    }

    private void DisplayOperationInfo(int a, int b)
    {
        // TODO: could we go even further and pass in
        // the operation string?!
        Console.WriteLine($"Adding {a} and {b}...");
    }

    private void DisplayResult(int result)
    {
        Console.WriteLine("The result is: " + result);
    }
}

In this particular case, this technique is not helping us reduce the duplication of existing code (right now), but it is helping us bring some organization to the code. Given that this is a very trivial example, the real benefit of this is hard to observe -- I mean, I added more code overall to make this happen. However, I'm trying to illustrate that we can conceptually group code into methods for better expression of what we're trying to achieve. It's just hard to show super simple examples of this!

Benefits of Extract Method

Using the Extract Method technique provides several benefits. Firstly, it improves the readability of our code by creating smaller, self-explanatory methods that focus on a single responsibility. This makes it easier for other developers to understand and maintain the code. This is what I tried to illustrate in the example code prior.

Secondly, it promotes code reusability. By extracting a block of code into a separate method, we can easily reuse it in multiple places within our codebase. This reduces duplication and enforces the DRY (Don't Repeat Yourself) principle. If we go to add other operators to our calculator, we could re-use the DisplayResult method and even refactor the DisplayOperationInfo method to pass in a parameter for which operation we're performing!

Scenarios Where Extract Method is Useful

The Extract Method technique is particularly useful in scenarios where:

  1. A block of code within a method is becoming too long and complex - By extracting the code into a separate method, we can make the original method more concise and easier to understand.
  2. Common functionality needs to be reused across different methods - Extracting the common functionality into a separate method promotes code reuse and eliminates duplication.
  3. A method is responsible for multiple concerns - By extracting specific parts of the method into separate methods, we can improve the clarity and maintainability of the code.

The Extract Method technique is a fundamental refactoring technique that once you start using, it almost becomes second nature in how you're coding. By breaking down complex methods into smaller, focused ones, we can create code that is easier to understand and maintain.


2 - Rename Variables and Methods

Clear and descriptive names are important in software engineering as they enhance code readability and maintainability. We spend so much of our time as programmers reading code that it's critical we understand how to write expressive and concise code. When naming variables and methods, it's important to choose names that accurately reflect their purpose and functionality.

But guess what? Things change and evolve. The awesome name you picked today might not be so awesome 12 months from now when you're trying to read your code and understand what the heck is going on. That's cool because not only does it show you're growing and improving, but because we have tools that can make it easy for us to make such changes.

Importance of Clear and Descriptive Names

Naming variables and methods with clear and descriptive names enables other developers to quickly understand their purpose and how they are used. This not only improves the readability of the code but also makes it easier to maintain and debug.

Consider the following example:

int x = 10;
int y = 5;
int z = x + y;

In this code snippet, it is not immediately clear what the purpose of the variables x, y, and z is. By renaming the variables to more descriptive names, we can make the code more self-explanatory:

int dollarsFromJack = 10;
int dollarsFromJill = 5;
int totalSpendingMoney = dollarsFromJack + dollarsFromJill;

Now, it is clear that dollarsFromJack and dollarsFromJill represent the numbers being added, and totalSpendingMoney represents their result -- AND we get some context for why we're using these numbers in the first place.

Using Visual Studio to Rename

When renaming variables and methods, it is important to follow a consistent naming convention. In C#, the common practice is to use PascalCase for method names and camelCase for variable names. But what happens if you picked a crappy name? What happens if you forgot about the naming conventions and need to go update the usage of something in 100 spots?

In Visual Studio, we have access to a rename refactoring shortcut. By default, this is F2 on your keyboard. Simply:

  • Highlight your variable that you want to rename
  • Press F2
  • Type in your new awesome name
  • Press enter
  • ...
  • Profit!

You can repeat this for many things like classes, interfaces, method names... You name it! (pun intended?)

Renaming things is one of the safest types of refactoring that we can do *IF* (and this is a REALLY BIG IF) you are renaming across all of the areas that are consuming your code. So for really small projects, especially if you don't dynamically load any types by name or heavily use reflection, you're probably okay if there's nobody consuming your library. But if you're creating a public API or otherwise can't update all of the consuming code paths at the same time you're renaming - You're probably going to break things.

Naming Conventions and Best Practices

Naming conventions are going to come down to stylistic choices that may vary from team to team. However, there's handful of general tenets that apply when considering how you name:

  1. Use descriptive names that accurately reflect the purpose of the variable or method.
  2. Avoid using single-character or ambiguous names, as they can make the code harder to understand.
  3. Consider using nouns for variables and verbs for methods to make their purpose clear.
  4. Avoid using reserved words or common language constructs as names.
  5. Consider reducing redundancy in your names.

Remember to discuss with your team about what things they value in naming and what styles they adhere to.


3 - Extract Interface

The Extract Interface refactoring technique is another thing that I rely on heavily in my own development. It involves creating an interface that defines a contract for a certain set of methods or properties, and then extracting that interface from existing code. This technique can help increase modularity, improve code reusability, and enhance testability. I generally start with the class itself so I can focus on what I'll need passed in and what I can provide back out, which I find helps me come up with a better API contract for the interface.

Let's consider an example where we have a class called Calculator that performs various mathematical operations:

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Subtract(int a, int b)
    {
        return a - b;
    }

    public int Multiply(int a, int b)
    {
        return a * b;
    }

    public int Divide(int a, int b)
    {
        return a / b;
    }
}

In this case, we can extract an interface, let's call it ICalculator, which will define the contract for these mathematical operations:

public interface ICalculator
{
    int Add(int a, int b);

    int Subtract(int a, int b);

    int Multiply(int a, int b);

    int Divide(int a, int b);
}

Once we have the interface, we can modify our Calculator class to implement it:

public class Calculator : ICalculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Subtract(int a, int b)
    {
        return a - b;
    }

    public int Multiply(int a, int b)
    {
        return a * b;
    }

    public int Divide(int a, int b)
    {
        return a / b;
    }
}

Now, instead of directly using the Calculator class, we can program to the interface by using ICalculator:

public void Calculate(ICalculator calculator)
{
    int result = calculator.Add(5, 3);
    // Do something with the result
}

And yes - I know this is a trivial example and an interface for this is likely overkill. However, remember that calculator I included at the beginning of these examples? The one that writes to the console? Yeah, if you wanted to swap that out for one that wasn't blowing up your console output then you could at least! But in all seriousness, this is just to illustrate to beginners that the interface defines an API and the class has the implementation.

Advantages of Extracting Interfaces

There are several advantages to using interfaces in software design and development:

  • Promote loose coupling between components - By programming to an interface rather than a specific implementation, you can easily swap out different implementations without affecting the rest of the codebase. This promotes flexibility and extensibility.
  • Code reusability - By defining a contract through an interface, you can create multiple classes that implement that interface. This allows for the reuse of code across different classes and reduces duplication.
  • Improve testability - With interfaces, you can easily create mock objects or stubs for testing purposes. This makes it simpler to write unit tests and verify the behavior of your code in isolation.

When should you use the Extract Interface technique? It's particularly useful when you have multiple classes that share a common set of methods or properties. By extracting an interface, you can ensure consistency among these classes and simplify their usage.

Of course, if you program like me, you potentially over-use interfaces. Sometimes there's no real benefit to slapping an interface onto a class. Truly. I'm the kind of person who likes to mock any dependency when I unit test (I have other tests for real dependencies), and I *still* don't think it makes sense to use an interface with everything. So use your judgement!


4 - Simplify Conditional Expressions

When writing code, it's important to have clear and concise conditional expressions. Complicated conditionals can lead to confusion and make code harder to read and maintain. When conditionals are too big, they're hard to understand. When conditionals are too compact with poorly-named acronym or single-character variables, they're hard to understand. So strike the right balance of verbosity to get your intention expressed clearly.

Use the Ternary Operator:

The ternary operator is a concise way to write simple conditional expressions. It has the form condition ? trueExpression : falseExpression. By using the ternary operator, you can replace if-else statements with a single line of code. Here's an example:

bool isEven = (number % 2 == 0) ? true : false;

What's not a fun time? Stacking multiple of these together. Try to avoid that.

Extract Complex Conditions into Separate Boolean Variables:

If you have complex conditions with multiple logical operators, it can be challenging to understand their meaning at a glance. To improve readability, consider extracting the conditions into separate boolean variables with descriptive names. Here's an example:

bool hasEnoughStock = (quantity > 0 && stockCount > 0);
bool isEligibleForDiscount = (customerAge >= 60 || isEmployee);
if (hasEnoughStock && isEligibleForDiscount)
{
    // Apply discount logic here
}

This code is much easier to read than trying to put all four conditions directly into the if statement, especially because we get added context with clear variable names!

Use the Null-Conditional Operator:

The null-conditional operator (?.) is a handy feature introduced in C# 6.0. It allows you to safely access members of an object, even if the object itself is null. This operator can simplify the null-checking code by avoiding unnecessary if statements. Here's an example:

// Will be null if 'text' is null
int? length = text?.Length;

This one is good to discuss with your teams first. The shorthand here may not be obvious to folks, especially if they're not used to this syntax in the code base.


Wrapping Up Refactoring C# Code

In this article, I've covered 4 important techniques for refactoring C# code

  • Extract Method
  • Rename Variables and Methods
  • Extract Interface
  • Simplify Conditional Expressions

These techniques are important for software engineers looking to improve the readability, maintainability, and overall quality of their code. Remember, refactoring code is not just about improving the aesthetics, but about making it easier to work with and maintain in the long run. If you found this useful and you're looking for more learning opportunities, consider subscribing to my free weekly software engineering newsletter and check out my free videos on YouTube!