Factoría de Objetos: Crea objetos con Expression Trees y atributos

Bueno, después de unos meses de inactividad – el trabajo y la familia también existen – vuelvo con más fuerzas que nunca a publicar algunas técnicas nuevas para vuestras aplicaciones en .NET

Esta vez vamos a dedicarnos a crear una factoría de objetos mediante Linq Expression Trees, sumándole el uso de CustomAttributes, lo que nos va a permitir un desacoplamiento total de la factoría respecto a los objetos que crea. Esto permitiría tener una factoría en una capa separada que nadie debería tocar para crear sus nuevos tipos de objeto. Simplemente definiendo la clase y colocando un atributo, la factoría detectará los objetos que sabe crear en el inicio de aplicación sin tener que modificar su código (de hecho no haría falta ni tener los fuentes de la factoría).

Como siempre, podéis bajaros el proyecto completo usando Git o simplemente en formato .ZIP. Tened en cuenta que si queréis usarlo con una versión anterior a Visual Studio 2015, necesitaréis cambiar los Console.WriteLine para que usen string format, ya que estoy usando la nueva sintaxis de C# 6 para generar los textos (que chulada es! ).

Descargar ZIP: https://bitbucket.org/joanvilarino/messagehandlersample/get/62edf410d1ae.zip
Clonar repositorio Git: https://joanvilarino@bitbucket.org/joanvilarino/messagehandlersample.git


Como sabéis los que seguís mis artículos, siempre procuro montar un escenario de utilidad para que veáis la técnica aplicada a algo lo más parecido a la realidad que os encontraréis en vuestros proyectos.

El gestor de mensajes

Para el ejemplo de hoy vamos a suponer que tenemos que hacer una librería que recibe mensajes de una aplicación externa, en formato string, y que cada clase de mensaje se ha de analizar de manera diferente porque contiene diferentes datos. En todos ellos, vamos a establecer un separador de campos con el carácter “|”. En el proyecto de demostración tendremos tres tipos diferentes de mensaje:

  • Mensaje tipo “TABLE” contiene una Id y un Nombre (Name)
  • Mensaje tipo “ARTICLE” contiene un Código (Code), si está activo (IsActive) y una cantidad (Quantity).
  • Mensaje tipo “CALENDAR” contiene una Id, una fecha (Date) y una descripción (Description)

Al ser todo esto respuestas de un servicio imaginario que deberíamos tener, les llamaremos Responses, y cada clase que las interpreta (response handler) generará un tipo diferente de DTO (Data Transfer Object) con las propiedades que necesite.

Aquí van los DTOs

public class TableDTO
{
    public long Id { get; set; }
    public string Name { get; set; }
}
public class ArticleDTO
{
    public string Code { get; set; }
    public bool IsActive { get; set; }
    public decimal Quantity { get; set; }
}
public class CalendarDTO
{
    public DateTime Date { get; set; }
    public string Description { get; set; }
}

Response handlers

Lo que yo he llamado response handlers son las clases que se encargarán de analizar el string y sacar la información que necesitamos en el formato de DTO requerido. Vamos a hacer que todos hereden de GenericResponse que al mismo tiempo hereda de BaseResponse (utilizamos esta para poder usar su tipo sin genéricos como tipo devuelto).

public abstract class BaseResponse
{
    public static char FieldSeparator = '|';

    protected string Value { get; set; }

    public BaseResponse(string value)
    {
        Value = value;
    }
}
public abstract class GenericResponse<T> : BaseResponse where T : class, new()
{
    public GenericResponse(string value) : base(value) { }

    public abstract T GetValue();
}

Como podéis ver, el punto común es el método GetValue de GenericResponse que devolverá el DTO ya analizado a partir del string y es común a todos los response handler.

Una vez tenemos las clases base, vamos a crear nuestras clases Response handler para cada tipo de DTO (os pongo solo la primera porque el resto simplemente cambia en el valor del tipo de registro que maneja puesto en el atributo, los campos que parsea y el genérico de DTO que devuelve):

[HandlesResponseType("TABLE")]
public class TableResponse : GenericResponse<TableDTO>
{
    public TableResponse(string value) : base(value) { }

    public override TableDTO GetValue()
    {
        try
        {
            var result = new TestDTO();
            var valueArray = this.Value.Split(FieldSeparator);
            result.Id = long.Parse(valueArray[1]);
            result.Name = valueArray[2];
            return result;
        }
        catch
        {
            throw new FormatException();
        }
    }
}

Aquí hay que tener en cuenta ese atributo HandlesResponseType, que es el que la factoría buscará después para conocer los tipos que ha de saber crear. Este atributo le dice, en primer lugar que esta clase es un Response handler, y en segundo lugar, que tipo de registro sabe manejar.

