1 may 2011

Patrones de Diseño: Pluggable Structure

Hace mucho que no posteaba en mi blog de tecnología...así que hoy vuelvo al ruedo.
Intentaré hacer posteos de forma mas seguida y de paso google no me baja su pagerank!
Tenía ganas de retomar el blog para presentar una solución a un problema que seguramente se habrán enfrentado y quizás (por no decir casi seguro) lo habrán resuelto de una manera no muy elegante. No quiero decir que esta solución pretenda ser la mejor ni mucho menos, pero aunque sea de forma didáctica está más que interesante.
El problema es el siguiente: Supongamos que tenemos que grabar un mensaje de log en diferentes salidas, una podría ser enviar un mensaje a través de un servicio WCF, otra en el FileSystem, otra en el EventLog de windows y la última en una base de datos.
Ya sé que muchos dirán.....ufff tenés un montón de librerías que hacen eso, como log4net por ejemplo que 100% customizable desde un archivo .config.
La idea de esta entrada no es reemplazar una funcionalidad ya existente, como el logging por ejemplo, sino mostrar a través de este ejemplo un patrón que lo podemos llamar Pluggable Structure. Por qué el nombre? Bueno, porque podés cambiar dinámicamente, esto es, sin recompilar, ni tocar el código .net, el proveedor de Logging, y todo con solo cambiar el nombre en un .config.
Voy a presentar mi app.config de esta forma:





















Si observamos, lo primero que hice fue definir una sección personalizada "myLogggingProvider" donde voy a setear los providers soportados y el que se usa por defecto. Si observamos tenemos una etiqueta "providers". Aquí se colocan los 4 proveedores de los que hablé antes. Por último tenemos un atributo "defaultProvider", aquí se coloca el proveedor que queremos usar. Listo, con eso ya estaría nuestro .config. Ahora lo que vamos a hacer es definir las clases que le den soporte a esta configuración.
Esta sería la clase que mapea contra nuestra sección "myLoggingProvider":


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;

namespace TestPatternsConsole.Code.Configuration
{
public class LogProviderConfigurationSection : ConfigurationSection
{
[ConfigurationProperty("providers")]
public ProviderSettingsCollection Providers
{
//gets the providers in the app.config file
get { return (ProviderSettingsCollection)base["providers"]; }
}

///
/// Gets the default provider in the app.config file
///

[StringValidator(MinLength = 1)]
[ConfigurationProperty("defaultProvider", DefaultValue = "WCFLogProvider")]
public string DefaultProvider
{
get { return (string)base["defaultProvider"]; }
set { base["defaultProvider"] = value; }
}
}
}


Como vemos, tiene definidas 2 propiedades: Providers y DefaultProvider, que luego seteamos en el app.config.
Atención: Debemos incluir en nuestro proyecto referencia a System.Configuration.dll.
Luego veamos la estructura general del proyecto, así se dan una idea:



Como ven tenemos los 4 proveedores de logging: DbLogProvider, EventLogLogProvider, WCFLogProvider y FileLogProvider.
Todas estas clases implementan la interfaz ILogProvider:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestPatternsConsole.Code.Base
{
///
/// Innterface for Logging providers
///

public interface ILogProvider
{
void Log(string logMessage);
}
}


Lo que me permite este factory, es trabajar contra la interfaz, independientemente del provider que la implemente.
Así por ejemplo la clase que loguea en WCF hará algo así:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TestPatternsConsole.Code.Base;

namespace TestPatternsConsole.Code
{
public class WCFLogProvider : ILogProvider
{
public void Log(string logMessage)
{
Console.WriteLine("This providers logs on a WCF Service: {0} ", logMessage);
}
}
}


Y ahora la parte más importante...la clase que va a llamar el cliente y que va a "levantar" la configuración del app.config.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using TestPatternsConsole.Code.Configuration;
using TestPatternsConsole.Code.Base;

namespace TestPatternsConsole.Code
{
public static class LogProviderLoader
{
private static ILogProvider _defaultProvider = null;
private static object _syncroot = new object();

public static void Log(string message)
{
SetDefaultProvider();
_defaultProvider.Log(message);
}

private static void SetDefaultProvider()
{
if (_defaultProvider == null)
{
lock (_syncroot)
{
//retrieves all the providers from the app.config file
LogProviderConfigurationSection section = (LogProviderConfigurationSection)ConfigurationManager.GetSection("sectionLoggingProviders/myLogggingProvider");

//gets the providers
Dictionary<string,ILogProvider> _logs = new Dictionary<string,ILogProvider>();

//generates instances for each provider
foreach (ProviderSettings p in section.Providers)
{
_logs.Add(p.Name,(ILogProvider)Activator.CreateInstance(Type.GetType(p.Type)));
}

_defaultProvider = _logs[section.DefaultProvider];
}
}
}
}
}


Aquí se aplican varios patrones, entre ellos el Singleton, que asegura tener una única instancia de ILogProvider. Como se ve en la clase de arriba, nunca se hace referencia directa a un proveedor en particular, más cuando se instancia un proveedor (haciendo uso de la clase Activator) se castea contra la interfaz, y la implementación queda para la clase en particular.
Luego desde un cliente se puede hacer:


namespace TestPatternsConsole
{
class Program
{
static void Main(string[] args)
{
LogProviderLoader.Log("hola");
}
}
}


Aquí subo el código.
y listo! El patrón implementado y funcionando.
Saludos,
Mike