miércoles, 8 de enero de 2014

Patrones de creación: Builder

Este post corresponde a la serie Patrones de diseño - Patrones de creación

Builder

"Separa la construcción de un objeto complejo de su representación, de forma que el mismo proceso de construcción puede crear representaciones distintas"

Como en la mayoría de patrones de diseño entender la definición a primera vista es difícil. Intentaré explicar exactamente qué quiere decir esta definición del patrón Builder.
La idea fundamental es separar la lógica de construcción de los objetos resultantes, de esta forma podemos reutilizar la misma lógica de construcción pero obtener distintos objetos complejos.

Vamos a definir un ejemplo del problema que el patrón Builder intenta solucionar e iremos viendo cómo solucionarlo.

El problema

Vamos a desarrollar un módulo de una aplicación de gestión. El módulo debe crear a partir de un listado de productos una serie de documentos distintos. 
  • Un archivo Excel con un listado de precios de los productos para los comerciales.
  • Un archivo en formato Word con información técnica de los productos para enviar a los responsables técnicos del cliente.
  • Un archivo en formato Pdf con información comercial para enviar a los responsables de compras de los clientes
Estos son sólo tres ejemplos, pero es fácil entender que la lista de posibles "exportaciones" de estos datos es muy amplia, y además se irá ampliando en el tiempo con distintas versiones etc.

Si  miramos el problema desde un nivel de abstracción un poco superior al detalle de cada documento a generar nuestro problema se reduce a regenerar distintas representaciones de la misma información. El proceso de construcción es prácticamente igual en todas las forma de representar los datos. El resultado final, sin embargo, es muy distinto para cada documento que tenemos que exportar.

El siguiente es una mala implementación en pseudocódigo que nos podríamos plantear:

class Program
{
    static void Main(string[] args)
    {
        // Obtenemos la lista de productos
        var productList = DataSource.GetProductList();

        // Decidimos que documento debemos generar
        switch (documentType)
        {
            case "PriceList":
                ExportPriceList(productList);
                break;
            case "TechnicalInfo":
                ExportTechnicalInfo(productList);
                break;
            case "CommercialInfo":
                ExportCommercialInfo(productList);
                break;
        }

    }

    private static void ExportPriceList(List<product> productList)
    {
        // Creamos un documento Excel e insertamos una fila
        // por cada producto con información sobre precio e impuestos.
        CreateExcelDocumet();
        foreach (var product in productList)
        {
            CreateExcelRow();
            WriteProductIdCell(product);
            WriteProductNameCell(product);
            WriteProductPriceCell(product);
            WriteProductTaxes(product);
        }
        SaveExcelDocument();
    }

    private static void ExportTechnicalInfo(List<product> productList)
    {
        // Creamos un documento Word e insertamos una página
        // por cada producto con información técnica sobre el producto.
        CreateWordDocumet();
        foreach (var product in productList)
        {
            CreateWordPage();
            WriteProductIdCell(product);
            WriteProductNameCell(product);
            DrawProductImage(product);
            WriteProductTechnicalDetails(product);
        }
        SaveWordDocument();
    }

    private static void ExportCommercialInfo(List<product> productList)
    {
        // Creamos un documento Pdf e insertamos una página
        // por cada producto con información sobre las ventajas
        // comerciales de nuestros productos.
        CreatePdfDocumet();
        foreach (var product in productList)
        {
            CreatePdfPage();
            WriteProductIdCell(product);
            WriteProductNameCell(product);
            DrawProductImage(product);
            WriteProductCommercialAdvantages(product);
        }
        SavePdfDocument();
    }
}

El problema con el código anterior es que el número de posibles formatos que debemos crear está abierto y debemos buscar una forma fácil de agregar nuevas conversiones.

Builder

El patrón builder propone una solución a este problema que se basa en separar la forma de construir estos objetos complejos de los objetos que resultan después de la construcción. Dicho de otra forma, propone crear un objeto que indique qué partes debemos construir y otro que construya esas partes como mejor crea de acuerdo con el resultado que se quiere obtener.

Veamos el diagrama UML del patrón Builder y los objetos que participan en el:


