Monte seu Type-Safe builder

Menu: programadores + fome = código sobre comida

Dias atrás, após um dia de trabalho, estava estudando como de costume e me deu uma baita fome!

Resolvi terminar os estudos antes de ir jantar, mas um lanche do Subway não saia da minha cabeça...

Então, quando menos percebi, a classe que eu tinha criado era assim:

public class Lanche {

    private final Tamanho tamanho;
    private final Pao pao;
    private final Recheio recheio;
    private final Queijo queijo;
    private final List<Vegetais> vegetais;
    private final Molho molho;

    public Lanche(Tamanho tamanho, Pao pao, Recheio recheio,
                  Queijo queijo, List<Vegetais> vegetais,
                  Molho molho) {
        this.tamanho = tamanho;
        this.pao = pao;
        this.recheio = recheio;
        this.queijo = queijo;
        this.vegetais = vegetais;
        this.molho = molho;
    }

}

Aparentemente, essa classe é muito simples: é Java puro, não tem nada demais... Mas, como todo código, sempre tem algo a ser melhorado.

Vamos tentar olhar de forma crítica, decifrando o que podemos melhorar.

Não precisamos getters e setters. Partiremos da ideia que só criamos getters quando realmente for necessário. Como colocamos tudo o que precisamos no construtor, não temos o perigo de construir objetos inválidos.

Vamos ver como ficou a chamada para criar um lanche:

new Lanche(Tamanho.GRANDE,Pao.PARMESAO_OREGANO,Recheio.CHURRASCO,
Queijo.PRATO, List.of(Vegetal.ALFACE, Vegetal.CEBOLA),
Molho.CHIPOTLE);

Bom, então o que tem para ser melhorado? No livro Código Limpo (Clean Code de Robert Martin), no capítulo de funções há um item a respeito de parâmetros de funções (Function Arguments):

A quantidade ideal de parâmetros para uma função é zero (nulo).
Depois vem um (mônade), seguido de dois (díade).
Sempre que possivel, devem-se evitar três parâmetros (tríade).
Para mais de três, deve-se ter um motivo muito especial (políade) - mesmo assim não devem ser utilizados.

Pensando nessa frase, como faríamos para evitar chamar um construtor complexo ou vários setters no objeto criado?

Entrada: o padrão Builder

Foi exatamente por esse motivo que surgiu o padrão Builder, descrito no livro Padrões de Projetos (Design Patterns: Elements of Reusable Object-Oriented Software do GOF). O Builder é um padrão de projetos criacional que permite a separação da construção de um objeto complexo da sua representação.

O código ficaria assim:

public class Lanche {
    private final Tamanho tamanho;
    private final Pao pao;
    private final Recheio recheio;
    private final Queijo queijo;
    private final List<Vegetal> vegetais;
    private final Molho molho;

    private Lanche(LancheBuilder lancheBuilder) {
        this.tamanho = lancheBuilder.tamanho;
        this.pao = lancheBuilder.pao;
        this.recheio = lancheBuilder.recheio;
        this.queijo = lancheBuilder.queijo;
        this.vegetais = lancheBuilder.vegetais;
        this.molho = lancheBuilder.molho;
    }

    public static LancheBuilder umLanche() {
        return new LancheBuilder();
    }

    public static class LancheBuilder {
        private Tamanho tamanho;
        private Pao pao;
        private Recheio recheio;
        private Queijo queijo;
        private List<Vegetal> vegetais;
        private Molho molho;

        //Nao deixando chamar o construtor de fora desta classe
        private LancheBuilder() {
        }

        public LancheBuilder grande() {
            this.tamanho = Tamanho.GRANDE;
            return this;
        }

        public LancheBuilder normal() {
            this.tamanho = Tamanho.NORMAL;
            return this;
        }

        public LancheBuilder comPao(Pao pao) {
            this.pao = pao;
            return this;
        }

        public LancheBuilder comRecheio(Recheio recheio) {
            this.recheio = recheio;
            return this;
        }

        public LancheBuilder comQueijo(Queijo queijo) {
            this.queijo = queijo;
            return this;
        }

        public LancheBuilder comVegetais(Vegetal... vegetais) {
            this.vegetais = List.of(vegetais);
            return this;
        }

        public LancheBuilder comMolho(Molho molho) {
            this.molho = molho;
            return this;
        }

        public Lanche constroi() {
            return new Lanche(this);
        }
    }
}

E a chamada fica assim:

