domingo, 23 de septiembre de 2012

Como evitar el uso del switch (2/2)

 

Introducción


Ester artículo será una expansión y mejora del anterior

Como evitar el uso del switch (1/2)

Básicamente se plantearan dos cambios significativos

  • definir una clase base, donde poder definir código repetitivo
  • el combobox resuelva de forma directa la instancia de calculo

 

Definir una clase base


Una de de los principales problemas que presenta a simple vista es el código similar en cada implementación de las clases de calculo, en la mayoría de los casos el bloque

imagen1

es idéntico, salvando lo resaltado en círculos que varia en cada implementación, lo resaltado se trata justamente de las funciones de fecha que el plazo determina en cada caso

Ahora bien, como se podría evitar repetir código ?

Para poder lograrlo realizaremos varias modificaciones, la primer será cambiar la Interface por una clase abstract, esto nos permitirá definir una clase base que puede definir código reutilizable

 

public abstract class ResolverPlazo
{
    public abstract PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota);

    protected PazoPagoResult Calcular(DateTime fechainicio, 
                                    decimal monto, 
                                    decimal montocuota, 
                                    Func<DateTime, int, DateTime> formulafechaFin, 
                                    Func<DateTime, int, DateTime> formulafechaItem)
    {
        //
        //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 = formulafechaFin(fechainicio, cantcuotas)
        };

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

            result.ListaPagos.Add(item);
        }

        return result;
    }
}

Hay algunos puntos importantes por resaltar, el primero se define un método como abstract, cada clase concreta será responsabilidad desarrollarlo, pero además para ayudar se define un método adicional que actuaria como témplate para el calculo, define el código que detectamos como repetitivo y añade un punto de extensibilidad por medio del uso de Func<>

En los lugares donde se requiere especificidad que solo la clase concreta conoce se deja el lugar abierto para poder hacerlo, básicamente el Func<> es un delegado que recibe una fecha y un valor numérico, y espera como respuesta una fecha

Las clases concretas se ven ahora beneficiadas, reduciéndose notablemente el código

 

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

public class UnSoloPago : ResolverPlazo
{
    public override 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 : ResolverPlazo
{
    public override PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return base.Calcular(fechainicio,
            monto,
            montocuota,
            (fecha, cuotas) => fecha.AddDays(7 * cuotas),
            (fecha, cuota) => fecha.AddDays(7 * cuota));

    }
}

public class Quincenal : ResolverPlazo
{
    public override PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return base.Calcular(fechainicio,
                            monto,
                            montocuota,
                            (fecha, cuotas) => fecha.AddDays(15 * cuotas),
                            (fecha, cuota) => fecha.AddDays(15 * cuota));
    }
}

public class Mensual : ResolverPlazo
{
    public override PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return base.Calcular(fechainicio,
                            monto,
                            montocuota,
                            (fecha, cuotas) => fecha.AddMonths(1 * cuotas),
                            (fecha, cuota) => fecha.AddMonths(1 * cuota));
    }
}

public class Bimestral : ResolverPlazo
{
    public override PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return base.Calcular(fechainicio,
                monto,
                montocuota,
                (fecha, cuotas) => fecha.AddMonths(2 * cuotas),
                (fecha, cuota) => fecha.AddMonths(2 * cuota));
    }
}

public class Trimestral : ResolverPlazo
{
    public override PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return base.Calcular(fechainicio,
                               monto,
                               montocuota,
                               (fecha, cuotas) => fecha.AddMonths(3 * cuotas),
                               (fecha, cuota) => fecha.AddMonths(3 * cuota));
    }
}

public class Semestral : ResolverPlazo
{
    public override PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return base.Calcular(fechainicio,
                               monto,
                               montocuota,
                               (fecha, cuotas) => fecha.AddMonths(6 * cuotas),
                               (fecha, cuota) => fecha.AddMonths(6 * cuota));
    }
}

public class Anual : ResolverPlazo
{
    public override PazoPagoResult Calcular(DateTime fechainicio, decimal monto, decimal montocuota)
    {
        return base.Calcular(fechainicio,
                               monto,
                               montocuota,
                               (fecha, cuotas) => fecha.AddYears(1 * cuotas),
                               (fecha, cuota) => fecha.AddYears(1 * cuota));
    }
}

Solo las dos primeras clases (que no tienes lógica en la calculo de los plazos de pago) quedaron como se definen originalmente, mientras que el resto hace uso de la funcionalidad definida en la clase base, nótese como las expresiones lambda en los parámetros del delegado permite variar el calculo de la fecha en cada caso.

 

Resolver de forma directa la clase de calculo


Este caso quizás no aplique en todas los casos ya que se requiere de entorno en donde los controles puedan conservar las instancias de los ítems con que con cargados, en este caso por tratarse de un ejemplo con Windows Application puede aplicarse, pero si seria un entorno web no se podria ya que los controles no conservan estado, se debería seguir usando la implementación original.

Lo que se pretende es poder quitar el método que usa el Dictionary<> para definir que clase concreta resolverá el calculo, el primer cambio se realiza en la clase que se usara para cargar el combo

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

se agrega una propiedad que define la instancia de la clase que resuelve el calculo de plazos para ese ítem

El siguiente cambio impacta en la forma como se devuelve la lista de ítems, se define nueva la propiedad con la instancia concreta de cada implementación de calculo

 

public static class PlazosHelper
{

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

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

}

