Estrategias de código de portable : #1 PCL - Portable Class Library | C#

Intermedio

C#, Visual Studio y herramientas como Xamarin nos permiten utilizar y aprovechar mucho mejor nuestro código a traves de las diferentes plataformas de Apps.

  • iOS
  • Android
  • Windows Phone
  • Windows 8 [WinRT]

Como developer debes tener las habilidades necesarias para sacar provecho a todas estas herramientas, por ello he creado esta serie de artículos junto con algunos videos (coming soon...) que estoy seguro te van a mostrar que tipo de magia es capaz de hacer un programador con las herramientas correctas y algo de creatividad.

Código fuente de este artículo

JuanKRuiz GitHub

El código fuente completo de este artículo se encuentra disponible en GitHub, incluye proyectos de Apps con todos los casos expuestos.

https://github.com/JuanKRuiz/DemoPCL

App de prueba

Nuestra App de prueba para WinRT y Windows Phone,lo único que hace es que al cargar la primera pantalla almacena la fecha y hora de la última ejecución.

Al cargar la Page principal programamos el evento Load y allí hacemos la lógica necesaria.

Los proyectos de la solución están de la siguiente forma:

  • Libreria PCL : Windows 8.1 + Windows Phone [Silverlight] 8.1
  • Windows 8.1
  • Windows Phone [Silverlight] 8.1

A continuación veremos la versión más simple y pobre de programar este requerimiento en cada una de las plataformas:

Código WinRT

 //LocalSettingsManager.cs
using System.Diagnostics;  
using Windows.Storage;

namespace WinRTApp.Manager  
{
    public class LocalSettingsManager
    {
        public void Set(string key, string value)
        {
            ValidateSecurity();
            ApplicationData.Current.LocalSettings.Values[key] = value;
            Audit();
        }

        public void ValidateSecurity()
        {
            //TODO a lot of things
        }

        public void Audit()
        {
            Debug.WriteLine("Parameter Set on WinRT");
            //TODO a lot of things
        }
    }
}
 //MainPage.xaml.cs [fragment]
private void Page_Loaded(object sender, RoutedEventArgs e)  
{
    var lsm = new LocalSettingsManager();
    lsm.Set("Last start:", DateTime.Now.ToString());
}

Código Windows Phone

 //LocalSettingsManager.cs
using System.Diagnostics;  
using System.IO.IsolatedStorage;

namespace WinPhoneApp.Manager  
{
    public class LocalSettingsManager
    {
        public void Set(string key, string value)
        {
            ValidateSecurity();
            IsolatedStorageSettings.ApplicationSettings[key] = value;
            Audit();
        }

        public void ValidateSecurity()
        {
            //TODO a lot of things
        }

        public void Audit()
        {
            Debug.WriteLine("Parameter Set on WindowsPhone");
            //TODO a lot of things
        }
    }
}
 //MainPage.xaml.cs [fragment]
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)  
{
    var lsm = new LocalSettingsManager();
    lsm.Set("Last start:", DateTime.Now.ToString());
}

Output

 Parameter Set on WinRT  
Parameter Set on WindowsPhone  

Código redundante

Como pueden apreciar en la solución tenemos una buena cantidad de código redundante.

Prácticamente toda la clase LocalSettingsManager esta duplicada con excepción de la línea de código que guarda los settings y la línea con el mensaje de auditoria.

Observen que tenemos otros dos métodos

  • Audit
  • ValidateSecurity

Y esto es porque en el mundo real guardar estos settings puede involucrar decenas o cientos de líneas de código más relacionados con temas de seguridad o lógia de negocio.

Para poner solución a ese problema les enseñaré diferentes estrategias de código portable con PCL.

Librerias Portables

PCL: Portable Class Library

Una libreria portable es una libreria cuyo código y dependencias externas pueden ser llevadas a diversas plataformas sin perder funcionalidad.

Técnicamente no necesitas nada especial para hacer una PCL, pero tendrías que tu mismo estar pendiente de que funcionalidades están disponibles en cada una de las plataformas que deseas soportar, esto de por si puede ser bastante complicado y muy propenso a cometer errores que te pueden costar luego muchísimas horas de trabajo.

Por suerte Visual Studio incorpora una funcionalidad para crear PCL, y el mismo se encarga de controlar que clases puedes utilizar según las plataformas que decidas soportar desde un comienzo.

Escoger proyecto PCL Escoger proyecto PCL

Establecer las plataformas a soportar

Escoger plataformas a soportar

Cuidado con lo que deseas

