sábado, 22 de septiembre de 2012

Como evitar el uso del switch (1/2)

 

Introducción


Suele suceder que muchas veces se tiene una lista de opciones y por cada una cierta lógica por ejecutar, para resolver el problema lo primero que se viene a la mente es usar un switch (c#), o un Select Case (vb.net), pero no se evalúa que existen algunas técnicas para evitar esto usando el poder de POO (Programación Orientada a Objetos)

Imaginamos una aplicación que debe calcular las cuotas basado en ciertos parámetros, siendo el plazo de cada pago el factor que afecta la formula

imagen1

Lo que primero viene a la mente seria realizar un switch por cada plazo de pago y allí realizar el calculo en cada caso, pero como evitarlo, bien ese será el objetivo del articulo.

 

Cargar combo opciones


El primer paso será la carga del combo, tarea que se realizara haciendo uso de clases y listas, es por eso que asignaremos

private void frmPrincipal_Load(object sender, EventArgs e)
{
    cmdPlazo.ValueMember = "Id";
    cmdPlazo.DisplayMember = "Desc";
    cmdPlazo.DataSource = PlazosHelper.ObtenerPlazosConItemOpcional();

}

para lo cual solo se necesitara de la definición de la clase

public class Plazo
{
    public int Id { get; set; }
    public string Desc { get; set; }
}

También se contara con la ayuda de una clase responsable (definida como static para que sea fácilmente accesible) que cargue los ítems

public static class PlazosHelper
{

    public static List<Plazo> ObtenerPlazos()
    {
        return new List<Plazo>()
        {
            new Plazo() { Id=1, Desc="Un solo pago" },
            new Plazo() { Id=2, Desc="Semanal" },
            new Plazo() { Id=3, Desc="Quincenal" },
            new Plazo() { Id=4, Desc="Mensual" },
            new Plazo() { Id=5, Desc="Bimestral" },
            new Plazo() { Id=6, Desc="Trimestral" },
            new Plazo() { Id=7, Desc="Semestral" },
            new Plazo() { Id=8, Desc="Anual" }
        };
    }

    public static List<Plazo> ObtenerPlazosConItemOpcional()
    {
        List<Plazo> plazos = ObtenerPlazos();
        plazos.Insert(0, new Plazo() { Id = 0, Desc = "<<<Seleccione>>>" });
        return plazos;
    }

}

 

Resolver Calculo


Para resolver el calculo se crea un método que podrá ser invocado desde dos eventos, el cual además proporcionara validaciones básicas

private void cmdPlazo_SelectionChangeCommitted(object sender, EventArgs e)
{
    Calcular();
}

private void cmdCalcular_Click(object sender, EventArgs e)
{
    Calcular();
}

private void Calcular()
{
    errorProv.Clear();

    decimal monto = 0;
    if (!decimal.TryParse(txtMonto.Text, out monto))
    {
        errorProv.SetError(txtMonto, "Debe ingresar un valor numerico");
        return;
    }

    decimal montoporcuota = 0;
    if (!decimal.TryParse(txtMontoPorCuota.Text, out montoporcuota))
    {
        errorProv.SetError(txtMontoPorCuota, "Debe ingresar un valor numerico");
        return;
    }

    PazoPagoResult result = PlazosHelper.CalcularPlazo(Convert.ToInt32(cmdPlazo.SelectedValue), 
                                                        dtpFechaInicio.Value,
                                                        monto,
                                                        montoporcuota);

    txtResultado.Text = result.FechaFin.ToShortDateString();

    dgvListaPagos.DataSource = result.ListaPagos;

}

Pero para llegar al calculo haremos uso de la potencia que provee las interfaces

public interface IResolverPlazo
{
    PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota);
}

La interfaz defina una entidad compleja para devolver la info del calculo

public class PazoPagoResult
{
    public PazoPagoResult()
    {
        this.ListaPagos = new List<ItemPago>();
    }
    public DateTime FechaFin { get; set; }
    public List<ItemPago> ListaPagos { get; set; }
}

public class ItemPago
{
    public DateTime Fecha { get; set; }
    public decimal Monto { get; set; }
}

cada implementación define como se resuelve la lógica, por supuesto aquí solo pondré algunas, pero en el código que descarguen están todas las clases implementadas

public class SinPlazoPago : IResolverPlazo
{
    public PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return new PazoPagoResult()
        {
            FechaFin = fechainicio,
            ListaPagos = null
        };
    }
}

