Patrones de creación (II): Patrón Builder (Constructor)


Objetivo:

“Separar la construcción de un objeto complejo de su representación, de modo que el mismo proceso de construcción pueda crear representaciones diferentes.”

Design Patterns: Elements of Reusable Object-Oriented Software

Patrón Builder

Hablando en plata, el patrón Builder es un patrón creacional cuyo objetivo es instanciar objetos complejos que generalmente están compuestos por varios elementos y que admiten diversas configuraciones. Cuando hablamos de “construcción” nos referimos al proceso, mientras que cuando hablamos de “representación” nos estaremos refiriendo a los datos que componen el objeto. Se encargará, por tanto, de encapsular todo el proceso de generación de modo que únicamente necesite los detalles necesarios para “personalizar” el objeto, devolviendo como resultado una instancia del objeto complejo que deseamos construir. Es un patrón fuertemente ligado a otro, el patrón estructural Composite, del que hablaremos en posteriores artículos.

Un ejemplo sencillo sería el siguiente:


            Vehiculo v = new Vehiculo();
            v.NumPuertas = 5;
            v.Matricula = "1034 CAA";
            v.Faros = new Faro[4];
            v.Faros[0] = new Faro(TipoFaro.Xenon);
            v.Faros[1] = new Faro(TipoFaro.Xenon);
            v.Faros[2] = new Faro(TipoFaro.Lexus);
            v.Faros[3] = new Faro(TipoFaro.Lexus);
            v.Color = "Rojo";
            v.Motor = new Motor(TipoMotor.Gasolina);
            v.Motor.Capacidad = 2200;
            v.Motor.Valvulas = 12;

Como vemos, la lógica de nuestro programa construye un vehículo compuesto de muchas partes (de hecho, podríamos seguir añadiendo detalles) de forma secuencial. El patrón Builder se encargará de encapsular todo este proceso haciendo que, mediante la obtención de estos detalles, el proceso de generación de este objeto sea transparente para quien lo solicita.


            DirectorConstruccion director = new DirectorConstruccion (new ConstructorFordFiestaSportEdition());
            Vehiculo v = director.ConstruirVehiculo();

El resultado a nivel computacional será más o menos el mismo, ya que el proceso de “ensamblado” debe realizarse de todos modos. La diferencia radica en que la lógica del programa no necesita saber cómo ensamblar el objeto, sino únicamente aquellas piezas que quiere que éste disponga. Si vas a comprar un vehículo, no le indicarás al vendedor el proceso de fabricación de éste paso a paso: él únicamente debe proporcionarte un vehículo que se ajuste a las características que tú le solicitas, dejándole a él los detalles de cómo el vehículo es construido.

Elementos del patrón

Utilizaremos el ejemplo de los vehículos para entender cómo funciona este patrón. Comenzaremos por el final: el producto que queremos obtener. Se trata del objeto complejo que queremos obtener, que suponemos que estará compuesto por un buen número de elementos. Imaginemos que queremos obtener un vehículo y que su estructura es la siguiente:

En código C# vendría traducido a lo siguiente:


    // Tipo de rueda: diámetro, llanta y neumático
    public class Rueda
    {
        public int Diametro { get; set; }
        public string Llanta { get; set; }
        public string Neumatico { get; set; }
    }
    // Tipo de carrocería
    public class Carroceria
    {
        public bool HabitaculoReforzado { get; set; }
        public string Material { get; set; }
        public string TipoCarroceria { get; set; }
    }
    // Interfaz que expone las propiedades del motor
    public interface IMotor
    {
        string ConsumirCombustible();
        string InyectarCombustible(int cantidad);
        string RealizarEscape();
        string RealizarExpansion();
    }
    // Motor diesel, que implementará la interfaz IMotor
    public class MotorDiesel : IMotor
    {
        #region IMotor Members

        public string ConsumirCombustible()
        {
            return RealizarCombustion();
        }

        public string InyectarCombustible(int cantidad)
        {
            return string.Format("MotorDiesel: Inyectados {0} ml. de Gasoil.", cantidad);
        }

        public string RealizarEscape()
        {
            return "MotorDiesel: Realizado escape de gases";
        }

        public string RealizarExpansion()
        {
            return "MotorDiesel: Realizada expansion";
        }

        #endregion

        private string RealizarCombustion()
        {
            return "MotorDiesel: Realizada combustion del Gasoil";
        }
    }

