Skip to main content

C# Refactoring Techniques

Introduction

Refactoring is the process of restructuring existing code without changing its external behavior. Think of it as cleaning and organizing your room - everything is still there, but it's arranged better. In C# development, refactoring helps you improve code quality, reduce technical debt, and make your codebase more maintainable over time.

This guide will introduce you to common refactoring techniques in C# that every developer should know. These techniques will help you write cleaner, more efficient, and more maintainable code.

Why Refactoring Matters

Before diving into specific techniques, let's understand why refactoring is important:

  • Improves code readability: Makes code easier to understand for you and other developers
  • Reduces complexity: Simplifies complex logic and structures
  • Eliminates code duplication: Follows the DRY (Don't Repeat Yourself) principle
  • Makes code more maintainable: Easier to fix bugs and add features
  • Improves performance: Often results in more efficient code

Common Code Smells

"Code smells" are indicators that something might be wrong with your code. Here are some common code smells in C# that signal the need for refactoring:

  • Long methods: Methods that do too many things
  • Large classes: Classes with too many responsibilities
  • Duplicate code: The same code appearing in multiple places
  • Long parameter lists: Methods with too many parameters
  • Switch statements: Especially those that change frequently
  • Feature envy: A class that seems more interested in another class's data
  • Primitive obsession: Using primitive types instead of small objects

Essential Refactoring Techniques

1. Extract Method

This technique involves taking a code fragment and turning it into a method with a name that explains its purpose.

Before refactoring:

csharp
public void PrintInvoice()
{
// Print header
Console.WriteLine("INVOICE");
Console.WriteLine("========");

// Print customer details
Console.WriteLine($"Customer: {customer.Name}");
Console.WriteLine($"Address: {customer.Address}");

// Print items
foreach (var item in items)
{
Console.WriteLine($"{item.Name}: ${item.Price}");
}

// Print total
double total = 0;
foreach (var item in items)
{
total += item.Price;
}
Console.WriteLine($"Total: ${total}");
}

After refactoring:

csharp
public void PrintInvoice()
{
PrintHeader();
PrintCustomerDetails();
PrintItems();
PrintTotal();
}

private void PrintHeader()
{
Console.WriteLine("INVOICE");
Console.WriteLine("========");
}

private void PrintCustomerDetails()
{
Console.WriteLine($"Customer: {customer.Name}");
Console.WriteLine($"Address: {customer.Address}");
}

private void PrintItems()
{
foreach (var item in items)
{
Console.WriteLine($"{item.Name}: ${item.Price}");
}
}

private void PrintTotal()
{
double total = 0;
foreach (var item in items)
{
total += item.Price;
}
Console.WriteLine($"Total: ${total}");
}

2. Extract Class

When a class is doing too much, extract some of its responsibilities into a new class.

Before refactoring:

csharp
public class Person
{
public string Name { get; set; }
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Phone { get; set; }

public string GetFullAddress()
{
return $"{StreetAddress}, {City}, {State} {ZipCode}";
}
}

After refactoring:

csharp
public class Person
{
public string Name { get; set; }
public Address Address { get; set; }
public string Phone { get; set; }
}

public class Address
{
public string StreetAddress { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }

public string GetFullAddress()
{
return $"{StreetAddress}, {City}, {State} {ZipCode}";
}
}

3. Replace Magic Numbers with Constants

Magic numbers are hard-coded values in your code that have no obvious meaning. Replace them with named constants.

Before refactoring:

csharp
public double CalculateDiscount(double price)
{
if (price > 1000)
{
return price * 0.1;
}
return 0;
}

After refactoring:

csharp
private const double DISCOUNT_THRESHOLD = 1000;
private const double DISCOUNT_RATE = 0.1;

public double CalculateDiscount(double price)
{
if (price > DISCOUNT_THRESHOLD)
{
return price * DISCOUNT_RATE;
}
return 0;
}

4. Introduce Parameter Object

When you have several parameters that naturally go together, replace them with an object.

Before refactoring:

csharp
public void ScheduleMeeting(string topic, DateTime startDate, DateTime endDate, string[] attendees, string location)
{
// Schedule the meeting
}

After refactoring:

csharp
public class MeetingDetails
{
public string Topic { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string[] Attendees { get; set; }
public string Location { get; set; }
}

public void ScheduleMeeting(MeetingDetails meeting)
{
// Schedule the meeting
}

5. Replace Conditional with Polymorphism

Replace complex conditional logic with polymorphism to make your code more object-oriented.

Before refactoring:

csharp
public double CalculatePay(Employee employee)
{
switch (employee.Type)
{
case EmployeeType.Regular:
return employee.HoursWorked * employee.HourlyRate;
case EmployeeType.Manager:
return (employee.HoursWorked * employee.HourlyRate) + employee.Bonus;
case EmployeeType.Executive:
return employee.MonthlySalary;
default:
throw new ArgumentException("Invalid employee type");
}
}

After refactoring:

csharp
public abstract class Employee
{
public abstract double CalculatePay();
}

public class RegularEmployee : Employee
{
public double HoursWorked { get; set; }
public double HourlyRate { get; set; }

public override double CalculatePay()
{
return HoursWorked * HourlyRate;
}
}

public class Manager : Employee
{
public double HoursWorked { get; set; }
public double HourlyRate { get; set; }
public double Bonus { get; set; }

public override double CalculatePay()
{
return (HoursWorked * HourlyRate) + Bonus;
}
}

public class Executive : Employee
{
public double MonthlySalary { get; set; }

public override double CalculatePay()
{
return MonthlySalary;
}
}

6. Remove Duplicate Code

Identify and eliminate duplicate code by extracting it into a shared method.

Before refactoring:

csharp
public void ProcessCustomerOrder(Order order)
{
// Validate order
if (order.Items.Count == 0)
{
throw new ArgumentException("Order must have at least one item");
}

// Process payment
PaymentProcessor.Process(order.Payment);

// Update inventory
foreach (var item in order.Items)
{
Inventory.Reduce(item.ProductId, item.Quantity);
}

// Send confirmation email
string message = $"Thank you for your order, {order.Customer.Name}!";
EmailSender.Send(order.Customer.Email, "Order Confirmation", message);
}

public void ProcessGiftOrder(GiftOrder order)
{
// Validate order
if (order.Items.Count == 0)
{
throw new ArgumentException("Order must have at least one item");
}

// Process payment
PaymentProcessor.Process(order.Payment);

// Update inventory
foreach (var item in order.Items)
{
Inventory.Reduce(item.ProductId, item.Quantity);
}

// Send gift message
string message = $"You've received a gift from {order.Sender.Name}!";
EmailSender.Send(order.Recipient.Email, "You've Received a Gift!", message);
}

After refactoring:

csharp
private void ValidateOrder(IOrder order)
{
if (order.Items.Count == 0)
{
throw new ArgumentException("Order must have at least one item");
}
}

private void ProcessPaymentAndUpdateInventory(IOrder order)
{
// Process payment
PaymentProcessor.Process(order.Payment);

// Update inventory
foreach (var item in order.Items)
{
Inventory.Reduce(item.ProductId, item.Quantity);
}
}

public void ProcessCustomerOrder(Order order)
{
ValidateOrder(order);
ProcessPaymentAndUpdateInventory(order);

// Send confirmation email
string message = $"Thank you for your order, {order.Customer.Name}!";
EmailSender.Send(order.Customer.Email, "Order Confirmation", message);
}

public void ProcessGiftOrder(GiftOrder order)
{
ValidateOrder(order);
ProcessPaymentAndUpdateInventory(order);

// Send gift message
string message = $"You've received a gift from {order.Sender.Name}!";
EmailSender.Send(order.Recipient.Email, "You've Received a Gift!", message);
}

Real-World Refactoring Example: Shopping Cart Application

Let's see how we can apply multiple refactoring techniques to improve a shopping cart implementation:

Original code:

csharp
public class ShoppingCart
{
private List<Product> products = new List<Product>();

public void AddProduct(Product product)
{
products.Add(product);
}

public void RemoveProduct(int productId)
{
foreach (var product in products.ToList())
{
if (product.Id == productId)
{
products.Remove(product);
break;
}
}
}

public double CalculateTotal()
{
double total = 0;
foreach (var product in products)
{
total += product.Price;

// Apply discount for expensive items
if (product.Price > 100)
{
total -= product.Price * 0.1;
}
}

// Apply tax
total += total * 0.08;

// Apply shipping cost
if (total < 50)
{
total += 5.99;
}

return total;
}

public void Checkout(string customerName, string address, string creditCardNumber)
{
double total = CalculateTotal();

Console.WriteLine($"Order for: {customerName}");
Console.WriteLine($"Shipping to: {address}");
Console.WriteLine("Items:");

foreach (var product in products)
{
Console.WriteLine($"- {product.Name}: ${product.Price}");
}

Console.WriteLine($"Total: ${total}");
Console.WriteLine($"Payment processed with card ending in: {creditCardNumber.Substring(creditCardNumber.Length - 4)}");

// Clear cart after checkout
products.Clear();
}
}

Refactored code:

csharp
public class ShoppingCart
{
private List<Product> products = new List<Product>();
private const double TAX_RATE = 0.08;
private const double SHIPPING_THRESHOLD = 50;
private const double SHIPPING_COST = 5.99;
private const double DISCOUNT_THRESHOLD = 100;
private const double DISCOUNT_RATE = 0.1;

public void AddProduct(Product product)
{
products.Add(product);
}

public void RemoveProduct(int productId)
{
var productToRemove = products.FirstOrDefault(p => p.Id == productId);
if (productToRemove != null)
{
products.Remove(productToRemove);
}
}

public double CalculateTotal()
{
double subtotal = CalculateSubtotal();
double discountAmount = CalculateDiscounts();
double taxAmount = CalculateTax(subtotal - discountAmount);
double shippingCost = CalculateShippingCost(subtotal - discountAmount);

return subtotal - discountAmount + taxAmount + shippingCost;
}

private double CalculateSubtotal()
{
return products.Sum(p => p.Price);
}

private double CalculateDiscounts()
{
return products
.Where(p => p.Price > DISCOUNT_THRESHOLD)
.Sum(p => p.Price * DISCOUNT_RATE);
}

private double CalculateTax(double amount)
{
return amount * TAX_RATE;
}

private double CalculateShippingCost(double amount)
{
return amount < SHIPPING_THRESHOLD ? SHIPPING_COST : 0;
}

public Receipt Checkout(Customer customer, PaymentInfo paymentInfo)
{
double total = CalculateTotal();

var receipt = new Receipt
{
Customer = customer,
Items = products.ToList(),
Total = total,
PaymentMethod = $"Card ending in {paymentInfo.GetMaskedCardNumber()}"
};

ProcessPayment(paymentInfo, total);
ClearCart();

return receipt;
}

private void ProcessPayment(PaymentInfo paymentInfo, double amount)
{
// Payment processing logic here
Console.WriteLine($"Processing payment of ${amount} with {paymentInfo.GetMaskedCardNumber()}");
}

private void ClearCart()
{
products.Clear();
}
}

public class Customer
{
public string Name { get; set; }
public Address ShippingAddress { get; set; }
}

public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }

public override string ToString()
{
return $"{Street}, {City}, {State} {ZipCode}";
}
}

