Why Delegates? Why not Call Methods Directly?

Delegates… being a developer/engineer/tinkerer, you naturally want to “get” them. You may kind of get them, but not feel like you can explain them to someone else.

One reason is the practicality of delegates isn’t always clear. Why couldn’t you just directly call a method on an object instead of using a delegate?

Without Delegates

Let’s see what directly calling methods on objects looks like so we can see how delegates improves this.

I’m going to use an example from the video C# Design Patterns with Jon Skeet: Outtakes where Mr. Skeet implements the Decorator Pattern using delegates.

Say we have a Pizza Shop and we need a way to apply different discounts that we are offering right now. Implementing this using a class that holds our different discount methods might look like this:

class PizzaOrderingSystemMethod {
    private readonly DiscountPoliciesMethods _discountPoliciesMethods;
    public PizzaOrderingSystemMethod() {
        _discountPoliciesMethods = new DiscountPoliciesMethods();
    }
    public decimal ComputePrice(PizzaOrder order) {
        decimal total = order.Pizzas.Sum(p => p.Price);

        decimal[] discounts = new[] {
            _discountPoliciesMethods.BuyOneGetOneFree(order),
            _discountPoliciesMethods.FivePercentOffMoreThanFiftyDollars(order),
            _discountPoliciesMethods.FiveDollarsOffStuffedCrust(order),
        };

        decimal bestDiscount = discounts.Max(discount => discount);
        total = total - bestDiscount;
        return total;
    }
}

public class DiscountPoliciesMethods {
    public decimal BuyOneGetOneFree(PizzaOrder order) {
        var pizzas = order.Pizzas;
        if (pizzas.Count < 2) {
            return 0m;
        }
        return pizzas.Min(p => p.Price);
    }
    public decimal FivePercentOffMoreThanFiftyDollars(PizzaOrder order) {
        decimal nonDiscounted = order.Pizzas.Sum(p => p.Price);
        return nonDiscounted >= 50 ? nonDiscounted * 0.05m : 0M;

    }
    public decimal FiveDollarsOffStuffedCrust(PizzaOrder order) {
        return order.Pizzas.Sum(p => p.Crust == Crust.Stuffed ? 5m : 0m);
    }
}

You see our Pizza Shop is offering 3 different discounts. We are calling each one individually to see if any of them apply to an order.

The disadvantage, here, is that if we need to update/add/remove any of our discount offers, we must change both our ComputePrice(PizzaOrder order) method in our PizzaOrderingSystem each time we make the change.

Open Closed Principle with Delegates

What we really want is to be able to extend the PizzaOrderingSystem without modifying it. We want to make it Open for Extension, Closed for Modification.

We can do this with delegates.

First, we’ll define the delegate:

public delegate decimal DiscountPolicy(PizzaOrder order);

Notice how this delegate’s signature matches our Discounts methods above:

Remind you of something?… Yea reminds me of interfaces too:

public interface IDiscountPolicy {
    decimal ApplyDiscount(PizzaOrder order);
}

Just like our delegate instances match the signature of our delegate definitions, interface methods match the signature of our interface method definitions.

If you think of delegates as being similar to interface definitions for a specific type of method, you can start to see why delegates exist. They allow clients of our delegates to ignore all the details of their implementations - even their names!

Thanks to Jon Skeet and Rob Conery for the full breakdown of the Decorator Pattern in their video, we can see how to get rid of the brittle code in our PizzaOrderingSystem class.

class PizzaOrderingSystem {
    readonly DiscountPolicy _discountPolicy;

    public PizzaOrderingSystem(DiscountPolicy discountPolicy) {
        _discountPolicy = discountPolicy;
    }

    public decimal ComputePrice(PizzaOrder order) {
        decimal nonDiscounted = order.Pizzas.Sum(p => p.Price);
        decimal discountedValue = _discountPolicy(order);
        return nonDiscounted - discountedValue;
    }
}

Here’s how the delegates are implemented, including one that returns the best discount for the current order:

public delegate decimal DiscountPolicy(PizzaOrder order);

public static class DiscountPolicyDelegates {
    public static decimal BuyOneGetOneFree(PizzaOrder order) {
        var pizzas = order.Pizzas;
        if (pizzas.Count < 2) {
            return 0m;
        }
        return pizzas.Min(p => p.Price);
    }
    public static decimal FivePercentOffMoreThanFiftyDollars(PizzaOrder order) {
        decimal nonDiscounted = order.Pizzas.Sum(p => p.Price);
        return nonDiscounted >= 50 ? nonDiscounted*0.05m : 0M;
    }
    public static decimal FiveDollarsOffStuffedCrust(PizzaOrder order) {
        return order.Pizzas.Sum(p => p.Crust == Crust.Stuffed ? 5m : 0m);
    }
    public static DiscountPolicy CreateBest(params DiscountPolicy[] policies) {
        return order => policies.Max(policy => policy.Invoke(order));
    }

    public static DiscountPolicy DiscountAllThePizzas() {
        DiscountPolicy best = CreateBest(
            BuyOneGetOneFree,
            FivePercentOffMoreThanFiftyDollars,
            FiveDollarsOffStuffedCrust);
        return best;
    }
}

Finally, a test to show that everything works:

[Fact]
public void Best_discount_for_big_order() {
    var pizzaOrderingSystem = new PizzaOrderingSystem(
        DiscountPolicyDelegates.DiscountAllThePizzas());
    var order = new PizzaOrder();
    // Buy one get one
    for (int i = 0; i < 2; i++) {
        var pizza = new Pizza() {
            Crust = Crust.Regular,
            Price = 10.00m,
            Size = Size.Large
        };
        order.Pizzas.Add(pizza);
    }
    // Over 50 5% off
    for (int i = 0; i < 6; i++) {
        var pizza = new Pizza() {
            Crust = Crust.Regular,
            Price = 10.00m,
            Size = Size.Large
        };
        order.Pizzas.Add(pizza);
    }
    // Stuffed crust
    var stuffedCrust = new Pizza() {
        Crust = Crust.Stuffed,
        Price = 10.00m,
        Size = Size.Large
    };
    order.Pizzas.Add(stuffedCrust);
    var price = pizzaOrderingSystem.ComputePrice(order);
    Assert.Equal(80.00m, price);
}

Summary

Hope you enjoyed this short discussion of why delegates exist in terms of the open closed principle. Indeed, I didn’t address how delegates are the cornerstone of events in Windows Forms development, but not everyone writes in that environment. Even experienced developers might feel intimidated by what’s going on under the surface of this language construct.

I hope you find this helpful and the next time you see delegates, you’ll feel more confident in knowing what’s going on and why they were used. Be sure to sign up for my newsletter. I look forward to hearing from you!



Related Posts:


Sources

Tweet
comments powered by Disqus