Seguindo na idéia de interfaces fluentes (expliquei o que elas são aqui), vou mostrar aqui o que é um builder.

Lembrando que o objetivo final é esse:

            var category = new Category();
            category.Categorize(item =>item.Named("iPhone")
                .ManufacturedBy(manufacturer =>
                    manufacturer.Named("Apple"))
                    .ForProduct(product =>
                        product.Numbered("AA1687")));

Bom, por este código estamos acrescentando um item à uma categoria. A categoria é criada na primeira linha. O item é criado nas linhas seguintes, com Lambdas. Além do item, é criado também um fabricante (manufacturer) e um produto (product). Como isso é feito?

Vamos dar uma olhada no builder de produtos, melhor do que explicar é ver:

    public class ProductBuilder : DomainObjectBuilder<Product>
    {
        public override Product Build()
        {
            return new Product(Number);
        }

        public ProductBuilder Numbered(string number)
        {
            Number = number;
            return this;
        }
        public string Number { get; set; }
    }

O que temos aqui é uma classe que cria Produtos. Ela herda de um DomainObjectBuilder, que não tem nada de mais, ela só dá uma assinatura para o método Build, que retorna uma entidade:

    public abstract class DomainObjectBuilder<T>
    {
        public abstract T Build();
    }

Então, para usar este builder, fazemos assim:

var product = new ProductBuilder().Numbered("AA1687").Build();

E assim um produto é criado.

Talvez você não tenha percebido, mas um ProductBuilder retorna, no método “Numbered”, a si mesmo. Porque?

O grande segredo das interfaces fluentes está aí, no retorno de métodos como esse. Com essa técnica é possível encadear chamadas. Logo em seguida de chamar “Numbered”, recebo o mesmo objeto de volta, e chamo “Build”. Se tivesse outras propriedades que quisesse setar, eu poderia fazer assim:

var product = new ProductBuilder()
                  .Numbered("AA1687")
                  .Named("iPhone")
                  .CreatedBy("Giovanni")
                  .On(DateTime.Now)
                  .Build();

Vocês verão mais sobre uso do retorno das funções quando eu mostrar o CatalogItemBuilder. Antes vamos ver o ManufacturerBuilder:

    public class ManufacturerBuilder : DomainObjectBuilder<Manufacturer>
    {
        public override Manufacturer Build()
        {
            return new Manufacturer(Name);
        }
        public ManufacturerBuilder Named(string name)
        {
            Name = name;
            return this;
        }
        protected string Name { get; set; }
    }

Muito parecido com o builder de produtos. Nem vou me dar ao trabalho de explicar, você já entendeu.

Agora o último, que agrega os outros dois. O CatalogItemBuilder:

    public class CatalogItemBuilder : DomainObjectBuilder<CatalogItem>, ICatalogItemBuilder
    {
        protected Manufacturer Manufacturer { get; set; }
        protected String Name { get; set; }
        protected Product Product { get; set; }

        public CatalogItemBuilder Named(String name)
        {
            Name = name;
            return this;
        }

        public CatalogItemBuilder ManufacturedBy(Action<ManufacturerBuilder> buildUsing)
        {
            var manufacturerBuilder = new ManufacturerBuilder();
            buildUsing(manufacturerBuilder);
            Manufacturer = manufacturerBuilder.Build();
            return this;
        }
        public CatalogItemBuilder ForProduct(Action<ProductBuilder> buildUsing)
        {
            var productBuilder = new ProductBuilder();
            buildUsing(productBuilder);
            Product = productBuilder.Build();
            return this;
        }
        public override CatalogItem Build()
        {
            return new CatalogItem(Name, Manufacturer, Product);
        }
    }

Comecemos pelo método “Named”. Nada de mais, parecido com os outros de produto (“Numbered”) e de manufacturer (“Named”). Ele recebe um parâmetro e retorna a si mesmo. Aí posso encadear a chamada:

            var item1 = new CatalogItemBuilder()
                .Named("iPhone")
                .Build();
            category.Categorize(item1);

Muito parecido com os outros. Só que deste jeito estou criando um item com nome, só que sem produtos ou fabricantes.

Mas ele tem outros métodos. Por exemplo, ele tem o método ManufacturedBy. Só que ManufacturedBy não recebe um Manufacturer. Recebe uma ação com um ManufacturerBuilder, um Action<ManufacturerBuilder>. Isso é um delegate, e posso criar delegates com lambdas. Posso continuar a chamada assim então:

            var item2 = new CatalogItemBuilder()
                .Named("iPhone")
                .ManufacturedBy(m => m.Named("Apple") )
                .Build();
            category.Categorize(item2);

Qual a única mudança? A linha 3 foi adicionada. Nada mais mudou.  Quem é “m”? É um Manufacturer? Não, é um ManufacturerBuilder. Lembre-se que parâmetro recebido pelo método ManufacturedBy é um delegate, é um Action<ManufacturerBuilder>, e não um Action<Manufacturer>. Por isso conseguimos chamar o método “Named” sobre ele. E o que retorna esse método? O próprio Builder, que tem então executado o seu método Build, que aí sim, retorna um Manufacturer que é repassado à um campo privado (terceira linha da classe CatalogItemBuilder). Essa ação toda está no método ManufacturedBy.

Para incluir o produto é muito parecido. Uma linha só a mais:

            var item3 = new CatalogItemBuilder()
                 .Named("iPhone")
                 .ManufacturedBy(m => m.Named("Apple"))
                 .ForProduct(p => p.Number = "AA1687")
                 .Build();
            category.Categorize(item3);

Mais simples impossível.

O método Categorize da classe Category é super simples, ele só adiciona o item à coleção de itens:

        public void Categorize(CatalogItem item)
        {
            Items.Add(item);
        }

Depois vou mostrar a solução para passar uma lambda para o método Categorize, permitindo a construção do começo do post. Ela segue a mesma idéia.

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.