Intuitivamente uno trataria de crear una PCL que soporte todas las plataformas y es perfectamente, pero tiene un problema.

Entre más plataformas decidas soportar menos cosas en común tendrán entre ellas por ende el código que puedes compartir fácilmente es mucho menor así como la cantidad de librerías compatibles.

Plataformas a soportar en una PCL

Una vez se ha creado el proyecto es tiempo de programar, sin embargo crear un proyecto de libreria portable es fácil solo cuando el 100% del código será utilizado en las diferentes plataformas, pero por lo general este no es el caso y es por ello que revisaremos estrategias de programaciónpara reutilizar tanto codigo como sea posible.

POO - Strategy Pattern

Este patrón esta basado principalmente en la inyección de comportamientos, por lo cual es una solución extremamente flexible pero también más exigente en cuanto diseño de la lógica.

Básicamente se debe programar contra interfaces para que luego se haga una composición con esas interfaces como si estas tuviesen toda la lógica necesaria, realmente la lógica se podría inclusive cambiar en tiempo de ejecución simplemente asignando un objeto distinto a la interfaz.

Lo primero que hacemos es encapsular lo que cambia, que en este caso es:

  • Guardar los settings
  • Mensaje de la plataforma

Dado que en ambas plataformas esto se hace diferente encapsulamos esta lógica en una interfaz ISettingsWriter, dentro del LocalSettingsManager modificamos la lógica para que esta funcione contra cualquier objeto que exponga dicha interfaz, e 'inyectamos' el comportamiento deseado desde el constructor.

Esta es la implementación en la PCL, como ven la lógica completa del componente está creada, pero la parte que cambia no ha sido implementada.

Código PCL

 //ISettingsWriter.cs
namespace PCL_S.Manager  
{
    public interface ISettingsWriter
    {
        void Set(string key, string value);

        string Message { get; }
    }
}
 //LocalSettingsManager.cs
using System.Diagnostics;

namespace PCL_S.Manager  
{
    public class LocalSettingsManager
    {
        public ISettingsWriter Writer { get; set; }

        public LocalSettingsManager(ISettingsWriter writer)
        {
            Writer = writer;
        }

        public void Set(string key, string value)
        {
            ValidateSecurity();
            Writer.Set(key, value);
            Audit();
        }

        public void ValidateSecurity()
        {
            //TODO a lot of things
        }

        public void Audit()
        {
            Debug.WriteLine("Parameter Set on {0}", Writer.Message);
            //TODO a lot of things
        }
    }
}

Ahora en cada uno de los proyectos agregamos una referencia a la PCL creada, y en cada uno creamos un objecto que implemente la interfaz ISettingsWriter con la lógica requerida en cada caso.

Así mismo modificamos el evento Load del Page para que invoque el LocalSettingsManager con el objeto ISettingsWriter correspondiente en cada caso.

Código WinRT

 //WinRTSettingsWriter.cs
using PCL_S.Manager;  
using Windows.Storage;

namespace WinRTS.Manager  
{
    class WinRTSettingsWriter : ISettingsWriter
    {
        public void Set(string key, string value)
        {
            ApplicationData.Current.LocalSettings.Values[key] = value;
        }

        public string Message
        {
            get { return "WinRT - Strategy"; }
        }
    }
}
 //MainPage.xaml.cs [fragment]
private void Page_Loaded(object sender, RoutedEventArgs e)  
{
    var writer = new WinRTSettingsWriter();
    var lsm = new LocalSettingsManager(writer);
    lsm.Set("Last start", DateTime.Now.ToString());
}

Código Windows Phone

 //PhoneSettingsWriter.cs
using PCL_S.Manager;  
using System.IO.IsolatedStorage;

namespace WinPhoneS.Manager  
{
    class PhoneSettingsWriter : ISettingsWriter
    {
        public void Set(string key, string value)
        {
            IsolatedStorageSettings.ApplicationSettings[key]=value;
        }

        public string Message
        {
            get
            { return "Windows Phone - Strategy"; }
        }
    }
}
 //MainPage.xaml.cs [fragment]
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)  
{
    var writer = new PhoneSettingsWriter();
    var lsm = new LocalSettingsManager(writer);
    lsm.Set("Last start",DateTime.Now.ToString());
}

Output

 Parameter Set on WinRT - Strategy  
Parameter Set on Windows Phone - Strategy  

POO - Template Method Pattern

La finalidad de este patrón es encapsular un algoritmo sin que este dependa explícitamente de la lógica interna de cada paso.