Lanche.umLanche().grande()
                .comPao(PARMESAO_OREGANO)
                .comMolho(CHIPOTLE)
                .comQueijo(PRATO)
                .comRecheio(CHURRASCO)
                .comVegetais(ALFACE, CEBOLA)
                .constroi();

Veja que deixei o construtor da classe Lanche como privado e disponibilizei o método umLanche, que devolve um Builder. Isso está descrito no livro Java Efetivo (Effective Java de Joshua Bloch) e é chamado de static factory. Diferente dos construtores conseguimos nomeá-los, fazendo com que fiquem mais descritivos. Além disso, podemos retornar um objeto que a princípio não é o mesmo de nossa classe.

Agora nosso problema foi resolvido: em nossa classe Lanche só temos um parâmetro no construtor: o LancheBuilder.

É o mais próximo do número de parâmetros ideal (zero/nulo).

Não sei se todos já foram a lanchonete chamada Subway, mas há regras para montar nosso lanche:
Subway funcionamento

Costuma a funcionar um pouquinho diferente da foto, por isso vou detalhar a forma que os atendentes fazem:

  1. Devemos escolher o recheio e o pão
  2. Escolhemos um tamanho
  3. Escolhemos o queijo
  4. Escolhemos os vegetais
  5. Por último, podemos ou não escolher um molho

Essa lanchonete funciona exatamente nessa ordem. Com isso, conseguimos identificar problemas em nossa solução atual: da forma que fizemos o Builder, não temos uma ordem de chamada, perceba que chamei tudo fora de ordem e veja o que conseguimos fazer:

Lanche.umLanche().constroi();

Nós pulamos todas as etapas, construimos um objeto em um estado inválido. OK, poderíamos lançar uma exceção caso algo esteja faltando.

Mas será que nós conseguiríamos pegar todos os erros em tempo de compilação? Aí, não precisaríamos esperar o projeto rodar para descobrir. O código nem seria compilado. Mas e agora? Como fazer para garantir que só vamos construir o objeto se passarmos por todas as etapas?

Prato Principal: os Type-safe Builders

Os Type-safe (ou Staged, ou Telescopic, ou Step) Builders nos auxiliam nas limitações que temos ao utilizar os Builders comuns.

A ideia é bem simples: aproveitar o sistema de tipos do Java para garantir que no momento de compilação todas as propriedades sejam definidas antes que a instância da classe seja construída.

Então, utilizando em nosso projeto a ideia do Type-safe Builder, construiremos nosso objeto em passos. Uma possível solução é a seguinte:

public class Lanche {
    private final Tamanho tamanho;
    private final Pao pao;
    private final Recheio recheio;
    private final Queijo queijo;
    private final List<Vegetal> vegetais;
    private final Molho molho;

    private Lanche(LanchePassoFinalBuilder lanchePassoFinalBuilder) {
        this.tamanho = lanchePassoFinalBuilder.tamanho;
        this.pao = lanchePassoFinalBuilder.pao;
        this.recheio = lanchePassoFinalBuilder.recheio;
        this.queijo = lanchePassoFinalBuilder.queijo;
        this.vegetais = lanchePassoFinalBuilder.vegetais;
        this.molho = lanchePassoFinalBuilder.molho;
    }

    public static LancheBuilder umLanche(Recheio recheio, Pao pao) {
        return new LancheBuilder(recheio, pao);
    }

    static class LancheBuilder {
        private final Recheio recheio;
        private final Pao pao;

        private LancheBuilder(Recheio recheio, Pao pao) {
            this.recheio = recheio;
            this.pao = pao;
        }

        public LancheTerceiroPassoBuilder grande() {
            return new LancheTerceiroPassoBuilder(recheio, pao,
                    Tamanho.GRANDE);
        }

        public LancheTerceiroPassoBuilder normal() {
            return new LancheTerceiroPassoBuilder(recheio, pao,
                    Tamanho.NORMAL);
        }
    }

    static class LancheTerceiroPassoBuilder {
        private final Recheio recheio;
        private final Pao pao;
        private final Tamanho tamanho;

        public LancheTerceiroPassoBuilder(Recheio recheio,
                                          Pao pao,
                                          Tamanho tamanho) {
            this.recheio = recheio;
            this.pao = pao;
            this.tamanho = tamanho;
        }

        public LancheQuartoPassoBuilder comQueijo(Queijo queijo) {
            return new LancheQuartoPassoBuilder(recheio,
                    pao, tamanho, queijo);
        }
    }