Creando nuestra factoría de objetos

El patrón de factoría es muy conocido en el mundo de la programación. Se trata de desacoplar la creación de objetos del negocio usando una clase (normalmente estática o con el patrón singleton) que nos “fabrique” nuestros objetos ya poblados con los datos que necesiten, más o menos de esta manera:

var myNewObj = ResponseFactory.Create(params);

Muchas veces, la factoria directamente hace new de las clases que necesita utilizando un bloque switch o un bloque if-elseif que decide que tipo de objeto generar. El problema de esa aproximación es que nos estamos “atando” a las clases que la factoría conoce en tiempo de compilación.

Para evitar ese problema y conseguir una factoría totalmente desacoplada, usaremos una clase base que la factoría conoce, y haremos que nuestros objetos hereden de ella, aunque esto no es obligatorio, pero dado que la estamos creando para algo común, no está de más usar la herencia y el polimorfismo que son dos herramientas brutales al programar. Ésta segunda manera es obteniendo el tipo a instanciar por Reflection y usando Activator.CreateInstance para hacerlo. Éste método puede ahorrarnos el acoplamiento de las clases, pero si tenemos que crear muchos objetos (decenas o cientos de miles) Activator es muy lento porque tiene que buscar todos los metadatos del tipo que queremos instanciar.

Expression Trees al rescate

Para evitar los problemas de velocidad de Activator, una técnica muy utilizada (y avanzada) son los Expression Trees. Un Expression tree utiliza la librería System.Linq.Expressions para generar datos que representan código, que puede ser modificado y compilado para obtener un delegado que haga lo que queramos en tiempo de ejecución, en vez de en tiempo de compilación, lo cual nos libera de las clases específicas y nos permite desacoplarnos totalmente.

Nuestra factoría va a utilizar esa técnica para generar unos delegados “activadores” de la clase que queramos, y guardaremos esos activadores en un diccionario “cache” una vez ya compilados al inicio de la aplicación, para utilizarlos en la creación de objetos con una rapidez bastantes veces superior a Activator, solo superado por la técnica Emit de la cual hablaremos en su día, ya que implica conocer algo de IL (si, ese monstruo parecido al assembler que se esconde debajo de .NET y sale por las noches a darnos pesadillas y comer niños programadores crudos).

Generando el diccionario de activadores

Al arranque de la aplicación el constructor static de la factoría se encargará de buscar en la ExecutingAssembly los tipos que nosotros marcaremos con un atributo HandlesResponseTypeAttribute (modificando el código podríamos incluso cargar assemblies externas a partir de su .dll), generará un delegado de activación para cada uno y lo almacenará usando como clave del diccionario el tipo de campo que hemos declarado en el atributo para obtenerlo después:

static ResponseFactory()
{
    // Generamos el diccionario que usaremos de cache de activadores
    _expressionTreeActivatorsCache = Assembly.GetExecutingAssembly().GetTypes()
        .Where(t => t.GetCustomAttributes<HandlesResponseTypeAttribute>().Any())
        .Select(type => {
            var handlesResponseTypeAttr = type.GetCustomAttribute<HandlesResponseTypeAttribute>().ResponseType;
            return new { responseType = handlesResponseTypeAttr, activator = BuildExpressionTreeActivatorFor(type) };
        })
        .ToDictionary(o => o.responseType, o => o.activator);
}

Generando nuestro activador

El método de generación de las expression trees se dedica a obtener los parámetros del constructor y montar expresiones para generarlos de esta manera:

private static T GetExpressionTreeActivatorFromCtor<T>(ConstructorInfo ctor) where T : class
{
     // Obtenemos los parámetros del constructor
     var ctorParams = ctor.GetParameters();
     var paramExp = Expression.Parameter(typeof(object[]), "args");
     // Creamos el expression tree para los argumentos del constructor
     var expList = ctorParams.Select((ctp, i) =>
         {
             var ctorType = ctp.ParameterType;
             var argExp = Expression.ArrayIndex(paramExp, Expression.Constant(i));
             var argExpConverted = Expression.Convert(argExp, ctorType);
             return argExpConverted;
         });
     // Creamos la expresion
     var newExp = Expression.New(ctor, expList);
     // La compilamos para obtener el activador
     return Expression.Lambda<T>(newExp, paramExp).Compile();
}

Si os fijáis, al final todas las expresiones de los parámetros se juntan en una expresión “padre” que contiene el constructor de nuestra clase y la lista generada:

var newExp = Expression.New(ctor, expList);

Una vez tenemos la expresión devolvemos el delegado compilado usando Expression.Lambda así:

return Expression.Lambda<T>(newExp, paramExp).Compile();