Estos son los objetos participantes en el patrón Builder:
  • Builder: Clase abstracta que define las partes a crear del objeto producto. Esta clase abstracta podemos sustituirla por una interfaz.
  • ConcreteBuilder: Implementa la interfaz de Builder para crear y ensamblar las partes de Product. Provee una interfaz para obtener el producto (GetResult())
  • Director: Construye un producto usando la interfaz Builder
  • Product: Representa el objeto complejo que se construye.
El patrón Builder propone crear un objeto que indique qué partes debemos construir y otro que construya esas partes como mejor crea de acuerdo con el resultado que se quiere obtener.

Será más fácil entender sobre nuestro ejemplo. Este es el diagrama UML de nuestra adaptación del patrón builder que aplicaremos en nuestro ejemplo:


Estos son nuestros objetos del Builder:
  • Builder: Clase abstracta donde definimos todas las partes que podemos crear del producto. En nuestro ejemplo la implementaremos como una clase abstracta con métodos virtuales con implementación vacía. Esto nos permitirá implementar en cada ConcreteBuilder implementar sólo los métodos que necesitemos.
  • PriceListBuilder, TechnicalCatalogBuilder, CommercialCatalogBuilder: Son los ConcreteBuilder para cada tipo de documento que queremos exportar. Cada uno sobrescribe los métodos virtuales de Builder que necesita.
  • ProductReader (Director): Es la clase responsable de leer el listado de productos y llamar a método Build de cada parte a construir.
  • Excel, Word, Pdf: Instancias concretas de cada producto que podemos construir. Archivos de distintos formatos.
Veamos la implementación en pseudocódigo ya que la implementación concreta de los ConcreteBuilder no es interesante para explicar el patrón:
public abstract class Builder
{
    public virtual void BuildNewProduct(Product product) { }
    public virtual void BuildProductId() { }
    public virtual void BuildProductName() { }
    public virtual void BuildProductPrice() { }
    public virtual void BuildProductTaxes() { }
    public virtual void BuildProductTechicalInfo() { }
    public virtual void BuildProductCommercialInfo() { }
    public abstract object GetResult();
}

public class PriceListBuilder : Builder
{

    private dynamic currentSheet;
    private Microsoft.Office.Interop.Excel.Application app;
    private int currentRow;
    private Product currentProduct;

    public PriceListBuilder()
    {
        // Creamos una aplicación Excel y obtenemos la hoja actual.
        app = new Microsoft.Office.Interop.Excel.Application();
        app.Workbooks.Add();
        app.Workbooks[1].Sheets.Add();
        currentSheet = app.Workbooks[1].Sheets[1];

        currentRow++;
        WriteHeader();
    }

    private void WriteHeader()
    {
        //Escribimos las cabeceras de las columnas.
    }

    public override void BuildNewProduct(Product product)
    {
        // Asignamos el product actual.
        currentProduct = product;

        // Nos movemos a la siguiente fila.
        currentRow++;
    }

    public override void BuildProductId()
    {
        currentSheet.Cells[currentRow, 1] = currentProduct.Id;
    }

    public override void BuildProductName()
    {
        currentSheet.Cells[currentRow, 2] = currentProduct.Name;
    }

    public override void BuildProductPrice()
    {
        currentSheet.Cells[currentRow, 3] = currentProduct.Price;
    }

    public override void BuildProductTaxes()
    {
        currentSheet.Cells[currentRow, 4] = currentProduct.Taxes;
    }

    public override object GetResult()
    {
        return app;
    }
}

public class TechnicalCatalogBuilder : Builder
{
    /*
        * Esta clase crea un documento word con información técnica
        * de los productos. Por lo tanto no sobreescribe los métodos que 
        * se refieren a información sobre precios, impuestos, etc.
        * 
        * 
        */



    public TechnicalCatalogBuilder()
    {

        // Creamos documento Word

    }

    public override void BuildNewProduct(Product product)
    {
        // Agregamos una nueva página al documento Word.

    }

    public override void BuildProductId()
    {
        // Agregamos un párrafo con el identificador del producto.
    }