Finalmente, la clase que simboliza el vehículo estará formada por los elementos anteriores más algún atributo más.


    public class Vehiculo
    {
        public bool CierreCentralizado { get; set; }
        public string Color { get; set; }
        public bool DireccionAsistida { get; set; }
        public string Marca { get; set; }
        public string Modelo { get; set; }
        public IMotor Motor { get; set; }
        public Carroceria TipoCarroceria { get; set; }
        public Rueda TipoRuedas { get; set; }

        public string GetPrestaciones()
        {
            StringBuilder sb = new StringBuilder();
            string nl = Environment.NewLine;

            sb.Append("El presente vehiculo es un ").Append(Marca).Append(" ").Append(Modelo);
            sb.Append(" estilo ").Append(TipoCarroceria.TipoCarroceria).Append(nl);
            sb.Append("Color: ").Append(Color).Append(nl);
            sb.Append(DireccionAsistida ? "Con " : "Sin ").Append("direccion asistida").Append(nl);
            sb.Append(CierreCentralizado ? "Con " : "Sin ").Append("cierre centralizado").Append(nl);
            sb.Append("Carroceria de ").Append(TipoCarroceria.Material);
            sb.Append(TipoCarroceria.HabitaculoReforzado ? " con " : " sin ").Append("el habitaculo reforzado").Append(nl);
            sb.Append("Ruedas con llantas ").Append(TipoRuedas.Llanta).Append(" de ").Append(TipoRuedas.Diametro).Append(" cm").Append(nl);
            sb.Append("Neumaticos ").Append(TipoRuedas.Neumatico);
            sb.Append("Respuesta del motor: ").Append(Motor.InyectarCombustible(100));

            return sb.ToString();
        }
    }

El producto, como podemos imaginar, será el elemento final una vez haya sido correctamente configurado. Marquémonos como objetivo la construcción del siguiente vehículo: Audi A3 Sportback. Este vehículo constará de llantas de aluminio de 17cm y neumáticos Michelín, color plata cromado, cierre centralizado y dirección asistida, con una carrocería reforzada de fibra de carbono. Ya tenemos el objetivo. Ahora necesitamos un constructor (builder) que lo construya por nosotros.

La clase Builder y los constructores concretos

La clase constructora será la encargada de construir nuestro objeto. Sin embargo, esta clase no debe preocuparse de cómo debe construir el producto, sino únicamente de qué partes han de construirse. Al igual que en una fábrica de ensamblaje, el peón debe centrarse en una tarea concreta de construcción, dejando el proceso del diseño para otros trabajadores más especializados.

Sin embargo, antes de ponernos a codificar el contenido de nuestro constructor, es preciso pensar un poco en la (más que probable) posibilidad de que otro cliente decida, en algún momento dado, un vehículo diferente. Por ello, definiremos una interfaz a la que todo constructor deberá ceñirse para asegurar la compatibilidad del proceso. Todos los constructores de coches deberán saber añadir ruedas, carrocería o motor, por lo que crearemos una interfaz IVehiculoBuilder que implemente todas estas operaciones. Recordemos que las operaciones deben ceñirse al proceso de construir las partes, nunca de cómo combinarlas (ni en qué orden), ya que la formación del constructor no le habilita para tomar decisiones de ingeniería.

Otra posibilidad, en lugar de utilizar una interfaz, será la de hacer uso de una clase que defina la funcionalidad común y declare como abstractos los métodos que deberá implementar el constructor concreto para cada coche en particular. Ciñámonos a esta segunda opción y creemos la clase VehiculoBuilder, que será como sigue:

La interfaz tendrá un aspecto como el siguiente:


    public abstract class VehiculoBuilder
    {
        // Declaramos la referencia del producto a construir
        protected Vehiculo v;

        // Declaramos el método que recuperará el objeto
        public Vehiculo GetVehiculo()
        {
            return v;
        }

        #region Métodos Abstractos

        public abstract void DefinirVehiculo();
        public abstract void ConstruirRuedas();
        public abstract void ConstruirHabitaculo();
        public abstract void ConstruirMotor();
        public abstract void DefinirExtras();

        #endregion
    }

