DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Composing Methods: A Practical Guide to Clean Code

Meta Description: Learn how to refactor your code effectively using composing methods. Discover practical techniques like Extract Method, Extract Variable, and Inline Temp with detailed before-and-after examples to write cleaner, maintainable, and more efficient code.

Refactoring is one of the most powerful tools in a developer's arsenal for improving code quality. At the heart of refactoring lies the concept of composing methods. But before we dive into the details, let's step back and answer an important question:

What makes a good method?

A good method is:

  • Concise: It should do one thing and do it well.
  • Understandable: Readable by developers of all skill levels.
  • Maintainable: Easy to modify without breaking functionality.
  • Reusable: General enough to be used in different contexts.

Unfortunately, many methods don’t meet these criteria and require refactoring to improve their structure.


What is Refactoring?

Refactoring means restructuring existing code without changing its external behavior. This is done to improve the design, structure, and implementation of the code.

Before defining refactoring, let’s start with factoring, which is splitting large functions into smaller ones. Refactoring is then the process of changing the factoring to enhance the code's design and readability.


Why Refactor?

Here are some reasons why you might need to refactor:

  1. Methods are too long and difficult to understand.
  2. Repetitive code violates the DRY (Don't Repeat Yourself) principle.
  3. Complex logic increases the likelihood of bugs and makes debugging harder.

Refactoring allows you to create code that is clean, testable, and easy to modify. One of the most effective refactoring techniques is composing methods.


Composing Methods: Techniques

Here are three commonly used techniques for composing methods, with clear examples to illustrate their power.


1. Extract Method

When to Use:

When you find repeated logic or a block of code that performs a distinct task, extract it into a separate method. This makes the code reusable and easier to maintain.

Example 1: Simple Extraction

Before:

public double CalculateTotalPrice(double price, double discount, double taxRate)
{
    double discountedPrice = price - (price * discount);
    double tax = discountedPrice * taxRate;
    return discountedPrice + tax;
}
Enter fullscreen mode Exit fullscreen mode

After:

public double CalculateTotalPrice(double price, double discount, double taxRate)
{
    double discountedPrice = CalculateDiscountedPrice(price, discount);
    double tax = CalculateTax(discountedPrice, taxRate);
    return discountedPrice + tax;
}

private double CalculateDiscountedPrice(double price, double discount)
{
    return price - (price * discount);
}

private double CalculateTax(double discountedPrice, double taxRate)
{
    return discountedPrice * taxRate;
}
Enter fullscreen mode Exit fullscreen mode

Why it’s Better:

The logic for calculating the discount and tax is now reusable and easier to test.


Example 2: Complex Logic Extraction

Before:

public bool IsEligibleForDiscount(Customer customer)
{
    if (customer.Orders.Count > 5 && customer.TotalSpent > 5000)
    {
        foreach (var order in customer.Orders)
        {
            if (order.OrderDate < DateTime.Now.AddYears(-1) && order.Status == "Completed")
            {
                return true;
            }
        }
    }
    return false;
}
Enter fullscreen mode Exit fullscreen mode

After:

public bool IsEligibleForDiscount(Customer customer)
{
    return HasManyOrders(customer) && HasHighSpending(customer) && HasOldCompletedOrder(customer);
}

private bool HasManyOrders(Customer customer)
{
    return customer.Orders.Count > 5;
}

private bool HasHighSpending(Customer customer)
{
    return customer.TotalSpent > 5000;
}

private bool HasOldCompletedOrder(Customer customer)
{
    return customer.Orders.Any(order => order.OrderDate < DateTime.Now.AddYears(-1) && order.Status == "Completed");
}
Enter fullscreen mode Exit fullscreen mode

Why it’s Better:

Each condition is broken into smaller, self-explanatory methods, improving readability and maintainability.


2. Extract Variable

When to Use:

When an expression is too complex or difficult to understand, break it into smaller steps using variables.

Example 1: Simplifying Calculations

Before:

public double CalculateFinalAmount(double price, double discountRate, double taxRate)
{
    return (price - (price * discountRate)) + ((price - (price * discountRate)) * taxRate);
}
Enter fullscreen mode Exit fullscreen mode

After:

public double CalculateFinalAmount(double price, double discountRate, double taxRate)
{
    double discountedPrice = price - (price * discountRate);
    double tax = discountedPrice * taxRate;
    return discountedPrice + tax;
}
Enter fullscreen mode Exit fullscreen mode

Why it’s Better:

Breaking the calculation into steps makes the code easier to read and debug.


Example 2: Handling Complex Expressions

Before:

public double CalculateOrderValue(Order order)
{
    return order.Items.Sum(item => item.Quantity * item.UnitPrice * (1 - item.Discount)) 
           + order.Tax 
           - order.DiscountCode == "SUMMER2024" ? 50 : 0;
}
Enter fullscreen mode Exit fullscreen mode

After:

public double CalculateOrderValue(Order order)
{
    double itemTotal = order.Items.Sum(item => item.Quantity * item.UnitPrice * (1 - item.Discount));
    double seasonalDiscount = order.DiscountCode == "SUMMER2024" ? 50 : 0;
    double totalValue = itemTotal + order.Tax - seasonalDiscount;

    return totalValue;
}
Enter fullscreen mode Exit fullscreen mode

Why it’s Better:

Separating the logic into variables makes each part of the calculation clear.


3. Inline Temp

When to Use:

If a temporary variable is used only once and doesn’t add clarity, remove it and inline the expression.

Example:

Before:

public double CalculateDiscountedPrice(double price)
{
    double discount = price * 0.1;
    return price - discount;
}
Enter fullscreen mode Exit fullscreen mode

After:

public double CalculateDiscountedPrice(double price)
{
    return price - (price * 0.1);
}
Enter fullscreen mode Exit fullscreen mode

Why it’s Better:

Removing the unnecessary variable makes the code more concise.


Combining Techniques

Sometimes, you need to combine multiple techniques to clean up complex code.

Before:

public void ProcessOrders(List<Order> orders)
{
    foreach (var order in orders)
    {
        if (order.Status == "Pending" && order.OrderDate > DateTime.Now.AddMonths(-1))
        {
            foreach (var item in order.Items)
            {
                if (item.Quantity > 0 && item.IsBackordered == false)
                {
                    double itemCost = item.Quantity * item.UnitPrice * (1 - item.Discount);
                    Console.WriteLine($"Processing item {item.Id} with cost: {itemCost}");
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

After:

public void ProcessOrders(List<Order> orders)
{
    var pendingOrders = GetRecentPendingOrders(orders);

    foreach (var order in pendingOrders)
    {
        ProcessOrderItems(order.Items);
    }
}

private IEnumerable<Order> GetRecentPendingOrders(List<Order> orders)
{
    return orders.Where(order => order.Status == "Pending" && order.OrderDate > DateTime.Now.AddMonths(-1));
}

private void ProcessOrderItems(IEnumerable<Item> items)
{
    foreach (var item in items.Where(item => item.Quantity > 0 && !item.IsBackordered))
    {
        double itemCost = CalculateItemCost(item);
        Console.WriteLine($"Processing item {item.Id} with cost: {itemCost}");
    }
}

private double CalculateItemCost(Item item)
{
    return item.Quantity * item.UnitPrice * (1 - item.Discount);
}
Enter fullscreen mode Exit fullscreen mode

Why it’s Better:

  • The nested logic is broken into smaller methods.
  • Each method handles a single responsibility, making the code easier to maintain.

Key Takeaways

  • Extract Method: Split repeated or complex logic into separate methods.
  • Extract Variable: Use variables to simplify complex expressions.
  • Inline Temp: Remove unnecessary temporary variables for conciseness.
  • Combine Techniques: Refactor step-by-step to tackle complex code.

By applying these techniques, your code will become cleaner, more maintainable, and easier to understand. Refactoring isn’t just about aesthetics—it’s about creating a foundation for robust, scalable software. Happy coding! 🚀

Top comments (0)