Refactoring for C# Developers — Safe Improvements
Refactoring techniques for C# code: extract method, replace conditional with polymorphism, introduce parameter object, remove duplication, and use IDE tooling to refactor safely.
Refactoring for C# Developers — Safe Improvements
Refactoring is changing the structure of code without changing its behaviour. Done with tests in place, it reduces complexity and makes future changes cheaper.
Extract Method
The most common refactoring. Pull a block of code into a named method.
// BEFORE — hard to understand without reading the whole method
public decimal CalculateInvoice(Order order)
{
decimal subtotal = 0;
foreach (var item in order.Items)
subtotal += item.Price * item.Quantity;
decimal discount = 0;
if (order.Customer.IsLoyalty && subtotal > 100)
discount = subtotal * 0.10m;
decimal tax = (subtotal - discount) * 0.20m;
return subtotal - discount + tax;
}
// AFTER — each concept has a name
public decimal CalculateInvoice(Order order)
{
decimal subtotal = CalculateSubtotal(order.Items);
decimal discount = CalculateLoyaltyDiscount(order.Customer, subtotal);
decimal tax = CalculateTax(subtotal, discount);
return subtotal - discount + tax;
}
private decimal CalculateSubtotal(IEnumerable<OrderItem> items)
=> items.Sum(i => i.Price * i.Quantity);
private decimal CalculateLoyaltyDiscount(Customer customer, decimal subtotal)
=> customer.IsLoyalty && subtotal > 100 ? subtotal * 0.10m : 0;
private decimal CalculateTax(decimal subtotal, decimal discount)
=> (subtotal - discount) * 0.20m;Introduce Parameter Object
When a method takes too many related parameters, bundle them into a class.
// BEFORE — 5 parameters, easy to get order wrong
public PagedResult<Order> SearchOrders(
string? customerId,
DateTime? from,
DateTime? to,
int page,
int pageSize)
{
// ...
}
// Called with easy-to-confuse positional args:
SearchOrders("CUST-001", null, DateTime.Today, 1, 20);
// AFTER — parameter object is self-documenting
public class OrderSearchQuery
{
public string? CustomerId { get; init; }
public DateTime? From { get; init; }
public DateTime? To { get; init; }
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 20;
}
public PagedResult<Order> SearchOrders(OrderSearchQuery query)
{
// ...
}
// Called with named properties — impossible to confuse order
SearchOrders(new OrderSearchQuery
{
CustomerId = "CUST-001",
To = DateTime.Today,
});Replace Conditional with Polymorphism
Large if/switch on type is a smell. Replace with a hierarchy or strategy.
// BEFORE — adding a new discount type requires modifying this method
public decimal CalculateDiscount(Customer customer, decimal amount)
{
if (customer.Type == "Standard")
return 0;
else if (customer.Type == "Silver")
return amount * 0.05m;
else if (customer.Type == "Gold")
return amount * 0.10m;
else if (customer.Type == "Platinum")
return amount * 0.20m;
return 0;
}
// AFTER — adding a new tier means adding a class, not modifying existing code
public interface IDiscountPolicy
{
decimal Calculate(decimal amount);
}
public class NoDiscount : IDiscountPolicy
{
public decimal Calculate(decimal amount) => 0;
}
public class PercentageDiscount(decimal rate) : IDiscountPolicy
{
public decimal Calculate(decimal amount) => amount * rate;
}
// Register by name (factory or DI)
public class DiscountPolicyFactory
{
private static readonly Dictionary<string, IDiscountPolicy> _policies = new()
{
["Standard"] = new NoDiscount(),
["Silver"] = new PercentageDiscount(0.05m),
["Gold"] = new PercentageDiscount(0.10m),
["Platinum"] = new PercentageDiscount(0.20m),
};
public IDiscountPolicy Get(string customerType)
=> _policies.TryGetValue(customerType, out var policy)
? policy
: new NoDiscount();
}Replace Magic Conditional with Specification
// BEFORE — business rule buried in service logic
public bool IsEligibleForPromotion(Customer customer)
{
return customer.TotalSpend > 500
&& customer.JoinDate < DateTime.UtcNow.AddYears(-1)
&& !customer.HasActiveClaim;
}
// AFTER — specification makes the rule explicit and composable
public interface ISpecification<T>
{
bool IsSatisfiedBy(T subject);
}
public class HighSpenderSpecification : ISpecification<Customer>
{
public bool IsSatisfiedBy(Customer c) => c.TotalSpend > 500;
}
public class LoyalCustomerSpecification : ISpecification<Customer>
{
public bool IsSatisfiedBy(Customer c)
=> c.JoinDate < DateTime.UtcNow.AddYears(-1);
}
public class NoActiveClaimSpecification : ISpecification<Customer>
{
public bool IsSatisfiedBy(Customer c) => !c.HasActiveClaim;
}
// Compose with And / Or operators if needed, or inline:
var isEligible = new HighSpenderSpecification().IsSatisfiedBy(customer)
&& new LoyalCustomerSpecification().IsSatisfiedBy(customer)
&& new NoActiveClaimSpecification().IsSatisfiedBy(customer);Inline Variable / Remove Noise
// BEFORE — variables that add no information
public bool IsOrderExpired(Order order)
{
DateTime expiryDate = order.CreatedAt.AddDays(30);
bool isExpired = DateTime.UtcNow > expiryDate;
return isExpired;
}
// AFTER — direct expression when no explanation is needed
public bool IsOrderExpired(Order order)
=> DateTime.UtcNow > order.CreatedAt.AddDays(30);
// BEFORE — temporary variable just passed through
var list = new List<string>();
list.Add("a");
list.Add("b");
return list;
// AFTER — collection initialiser
return new List<string> { "a", "b" };Refactoring Safety Checklist
Before refactoring:
✓ Tests pass (if no tests: write characterisation tests first)
✓ Source control commit — gives you a rollback point
During refactoring:
✓ One refactoring at a time — don't mix behaviour changes
✓ Run tests after each change
✓ Use IDE tools (Rider/VS rename, extract method) over manual edits
After refactoring:
✓ Tests still pass
✓ Review diff — is the new code clearly simpler or more expressive?
✓ Commit with a message that says what was refactored (not what the code does)Interview Answer
"Refactoring is changing code structure without changing behaviour — always done with tests as the safety net. Key techniques: Extract Method (pull a named block out of a long method), Introduce Parameter Object (replace many related parameters with a class), Replace Conditional with Polymorphism (open/closed — add new behaviour by adding classes, not modifying switch statements), and Inline Variable (remove variables that don't add explanatory value). In C#, IDE tooling (Visual Studio or Rider) automates most refactorings safely — rename, extract method, introduce parameter — which eliminates manual error. Refactoring debt accumulates when features are added to code that was never designed to support them; the right time to refactor is before adding a new feature, not after."
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.