    public override void BuildProductName()
    {
        // Agregamos un párrafo con el nombre del producto.
    }

    public override void BuildProductTechicalInfo()
    {
        // Agregamos un párrafo con la información técnica del producto.
    }

    public override object GetResult()
    {
        // Devolvemos el documento Word.
        return wordDocument;
    }
}

public class CommercialCatalogBuilder : Builder
{
    /*
        * Esta clase crea un documento pdf con información comercial
        * de los productos. Por lo tanto no sobreescribe los métodos que 
        * se refieren a información técnica, etc.
        * 
        * 
        */

    public CommercialCatalogBuilder()
    {

        // Creamos documento Pdf

    }

    public override void BuildNewProduct(Product product)
    {
        // Agregamos una nueva página al documento Pdf.

    }

    public override void BuildProductId()
    {
        // Agregamos un párrafo con el identificador del producto.
    }

    public override void BuildProductName()
    {
        // Agregamos un párrafo con el nombre del producto.
    }

    public override void BuildProductCommercialInfo()
    {
        // Agregamos un párrafo con la información commercial del producto.
    }

    public override object GetResult()
    {
        // Devolvemos el documento Pdf.
        return pdfDocument;
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
    public double Taxes { get; set; }
}

public class ProductReader
{
    private IEnumerable<product> products;
    private Builder builder;

    public ProductReader(IEnumerable<product> productList, Builder concreteBuilder)
    {
        products = productList;
        builder = concreteBuilder;
    }

    public object Construct()
    {
        foreach (var product in products)
        {
            builder.BuildNewProduct(product);
            builder.BuildProductId();
            builder.BuildProductName();
            builder.BuildProductPrice();
            builder.BuildProductTaxes();
            builder.BuildProductTechicalInfo();
            builder.BuildProductCommercialInfo();
        }

        return builder.GetResult();
    }
}

class Program
{
    static void Main(string[] args)
    {
        IEnumerable<product> productList = ProductProvider.GetProducts();
        Builder concreteBuilder = GetConcreteBuilder();
        ProductReader director = new ProductReader(productList, concreteBuilder);

        var resultado = director.Construct();

        // Manejar el resultado como necesitemos,
        // Guardamos a disco, enviar por email, etc.

    }

    private static Builder GetConcreteBuilder()
    {
        // Devolvemos el builder concreto que necesitamos
        // en base a los criterios que requiera nuestra
        // aplicación. Por ejemplo selección del usuario.

        // La lógica de cómo obtener el builder concreto
        // dependerá de cada aplicación.

        return new PriceListBuilder();
    }
}

En el ejemplo hemos decidido crear archivos de diferentes formatos, pero podría haber sido diferentes versiones de un archivo, etc.
Como podemos observar hemos encapsulado la lógica de construcción en el método Construct() de la clase ProductReader y la forma concreta en que se ensamblan todas las piezas de cada producto (Excel, word, Pdf) a los ConcreteBuilder. Con esto conseguimos reutilizar la lógica de construcción del objeto complejo y para crear un nuevo tipo de "exportación" solamente tenemos que crear un nuevo ConcreteBuilder.

Consecuencias de usar el patrón Builder

Estas son las ventajas e inconvenientes principales de usar el patrón Builder en la creación de objetos complejos:

Ventajas

  • Nos permite cambiar la representación interna de los productos. El director (ProductReader) construye el objeto a través de una clase abstracta por lo que está aislado de la forma en que se ensambla el objeto. Ya que la clase Builder oculta al director la representación del objeto (la forma en que se ensambla), todo lo que tenemos que hacer para cambiar la representación del producto es crear un nuevo ConcreteBuilder. Por ejemplo una clase AcmeErpInteropBuilder que creará un archivo de productos para importar en el ERP de la empresa Acme.
  • Aísla el código de la construcción (el código de ProductReader) del código de la representación (el código de cada ConcreteBuilder). El cliente no necesita saber nada sobre las clases que definen la estructura de los archivos que se generan ya que las clases ConcreteBuilder no aparecen en la interfaz pública del patrón.
  • Tenemos mucho control sobre el proceso de construcción del objeto. El patrón Builder crea los objetos complejos paso a paso bajo la "supervisión" del director. Esto nos da mucho control sobre el proceso de construcción en aspectos como los pasos necesarios para la construcción del objeto complejo así como el orden de esos pasos.