Solo queda cambiar la forma como se recupera el ítem del combo y se invoca el método de calculo

 

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;
    }
   
    //
    // Recuperamos la instancia de la entidad bindeada al combo
    //
    ResolverPlazo calcularPlazo = ((Plazo)cmdPlazo.SelectedItem).Instancia;

    //
    // se invoca de forma directa la operacion de calcular
    //
    PazoPagoResult result = calcularPlazo.Calcular(dtpFechaInicio.Value,
                                                        monto,
                                                        montoporcuota);

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

    dgvListaPagos.DataSource = result.ListaPagos;

}

La clave esta en estas dos líneas

//
// Recuperamos la instancia de la entidad bindeada al combo
//
ResolverPlazo calcularPlazo = ((Plazo)cmdPlazo.SelectedItem).Instancia;

//
// se invoca de forma directa la operacion de calcular
//
PazoPagoResult result = calcularPlazo.Calcular(dtpFechaInicio.Value,
                                                    monto,
                                                    montoporcuota);

Como cada ítem del combo permite recuperar la clase con que fue creado, se puede castear al tipo concreto para así recuperar la instancia de la clase que posee la lógica de calculo de las cuotas, con eso se evita usa un método adicional que defina un diccionario de correspondencias entre un id y su instancia

Ya no se requiere el SelectedValue, porque no se trabaja con ningún id o código, sino que se accede directo a la instancia de la clase concreta, acortando las interacciones

 

Código


[C#]
 

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#]
 

[ASP.NET] Master Page - Referencia relativa a elementos de la página

 

Introducción


Durante el diseño de un sitio web la referencia a los elementos dentro de la página es un aspecto que genera errores en el desarrollo.

Este problema se potencia aun mas si se hace uso de Master Page, pues el diseño del master contendrá objetos que aplicaran con diferentes url relativas según donde se encuentre la página web desarrollada.

Recordemos que al final quien determina la url final es la pagina aspx y no el master page.

 

Detección de problema


Imaginemos que tenemos un desarrollo simple de solo dos paginas y una master page, como ser:

imagen1

Como se observa en la estructura del ejemplo tenemos el master page se encuentra en el raíz del sitio junto con una pagina de inicio, pero también observamos una carpeta que contendrá paginas pertenecientes a un modulo de administración, ambos aspx observados implementaran el mismo MasterPage.

Un diseño lógicamente nos indicaría que aseguráramos que la vista del master page en el Visual Studio este correcta, por lo que si usáramos:

imagen2

En el diseñador del Visual Studio veríamos que parece ir todo sobre ruedas.

Al aplicar el master page a la pagina Default.aspx, no notaríamos mayor problema ya que al igual que la master están ambos al mismo nivel.

imagen3

Pero que sucede si vamos a la pagina que se encuentra dentro de la carpeta Admin

imagen4

Rápidamente se observa que la imagen que define la master no puede ser resuelta adecuadamente, aquí no se observa pero esto mismo que claramente se aprecia con la imagen sucederá con los archivo .js, si se ejecuta la pagina el código cliente fallara porque las librerías de jquery no podrán encontrarse.

 

Resolver el problema


Existen algunas técnicas que pueden ser usadas para resolver el problema de la url relativa.

Una forma simple de lograrlo es por medio del ResolveClientUrl(), si bien en tiempo de diseño no se podrá apreciar el recurso de forma visual en el diseñador del Visual Studio

imagen5

se podrá asegurar con esta técnica que sin importar en que nivel se encuentre la pagina que implementa la master las url generada resolverán de manera correcta.

Ahora al acceder al código de la pagina que se encuentra dentro de la carpeta a un nivel diferente

imagen6

se ha creado la ruta relativa correcta desde la master.

Desde la pagina Default.aspx por encontrarse al mismo nivel la url es distinta pero sigue resolviendo de forma adecuada

imagen7

 

Otras técnicas


Existen otras técnicas que implican código, se podría usar

protected void Page_Load(object sender, EventArgs e)
{
    ScriptManager.RegisterClientScriptInclude(Page, typeof(Page), "jquery", ResolveClientUrl(@"~/Scripts/jquery-1.4.1.js"));
    ScriptManager.RegisterClientScriptInclude(Page, typeof(Page), "jquery.validate", ResolveClientUrl(@"~/Scripts/jquery.validate.js"));
}

El tema es que el ScriptManager.RegisterClientScriptInclude() crea la definición del <script> dentro del tag <form> por lo que el código que podríamos definir en el <head> causaría error.

También se podría intentar usar

protected void Page_Load(object sender, EventArgs e)
{

    HtmlGenericControl jqueryctrl = new HtmlGenericControl("script");
    jqueryctrl.Attributes.Add("type", "text/javascript");
    jqueryctrl.Attributes.Add("src", ResolveClientUrl(@"~/Scripts/jquery-1.4.1.js"));
    this.Page.Header.Controls.Add(jqueryctrl);

    HtmlGenericControl jqvalidatectrl = new HtmlGenericControl("script");
    jqvalidatectrl.Attributes.Add("type", "text/javascript");
    jqvalidatectrl.Attributes.Add("src", ResolveClientUrl(@"~/Scripts/jquery.validate.js"));
    this.Page.Header.Controls.Add(jqvalidatectrl);


}

Esto resolvería el problema anterior colocando el tag <script> dentro del <head>, pero aun así lo ubica por debajo de la sección del ContentPlaceHolder que se define como lugar donde colocar el script de la pagina requiere

Es por eso que lo mas acertado seria definir el html donde se necesita utilizando el ResolveClientUrl()

 

Código