C# -  Padrão Builder


Neste artigo vou apresentar o padrão de projeto Builder.

O objetivo do padrão Builder é separar a construção de um objeto complexo da sua representação de modo que o processo de construção possa criar diferentes representações.

Moral da História: O padrão Builder é usado para separar o código que cria e o código que usa o objeto.

Destacando que objetos complexos podem ser objetos com um construtor muito grande com muitos parâmetros e objetos que possuem uma composição de vários objetos



Dessa forma este padrão criacional pode produzir diferentes tipos e representações de um objeto usando o mesmo código de construção, e, pode construir um objeto complexo usando muitos objetos simples em uma abordagem passo a passo.

Ele permite realizar o encadeamento de chamadas de métodos (method chaining). Neste caso se você estiver usando uma programação fluente.

Este padrão de projeto é mais complexo que o Factory Method e que o Abstract Factory.

Diagrama UML

A seguir temos o diagrama UML do padrão Builder segundo o Gof:

 

Product –  A classe que representa o tipo de objeto complexo a ser construído pelo Builder; inclui classes que definem as partes constituintes, incluindo interfaces para montar as partes no resultado final;


Builder – Define uma interface/classe abstrata para criar partes de um objeto Product ; Define todos os passos que devem ser executados para criar um Product;


ConcreteBuilder – Constrói e monta partes do produto implementando a interface Builder, define e acompanha a representação criada e fornece uma interface para retornar o produto; 


Director – Constrói um objeto usando a interface Builder . (Controla o algoritmo que gera o objeto do produto final)

Aqui temos um relacionamento de agregação que é um tipo especial de associação onde as informações de um objeto (chamado objeto-todo) precisam ser complementados pelas informações contidas em um ou mais objetos de outra classe (chamados objetos-parte); temos então o que conhecemos como todo/parte.

Este relacionamento referido como relacionamento "tem um" , e, aqui temos que todo o Director tem um Builder.

Nota: O relacionamento de agregação representa um vínculo fraco entre duas classes.

Vantagens e desvantagens do padrão Builder

As vantagens em usar o padrão são :

a- Permite esconder os detalhes de como os objetos são criados;
b- Permite uma grande variedade de representações internas do objeto a ser construído;
c- Fornece um grande controle sobre o processo de criação de objetos complexos;
d- Cada Builder é independente dos outros builders e do restante da aplicação;
e- O Princípio da Responsabilidade Única (SRP) é aplicado, uma vez que a construção complexa do objeto é isolada da lógica de negócio deste objeto

E as desvantagens são :

a- O número de linhas de código aumenta conforme a complexidade do objeto e os tipos de objetos que vamos construir;
b- Requer a criação de um ConcreteBuilder separado para cada tipo diferente de produto;

Usos do padrão Builder

Podemos usar o padrão Builder:

- Quando a instanciação de um objeto exige muitos parâmetros no construtor;
- Você deseja criar um conjunto de objetos relacionados ou dependentes que devem ser usados juntos;
- O sistema precisar ser independente de como seus produtos são criados, compostos ou representados;
- Quando você deseja esconder os detalhes do processo de construção do objeto complexo;

Aplicando o padrão Adapter

Para mostrar um exemplo de implementação usando o padrão Builder veremos a montagem e a criação de Pizzas. Vamos criar uma aplicação Console usando o .NET 5.0 e a linguagem C#.



Primeiro veremos uma implementação para criar pizzas sem usar o padrão Builder usando uma abordagem bem ingênua onde vamos criar uma classe Pizza e nesta classe vamos definir os parâmetros que serão usados para criar a pizza e faremos isso no construtor,

Assim vamos criar 3 enumerações para definir o tipo da borda, tipo da massa e o tamanho e na classe Pizza vamos criar 4 campos e dois métodos. Em uma pizza real a quantidade de parâmetros com certeza seria maior e assim usar esta abordagem implicaria em ter que informar diversos parâmetros no construtor o que não é uma boa pratica.

1- Enumerações

public enum Tamanho  
{
   Pequena = 1,
   Media = 2,
   Grande = 3
}
public enum TipoBorda 
{
    Normal = 1,
    Recheada = 2
}
public enum TipoMassa
{
    Normal = 1,
    Fina = 2,
    Grossa = 3
}

 2- Classe Pizza

using System;
using System.Collections.Generic;

namespace PizzariaSemBuilder
{
    public class Pizza
    {
        private readonly TipoMassa tipoMassa;
        private readonly Tamanho tamanho;
        private readonly TipoBorda tipoBorda;
        private readonly List<string> ingredientes;

        public Pizza(TipoMassa tipoMassa, Tamanho tamanho,
            TipoBorda tipoBorda, List<string> ingredientes)
        {
            this.tipoMassa = tipoMassa;
            this.tamanho = tamanho;
            this.tipoBorda = tipoBorda;
            this.ingredientes = ingredientes;
        }

        public void PizzaConteudo()
        {
            Console.WriteLine("Pizza com massa : {0}", tipoMassa);
            Console.WriteLine($"Tamanho : {tamanho}");
            Console.WriteLine($"Tipo Borda : {tipoBorda}");
            Console.WriteLine("Ingredientes:");
            foreach (var item in ingredientes)
            {
                Console.WriteLine($" {item}");
            }
        }
    }
}

Esta implementação não usa o padrão Builder e na classe Program temos que criar uma instância da Pizza e passar os parâmetros que serão usados para criar a pizza:

using System;
using System.Collections.Generic;