    static class LancheQuartoPassoBuilder {
        private final Recheio recheio;
        private final Pao pao;
        private final Tamanho tamanho;
        private final Queijo queijo;

        public LancheQuartoPassoBuilder(Recheio recheio,
                                        Pao pao, Tamanho tamanho,
                                        Queijo queijo) {
            this.recheio = recheio;
            this.pao = pao;
            this.tamanho = tamanho;
            this.queijo = queijo;
        }

        public LanchePassoFinalBuilder comVegetais(Vegetal... vegetais) {
            return new LanchePassoFinalBuilder(tamanho,
                    recheio, pao,
                    queijo, List.of(vegetais));
        }
    }

    static class LanchePassoFinalBuilder {
        private final Tamanho tamanho;
        private final Recheio recheio;
        private final Pao pao;
        private final Queijo queijo;
        private final List<Vegetal> vegetais;
        private Molho molho;

        public LanchePassoFinalBuilder(Tamanho tamanho,
                                       Recheio recheio, Pao pao,
                                       Queijo queijo,
                                       List<Vegetal> vegetais) {
            this.tamanho = tamanho;
            this.recheio = recheio;
            this.pao = pao;
            this.queijo = queijo;
            this.vegetais = vegetais;
        }

        public LanchePassoFinalBuilder comMolho(Molho molho) {
            this.molho = molho;
            return this;
        }

        public Lanche build() {
            return new Lanche(this);
        }
    }
}

Dessa forma, não conseguimos pular nenhuma etapa obrigatória. Temos que seguir certinho as etapas para construção. E o melhor de tudo: quem for utilizar o Builder, não precisa conhecer a regra de negocio da lanchonete previamente. O código esta sucinto e claro o suficiente para que funcione como sua própria documentação.

Lembre-se: escrevemos códigos para que outras pessoas entendam.

O resultado da criação de um lanche com o Type-safe Builder ficou assim:

Lanche.umLanche(CHURRASCO, TRES_QUEIJOS)
                .grande()
                .comQueijo(PRATO)
                .comVegetais(CEBOLA, TOMATE, ALFACE)
                .comMolho(CHIPOTLE)
                .build();

Os Type-safe Builders lidam muito bem com parâmetros opcionais. Basta trazê-los para perto da classe que permite a criação final do objeto.

Em Java, temos uma biblioteca chamada jilt, que pode nos auxiliar na criação de Type-safe Builders. Linguagens como o Kotlin também ajudam na criação desse tipo de Builder.

Sobremesa: removendo indireções e finalizando com classe(ou interfaces?)

Podemos dizer que criamos uma interface fluente.

Fizemos o nosso design de código de uma forma que ao ler nós conseguimos entendê-lo como uma simples e bela frase de uma música.

Porém, vou te mostrar que ainda temos algumas indireções que podem gerar conflitos:

Viu só? Quando digitamos um ponto e nossa IDE nos auxilia em qual método chamar, aparecem os passos internos.

Isso acontece porque definimos nossas classes internas de Lanche (os Builders dos passos) como pacote-privado (package-private).

Como será que podemos fazer para restringir ainda mais o acesso dessas classes internas?

Não podemos apenas definir como privado (private), pois assim seriam inacessíveis fora da classe Lanche.

Em Java, nós temos as interfaces. Assim como as classes (não internas), podem ser apenas pacote-privado ou públicas.

Com as interfaces, conseguimos garantir uma API, um contrato que é assinado independente de quem a implemente.

As interfaces de definição dos passos do nosso Type-safe Builder ficariam assim:

public interface CriacaoLanchePassos {

    interface LancheBuilder {
        LancheTerceiroPasso grande();
        LancheTerceiroPasso normal();
    }

    interface LancheTerceiroPasso {
        LancheQuartoPasso comQueijo(Queijo queijo);
    }

    interface LancheQuartoPasso {
        LanchePassoFinal comVegetais(Vegetal... vegetais);
    }

    interface LanchePassoFinal {
        LanchePassoFinal comMolho(Molho molho);
        Lanche build();
    }
}

Nossas implementações dessa interfaces podem ser classes internas e privadas.

Bom, seguindo essas alterações, nosso código ficou:

public class Lanche {
    private final Tamanho tamanho;
    private final Pao pao;
    private final Recheio recheio;
    private final Queijo queijo;
    private final List<Vegetal> vegetais;
    private final Molho molho;