public class UnSoloPago : IResolverPlazo
{
    public PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return new PazoPagoResult()
        {
            FechaFin = fechainicio,
            ListaPagos = new List<ItemPago>()
            {
                new ItemPago(){ Fecha = fechainicio, Monto = monto }
            }
        };
    }
}

public class Semanal : IResolverPlazo
{
    public PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        //
        //Se calculan la cantidad de cuotas
        //
        int cantcuotas = Convert.ToInt32(Math.Floor(monto / montocuota));

        //
        // Se define la entidad de respuesta
        //
        PazoPagoResult result = new PazoPagoResult()
        {
            FechaFin = fechainicio.AddDays(7 * cantcuotas)
        };

        //
        //crea la lista de Pagos
        //
        for (int cuota = 1; cantcuotas >= cuota; cuota++)
        {
            ItemPago item = new ItemPago()
            {
                Fecha = fechainicio.AddDays(7 * cuota),
                Monto = montocuota * cuota
            };

            result.ListaPagos.Add(item);
        }

        return result;
    }
}


public class Bimestral : IResolverPlazo
{
    public PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        int cantcuotas = Convert.ToInt32(Math.Floor(monto / montocuota));

        PazoPagoResult result = new PazoPagoResult()
        {
            FechaFin = fechainicio.AddMonths(2 * cantcuotas)
        };


        for (int cuota = 1; cantcuotas >= cuota; cuota++)
        {
            ItemPago item = new ItemPago()
            {
                Fecha = fechainicio.AddMonths(2 * cuota),
                Monto = montocuota * cuota
            };

            result.ListaPagos.Add(item);
        }

        return result;
    }
}

La clase helper solo define el diccionario que mapea el id de la opción con la implementación que resuelve el calculo

public static class PlazosHelper
{

    public static PazoPagoResult CalcularPlazo(int plazo, DateTime fechainicio, decimal monto, decimal montocuota)
    {
        Dictionary<int, IResolverPlazo> resolverPlazos = new Dictionary<int, IResolverPlazo>()
        {
            {0, new SinPlazoPago()},
            {1, new UnSoloPago()},
            {2, new Semanal()},
            {3, new Quincenal()},
            {4, new Mensual()},
            {5, new Bimestral()},
            {6, new Trimestral()},
            {7, new Semestral()},
            {8, new Anual()},
        };

        return resolverPlazos[plazo].Calcular(fechainicio, monto, montocuota);
    }


}

Se usa el id del plazo para determinar la posición del Dictionary<> y poder así determinar que instancia hacer uso para lanzar el calculo, como todas las clase implementan la misma interfaz se puede invocar sin problemas

Conclusión


La ventaja de usar clases por sobre un simple switch se ve reflejada en la posibilidad de extender el código con el solo hecho de definir nuevas implementaciones de las clases que heredan de la interfaz definida.

En este caso no se implemento, pero podría llevarse el código de las clases que resuelven el calculo a una assembly (dll) separado y poder de forma dinámica alterar la lógica según sea necesario, sin afectar por ello al código core de la aplicación, por configuración se podría indicar que librería cargar en cada momento.

 

Código


[C#]
 

3 comentarios:

  1. Hola leandro, como estas?. soy victor y justamente estoy necesitando de esta info, por que estoy trabajando en un sistema de control de cuotas y mi gran falencia es que no estoy encarando bien este modulo del programa.El problema es que estoy programando en vb.net, me darias una gran mano si tuvieras algo parecido en vb.net o si me guiaras para darme cuenta yo mismo del tema. Muchas gracias!

    ResponderEliminar
  2. Yo prefiero ir por lo funcional, lo hago sobretodo cuando utilizo enums, que luego si usas switch con un enum, y te da por agregar otro elemento y usas en muchas partes el switch, puedes terminar con bugs.

    Por ejemplo, si tengo un enum asi:

    public Enum TipoPago { Efectivo, Cheque, Transferencia, Otro}

    entonces creo una función asi:

    public T Match(TipoPago value, Func efectivo, Func cheque, Func transferencia, Func otro)

    y en vez de usar switch uso algo como

    string texto = Match(TipoPago.Cheque, efectivo: () => "Es efectivo", cheque: () => "es un cheque", transferencia: () => "es una transferencia", otro: () => "es otro método de pago");

    de esa forma, si agrego otro elemento al enum (por ejemplo, que ahora necesito manejar vales), simplemente modifico la función agregando el parámetro, y el compilador me va a marcar con error en todos los lugares donde utilizo la función y es mas difícil que se cuelen bugs.

    ResponderEliminar