.NET Refactoring
Introduction
Refactoring is the process of restructuring existing code without changing its external behavior. It's like renovating a house while keeping its foundation intact. In .NET development, refactoring is an essential practice that helps maintain code quality, readability, and extensibility as your application grows.
This guide will introduce you to common refactoring techniques in .NET, explain when and how to apply them, and provide practical examples to help you incorporate refactoring into your development workflow.
Why Refactoring Matters
Before diving into specific techniques, let's understand why refactoring is crucial:
- Improved Readability: Makes code easier to understand for you and other developers
- Enhanced Maintainability: Simplifies future modifications and bug fixes
- Better Performance: Often leads to more efficient code execution
- Technical Debt Reduction: Addresses accumulated code issues before they become problematic
- Easier Testing: Well-structured code is typically easier to test
Common Code Smells in .NET Applications
"Code smells" are indicators that your code might need refactoring. Here are some common ones in .NET applications:
- Long Methods: Methods that do too many things
- Large Classes: Classes with too many responsibilities
- Duplicate Code: Similar code repeated throughout the application
- Complex Conditional Logic: Nested if statements or switch blocks
- Primitive Obsession: Using primitive types instead of small objects
- Feature Envy: A method that uses more features of another class than its own
Essential Refactoring Techniques for .NET
1. Extract Method
This technique involves taking a code fragment and turning it into a method with a name that explains its purpose.
Before:
public decimal CalculateInvoice(Order order)
{
decimal total = 0;
// Calculate subtotal
foreach (var item in order.LineItems)
{
total += item.Price * item.Quantity;
}
// Apply discount
if (order.Customer.IsPreferred)
{
total *= 0.9m; // 10% discount
}
// Add tax
total *= 1.07m; // 7% tax
return total;
}
After:
public decimal CalculateInvoice(Order order)
{
decimal subtotal = CalculateSubtotal(order);
decimal discountedTotal = ApplyDiscount(subtotal, order.Customer);
decimal finalTotal = ApplyTax(discountedTotal);
return finalTotal;
}
private decimal CalculateSubtotal(Order order)
{
decimal subtotal = 0;
foreach (var item in order.LineItems)
{
subtotal += item.Price * item.Quantity;
}
return subtotal;
}
private decimal ApplyDiscount(decimal total, Customer customer)
{
if (customer.IsPreferred)
{
return total * 0.9m; // 10% discount
}
return total;
}
private decimal ApplyTax(decimal total)
{
return total * 1.07m; // 7% tax
}
2. Extract Class
When a class is doing too much, it's time to split its responsibilities into separate classes.
Before:
public class Customer
{
public string Name { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public string GetFullAddress()
{
return $"{Street}\n{City}, {State} {ZipCode}";
}
public bool ValidateContactInfo()
{
return !string.IsNullOrEmpty(Phone) && !string.IsNullOrEmpty(Email);
}
}
After:
public class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
public ContactInfo ContactInfo { 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 string GetFullAddress()
{
return $"{Street}\n{City}, {State} {ZipCode}";
}
}
public class ContactInfo
{
public string Phone { get; set; }
public string Email { get; set; }
public bool Validate()
{
return !string.IsNullOrEmpty(Phone) && !string.IsNullOrEmpty(Email);
}
}
3. Replace Conditional with Polymorphism
This technique replaces conditional logic with polymorphic behavior using inheritance and interfaces.
Before:
public class PaymentProcessor
{
public void ProcessPayment(Payment payment)
{
if (payment.Type == PaymentType.CreditCard)
{
// Process credit card payment
ValidateCard(payment);
ChargeCard(payment);
}
else if (payment.Type == PaymentType.PayPal)
{
// Process PayPal payment
ValidatePayPalInfo(payment);
CompletePayPalTransaction(payment);
}
else if (payment.Type == PaymentType.BankTransfer)
{
// Process bank transfer
ValidateBankDetails(payment);
InitiateBankTransfer(payment);
}
}
// Methods for each payment type...
}
public enum PaymentType
{
CreditCard,
PayPal,
BankTransfer
}
public class Payment
{
public PaymentType Type { get; set; }
public decimal Amount { get; set; }
// Other payment properties...
}
After:
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}
public class CreditCardProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
ValidateCard();
ChargeCard(amount);
}
private void ValidateCard() { /* validation logic */ }
private void ChargeCard(decimal amount) { /* charging logic */ }
}
public class PayPalProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
ValidatePayPalInfo();
CompletePayPalTransaction(amount);
}
private void ValidatePayPalInfo() { /* validation logic */ }
private void CompletePayPalTransaction(decimal amount) { /* transaction logic */ }
}
public class BankTransferProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
ValidateBankDetails();
InitiateBankTransfer(amount);
}
private void ValidateBankDetails() { /* validation logic */ }
private void InitiateBankTransfer(decimal amount) { /* transfer logic */ }
}
// Usage:
public class PaymentService
{
private readonly Dictionary<PaymentType, IPaymentProcessor> _processors;
public PaymentService()
{
_processors = new Dictionary<PaymentType, IPaymentProcessor>
{
{ PaymentType.CreditCard, new CreditCardProcessor() },
{ PaymentType.PayPal, new PayPalProcessor() },
{ PaymentType.BankTransfer, new BankTransferProcessor() }
};
}
public void ProcessPayment(Payment payment)
{
if (_processors.TryGetValue(payment.Type, out var processor))
{
processor.ProcessPayment(payment.Amount);
}
else
{
throw new NotSupportedException($"Payment type {payment.Type} is not supported.");
}
}
}
4. Replace Magic Numbers with Constants or Enums
Before:
public decimal CalculateDiscount(decimal price, int customerType)
{
if (customerType == 1) // Regular customer
{
return price * 0.05m; // 5% discount
}
else if (customerType == 2) // Premium customer
{
return price * 0.1m; // 10% discount
}
else if (customerType == 3) // VIP customer
{
return price * 0.15m; // 15% discount
}
return 0;
}
After:
public enum CustomerType
{
Regular = 1,
Premium = 2,
VIP = 3
}
public static class DiscountRates
{
public const decimal RegularDiscount = 0.05m;
public const decimal PremiumDiscount = 0.10m;
public const decimal VIPDiscount = 0.15m;
}
public decimal CalculateDiscount(decimal price, CustomerType customerType)
{
switch (customerType)
{
case CustomerType.Regular:
return price * DiscountRates.RegularDiscount;
case CustomerType.Premium:
return price * DiscountRates.PremiumDiscount;
case CustomerType.VIP:
return price * DiscountRates.VIPDiscount;
default:
return 0;
}
}
5. Introduce Extension Methods
Extension methods allow you to "add" methods to existing types without modifying the original type.
Before:
public class StringHelper
{
public static bool IsValidEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
return false;
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
}
// Usage
if (StringHelper.IsValidEmail(customer.Email))
{
// Do something
}
After:
public static class StringExtensions
{
public static bool IsValidEmail(this string email)
{
if (string.IsNullOrWhiteSpace(email))
return false;
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
}
// Usage
if (customer.Email.IsValidEmail())
{
// Do something
}
Real-world Refactoring Example
Let's refactor a data access method commonly found in many .NET applications:
Original code:
public List<Product> GetProducts(int categoryId, bool includeInactive, string searchTerm)
{
List<Product> results = new List<Product>();
using (var connection = new SqlConnection(_connectionString))
{
connection.Open();
string sql = "SELECT * FROM Products WHERE 1=1";
if (categoryId > 0)
{
sql += " AND CategoryId = @CategoryId";
}
if (!includeInactive)
{
sql += " AND IsActive = 1";
}
if (!string.IsNullOrEmpty(searchTerm))
{
sql += " AND (ProductName LIKE @SearchTerm OR Description LIKE @SearchTerm)";
}
using (var command = new SqlCommand(sql, connection))
{
if (categoryId > 0)
{
command.Parameters.AddWithValue("@CategoryId", categoryId);
}
if (!string.IsNullOrEmpty(searchTerm))
{
command.Parameters.AddWithValue("@SearchTerm", "%" + searchTerm + "%");
}
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
results.Add(new Product
{
Id = (int)reader["Id"],
ProductName = (string)reader["ProductName"],
Description = reader["Description"] != DBNull.Value ? (string)reader["Description"] : null,
Price = (decimal)reader["Price"],
CategoryId = (int)reader["CategoryId"],
IsActive = (bool)reader["IsActive"]
});
}
}
}
}
return results;
}
Refactored code:
public class ProductSearchCriteria
{
public int? CategoryId { get; set; }
public bool IncludeInactive { get; set; }
public string SearchTerm { get; set; }
}
public List<Product> GetProducts(ProductSearchCriteria criteria)
{
var sql = BuildProductSearchQuery(criteria);
return ExecuteProductQuery(sql, criteria);
}
private string BuildProductSearchQuery(ProductSearchCriteria criteria)
{
var sqlBuilder = new StringBuilder("SELECT * FROM Products WHERE 1=1");
if (criteria.CategoryId.HasValue && criteria.CategoryId.Value > 0)
{
sqlBuilder.Append(" AND CategoryId = @CategoryId");
}
if (!criteria.IncludeInactive)
{
sqlBuilder.Append(" AND IsActive = 1");
}
if (!string.IsNullOrEmpty(criteria.SearchTerm))
{
sqlBuilder.Append(" AND (ProductName LIKE @SearchTerm OR Description LIKE @SearchTerm)");
}
return sqlBuilder.ToString();
}
private List<Product> ExecuteProductQuery(string sql, ProductSearchCriteria criteria)
{
List<Product> results = new List<Product>();
using (var connection = new SqlConnection(_connectionString))
using (var command = new SqlCommand(sql, connection))
{
AddParametersToCommand(command, criteria);
connection.Open();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
results.Add(MapProductFromReader(reader));
}
}
}
return results;
}
private void AddParametersToCommand(SqlCommand command, ProductSearchCriteria criteria)
{
if (criteria.CategoryId.HasValue && criteria.CategoryId.Value > 0)
{
command.Parameters.AddWithValue("@CategoryId", criteria.CategoryId.Value);
}
if (!string.IsNullOrEmpty(criteria.SearchTerm))
{
command.Parameters.AddWithValue("@SearchTerm", "%" + criteria.SearchTerm + "%");
}
}
private Product MapProductFromReader(SqlDataReader reader)
{
return new Product
{
Id = (int)reader["Id"],
ProductName = (string)reader["ProductName"],
Description = reader["Description"] != DBNull.Value ? (string)reader["Description"] : null,
Price = (decimal)reader["Price"],
CategoryId = (int)reader["CategoryId"],
IsActive = (bool)reader["IsActive"]
};
}
Tools for .NET Refactoring
Several tools can assist you with refactoring in .NET:
- Visual Studio Built-in Tools: Visual Studio includes powerful refactoring capabilities accessible via right-click menu or keyboard shortcuts
- ReSharper: A popular Visual Studio extension that provides enhanced refactoring capabilities
- Roslynator: A collection of 500+ analyzers and refactorings for C#
- NDepend: Helps identify code quality issues and complexity that might need refactoring
Best Practices for Refactoring
- Follow the Boy Scout Rule: Always leave the code better than you found it
- Refactor Incrementally: Make small, focused changes rather than massive overhauls
- Have Tests in Place: Ensure tests exist before making significant changes
- Use Source Control: Commit frequently during refactoring
- Focus on One Thing: Don't mix feature additions with refactoring
- Communicate: Let your team know when and why you're refactoring code
Summary
Refactoring is an essential skill for every .NET developer. By recognizing code smells and applying appropriate refactoring techniques, you can significantly improve the quality, maintainability, and performance of your .NET applications. Always remember that refactoring is a continuous process—not a one-time event—that should be integrated into your regular development workflow.
Additional Resources
- Refactoring: Improving the Design of Existing Code by Martin Fowler
- Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin
- Microsoft's Documentation on Visual Studio Refactoring
Exercises
- Practice Extract Method: Find a long method (20+ lines) in your codebase and extract parts of it into separate methods
- Implement Extension Methods: Create useful extension methods for string manipulation or DateTime handling
- Reduce Code Duplication: Identify duplicate code across classes and extract it into shared methods/classes
- Replace Conditionals: Find a switch statement or nested if/else block and refactor it using polymorphism
- Refactor Entity Classes: Take a large entity class and break it into smaller, focused classes using extraction techniques
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)