Esto convierte los “datos” de la expresión en código compilado en tiempo de ejecución, ya que la generación es costosa, y por eso la guardamos en un diccionario para no tener que hacerlo cada vez que queramos instanciar un objeto (sino sería más rápido Activator).

Aunque podría haber utilizado un delegado con el parámetro string a secas que van a usar todos nuestros handlers, para el ejemplo he utilizado un método que ya tenía por ahí que sirve para cualquier clase, obtiene su constructor por defecto y genera el activador para tantos parámetros como tenga el constructor.

Creando los objetos

Cuando llamamos a Create desde nuestra aplicación pasando el string que nos ha llegado de nuestro servicio ficticio, la factoría extrae el “tipo de registro” (primer campo del string separado por “|”) y lo usa para buscar en el diccionario su activador, generar el nuevo objeto y devolverlo.

public static BaseResponse Create(string value)
{
    if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException();
    // Obtener el tipo de response
    var responseType = value.Split(BaseResponse.FieldSeparator)[0];
    // Obtener el delegado activador para crear el objeto desde la cache
    var activator = GetActivatorFor(responseType);
    // Instanciamos el objeto llamando al activador con el parámetro
    var result = activator(value);

    return (BaseResponse)result;
}

Pero, ¿esto funciona? Se le supone…

Bien, ya tenemos nuestra arquitectura montada, así que vamos a probarla en el método main de nuestro programa para ver que tal se comporta:

static void Main(string[] args)
{

    var responseList = new List<string>
    {
        "TABLE|10|Item Id 10",
        "ARTICLE|ABCCDEE|true|2,5",
        "TABLE|15|Item Id 15",
        "TABLE|20|Item Id 20",
        "CALENDAR|01/05/2016 14:00|Meeting",
        "ARTICLE|9388d99|false|0,20",
        "CALENDAR|06/02/2017 17:00|Buy more Redbull"
    };

    Console.WriteLine("Processing list: ");
    Console.WriteLine();


    var responses = responseList.Select(responseString => ResponseFactory.Create(responseString));

    // If you want to process the responses
    ProcessTableResponses(responses.OfType<TableResponse>());
    ProcessArticleResponses(responses.OfType<ArticleResponse>());
    ProcessCalendarResponses(responses.OfType<CalendarResponse>());

    // If you just want the DTOs
    var tableDtos = responses.OfType<TableResponse>().Select(r => r.GetValue());
    var articleDtos = responses.OfType<articleResponse>().Select(r => r.GetValue());
    var calendarDtos = responses.OfType<CalendarResponse>().Select(r => r.GetValue());

    Console.WriteLine();
    Console.Write("Press any key to exit...");
    Console.ReadKey();
}

Como podéis ver, hemos creado una lista de string simulando lo que podríamos recibir de nuestro servicio imaginario con registros de los tres tipos, para a continuación, utilizando la factoría, obtener una lista de objetos Response handler.

La factoría se encargará de seleccionar la clase correcta para cada tipo de mensaje como hemos visto antes, y nos quedaremos con un IEnumerable en “responses” con los diferentes tipos de response en una lista polimórfica (todos heredan de ResponseBase, ¿verdad?).

A continuación para probar el proceso llamamos a unos métodos privados filtrando los response handlers de cada tipo usando .OfType (bendito método) para que nos muestren los resultados en la cónsola.

Por si lo que queréis es simplemente obtener los DTOs, debajo hay 3 lineas con tableDtos, articleDtos y thirdDtos que usan los response handler de cada tipo para obtener sus DTOs, aunque después no se hace nada con ellos. Están ahí simplemente como demostración.

Gracias!

Bueno… hoy el artículo me ha quedado un poco espeso, pero hay que reconocer que el tema era denso y he intentado abreviar lo máximo posible. Si aún así os quedan dudas, aquí estoy para resolverlas. Dejad un post y intentaré contestarlas lo mejor que pueda.

Como siempre, podéis bajaros el proyecto completo usando Git o simplemente en formato .ZIP. Tened en cuenta que si queréis usarlo con una versión anterior a Visual Studio 2015, necesitaréis cambiar los Console.WriteLine para que usen string format, ya que estoy usando la nueva sintaxis de C# 6 para generar los textos.

Descargar ZIP: https://bitbucket.org/joanvilarino/messagehandlersample/get/62edf410d1ae.zip
Clonar repositorio Git: https://joanvilarino@bitbucket.org/joanvilarino/messagehandlersample.git

Saludos y paz para todos!

Follow me

Joan Vilariño

Senior .NET Developer at Ohpen
Follow me

1 comentario

  1. “- el trabajo y la familia también existen – ” y el WoW….

Deja un comentario

Tu dirección de correo electrónico no será publicada.

*

A %d blogueros les gusta esto:
Ir a la barra de herramientas