lunes, 7 de noviembre de 2011

Builders genéricos: Implementación (2)

Continuación del post: Builders genéricos: Introducción (1).

En principio, ¿Qué es un builder?

Según Wikipedia, es un patrón de diseño que es usado para permitir la creación de una variedad de objetos complejos desde un objeto fuente (Producto).

Intención: Abstrae el proceso de creación de un objeto complejo, centralizando dicho proceso en un único punto, de tal forma que el mismo proceso de construcción pueda crear representaciones diferentes.

Vamos a definir las clases básicas que manipularemos.

   
public class Person
{
    public string Name { get; set; }
    public int? Age { get; set; }
}

En este ejemplo, la persona sería el producto que queremos construir.

Empezemos con definir nuestra interfaz IPersonaBuilder que nos permitirá construir nuestra persona:

public interface IPersonBuilder
{
    IPersonBuilder WithName(string name);
    IPersonBuilder WithAge(int age);
    Person Build();
}

La interfaz tiene 2 métodos que devuelven un objeto del mismo tipo que la interfaz para poder utilizarse de forma Fluent (como se vió en mi post anterior) y un método que devuelve el objeto creado.

Una posible implementación sería la siguiente:

   
public class PersonBuilder : IPersonBuilder
{
    protected Person person;
    public PersonBuilder()
    {
        person = new Person();
    }
    public virtual IPersonBuilder WithName(string name)
    {
        person.Name = name;
        return this;
    }
    public virtual IPersonBuilder WithAge(int age)
    {
        person.Age = age;
        return this;
    }
    public Person Build()
    {
        Person personAux = person;
        person = null;
        return personAux;
    }
}


La implementación no tiene nada fuera de lo común, cada método "With" setea una propiedad de la persona y devuelve el builder.

Se utilizaría de la siguiente manera:
Person juan = new PersonBuilder()
                .WithName("Juan")
                .WithAge(30)
                .Build();


Quizás alguien me pregunten porque en lugar de lo anterior no utilicé Object Initializers y, aprovechando las ventajas de C# 3.0, el código se reduzca a esto:
Person juan = new Person
                {
                    Name = "Juan",
                    Age = 30
                };


En este caso, se puede utilizar Object Initializers porque es un caso trivial pero si el objeto a construir necesite otro tipo de propiedades más complejas, o si la lógica de construcción pueda ser cambiada, esto no sería tan simple.

Ahora supongamos que queremos crear otro builder de persona para, por ejemplo, un Doctor. En este caso, queremos que le agregue el prefijo "Dr. " al nombre. La clase extendería a PersonBuilder y quedaría así:
public class DoctorBuilder : PersonBuilder
{
    public override IPersonBuilder WithName(string name)
    {
        person.Name = "Dr. " + name;
        return this;
    }
}


Hagamos unos Tests simples con NUnit para comprobar que funcionan correctamente los Builders:

[Test]
public void TestPersonBuilder()
{
    Person juan = new PersonBuilder()
                        .WithName("Juan")
                        .WithAge(30)
                        .Build();
    Assert.That(juan, Is.Not.Null, "El Builder no devolvió el objeto!");
    Assert.That(juan.Name, Is.Not.Null.And.Not.Empty, "No se pudo setear el nombre!");
    Assert.That(juan.Name, Is.EqualTo("Juan"), "Nombre incorrecto!");
    Assert.That(juan.Age.HasValue, Is.True, "No se pudo setear la edad!");
    Assert.That(juan.Age.Value, Is.EqualTo(30), "Edad mal seteada");
}

[Test]
public void TestDoctorBuilder()
{
    Person juan = new DoctorBuilder()
                        .WithName("Juan")
                        .WithAge(30)
                        .Build();
    Assert.That(juan, Is.Not.Null, "El Builder no devolvió el objeto!");
    Assert.That(juan.Name, Is.Not.Null.And.Not.Empty, "No se pudo setear el nombre!");
    Assert.That(juan.Name, Is.EqualTo("Dr. Juan"), "Nombre incorrecto!");
    Assert.That(juan.Age.HasValue, Is.True, "No se pudo setear la edad!");
    Assert.That(juan.Age.Value, Is.EqualTo(30), "Edad mal seteada");
}