public class PaymentInfo
{
public string CardholderName { get; set; }
public string CreditCardNumber { get; set; }
public string ExpirationDate { get; set; }
public string CVV { get; set; }

public string GetMaskedCardNumber()
{
if (string.IsNullOrEmpty(CreditCardNumber) || CreditCardNumber.Length < 4)
{
return "Invalid card";
}

return $"****-****-****-{CreditCardNumber.Substring(CreditCardNumber.Length - 4)}";
}
}

public class Receipt
{
public Customer Customer { get; set; }
public List<Product> Items { get; set; }
public double Total { get; set; }
public string PaymentMethod { get; set; }

public void Print()
{
Console.WriteLine($"Order for: {Customer.Name}");
Console.WriteLine($"Shipping to: {Customer.ShippingAddress}");
Console.WriteLine("Items:");

foreach (var item in Items)
{
Console.WriteLine($"- {item.Name}: ${item.Price}");
}

Console.WriteLine($"Total: ${Total}");
Console.WriteLine($"Payment processed with {PaymentMethod}");
}
}

In this refactoring:

  1. Extract Method - Split the large CalculateTotal method into smaller, focused methods
  2. Extract Class - Created separate Customer, Address, PaymentInfo, and Receipt classes
  3. Replace Magic Numbers with Constants - Added constants for tax rate, discount threshold, etc.
  4. Introduce Parameter Object - Replaced individual customer parameters with a Customer object
  5. Remove Duplicate Code - Cleaned up duplicate logic and used LINQ for cleaner code
  6. Single Responsibility Principle - Each class now has a focused responsibility