    private Lanche(LanchePassoFinalBuilder lanchePassoFinalBuilder) {
        this.tamanho = lanchePassoFinalBuilder.tamanho;
        this.pao = lanchePassoFinalBuilder.pao;
        this.recheio = lanchePassoFinalBuilder.recheio;
        this.queijo = lanchePassoFinalBuilder.queijo;
        this.vegetais = lanchePassoFinalBuilder.vegetais;
        this.molho = lanchePassoFinalBuilder.molho;
    }

    public static CriacaoLanchePassos.LancheBuilder umLanche(Recheio recheio, Pao pao) {
        return new LancheBuilder(recheio, pao);
    }

    private static class LancheBuilder implements CriacaoLanchePassos.LancheBuilder {
        private final Recheio recheio;
        private final Pao pao;

        public LancheBuilder(Recheio recheio, Pao pao) {
            this.recheio = recheio;
            this.pao = pao;
        }

        public CriacaoLanchePassos.LancheTerceiroPasso grande() {
            return new LancheTerceiroPassoBuilder(recheio, pao,
                    Tamanho.GRANDE);
        }

        public CriacaoLanchePassos.LancheTerceiroPasso normal() {
            return new LancheTerceiroPassoBuilder(recheio, pao,
                    Tamanho.NORMAL);
        }
    }

    private static class LancheTerceiroPassoBuilder implements CriacaoLanchePassos.LancheTerceiroPasso {
        private final Recheio recheio;
        private final Pao pao;
        private final Tamanho tamanho;

        private LancheTerceiroPassoBuilder(Recheio recheio,
                                          Pao pao,
                                          Tamanho tamanho) {
            this.recheio = recheio;
            this.pao = pao;
            this.tamanho = tamanho;
        }

        public CriacaoLanchePassos.LancheQuartoPasso comQueijo(Queijo queijo) {
            return new LancheQuartoPassoBuilder(recheio,
                    pao, tamanho, queijo);
        }
    }


    private static class LancheQuartoPassoBuilder implements CriacaoLanchePassos.LancheQuartoPasso {
        private final Recheio recheio;
        private final Pao pao;
        private final Tamanho tamanho;
        private final Queijo queijo;

        private LancheQuartoPassoBuilder(Recheio recheio,
                                        Pao pao, Tamanho tamanho,
                                        Queijo queijo) {
            this.recheio = recheio;
            this.pao = pao;
            this.tamanho = tamanho;
            this.queijo = queijo;
        }

        public CriacaoLanchePassos.LanchePassoFinal comVegetais(Vegetal... vegetais) {
            return new LanchePassoFinalBuilder(tamanho,
                    recheio, pao,
                    queijo, List.of(vegetais));
        }
    }

    private static class LanchePassoFinalBuilder implements CriacaoLanchePassos.LanchePassoFinal {
        private final Tamanho tamanho;
        private final Recheio recheio;
        private final Pao pao;
        private final Queijo queijo;
        private final List<Vegetal> vegetais;
        private Molho molho;

        private LanchePassoFinalBuilder(Tamanho tamanho,
                                       Recheio recheio, Pao pao,
                                       Queijo queijo,
                                       List<Vegetal> vegetais) {
            this.tamanho = tamanho;
            this.recheio = recheio;
            this.pao = pao;
            this.queijo = queijo;
            this.vegetais = vegetais;
        }

        public LanchePassoFinalBuilder comMolho(Molho molho) {
            this.molho = molho;
            return this;
        }

        public Lanche build() {
            return new Lanche(this);
        }
    }
}

Com isso, não temos mais acesso a detalhes internos de implementação:

Conseguimos seguir estritamente a ideia de ocultação de informação (information hiding) ou encapsulamento. Não temos nenhuma indireção, temos apenas uma API fluente. Códigos assim são comumente encontrados em bibliotecas com um bom design de código.

Você pode encontrar o código desse post neste repositório: github.com/gabrielronei/typesafebuilder-subway-exemplo.

O repositório está divido em três branches, que são os três principais pontos que chegamos até aqui: primeira_refatoracao, segunda_refatoracao e terceira_refatoracao.

Existem várias soluções possiveis para esse código, porém essa foi uma solução que achei legal e quis mostrar para vocês.

Espero que a ideia tenha ficado clara e que vocês consigam aproveitar algo desse tipo no dia-a-dia.

Lembrem-se, como tudo em Arquitetura/Design de Software, não é uma bala de prata. Sendo utilizada em excesso pode causar problemas e dificuldade de entendimento.

Mas é algo muito interessante que podemos ter como uma carta em nossa manga! :)

Referências:

Livros

18