Fluent Builder com região crítica
Durante o trabalho, me deparei com uma situação bem interssante: precisava de um builder
fluente para
preparar uma classe específica. Mais notoriamente, um esquema para construir o AsyncCallback
do GWT.
Os pontos principais são:
- o dito callback será usado como argumento de uma chamada assíncrona GWT-RPC
- posso definir o que fazer no sucesso da chamada (
onSuccess
) - posso definir o que fazer na falha da chamada (
onFailure
) - posso definir o que fazer após a resposta do servidor e de tratar a resposta (
onFinally
) - posso definir o que fazer antes de realizar a chamada (
onBefore
) - os passos 2…5 são opcionais
- posso definir diversas tratativas de antes, sucesso, falha e pós-resposta
Os detalhes dessa implementação não precisam ser tratados a fundo aqui, dá para abstrair completamente isso por hora. O padrão que gostaria de enfatizar independe dos detalhes internos, então vamos focar apenas na API, ok? Em tese, isso poderia ser feito de maneira engessada, não fluente, mais ou menos assim:
// seja T o tipo da resposta da chamada GWT-RPC service.fazChamada
AsyncCallbackBuilder<T> builder = new AsyncCallbackBuilder<>();
builder.onSuccess(this::fun1);
builder.onSuccess(this::fun2);
builder.onBefore(Loader::startLoading);
builder.onFinally(Loader::endLoading);
builder.onFailure(logger::consoleFailure);
builder.onFailure(e -> showToastFailure(e.getMessage()));
AsyncCallback<T> callback = builder.build();
service.fazChamada(arg0, arg1, callback);
Mas, isso é meio bruto, não é? Que tal se, no lugar de fazer assim, pudéssemos fazer de modo mais fluente? APIs fluente normalmente retornam o próprio tipo para que você posso continuar trabalhando em cima dele, então algumas coisas ficam menos engessadas de escrever:
// seja T o tipo da resposta da chamada GWT-RPC service.fazChamada
AsyncCallback<T> callback = new AsyncCallbackBuilder<T>()
.onSuccess(this::fun1)
.onSuccess(this::fun2)
.onBefore(Loader::startLoading)
.onFinally(Loader::endLoading)
.onFailure(logger::consoleFailure)
.onFailure(e -> showToastFailure(e.getMessage()))
.build();
service.fazChamada(arg0, arg1, callback);
Ao menos soa um pouco mais natural, não é? Agora, normalmente essas chamadas, essas preparações tem uma grande coesão entre callback e chamada assíncrona. Então, eu poderia colocar nessa chamada assíncrona, de certo modo, a chamada assíncrona e, no lugar de falar que fiz a construção, mandar rodar. Fica mais ou menos assim:
// seja T o tipo da resposta da chamada GWT-RPC service.fazChamada
new AsyncCallbackBuilder<T>()
.onSuccess(this::fun1)
.onSuccess(this::fun2)
.onBefore(Loader::startLoading)
.setAsyncCall(cb -> service.fazChamada(arg0, arg1, cb))
.onFinally(Loader::endLoading)
.onFailure(logger::consoleFailure)
.onFailure(e -> showToastFailure(e.getMessage()))
.run();
Com isso, tenho tudo que preciso para fazer a chamada assíncrona, consigo programar feliz e já trato o consumo
do AsyncCallback
dentro do meu fluent builder, já que a intenção aqui é englobar de modo mais natural
o ciclo de vida da chamada assíncrona. Não faz sentido armazenar um AsyncCallback
em uma variável longeva,
normalmente esse callback tem uma vida muito curta.
Agora, isso tem um lado negativo. Se alguém esquecer de colocar um setAsyncCall
e chamar o run
, o erro só será
detectado quando o construtor criar o AsyncCallback
ou, pior ainda, quando for tentar chamar o GWT-RPC e dar ruim
porque o Consumer<AsyncCallback<T>>
passado como setAsyncCall
ser nulo, gerando o equivalente JavaScript do
famigerado NullPointerException
.
Uma coisa que me chamou bastante atenção em alguns papos no Twitter com pessoal que gosta mais de programação funcional é que eles sempre falam para deixar o compilador fazer o trabalho de verificação por você. Então, como fazer isso em Java? Que tal… manipulando os tipos?
Pois bem, eu preciso de um construtor fluente. Não preciso instanciar ele, mas posso pegar algo que o representa. Isso também
abstrai alguns passos que são comuns no projeto, como o startLoading
, endLoading
, consoleFailure
que são coisas que eu
normalmente gosto de usar. Então, digamos que tenho uma chamada estática que resgata esse builder pré-montado.
Ficaria assim o código cliente:
// seja T o tipo da resposta da chamada GWT-RPC service.fazChamada
AsyncCallbackBuilder.<T>getDefaultBuilder()
.onSuccess(this::fun1)
.onSuccess(this::fun2)
.setAsyncCall(cb -> service.fazChamada(arg0, arg1, cb))
.onFailure(e -> showToastFailure(e.getMessage()))
.run();
Divertido? Bem, sim. Eu curto isso. Ainda posso determinar outras questões de onBefore
, onFinally
, onFailure
de modo privado. Também posso tentar pegar de modo “cru” o builder, caso não queira o loading na tela:
AsyncCallbackBuilder.<T>getRawBuilder()
.onSuccess(this::fun1)
.onSuccess(this::fun2)
.setAsyncCall(cb -> service.fazChamada(arg0, arg1, cb))
.onFailure(logger::consoleFailure)
.run();
Mas ainda tenho o problema de como fazer chamar o run
única e exclusivamente após garantir que o meu builder fluente
tenha passado por setAsyncCall
. Note que nesse caso específico eu não quero garantir apenas a chamada do setAsyncCall
, mas
quero também que ela seja única. O que o compilador poderia garantir para mim?
Bem, a maior coisa que o compilador me permite é brincar com tipos. Aqui não adianta ser um tipo de parâmetro do generics, então preciso mexer nos tipos do builder mesmo. De grosso modo, posso definir que tenho 2 APIs distintas para lidar com isso:
BeforeAsyncBuilder<T>:
onSuccess: Consumer<T> => BeforeAsyncBuilder<T>
onFailure: Consumer<Throwable> => BeforeAsyncBuilder<T>
onBefore: Runnable => BeforeAsyncBuilder<T>
onFinally: Runnable => BeforeAsyncBuilder<T>
setAsyncCall: Consumer<AsyncCallback<T>> => AsyncSettedBuilder<T>
AsyncSettedBuilder<T>:
onSuccess: Consumer<T> => AsyncSettedBuilder<T>
onFailure: Consumer<Throwable> => AsyncSettedBuilder<T>
onBefore: Runnable => AsyncSettedBuilder<T>
onFinally: Runnable => AsyncSettedBuilder<T>
run: () => void
O retorno de
run
não é relevante para a discussão agora, poderia ser qualquer coisa. Estou colocandovoid
mas sem perder generalidade.
Notou que os 4 primeiros métodos são quase iguais em ambas as APIs? Hmmm, interessante. Não vamos fazer nada com isso agora, mas mantenhamos isso na cabeça.
Bem, uma coisa que gostaria de salientar é que não preciso ter dois objetos distintos para lidar com isso, seria melhor que tudo isso fossem apenas interfaces. Então, se são interfaces, poderia fazer algo como:
public class AsyncCallbackBuilder<T> implements BeforeAsyncBuilder<T>, AsyncSettedBuilder<T> {
public static <T> BeforeAsyncBuilder<T> getDefaultBuilder<T>() {
AsyncCallbackBuilder<T> builder = new AsyncCallbackBuilder<>();
// ... prepara com valores padrão o builder...
return builder;
}
public static <T> BeforeAsyncBuilder<T> getRawBuilder<T>() {
return new AsyncCallbackBuilder<>();
}
// só para garantir que apenas os métodos estáticos possam instanciar o AsyncCallbackBuilder
private AsyncCallbackBuilder() {
}
// ... diversas chamadas...
// apenas como exemplo de chamada fluente
public AsyncCallbackBuilder<T> onSuccess(Consumer<T> successFunction) {
// faz algo significativo com successFunction
return this;
}
}
Pois bem, as chamadas estáticas de fato obedecem nosso desejo de impedir chamar o run
antes de ter garantido o
setAsyncCall
. Agora… precisamos lidar com os tipos. Note que o compilador vai chiar porque vou precisar implementar
o método onBefore
, recebendo os mesmos argumentos, porém com retornos conflitantes vindos de dois pais distintos…
E se… no lugar de ter 2 tipos, eu tivesse só 1?
Pois bem, isso é possível se eu tiver um tipo comum, que tanto BeforeAsyncBuilder<T>
quanto AsyncSettedBuilder<T>
derivem. Também posso colocar nesse tipo os 4 métodos comuns entre BeforeAsyncBuilder
e AsyncSettedBuilder
. Posso
me utilizar de que o Java permite que uma subclasse (ou subinterface) pode retornar um tipo mais específico do que o método
do supertipo.
Isto é, se eu tenho HasReturn
que tem o método Object getReturn()
, eu posso implementar um ChildHasReturn
com o método
String getReturn()
, já que String
è um subtipo de Object
. Com isso, as APIs que mencionei acima se mantém, porém agora
adiciono um tipo base (FluentAsyncCallbackBuilder<T>
) cujas interfaces herdam dele:
FluentAsyncCallbackBuilder<T>:
onSuccess: Consumer<T> => FluentAsyncCallbackBuilder<T>
onFailure: Consumer<Throwable> => FluentAsyncCallbackBuilder<T>
onBefore: Runnable => FluentAsyncCallbackBuilder<T>
onFinally: Runnable => FluentAsyncCallbackBuilder<T>
BeforeAsyncBuilder<T>: FluentAsyncCallbackBuilder<T>
onSuccess: Consumer<T> => BeforeAsyncBuilder<T>
onFailure: Consumer<Throwable> => BeforeAsyncBuilder<T>
onBefore: Runnable => BeforeAsyncBuilder<T>
onFinally: Runnable => BeforeAsyncBuilder<T>
setAsyncCall: Consumer<AsyncCallback<T>> => AsyncSettedBuilder<T>
AsyncSettedBuilder<T>: FluentAsyncCallbackBuilder<T>
onSuccess: Consumer<T> => AsyncSettedBuilder<T>
onFailure: Consumer<Throwable> => AsyncSettedBuilder<T>
onBefore: Runnable => AsyncSettedBuilder<T>
onFinally: Runnable => AsyncSettedBuilder<T>
run: () => void
Note que, caso o programador se depare usando no código cliente FluentAsyncCallbackBuilder<T>
ele estará em um
beco sem saída. Não é interessante exibir essa interface para ele. Note que, ao implementar AsyncCallbackBuilder.onSuccess
,
como AsyncCallbackBuilder
implementa tanto BeforeAsyncBuilder
quanto AsyncSettedBuilder
, continuar retornando this
é positivo pois ele atende as interfaces que o requerem sem erro.
O código fica com a seguinte estrutura:
// não queremos expor esta interface
interface FluentAsyncCallbackBuilder<T> {
FluentAsyncCallbackBuilder<T> onSuccess(Consumer<T> successFunction);
// ... demais métodos declarados
}
public interface BeforeAsyncBuilder<T> extends FluentAsyncCallbackBuilder<T> {
BeforeAsyncBuilder<T> onSuccess(Consumer<T> successFunction);
// ... demais métodos advindos de FluentAsyncCallbackBuilder
// note que eu **preciso** colocar esses métodos com o retorno correto,
// caso contrário jogo o programador no beco sem saída do FluentAsyncCallbackBuilder
AsyncSettedBuilder<T> setAsyncCall(Consumer<AsyncCallback<T>> asyncCall);
}
public interface AsyncSettedBuilder<T> extends FluentAsyncCallbackBuilder<T> {
AsyncSettedBuilder<T> onSuccess(Consumer<T> successFunction);
// ... demais métodos advindos de FluentAsyncCallbackBuilder
// note que eu **preciso** colocar esses métodos com o retorno correto,
// caso contrário jogo o programador no beco sem saída do FluentAsyncCallbackBuilder
void run();
}
public class AsyncCallbackBuilder<T> implements BeforeAsyncBuilder<T>, AsyncSettedBuilder<T> {
// métodos estáticos, construtor privado e todo esse auê
public AsyncCallbackBuilder<T> onSuccess(Consumer<T> successFunction) {
// faz algo significativo com successFunction
return this;
}
// ... demais métodos advindos de FluentAsyncCallbackBuilder, BeforeAsyncBuilder e AsyncSettedBuilder
Parece algo bobo, não é? Exemplo clássico de overengineering?
Pois bem… algo aconteceu recentemente que mostrou motivos para garantir esse tipo de coisa…
No trabalho, está sendo refeita uma tratativa, ainda bem modesta, do ciclo de vida das requisições ao Prometheus. Como estamos no GWT, de toda sorte vou ter uma chamada assíncrona para lidar com a requisição ao ciclo de validação do Prometheus, então preciso lidar com alguns tipos de respostas:
- quando tudo ocorre bem e o dado é (supostamente) persistido
- quando ocorrem explosões
- quando ocorrem dúvidas
- quando preciso exibir informações emitidas por um dos processamentos do ciclo de validação
Na lida das dúvidas, preciso fazer uma segunda chamada assíncrona falando “ciclo de validação, eu perguntei pro
usuário e ele disse que tá tudo bem, ele come a bronca, vai fundo”. Nem sempre é necessário lidar com as dúvidas,
mas sempre é necessário fazer a chamada. No caso, quem lida com isso tudo é a classe PrometheusMgr
, com o builder
fluente PrometheusMgrBuilder
.
Então, em cima dessa informação, foi colocada a seguinte linha no construtor de PrometheusMgr
:
PrometheusMgr(Consumer<List<Inform>> actionInform, Consumer<List<Doubt>> actionDoubts, Consumer<List<Explode>> actionExplode,
Consumer<PrometheusDTO<T>> endAction, BiFunction<T, AsyncCallback<PrometheusDTO<T>>, Request> asyncCall) {
// ... demais inicializações ...
this.asyncCall = Objects.requireNonNull(asyncCall);
}
E… aconteceu exatamente de, em um único uso de PrometheusMgrBuilder
, não ter sido alterado para fornecer o asyncCall
. Esse
caso específico estava em uma classe que está marcada para morrer com @Deprecated
e servindo de proxy para PrometheusMgr
.
Eis o código que gerou o problema:
// bannerContainer é variável de classe
// prometheusDTO é argumento do método que tem esse código aqui
PrometheusMgr<T> prometheusMgr = new PrometheusMgrBuilder<T>()
.setActionInform((listaInform) -> bannerContainer.add(new BasserInform(listaInform)))
.setActionExplode((listaExplode) -> bannerContainer.add(new BannerExplode(listaExplode)))
.setActionDoubts((listaDoubts) -> tratamentoDoubts.accept(prometheusDTO))
.build();
Bem fácil perder esse ponto crítico, hein? Note que, no caso do PrometheusMgr
, faz sentido construir o objeto
e manter ele ao longo da vida da parte do app que faz chamadas ao Prometheus.
Resumindo o padrão fluent builder
Você precisa de um conjunto de métodos de preparação. Você também precisa de um método crítico que vai determinar “esse objeto está pronto para ser construído”. Esse método precisa ser chamado pelo menos uma única vez, mas talvez mais.
Talvez você precise ter vários desses métodos críticos, porém a complexidade de manter tudo através de tipos sobe exponencialmente.
Aqui, temos os seguintes participantes:
- interface fluente base (
FluentBase
) - interface fluente antes do método crítico (
BeforeCritic
) - interface fluente após o método crítico (
ReadyToBuild
) - objeto que implementa as interfaces sempre retornado
this
(FluentBuilder
) - o objeto de construção complicada (
Target
)
FluentBase:
método preparação: ... => FluentBase
// demais métodos
BeforeCritic: FluentBase
método preparação: ... => BeforeCritic
// demais métodos de preparação, sempre retornando BeforeCritic
método crítico: ... => ReadyToBuild
ReadyToBuild: FluentBase
método preparação: ... => ReadyToBuild
// demais métodos de preparação, sempre retornando ReadyToBuild
build: () => Target
// se puder chamar diversos métodos críticos, só colocar "método crítico: ... => ReadyToBuild" aqui
FluentBuilder: BeforeCritic & ReadyToBuild
[static] get builder: ... => BeforeCritic
método preparação: ... => FluentBuilder
// demais métodos de preparação, sempre retornando FluentBuilder
método crítico: ... => FluentBuilder
build: () => Target
Note que Target
pode ter construtor público sem nenhum problema, mas o FluentBuilder
vai facilitar sua chamada no código cliente.
Note também que, no caso do AsyncCallbackBuilder
, não precisei de fato usar o build: () => Target
, já que o objeto a ser construído
não necessitava em nenhum momento ser mantido já que se cadastrava também o próprio consumo dele.
Como todo padrão de projeto, isso foi algo percebido como útil em alguns cenários e deve ser usado com carinho e cautela. Não tente forçar um uso para esse padrão de projeto, ele tem escopo extremamente limitado para linguagens com tipagem estática estilo Java onde se deseja garantir determinados valores antes de chamar o construtor de algum objeto. Isso não exime jamais do construtor desse objeto ter suas próprias regras para evitar uma instanciação danosa. O uso desse padrão de projeto é apenas fazer o compilador impedir a construção do objeto em estado inválido.