Si corren los test el resultado será satisfactorio.

Ahora supongamos que queremos generalizar la interfaz base de un builder con las siguientes premisas:

  1. La construcción final del objeto se hace llamando al método Build.
  2. Se pueden setear propiedades mediante actions llamando al método With. 

La interfaz podría quedar de la siguiente manera:

public interface IBuilder<TObject>
{
    IBuilder<TObject> With(Action<TObject> setProperties);
    TObject Build();
}



Sin embargo, como vimos en el post anterior, tenemos los problemas de casteo al extender las clases y utilizar el método With ya que deberíamos estar casteando constantemente IBuilder<TObject> a la clase correspondiente.

Por lo tanto, haciendo mejor uso de Generics la interfaz quedaría de la siguiente manera:


public interface IBuilder<TBuilder,TObject>
    where TBuilder : IBuilder<TBuilder, TObject>
{
    TBuilder With(Action<TObject> setProperties);
    TObject Build();
}


La primera implementación de nuestro Builder genérico, siguiendo la interfaz anterior, quedaría así:

public abstract class AbstractBuilder<TBuilder,TObject> : IBuilder<TBuilder,TObject>
    where TBuilder : AbstractBuilder<TBuilder, TObject>
    where TObject : class, new()
{
    protected TObject obj;
    protected AbstractBuilder()
    {
        obj = new TObject();
    }

    public virtual TBuilder With(Action<TObject> setProperties)
    {
        if (setProperties == null)
            throw new ArgumentNullException("setProperties");
        setProperties(obj);
        return this as TBuilder;
    }

    public virtual TObject Build()
    {
        TObject objAux = obj;
        obj = null;
        return objAux;
    }
}


La contra que tiene esta solución es que el objeto a construir debe tener un constructor sin parámetros por defecto. Esto igualmente puede solucionarse fácilmente creando un método abstracto TObject New() que los builders concretos sobreescriban para generar el objeto por defecto.
Por ahora con esta solución nos alcanza.

¿Cómo quedaría nuestra clase PersonBuilder utilizando esto y cómo se utilizaría de la manera más genérica posible?

public class GenericPersonBuilder : AbstractBuilder<GenericPersonBuilder,Person>
{
}

[Test]
public void TestGenericPersonBuilder()
{
    Person juan = new GenericPersonBuilder()
                        .With(person => person.Name = "Juan")
                        .With(person => person.Age = 30)
                        .Build();
    Assert.That(juan, Is.Not.Null, "El Builder no devolvió el objeto!");
    Assert.That(juan.Name, Is.Not.Null.And.Not.Empty, "No se pudo setear el nombre!");
    Assert.That(juan.Name, Is.EqualTo("Juan"), "Nombre incorrecto!");
    Assert.That(juan.Age.HasValue, Is.True, "No se pudo setear la edad!");
    Assert.That(juan.Age.Value, Is.EqualTo(30), "Edad mal seteada");
}


¿Cómo quedarían nuestras clases PersonBuilder y DoctorBuilder agregándole métodos concretos?

public abstract class GenericPersonBuilder<TBuilder> : AbstractBuilder<TBuilder, Person>
    where TBuilder : GenericPersonBuilder<TBuilder>
{
    public virtual TBuilder WithName(string name)
    {
        return With(person => person.Name = name);
    }
    public virtual TBuilder WithAge(int age)
    {
        return With(person => person.Age = age);
    }
}

public class PersonBuilder2 : GenericPersonBuilder<Personbuilder2>
{
}

public class DoctorBuilder2 : GenericPersonBuilder<Doctorbuilder2>
{
    public override DoctorBuilder2 WithName(string name)
    {
        return With(person => person.Name = "Dr. " + name);
    }
}


Los tests serían exáctamente iguales a los primeros 2 solamente habría que cambiar la clase PersonBuilder por PersonBuilder2 y DoctorBuilder por DoctorBuilder2 así que no las voy a escribir nuevamente. Si quieren, prueben correr los tests y verán que funcionan correctamente.

En la tercera parte, hablaré de casos más complejos en la que la utilización de Builders es muy útil y una implementación distinta del AbstractBuilder.

No hay comentarios:

Publicar un comentario