Basics of Delegates



This content originally appeared on DEV Community and was authored by El cat bot

In C#, we have a powerful feature that can help us to reference methods for future execution. This implementation is the Delegate type.

Specifically it defines methods, parameters and, return type. Think about delegates as Callback method it is passed into other methods.

Ordinary Method call.

Let’s say We are writing some program that needs to make a discount calculation based on a client type:

  • Basic: 10%
  • Premium: 30%
  • No-membership: 0%
public decimal GetDiscount(decimal price, string customerType)
{
    if(customerType == "Basic")
    {
        return price = price * 0.90M;
    }
    else if(customerType == "Premium")
    {
        return price = price * 0.70M;
    }
    else
    {
        return price;
    }
}

var price = GetDiscount(450, "Premium");

Console.WriteLine(price); // 315.00

Passing a price of 450, after discount will be 315. GetDiscount does its work but if we think about validations, this method shouldn’t have to validate the customer has membership. In terms of Single Responsibility SOLID principle, GetDiscount should only have to calculate discount and return value. That’s all.

Delegate implementation

Fortunately, it is possible to add a delegate to organize our code:

1. Declare the delegate:

public delegate decimal Calculation(decimal price);

Remember that this delegate is a method wrapper and method signature must match with our GetDiscount method. So, method return type and parameters must be equals.

2. Modify GetDiscount to receive only Calculation delegate as a parameter:

public decimal GetDiscount(Calculation calculation, decimal price)
{
    return calculation(price);
}

Note our code is reduced to on line of code. That’s because GetDiscount performs discount and method logic will be added as parameter when GetDiscount is called.

3. Call GetDiscount and pass our delegate and price as parameters:

GetDiscount(price => price * 0.90M, 450); // 405.00

GetDiscount(price => price * 0.70M, 450); // 315.00

When calling GetDiscount, the first parameter will be the discount operation using anonymous function. Instead of writing discount process in GetDiscount, we pass it as parameter to be invoked later. The second parameter is price.

Complete code:

public delegate decimal Calculation(decimal price);

public void Main()
{
    var discountBasic = GetDiscount(price => price * 0.90M, 450);
    var discountPremium =  GetDiscount(price => price * 0.70M, 450);

    Console.WriteLine("Having a price of 450: ");
    Console.WriteLine($"    Premium membership has a discount => {discountBasic}");
    Console.WriteLine($"    Basic membership has a discount  => {discountPremium}");
}

public decimal GetDiscount(Calculation calculation, decimal price)
{
    return calculation(price);
}

Multicast Delegate

There are situations where we have calls to implementations one after another, sharing method signature (same return type, same parameters).

Let’s say we have a program that creates an User and then calls three methods to log a message into Console, Local File and Azure Cloud:

public void LogToConsole(string message)
{
    Console.WriteLine($"Console => {message}");
}

public void LogToFile(string message)
{
    // File.Write....
    Console.WriteLine($"File => {message}");
}

public void LogToAzure(string message)
{
    // Save to Azure Cloud....
    Console.WriteLine($"Cloud => {message}");
}

A CreateUser method calls each Log implementation:

public void CreateUser()
{
    // Create User logic...
    // User created
    var message = "User created Successfully";

    LogToConsole(message);
    LogToFile(message);
    LogToCloud(message);
}

// Console => User created Successfully
// File => User created Successfully
// Azure=> User created Successfully

That’s cool but what happens if new Log destinations are addded. For example AWS, ElasticSearch and more. It won’t be clean if adding more methods that do the same for different providers.

With Multicast delegates we can declare MyLogger class with a LogMessage method. LogMessage will invoke one or more methods from the caller. Let’s see how it works:

1. Declare LogHandler delegate:

public delegate void LogDelegate(string message);

2. Create MyLogger class:

public class MyLogger
{
    public LogHandler Log;

    public void LogMessage(string message)
    {
        Log?.Invoke(message);
    }
}

Here, we declare MyLogger class, a local LogHandler variable and LogMessage method that accepts a string and then invoke our delegate. See the method signature is the same as all the previous Log methods.

3. Modify CreateUser method to call and execute the delegate:

public void CreateUser()
{
    // Create User logic...
    // User created
    var message = "User created Successfully";

    var MyLogger = new MyLogger();

    myLogger.Log +=  LogToConsole;

    myLogger.Log +=  (message) => 
    { 
        // File.Write....
        Console.WriteLine($"File => {message}");
    };

    myLogger.Log +=  (message) => 
    { 
        // Save to Azure, AWS Cloud....
        Console.WriteLine($"Azure => {message}");
    };

    myLogger.Log +=  (message) => 
    { 
        // Save to Azure, AWS Cloud....
        Console.WriteLine($"Elasticsearch => {message}");
    };

    // Execute Delegate
    MyLogger.LogMessage(message);

        //Console => User created Successfully
    //File => User created Successfully
    //Azure => User created Successfully
    //Elasticsearch => User created Successfully
}

First we create an instance of MyLogger class. Then we assign anonymous function to Log delegate within MyLogger instance. Anonymous functions offer more flexibility and previous Logging methods, goes away. That way we can assign different logic and destination logger based on a centralized delegate. Our LogMessage method just needs to invoke the logic inside the delegate and it runs as expected.

With this implementation I expect you’ve learned the basic about delegates and more important, in which scenarios you can implement delegates.

So, Thanks for reading and keep learning =)


This content originally appeared on DEV Community and was authored by El cat bot