Lo que el constructor debe saber hacer, por lo tanto, es definir marca y modelo, construir las ruedas, el habitáculo, el motor y añadir los extras. También debe contar con la instancia del vehículo que debe devolver, al igual que un método para poder devolverlo.

Una vez que hemos definido de forma genérica qué operaciones debe realizar el constructor, codificaremos una clase A3SportbackBuilder que herede de VehiculoBuilder y que implemente el código de los métodos abstractos, de modo que proporcione una instancia de un Audi A3 Sportback Edition:


    public class A3SportbackBuilder : VehiculoBuilder
    {
        public override void DefinirVehiculo()
        {
            v = new Vehiculo();
            v.Marca = "Audi";
            v.Modelo = "A3 Sportback";
        }

        // Método que construirá las ruedas
        public override void ConstruirRuedas()
        {
            v.TipoRuedas = new Rueda();
            v.TipoRuedas.Diametro = 17;
            v.TipoRuedas.Llanta = "aluminio";
            v.TipoRuedas.Neumatico = "Michelin";
        }

        // Método que construirá el motor
        public override void ConstruirMotor()
        {
            v.Motor = new MotorDiesel();
        }

        // Método que construirá el habitaculo
        public override void ConstruirHabitaculo()
        {
            v.TipoCarroceria = new Carroceria();
            v.TipoCarroceria.TipoCarroceria = "deportivo";
            v.TipoCarroceria.HabitaculoReforzado = true;
            v.TipoCarroceria.Material = "fibra de carbono";
            v.Color = "plata cromado";
        }

        public override void DefinirExtras()
        {
            v.CierreCentralizado = true;
            v.DireccionAsistida = true;
        }
    }

Si otro cliente llegara y nos pidiera un Citroen Xsara Picasso negro con llantas de 15cm estándar, neumáticos Firestone y sin extras, codificaríamos una nueva clase que también herede de VehiculoBuilder y que implemente de una forma distinta todos los pasos necesarios para construir el vehículo.


    public class CitroenXsaraBuilder : VehiculoBuilder
    {
        public override void DefinirVehiculo()
        {
            v = new Vehiculo();
            v.Marca = "Citroen";
            v.Modelo = "Xsara Picasso";
        }

        // Método que construirá las ruedas
        public override void ConstruirRuedas()
        {
            v.TipoRuedas = new Rueda();
            v.TipoRuedas.Diametro = 15;
            v.TipoRuedas.Llanta = "hierro";
            v.TipoRuedas.Neumatico = "Firestone";
        }

        public override void ConstruirMotor()
        {
            v.Motor = new MotorDiesel();
        }

        // Método que construirá el habitaculo
        public override void ConstruirHabitaculo()
        {
            v.TipoCarroceria = new Carroceria();
            v.TipoCarroceria.TipoCarroceria = "monovolumen";
            v.TipoCarroceria.HabitaculoReforzado = false;
            v.TipoCarroceria.Material = "acero";
            v.Color = "negro";
        }

        public override void DefinirExtras()
        {
            v.CierreCentralizado = false;
            v.DireccionAsistida = false;
        }
    }

La estructura de nuestra clase constructora (VehiculoBuilder) y los constructores concretos (A3SportbackBuilder y CitroenXaraBuilder) sería la siguiente:

La clase directora

Ahora que ya sabemos cómo generar las partes, es necesario saber cómo ensamblarlas. Esta operación no se realizará en el constructor, sino que se creará una nueva clase, denominada Directora, que se encargará de toda la lógica del ensamblado, lo cual incluye orden, validaciones, comprobaciones, etc.

Nuestra clase directora, por tanto, no sabrá construir las partes, tarea que delegará en los constructores, sino que sabrá cómo construir el todo a partir de éstas. Lo normal es que la clase directora reciba un único parámetro, de tipo VehiculoConstructor (es decir, del constructor genérico) y genere las partes invocando los métodos comunes, sin necesidad de preocuparse de los detalles.