Tools for Refactoring in C#

Several tools can help you with refactoring in C#:

  1. Visual Studio's built-in refactoring tools: Press Ctrl+. to access the Quick Actions menu for refactoring suggestions
  2. ReSharper: A popular Visual Studio extension with powerful refactoring capabilities
  3. CodeRush: Another Visual Studio extension for refactoring
  4. Rider: JetBrains IDE for .NET development with excellent refactoring support

Refactoring Best Practices

  1. Start with tests: Ensure you have good test coverage before refactoring
  2. Make small, incremental changes: Don't try to refactor everything at once
  3. Test after each change: Make sure your changes don't break existing functionality
  4. Use version control: Commit frequently so you can roll back if necessary
  5. Follow coding standards: Make sure your refactored code follows your team's standards
  6. Document your changes: Let others know why you made certain refactoring decisions

Summary

Refactoring is a crucial skill for C# developers who want to write maintainable, high-quality code. By regularly applying these refactoring techniques, you'll improve code readability, reduce complexity, and make your codebase more maintainable. Remember that refactoring is an ongoing process—it's not something you do once and forget about.

Start with small, safe refactorings and gradually tackle larger code smells as you gain confidence. Always ensure you have tests to verify that your refactored code maintains the expected behavior.

Additional Resources

  1. Books:

    • "Refactoring: Improving the Design of Existing Code" by Martin Fowler
    • "Clean Code: A Handbook of Agile Software Craftsmanship" by Robert C. Martin
  2. Online Resources:

Exercises

  1. Identify three code smells in your current C# project and apply appropriate refactoring techniques
  2. Take a long method (more than 50 lines) and break it down using the Extract Method technique
  3. Find a class with too many responsibilities and refactor it using the Extract Class technique
  4. Practice replacing conditional statements with polymorphism in a switch statement you have in your code
  5. Set up automated tests for a piece of code before refactoring it, then refactor while ensuring the tests still pass


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)