C# -  Evitando o uso de condições com delegate e Dictionary


  Hoje vou mostrar uma aplicação prática do uso de delegates e da classe Dictionary como uma alternativa a usar as instruções else-if ou switch-case.

Seria muito difícil para uma linguagem imperativa evitar completamente o uso das instruções if-else ou switch-case , visto que definir condições é uma prática recorrente.

Podemos entretanto evitar ao máximo usar as instruções condicionais mais complexas de forma evitar erros e facilitar a manutenção e os testes.

Dentre os diversos recursos disponíveis para isso podemos usar o conceito de delegate e a classe Dictionary.

Um Dictionary é uma estrutura de dados para armazenar um grupo de objetos que são armazenados como uma coleção de pares chave/valor. A linguagem C# oferece a classe genérica Dictionary<TKey,TValue>  que representa uma coleção de pares de chave/valor, onde cada chave é única e mapeada para um valor correspondente.

Em C#, um delegate é um tipo de dado que representa uma referência a um ou mais métodos com uma determinada assinatura (ou seja, um conjunto específico de parâmetros e tipo de retorno).

Podemos usar um delegate para encapsular uma chamada a um ou mais métodos, permitindo que ele seja passado como argumento para outros métodos, armazenado em variáveis ou usado como um retorno de outro método.

Vejamos a seguir um cenário bem simples onde temos uma classe CalculadoraNatural:

public class CalculadoraNatural
{
    public static float Soma(float x, float y)
    {
        return x + y;
    }

    public static float Subtracao(float x, float y)
    {
        return x - y;
    }

    public static float Multiplicacao(float x, float y)
    {
        return x * y;
    }

    public static float Divisao(float x, float y)
    {
        if (y == 0)
            throw new ArgumentException("Não existe divisão por zero");

        return x / y;
    }
}

E o seguinte código na classe Program:

using DelegateDictionary;

int opcao;
float num1, num2, resultado = 0;

Console.WriteLine("Calculadora Simples");
Console.WriteLine("-------------------");
Console.WriteLine("Selecione a operação desejada:");
Console.WriteLine("1 - Somar");
Console.WriteLine("2 - Subtrair");
Console.WriteLine("3 - Multiplicar");
Console.WriteLine("4 - Dividir");
Console.Write("Opção: ");