A la clase directora no le hace falta saber si está construyendo un Audi A3 o un Citroen Xsara. La clase directora debe saber que el chasis se monta antes que las ruedas o que es necesario incluir dirección asistida en un modelo deportivo.

Por lo tanto, crearemos una clase directora cuyo método principal será ConstruirVehiculo() y que constará de un elemento VehiculoBuilder, que recibirá como parámetro, así:

El código de nuestro director será el siguiente:


    public class VehiculoDirector
    {
        private VehiculoBuilder builder;

        // Constructor que recibirá un Builder concreto y lo asociará al director
        public VehiculoDirector(VehiculoBuilder builder)
        {
            this.builder = builder;
        }

        public void ConstruirVehiculo()
        {
            // Construimos el vehiculo definiendo el orden del proceso
            builder.DefinirVehiculo();
            builder.ConstruirHabitaculo();
            builder.ConstruirMotor();
            builder.ConstruirRuedas();
            builder.DefinirExtras();

            // Se realizan comprobaciones y validaciones
            if ((builder.GetVehiculo().TipoCarroceria.TipoCarroceria == "deportivo") &&
                (builder.GetVehiculo().DireccionAsistida == false))
                throw new Exception("Error en el ensamblado: Un deportivo no puede carecer de direccion asistida");
        }

        public Vehiculo GetVehiculo()
        {
            return builder.GetVehiculo();
        }
    }

Utilizando el patrón Builder

La utilización de este patrón, por lo tanto, consistirá en lo siguiente:

  • Instanciar un nuevo director pasándole como parámetro el constructor concreto que se encargará de construir las piezas.
  • Indicarle al constructor que construya el objeto.
  • Recuperar el objeto del director (que, a su vez, lo recupera del constructor concreto).

El siguiente código muestra cómo se usan dos directores que reciben dos constructores concretos distintos para generar, a su vez, dos vehículos distintos.


            // Definimos un director, pasándole un constructor de Audi como parámetro
            VehiculoDirector directorAudi = new VehiculoDirector(new A3SportbackBuilder());

            // El director construye el vehiculo, delegando en el constructor la tarea de
            // creación de cada una de las piezas
            directorAudi.ConstruirVehiculo();

            // Obtenemos el vehículo directamente del director, aunque la instancia del vehículo
            // se encuentra en el constructor.
            Vehiculo audiA3 = directorAudi.GetVehiculo();

            // Repetimos el proceso con un constructor distinto.
            VehiculoDirector directorCitroen = new VehiculoDirector(new CitroenXsaraBuilder());
            directorCitroen.ConstruirVehiculo();
            Vehiculo citroen = directorCitroen.GetVehiculo();

            // Mostramos por pantalla los dos vehiculos:
            Console.WriteLine("PRIMER VEHICULO:" + Environment.NewLine);
            Console.WriteLine(audiA3.GetPrestaciones());

            Console.WriteLine("SEGUNDO VEHICULO:" + Environment.NewLine);
            Console.WriteLine(citroen.GetPrestaciones());

            Console.ReadLine();

El resultado de este código se muestra a continuación:

¿Cuándo utilizar este patrón? Ejemplos reales

Este patrón es útil en alguno de los siguientes supuestos:

  • Cuando el algoritmo para crear un objeto complejo puede independizarse de las partes que componen el objeto y de cómo son ensambladas.
  • Cuando el proceso de construcción debe permitir distintas representaciones para el objeto que se construye.
  • Cuando el objeto a construir es complejo y sus distintas configuraciones son limitadas. En caso de que necesitemos un objeto complejo pero cada una de sus partes deba ser configurado de forma individual (en el ejemplo que nos ocupa, se trataría de definir cada elemento “al gusto” del consumidor en lugar de objetos predefinidos), este patrón no será una buena idea, ya que será necesario realizar el proceso de asignación de cada elemento paso a paso.

Como ejemplos reales, GoF ilustra su aplicación con un parser
RTF, en el que separa los algoritmos de procesamiento dependiendo del texto a transformar (ASCII, TEX…). Puede verse el ejemplo aquí.

Otro ejemplo puede ser la generación de distintos tipos de Sitemaps (Google, HTML…). El ejemplo puede verse aquí.

Fuentes:

One comment

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s