https://a.storyblok.com/f/270183/34622/518832f8a7/builder-pattern.png

Padrão Builder com herança em Java

Publicado em August 3, 2022

Tempo de leitura: 6 minutos

Este artigo foi atualizado em abril de 2025

Introdução

A maneira mais comum de instanciar objetos é por meio de um construtor, passando diretamente os parâmetros necessários. Para maior concisão, podemos agrupar parâmetros relacionados em uma classe e construir uma instância dessa classe separadamente, passando-a ao objeto principal como um parâmetro.

Isso tem a vantagem de reduzir o número de parâmetros necessários para a construção do objeto, dividindo-a em várias etapas. No entanto, há várias desvantagens no uso de um construtor. Por um lado, a ordem em que os parâmetros são passados é importante, o que pode ser complicado quando há muitos parâmetros e ainda mais propenso a erros quando esses parâmetros são do mesmo tipo (por exemplo, String).

Também pode haver parâmetros opcionais. Embora possamos contornar isso utilizando vários construtores com assinaturas diferentes, isso gera muito código repetitivo e aumenta a carga cognitiva tanto para o usuário quanto para o desenvolvedor. Além disso, torna-se inviável quando vários parâmetros opcionais são do mesmo tipo.

O padrão Builder é um padrão de design bem conhecido em linguagens orientadas a objetos para controlar a construção de objetos. Assim como muitos (talvez a maioria) dos padrões de design, ele existe para suprir uma deficiência da linguagem. Em linguagens com parâmetros nomeados (como Kotlin, Scala, Python, C#, Ruby e muitas outras), a necessidade de builders é reduzida em muitos casos.

O uso do padrão builder é recomendado no livro “Effective Java”, mas, para linguagens com argumentos nomeados, sua utilidade torna-se questionável (veja, por exemplo, este artigo, que explora sua utilidade no Kotlin).

A discussão sobre até que ponto os parâmetros opcionais tornam obsoleto o padrão Builder está além do escopo deste artigo. Em vez disso, o objetivo é explicar a lógica por trás do que parece ser uma aplicação complexa do padrão Builder na recém-lançada implementação da Messages API no SDK Java da Vonage.

MessageRequest Aulas

Para modelar os diversos tipos de mensagens que podem ser enviadas por meio da Messages API, utiliza-se uma abordagem orientada a objetos, na qual é criada uma classe para cada combinação válida de tipo de mensagem e serviço. Existe uma hierarquia de herança de três níveis. Veja o exemplo a seguir:

  • MessageRequest

    • MmsRequest

      • MmsVcardRequest

MessageRequest e MmsRequest são classes abstratas, e MmsVcardRequest é a classe que representa a combinação do envio de um vCard por MMS.

A classe base MessageRequest recebe como argumentos o canal e o tipo de mensagem em seu construtor, que são definidos pelas subclasses. Ela também recebe um Builder como parâmetro, onde são definidos os principais detalhes da mensagem. Alguns parâmetros são opcionais, como clientRef, e todas as mensagens têm um remetente e um destinatário; portanto, estes são declarados na classe base MessageRequest.

No entanto, essa hierarquia de herança é transparente para o usuário ao construir um MmsVcardRequest, que se parece com isto:

MmsVcardRequest message = MmsVcardRequest.builder()
        .from("447900090000").to("447900090001")
        .url("https://www.example.com/contact.vcf")
        .clientRef("vCard-msg-#1")
        .build();

Exemplos completos estão disponíveis no repositório de exemplos de código.

Isso parece um tanto elegante do ponto de vista do usuário, dada a ausência de parâmetros nomeados e opcionais em métodos e construtores no Java. Nos bastidores, porém, tornar o código acima possível com uma hierarquia de herança requer um projeto que não é imediatamente óbvio ou intuitivo do ponto de vista do desenvolvedor do SDK.

As complicações estão relacionadas às classes `Builder` aninhadas associadas a cada subclasse de MessageRequest. Vejamos MmsRequest como exemplo. Ela ainda é abstrata, já que estamos apenas definindo o Channel.

Mas e quanto àquela Builder ? O restante deste artigo tentará ajudá-lo a entender isso:

protected abstract static class Builder<M extends MmsRequest, B extends Builder<? extends M, ? extends B>> extends MessageRequest.Builder<M, B>

O <M> Parâmetro

A classe base `Builder`, em MessageRequest recebe como parâmetros o tipo do MessageRequest a ser construído e o tipo de Builder.

O primeiro (M) é fácil de explicar: public abstract M build() no MessageRequest.Builder é o que o usuário chama assim que termina de definir os parâmetros, retornando a ele a MessageRequest .

É claro que isso poderia ser omitido, já que a herança em Java suporta tipos de retorno covariantes. Ou seja, poderíamos omitir M se quiséssemos e obter o mesmo resultado do ponto de vista do usuário. Assim, MessageRequest.Builder fica da seguinte forma:

public abstract static class Builder<B extends Builder<? extends B>> {
    protected String from, to, clientRef;

    public B from(String from) {
        this.from = from;
        return (B) this;
    }

    public B to(String to) {
        this.to = to;
        return (B) this;
    }

    public B clientRef(String clientRef) {
        this.clientRef = clientRef;
        return (B) this;
    }

    public abstract MessageRequest build();
}

**

Então, precisamos nos lembrar de sobrescrever isso nas subclasses, de preferência até mesmo em classes abstratas como MmsRequest.Builder, da seguinte maneira:

protected abstract static class Builder<B extends Builder<? extends B>> extends MessageRequest.Builder<B> {
    String url;

    protected B url(String url) {
        this.url = url;
        return (B) this;
    }

    @Override
    public abstract MmsRequest build();
}

A classe concreta MmsVcardRequest.Builder teria a mesma aparência, já que é lá que declaramos o tipo:

public static final class Builder extends MmsRequest.Builder<Builder> {
    Builder() {}

    public Builder url(String url) {
        return super.url(url);
    }

    @Override
    public MmsVcardRequest build() {
        return new MmsVcardRequest(this);
    }
}

Então, por que estamos adicionando esse parâmetro aparentemente redundante se podemos usar tipos de retorno covariantes? É simplesmente para garantir que não nos esqueçamos de sobrescrever o tipo de retorno.

Ao generalizar o tipo de retorno, o compilador garante que o build() método tenha a assinatura correta. Na ausência disso, a seguinte versão de MmsVcardRequest.Builder também seria válida, embora imprecisa:

public static final class Builder extends MmsRequest.Builder<Builder> {
    Builder() {}

    public Builder url(String url) {
        return super.url(url);
    }

    @Override
    public MmsRequest build() {
        return new MmsVcardRequest(this);
    }
}

Como nos lembramos de redefinir o tipo de retorno de MmsRequest.Builder#build(), o compilador detectará a seguinte build() :

@Override
public MessageRequest build() {
    return new MmsVcardRequest(this);
}

com a mensagem de erro:

'build()' em 'com.vonage.client.messages.mms.MmsVcardRequest.Builder' entra em conflito com 'build()' em 'com.vonage.client.messages.mms.MmsRequest.Builder'; tentando usar um tipo de retorno incompatível.

Isso só aconteceu porque nos lembramos de redefinir manualmente a assinatura do método em MmsRequest.Builder. Se não tivéssemos feito isso, não haveria erro. Ao parametrizar o tipo de retorno, somos obrigados a declarar o tipo correto e não precisamos sobrescrever o build() método nas subclasses de MessageRequest — o compilador cuida disso para nós.

O <B> Parâmetro

Vamos voltar ao último parâmetro do Builder — B. Se você estiver familiarizado com o padrão Builder, saberá que cada chamada de método no Builder retorna o próprio Builder, de modo que é possível encadear chamadas de método com fluidez para definir parâmetros com facilidade.

Isso funciona bem quando não há herança, mas queremos que o usuário possa definir os parâmetros em qualquer ordem — afinal, não é essa uma das principais razões para usar o padrão Builder? Portanto, precisamos garantir que a classe Builder concreta mais específica seja retornada, independentemente de quais métodos sejam chamados primeiro.

Caso contrário, perderíamos a capacidade de encadear chamadas de método e teríamos que recorrer à conversão do valor de retorno todas as vezes — o que prejudica a fluidez que estamos tentando alcançar ao usar um construtor.

Para deixar isso mais claro, vamos considerar o caso em que, ingenuamente, retornamos o construtor atual:

public abstract static class Builder<M extends MessageRequest> {
    protected String from, to clientRef;

    public Builder<M> from(String from) {
        this.from = from;
        return this;
    }

    public Builder<M> to(String to) {
        this.to = to;
        return this;
    }

    public Builder<M> clientRef(String clientRef) {
        this.clientRef = clientRef;
        return this;
    }

    public abstract M build();
}

Isso compila normalmente e funciona. Então MmsRequesto construtor fica assim:

protected abstract static class Builder<M extends MmsRequest> extends MessageRequest.Builder<M> {
    String url;

    protected Builder<M> url(String url) {
        this.url = url;
        return this;
    }
}

Por fim, a subclasse concreta (mantendo o MmsVcardRequest exemplo) fica assim:

public static final class Builder extends MmsRequest.Builder<MmsVcardRequest> {
    Builder() {}

    /**
    * (REQUIRED)
    * Sets the URL of the vCard attachment. Supports only <code>.vcf</code> file extension.
    *
    * @param url The URL as a string.
    * @return This builder.
    */
    public Builder url(String url) {
        return (Builder) super.url(url);
    }

    @Override
    public MmsVcardRequest build() {
        return new MmsVcardRequest(this);
    }
}

Observe que tivemos que converter o tipo de retorno super.url(url) para este Builder, já que o tipo de retorno do método super é com.vonage.messages.mms.MmsRequest.Builder, e não com.vonage.messages.mms.MmsVcardRequest.Builder.

Observe que só sobrescrevemos esse método para adicionar Javadocs a ele, e não para alterar sua funcionalidade. Mas isso destaca perfeitamente o problema que o tipo Builder parametrizado tenta resolver. Para ilustrar, vamos tentar usar esse builder:

MmsVcardRequest message = MmsVcardRequest.builder()
    .from("447900090000").to("447900090001")
    .url("https://www.example.com/path/to/contact.vcf")
    .build();

O compilador exibe um erro: Cannot resolve method 'url' in 'Builder'. Em contrapartida, o seguinte funciona:

MmsVcardRequest message = MmsVcardRequest.builder()
    .url("https://www.example.com/path/to/contact.vcf")
    .from("447900090000").to("447900090001")
    .build();

O que está acontecendo? São exatamente as mesmas informações, mas os métodos são chamados em uma ordem diferente. O objetivo de um construtor não é justamente permitir flexibilidade na ordem em que os métodos são chamados? Para uma boa experiência do usuário, é necessário levar essas situações em consideração.

O usuário não deve se preocupar com quais classes contribuem com quais propriedades — esses são detalhes internos de implementação. Uma solução mais detalhada consiste em sobrescrever todos os métodos na subclasse concreta de MessageRequest.Builder. Por exemplo, em MmsVcardRequest, teríamos:

public static final class Builder extends MmsRequest.Builder<MmsVcardRequest> {
    Builder() {}

    public Builder from(String from) {
        return (Builder) super.from(from);
    }

    public Builder to(String to) {
        return (Builder) super.to(to);
    }

    public Builder clientRef(String clientRef) {
        return (Builder) super.clientRef(clientRef);
    }

    public Builder url(String url) {
        return (Builder) super.url(url);
    }

    @Override
    public MmsVcardRequest build() {
        return new MmsVcardRequest(this);
    }
}

Mas isso vai contra o próprio sentido da herança, já que estamos repetindo informações! É por isso que parametrizamos o tipo do construtor. O compilador garante que o subtipo mais concreto seja sempre retornado.

No entanto, como nossas classes construtoras podem ser estendidas, precisamos refletir isso também nas declarações de parâmetros; daí a restrição B extends Builder<? extends M, ? extends B> em vez de B extends Builder<M, B>.

Infelizmente, ainda precisamos converter o tipo de retorno do construtor para B sempre que chamamos return this, mas, pelo que percebo, essa é uma limitação do compilador. Felizmente, a conversão só precisa ocorrer nas classes abstratas `Builder`, e não nos tipos concretos.

Conclusão

Espero que este artigo tenha lhe ensinado um padrão um tanto útil (embora talvez pareça um pouco complicado) para usar o padrão Builder quando há classes abstratas e herança envolvidas. Talvez um dia esses padrões se tornem obsoletos, quando a linguagem oferecer maneiras melhores de instanciar objetos. Até lá, pelo menos temos os genéricos para nos ajudar, por mais desafiadores que possam ser de se trabalhar às vezes!

Sempre incentivamos a participação da comunidade. Fique à vontade para se juntar a nós no Slack da Comunidade Vonage ou nos enviar uma mensagem no Twitter. Se você tiver alguma sugestão de melhorias, aprimoramentos ou se encontrar um bug, não hesite em abrir um ticket no GitHub.


Compartilhar:

https://a.storyblok.com/f/270183/400x400/46a3751f47/sina-madani.png
Sina MadaniEx-funcionários da Vonage

Sina é um ex-membro da equipe da Vonage. Ele atuou como Java Developer Advocate na Vonage. Com formação acadêmica, ele tem uma curiosidade geral por tudo o que se relaciona a carros, computadores, programação, tecnologia e natureza humana. Em seu tempo livre, ele gosta de caminhar ou jogar videogames competitivos.