  Inconvenientes

  • La interfaz de la clase Builder debe ser lo suficientemente genérica para permitir la construcción de cualquier tipo de objetos por parte de los ConcreteBuilder. Esto hace que tengamos que tener en la interfaz de Builder métodos que no usarán todos con ConcreteBuilder.
  • El objeto complejo creado a veces se devuelve poco tipado. Como en nuestro caso cada builder concreto devuelve objetos diferentes la el tipado de la interfaz del método Construct() es débil con respecto al tipo devuelto, en nuestro caso object.
Al igual que el patrón Abstract Factory, el patrón Builder se usa en la construcción de objetos complejos. La principal diferencia es que el patrón Builder se centra en la creación paso a paso del objeto, mientras que el patrón Abstract Factory se centra en crear familias de objetos (simples o complejos). El patrón Builder crea objetos mediante una serie de pasos de los que el último es obtener el objeto creado. Desde el punto de vista de Abstract Factory el objeto se crea inmediatamente, en un solo paso.

Usos conocidos

Podemos ver una variante del patrón Builder en .NET Framework en objetos como DbConnectionStringBuilder, SqlConnectionStringBuilder, OracleConnectionStringBuilder, etc. que mediante el establecimiento de propiedades permiten crear cadenas de conexión sintácticamente correctas.

Hasta aquí el patrón Builder. En el próximo post hablaremos de otro patrón de creación. Factory Method

Espero vuestros comentarios.

4 comentarios:

  1. Hola!
    Me encantan los patrones de diseño, y encontrar un sitio en el que se explican claramente, de manera sencilla y en español siempre es de agradecer.
    Muy bueno!!

    Volveré a por más.
    Un saludo!

    ResponderEliminar
  2. Gracias Antonio.
    Echa un vistazo al resto de post y comenta cualquier cosa que creas que podría mejorar el blog.
    Un saludo!

    ResponderEliminar
  3. A mi me parece mucho mas facil y mas entendible el pseudocodigo que lo definiste como "malo". Yo creo que la OOP es buena pero llevarla a extremos causa problemas y perdida de productividad

    ResponderEliminar
  4. Hola rafaelSC.
    Ante todo vaya por delante mi disculpa por el retraso de la respuesta y mi agradecimiento por tu participación en el blog.

    En cuanto a tu comentario tengo que decirte que estoy completamente en desacuerdo contigo.

    Si bien es posible que para alguien aún no demasiado ducho en el diseño orientado a objetos pueda parecer la primera opción fácil de seguir. Para los que ya tenemos una experiencia en esto es evidente en que acabará el código basado en el "switch" que yo he llamado malo.

    Según la crezca el número de exportaciones a realizar crecerá el switch. Esto llevará a crear mucho código duplicado y cuando algún compañero o uno mismo vuelva después de un tiempo a mantener esto será bastante complicado y costoso.

    Otro punto en contra de la primera implementación es cómo explicar a otro desarrollador de tu equipo cómo está implementada es funcionalidad. Me imagino una explicación que empieza algo así como: "Bueno, según se elige una opción u otra hay un switch con la 36 cases y en cada uno de estos cases ..." es decir, no hay un lenguaje común entre el que habla y el que escucha.

    Al aplicar el patrón Builder en este caso conseguimos tener clases pequeñas, bien cohesionadas y con pocas dependencias.
    Además si alguien te pregunta cómo está implementada la funcionalidad con decir que la implementación se basa en un patrón Builder el que te escucha ya tiene una buena idea de que hay escrito. Si además añades que el Director se encarga de leer el listado de productos y los Builder crean los archivos concretos que se exportan, entonces tu compañero ya debe entender perfectamente cómo mantener este código.

    Bueno esa es mi opinión, la de alguien que ha sufrido mucho con implementaciones como las que he marcado como mala.

    Un saludo rafaelSC y gracias de nuevo por tu comentario.

    ResponderEliminar