namespace PizzariaSemBuilder
{
    class Program
    {
        static void Main(string[] args)
        {
            Pizza pizza1 = new Pizza(TipoMassa.Fina,
                                     Tamanho.Grande,
                                     TipoBorda.Normal,
                                     new List<string> { "Mussarela", "Molho de tomate", "Oregano" });

            pizza1.PizzaConteudo();
            Console.ReadKey();
        }
    }
}

Conforme a quantidade de parâmetros cresce fica mais difícil usar esta implementação.

Aplicando o padrão Builder para criar pizzas

Vejamos agora uma implementação onde vamos usar o padrão Builder para a criação de pizzas.

As enumerações permanecem as mesmas:

public enum Tamanho  
{
   Pequena = 1,
   Media = 2,
   Grande = 3
}
public enum TipoBorda 
{
    Normal = 1,
    Recheada = 2
}
public enum TipoMassa
{
    Normal = 1,
    Fina = 2,
    Grossa = 3
}

Vamos criar a classe Pizza que define o produto a ser criado:

using System;
using System.Collections.Generic;

namespace Builder1.Product
{
    public class Pizza
    {
        public TipoMassa TipoMassa { get; set; }
        public TipoBorda TipoBorda { get; set; }
        public Tamanho Tamanho { get; set; }
        public List<string> Ingredientes { get; set; }

        public void PizzaConteudo()
        {
            Console.WriteLine($"Pizza com massa : {TipoMassa}");
            Console.WriteLine($"Tipo de borda : {TipoBorda}");
            Console.WriteLine($"Tamanho : {Tamanho}");
            Console.WriteLine("Ingredientes :");
            foreach (var item in Ingredientes)
            {
                Console.WriteLine($" {item}");
            }
            Console.WriteLine("\n\n");
        }
    }
}

A seguir vamos criar a classe PizzaBuilder que representa o Builder e define a interface abstrata para criar partes da pizza:

using Builder1.Product;
namespace Builder1.Builder
{
    public abstract class PizzaBuilder
    {
        protected Pizza pizza;
        public void CriaPizza()
        {
            pizza = new Pizza();
        }
        public Pizza GetPizza()
        {
            return pizza;
        }
        public abstract void PreparaMassa();
        public abstract void IncluiIngredientes();
    }
}

 

A seguir temos as classes concretas PizzaCalabreza e PizzaMussarela que implementam PizzaBuilder e criam as pizzas de calabreza e mussarela:

1- PizzaCalabreza

using Builder1.Builder;
using Builder1.Product;
using System.Collections.Generic;

namespace Builder1.ConcreteBuilder
{
    public sealed class PizzaCalabreza : PizzaBuilder
    {
        public override void PreparaMassa()
        {
            pizza.TipoMassa = TipoMassa.Grossa;
            pizza.TipoBorda = TipoBorda.Normal;
            pizza.Tamanho = Tamanho.Grande;
        }
        public override void IncluiIngredientes()
        {
            pizza.Ingredientes = new List<string> { "Calabreza em fatias",
                "Molho de tomate" };
        }
    }
}

2- PizzaMussarela

using Builder1.Builder;
using Builder1.Product;
using System.Collections.Generic;

namespace Builder1.ConcreteBuilder
{
    public sealed class PizzaMussarela : PizzaBuilder
    {
        public override void PreparaMassa()
        {
            pizza.TipoMassa = TipoMassa.Normal;
            pizza.TipoBorda = TipoBorda.Recheada;
            pizza.Tamanho = Tamanho.Grande;
        }
        public override void IncluiIngredientes()
        {
            pizza.Ingredientes = new List<string> { "Mussarela",
                "Molho de tomate", "Orégano" };
        }
    }
}

A seguir vamos criar a classe Pizzaria que representa o Director e que constrói um objeto usando a interface Builder definindo a ordem de criação das partes :

using Builder1.Builder;
using Builder1.Product;

namespace Builder1.Director
{
    public class Pizzaria
    {
        private readonly PizzaBuilder builder;
        public Pizzaria(PizzaBuilder builder)
        {
            this.builder = builder;
        }

        //Constroi
        public void MontaPizza()
        {
            builder.CriaPizza();
            builder.PreparaMassa();
            builder.IncluiIngredientes();
        }

        public Pizza GetPizza()
        {
            return builder.GetPizza();
        }
    }
}

E na classe Program estamos usando o Director para criar as pizzas por partes:

using Builder1.ConcreteBuilder;
using Builder1.Director;
using System;

namespace Builder1
{
    public class Program
    {
        private static void Main(string[] args)
        {
            //Director
            var pizzaria = new Pizzaria(new PizzaCalabreza());
            pizzaria.MontaPizza();
            var pizza1 = pizzaria.GetPizza();
            pizza1.PizzaConteudo();

            pizzaria = new Pizzaria(new PizzaMussarela());
            pizzaria.MontaPizza();
            var pizza2 = pizzaria.GetPizza();
            pizza2.PizzaConteudo();

            Console.ReadKey();
        }
    }
}

Abaixo temos o resultado da execução do programa:



A seguir temos o diagrama de classes gerado pelo Visual Studio 2019 na implementação:

O padrão Builder é muitas vezes comparado com o padrão Abstract Factory pois ambos podem ser utilizados para a construção de objetos complexos.

A principal diferença entre eles é que o Builder constrói objetos complexos passo a passo e procura evitar ser um anti-pattern, enquanto o Abstract Factory constrói famílias de objetos, simples ou mesmo complexos, de uma só vez.

Pegue o código do projeto aqui :   Builder1.zip

"E Jesus clamou, e disse: Quem crê em mim, crê, não em mim, mas naquele que me enviou.
E quem me vê a mim, vê aquele que me enviou."
João 12:44,45

Referências:


José Carlos Macoratti