Otra forma de verlo es pensar en una receta, en algún momento de la receta te pedirán calentar la preparación, pero dependiendo del contexto a veces lo harás con una estufa, a veces on un horno o a veces con un micro ondas, incluso a veces con una vela o una cobija... no debería importar pues los pasos del algoritmo no dependen de la implementación de "Calentar" .

Como lo hemos anotado anteriormente las partes de la lógica que cambian son:

  • Guardar los settings
  • Mensaje de la plataforma

Por lo que podemos crear una clase abstracta que haga todo lo que se requiere y en estas partes específicas se llamen a métodos que no han sido implementados aún pero que se espera que sean implementados por las clases que hereden de la clase abstracta.

Hay dos mecanismos para lograr esto

  • Método | Propiedad abstract : El método debe ser implementado por la clase que herede
  • Método | Propiedad virtual : El método no necesariamente debe ser implementado por la clase que herede, ya que hay una implementación por defecto.

Haremos uso de ambas opciones solo con fines didácticos.

Código PCL

 //LocalSettingsManager.cs
using System.Diagnostics;

namespace PCL_T.Manager  
{
    public abstract class LocalSettingsManager
    {
        public void Set(string key, string value)
        {
            ValidateSecurity();
            WriteSetting(key, value);
            Audit();
        }

        public void ValidateSecurity()
        {
            //TODO a lot of things
        }

        public void Audit()
        {
            Debug.WriteLine("Parameter Set on {0}", this.Message);
            //TODO a lot of things
        }

        public abstract void WriteSetting(string key, string value);

        public virtual string Message { get{return "[Generic]";} }
    }
}

Ahora en cada uno de los proyectos agregamos una referencia a la PCL creada, y en cada uno creamos un objecto que herede de la clase abstracta LocalSettingsManager implementando la lógica requerida en cada caso.

Así mismo modificamos el evento Load del Page para que invoque el LocalSettingsManager correspondiente de cada plataforma.

Código WinRT

 //WinRTLocalSettingsManager.cs
using PCL_T.Manager;  
using Windows.Storage;

namespace WinRTAppT.Manager  
{
    class WinRTLocalSettingsManager: LocalSettingsManager
    {
        public override void WriteSetting(string key, string value)
        {
            ApplicationData.Current.LocalSettings.Values[key] = value;
        }

        public override string Message
        { get { return "WinRT - Template Method"; } }
    }
}
 //MainPage.xaml.cs [fragment]
private void Page_Loaded(object sender, RoutedEventArgs e)  
{
    var lsm = new WinRTLocalSettingsManager();
    lsm.Set("Last start",DateTime.Now.ToString());
}

Código Windows Phone

En esta implementación intencionalmente he dejado comentadas dos versiones de la propiedad Message con el fin de que ustedes exploren los diferentes resultados

  • Al dejarlos comentados
  • Al dejar la primera implementación
  • Al dejar la segunda implementación
 //WinPhoneLocalSettingsManager.cs
using PCL_T.Manager;  
using System.IO.IsolatedStorage;

namespace WinPhoneAppT.Manager  
{
    class WinPhoneLocalSettingsManager : LocalSettingsManager
    {
        public override void WriteSetting(string key, string value)
        {
            IsolatedStorageSettings.ApplicationSettings[key] = value;
        }

        //public override string Message
        //{ get { return "Windows Phone - Template Method"; } }

        //public override string Message
        //{ get { return base.Message + ": Windows Phone - Template Method"; } }
    }
}
 //MainPage.xaml.cs [fragment]
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)  
{
    var lsm = new WinPhoneLocalSettingsManager();
    lsm.Set("Last start", DateTime.Now.ToString());
}

Output

 Parameter Set on WinRT - Template Method  
Parameter Set on [Generic]  

Conclusiones

Las PCL nos brindan una oportunidad importante de optimización de nuestro código, pero debemos ser ingeniosos en la construcción del código y en mayor medida debemos ser capaces de detectar oportunidades de refactoring para que nuestra PCL evolucione de la manera adecuada.

En cuanto a los patrones, podemos decir que ambas soluciones son ingeniosas y nos permiten maximizar la cantidad de código reutilizable, en este ejemplo fue evidente la conveniencia de utilizar Template Method ya que la cantidad de esfuerzo requerido para la creación es notablemente menor que con Estrategy. Sin embargo en muchos casos de la vida real se requiere la mayor flexibilidad del patrón estragia, ya que al trabajar por composición nos es mucho más fácil extender y encapsular las funcionalidades.