try
{
    opcao = int.Parse(Console.ReadLine());   

    if (opcao < 1 || opcao > 4)
        throw new ArgumentException("Opção inválida");

    Console.Write("Digite o primeiro número: ");
    num1 = float.Parse(Console.ReadLine());
    Console.Write("Digite o segundo número: ");
    num2 = float.Parse(Console.ReadLine());

    switch (opcao)
    {
        case 1:
            resultado = CalculadoraNatural.Soma(num1, num2);
            break;
        case 2:
            resultado = CalculadoraNatural.Subtracao(num1, num2);
            break;
        case 3:
            resultado = CalculadoraNatural.Multiplicacao(num1, num2);
            break;
        case 4:
            resultado = CalculadoraNatural.Divisao(num1, num2);
            break;
        default:
            Console.WriteLine("Opção inválida!");
            break;
    }
    Console.WriteLine("Resultado: " + resultado);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
Console.ReadKey();

Aqui poderíamos ter usado a instrução if-else :

if (opcao == 1)
{
    resultado = CalculadoraNatural.Soma(num1, num2);
}
else if (opcao == 2)
{
    resultado = CalculadoraNatural.Subtracao(num1, num2);
}
else if (opcao == 3)
{
    resultado = CalculadoraNatural.Multiplicacao(num1, num2);
}
else if (opcao == 4)
{
    resultado = CalculadoraNatural.Divisao(num1, num2);
}
else
{
    Console.WriteLine("Opção inválida!");
}

Vejamos agora como usar delegates e a classe Dictionary para melhorar um pouco o código acima.

Todos os métodos da classe CalculadoraNatural recebem dois argumentos do tipo float e retornam um float.

Já sabemos que que o delegate Func é um tipo de delegate genérico que pode ser usado para definir métodos que recebem um ou mais argumentos e retornam um valor. O número de argumentos e o tipo de retorno podem ser especificados ao declarar uma instância do delegate.

Assim podemos definir referências aos métodos da classe CalculadoraNatural usando o delegate Func, usando a assinatura : Func<float, float, float>();

A seguir podemos criar um objeto Dictionary que vai armazenar as opções de um menu que representa as operações da calculadora como chave e como valor podemos incluir o delegate Func para cada opção.

O código ficaria assim:

using DelegateDictionary;

float num1, num2, resultado = 0;
int opcao;

var operacoes = new Dictionary<int, Func<float,float,float>>();

operacoes.Add(1, new Func<float,float,float>(CalculadoraNatural.Soma));
operacoes.Add(2, new Func<float, float, float>(CalculadoraNatural.Subtracao));
operacoes.Add(3, new Func<float, float, float>(CalculadoraNatural.Multiplicacao));
operacoes.Add(4, new Func<float, float, float>(CalculadoraNatural.Divisao));

Console.WriteLine("Calculadora Simples");
Console.WriteLine("-------------------");
Console.WriteLine("Selecione a operação desejada:");
Console.WriteLine("1 - Somar");
Console.WriteLine("2 - Subtrair");
Console.WriteLine("3 - Multiplicar");
Console.WriteLine("4 - Dividir");
Console.Write("Opção: ");

opcao = int.Parse(Console.ReadLine());

Console.Write("Digite o primeiro número: ");
num1 = float.Parse(Console.ReadLine());

Console.Write("Digite o segundo número: ");
num2 = float.Parse(Console.ReadLine());

if (operacoes.ContainsKey(opcao))
{
    var operacao = operacoes[opcao];
    resultado = operacao(num1, num2);

}
else
{
    Console.WriteLine("Opção inválida!");
}

Console.WriteLine("Resultado: " + resultado);

Console.ReadKey();

O código não ficou muito mais enxuto pois a classe Dictionary exige o mapeamento mas eliminamos o uso da condição definida pela instrução switch-case.

Poderíamos melhorar o código usando polimorfismo ou aplicando um padrão de projeto como o padrão Strategy.

O padrão Strategy é um padrão comportamental que permite definir uma família de algoritmos, encapsular cada um deles e torná-los intercambiáveis. Dessa forma, o cliente pode escolher o algoritmo que deseja usar em tempo de execução, sem precisar conhecer os detalhes de implementação de cada um.

No caso da calculadora, podemos criar uma interface IOperacao que define um método Calcular que recebe dois números e retorna um resultado. Em seguida, podemos criar diferentes implementações dessa interface para cada uma das operações disponíveis: soma, subtração, multiplicação e divisão.

A classe CalculadoraNatural teria uma propriedade do tipo IOperacao que seria definida de acordo com a opção selecionada pelo usuário. Dessa forma, não seria mais necessário utilizar um switch-case para escolher a operação correta.

1- Interface IOperacao

public interface IOperacao
{
  float Calcular(float num1, float num2);
}

2- Classes concretas para realizar os cálculos

    public class Soma : IOperacao
    {
        public float Calcular(float num1, float num2)
        {
            return num1 + num2;
        }
    }

    public class Subtracao : IOperacao
    {
        public float Calcular(float num1, float num2)
        {
            return num1 - num2;
        }
    }

    public class Multiplicacao : IOperacao
    {
        public float Calcular(float num1, float num2)
        {
            return num1 * num2;
        }
    }

    public class Divisao : IOperacao
    {
        public float Calcular(float num1, float num2)
        {
            if (num2 == 0)
            {
                throw new DivideByZeroException("Não é possível dividir por zero");
            }

            return num1 / num2;
        }
    }

 3- Classe CalculadoraNatural

    public class CalculadoraNatural
    {
        private IOperacao operacao;

        public CalculadoraNatural(IOperacao operacao)
        {
            this.operacao = operacao;
        }

        public float Calcular(float num1, float num2)
        {
            return operacao.Calcular(num1, num2);
        }
    }

A definição da classe Dictionary seria feita da seguinte forma:

var operacoes = new Dictionary<int, Func<float, float, float>>();

operacoes.Add(1, new Func<float, float, float>(new Soma().Calcular));
operacoes.Add(2,
new Func<float, float, float>(new Subtracao().Calcular));
operacoes.Add(3,
new Func<float, float, float>(new Multiplicacao().Calcular));
operacoes.Add(4,
new Func<float, float, float>(new Divisao().Calcular));
 

O resto do código permaneceria idêntico.

Pegue o exemplo aqui : DelegateDictionary.zip

E estamos conversados...

"Não tenhas inveja dos homens malignos, nem desejes estar com eles.
Porque o seu coração medita a rapina, e os seus lábios falam a malícia."
Provérbios 24:1-2

Referências:


José Carlos Macoratti