C Sharp

Delegate Composition

The ability to compose delegates⏼by creating a single delegate out of multiple delegates-is one of those features that at first doesn't seem very handy, but if you ever need it you'll be happy that the C# design team thought of it. Let's look at some examples in which delegate composition is useful. In the first example, you have a distribution system and a class that iterates through the parts for a given location, calling a callback method for each part that has an "on-hand" value of less than 50. In a more realistic distribution example, the formula would take into account not only "on-hand" but also "on order" and "in transit" in relation to the lead times, and it would subtract the safety stock level, and so on. But let's keep this simple: if a part's on-hand value is less than 50, an exception has occurred.

The twist is that we want two distinct methods to be called if a given part is below stock: we want to log the event, and then we want to email the purchasing manager. So, let's take a look at how you programmatically create a single composite delegate from multiple delegates: -

using System;
using System.Threading;
class Part
{
    public Part(string sku)
    {
        this.Sku = sku;
        Random r = new Random(DateTime.Now.Millisecond);
        double d = r.NextDouble() * 100;
        this.OnHand = (int)d;
    }
    protected string Sku;
    public string sku
    {
        get
        {
            return this.Sku;
        }
        set
        {
            this.Sku = value;
        }
    }
    protected int OnHand;
    public int onhand
    {
        get
        {
            return this.OnHand;
        }
        set
        {
            this.OnHand = value;
        }
    }
}
class InventoryManager
{
    protected const int MIN_ONHAND = 50;
    public Part[] parts;
    public InventoryManager()
    {
        parts = new Part[5];
        for (int i = 0; i < 5; i++)
        {
            Part part = new Part("Part " + (i + 1));
            Thread.Sleep(10); // Randomizer is seeded by time.
            parts[i] = part;
            Console.WriteLine("Adding part '{0}' on-hand = {1}",
                              part.sku, part.onhand);
        }
    }
    public delegate void OutOfStockExceptionMethod(Part part);
    public void ProcessInventory(OutOfStockExceptionMethod exception)
    {
        Console.WriteLine("\nProcessing inventory...");
        foreach (Part part in parts)
        {
            if (part.onhand < MIN_ONHAND)
            {
                Console.WriteLine
                    ("{0} ({1}) is below minimum on-hand {2}",
                    part.sku, part.onhand, MIN_ONHAND);
                exception(part);
            }
        }
    }
}
class CompositeDelegate1App
{
    public static void LogEvent(Part part)
    {
        Console.WriteLine("\tlogging event...");
    }
    public static void EmailPurchasingMgr(Part part)
    {
        Console.WriteLine("\temailing Purchasing manager...");
    }
    public static void Main()
    {
        InventoryManager mgr = new InventoryManager();
        InventoryManager.OutOfStockExceptionMethod LogEventCallback =
            new InventoryManager.OutOfStockExceptionMethod(LogEvent);
        InventoryManager.OutOfStockExceptionMethod
            EmailPurchasingMgrCallback = new
            InventoryManager.OutOfStockExceptionMethod(EmailPurchasingMgr);
        InventoryManager.OutOfStockExceptionMethod
            OnHandExceptionEventsCallback =
            EmailPurchasingMgrCallback + LogEventCallback;
        mgr.ProcessInventory(OnHandExceptionEventsCallback);
    }
}

Running this application produces results like the following: -

Adding part 'Part 1' on-hand = 16
Adding part 'Part 2' on-hand = 98
Adding part 'Part 3' on-hand = 65
Adding part 'Part 4' on-hand = 22
Adding part 'Part 5' on-hand = 70
Processing inventory...
Part 1 (16) is below minimum on-hand 50
    logging event...
    emailing Purchasing manager...
Part 4 (22) is below minimum on-hand 50
    logging event...
    emailing Purchasing manager...

Therefore, using this feature of the language, we can dynamically discern which methods comprise a callback method, aggregate those methods into a single delegate, and pass the composite delegate as though it were a single delegate. The runtime will automatically see to it that all of the methods are called in sequence. In addition, you can also remove desired delegates from the composite by using the minus operator.

However, the fact that these methods get called in sequential order does beg one important question: why can't I simply chain the methods together by having each method successively call the next method? In this section's example, where we have only two methods and both are always called as a pair, we could do that. But let's make the example more complicated. Let's say we have several store locations with each location dictating which methods are called. For example, Location1 might be the warehouse, so we'd want to log the event and email the purchasing manager, whereas a part that's below the minimum on-hand quantity for all other locations would result in the event being logged and the manager of that store being emailed.

We can easily address these requirements by dynamically creating acomposite delegate based on the location being processed. Without delegates, we'd have to write a method that not only would have to determine which methods to call, but also would have to keep track of which methods had already been called and which ones were yet to be called during the call sequence. As you can see in the following code, delegates make this potentially complex operation very simple.

using System;
class Part
{
    public Part(string sku)
    {
        this.Sku = sku;
        Random r = new Random(DateTime.Now.Millisecond);
        double d = r.NextDouble() * 100;
        this.OnHand = (int)d;
    }
    protected string Sku;
    public string sku
    {
        get
        {
            return this.Sku;
        }
        set
        {
            this.Sku = value;
        }
    }
    protected int OnHand;
    public int onhand
    {
        get
        {
            return this.OnHand;
        }
        set
        {
            this.OnHand = value;
        }
    }
}
class InventoryManager
{
    protected const int MIN_ONHAND = 50;
    public Part[] parts;
    public InventoryManager()
    {
        parts = new Part[5];
        for (int i = 0; i < 5; i++)
        {
            Part part = new Part("Part " + (i + 1));
            parts[i] = part;
            Console.WriteLine
                ("Adding part '{0}' on-hand = {1}",
                 part.sku, part.onhand);
        }
    }
    public delegate void OutOfStockExceptionMethod(Part part);
    public void ProcessInventory(OutOfStockExceptionMethod exception)
    {
        Console.WriteLine("\nProcessing inventory...");
        foreach (Part part in parts)
        {
            if (part.onhand < MIN_ONHAND)
            {
                Console.WriteLine
                    ("{0} ({1}) is below minimum onhand {2}",
                    part.sku, part.onhand, MIN_ONHAND);
                exception(part);
            }
        }
    }
}
class CompositeDelegate2App
{
    public static void LogEvent(Part part)
    {
        Console.WriteLine("\tlogging event...");
    }
    public static void EmailPurchasingMgr(Part part)
    {
        Console.WriteLine("\temailing Purchasing manager...");
    }
    public static void EmailStoreMgr(Part part)
    {
        Console.WriteLine("\temailing store manager...");
    }
    public static void Main()
    {
        InventoryManager mgr = new InventoryManager();
        InventoryManager.OutOfStockExceptionMethod[] exceptionMethods
            = new InventoryManager.OutOfStockExceptionMethod[3];
        exceptionMethods[0] = new
            InventoryManager.OutOfStockExceptionMethod
                (LogEvent);
        exceptionMethods[1] = new
            InventoryManager.OutOfStockExceptionMethod
                (EmailPurchasingMgr);
        exceptionMethods[2] = new
            InventoryManager.OutOfStockExceptionMethod
                (EmailStoreMgr);
        int location = 1;
        InventoryManager.OutOfStockExceptionMethod compositeDelegate;
        if (location == 2)
        {
            compositeDelegate =
                exceptionMethods[0] + exceptionMethods[1];
        }
        else
        {
            compositeDelegate =
                exceptionMethods[0] + exceptionMethods[2];
        }
        mgr.ProcessInventory(compositeDelegate);
    }
}

Now the compilation and execution of this application will yield different results based on the value you assign the location variable.