Aberto/Fechado em MuleSoft: estender sem editar, e quanto isso custa
Código deste artigo:
src/main/mule/ocp· testes:ocp-test-suite.xml
A regra de Bertrand Meyer diz que um módulo deve ser aberto para extensão e fechado para modificação: você acrescenta comportamento acrescentando código, e não editando código que já funciona e já foi testado. Dos cinco princípios, este é o que menos se encaixa no Mule, e vale deixar claro por quê.
O que significa numa aplicação Mule
Numa linguagem orientada a objetos, estende-se o comportamento com uma nova classe que implementa uma interface existente. O Mule não tem interfaces, então o equivalente mais próximo é: o flow que decide continua igual, e o comportamento novo chega como novos flows mais configuração.
O exemplo é um endpoint de pagamento. Ele aceita cartão de crédito e boleto, e o negócio agora quer pix.
Antes: um roteador editado a cada novo meio
<flow name="ocp-before-process-payment">
<http:listener config-ref="http-listener-config" path="/ocp/before/payments" allowedMethods="POST">
<http:response statusCode="#[vars.httpStatus default 200]" />
</http:listener>
<choice>
<when expression="#[payload.paymentMethod == 'creditCard']">
<flow-ref name="ocp-before-process-credit-card" />
</when>
<when expression="#[payload.paymentMethod == 'bankSlip']">
<flow-ref name="ocp-before-process-bank-slip" />
</when>
<otherwise>
<raise-error type="APP:BAD_REQUEST"
description="#['Unsupported payment method: ' ++ (payload.paymentMethod default 'none')]" />
</otherwise>
</choice>
</flow>
Aceitar pix exige um novo when num roteador pelo qual passam todos os pagamentos. A edição é pequena, mas reabre um flow do qual os caminhos de cartão e boleto dependem.
Depois: o roteador busca a implementação
# config/app.yaml
payment:
flow:
creditCard: "ocp-after-process-credit-card"
bankSlip: "ocp-after-process-bank-slip"
pix: "ocp-after-process-pix"
<flow name="ocp-after-process-payment">
<http:listener config-ref="http-listener-config" path="/ocp/after/payments" allowedMethods="POST">
<http:response statusCode="#[vars.httpStatus default 200]" />
</http:listener>
<set-variable variableName="paymentFlow"
value="#[output application/java --- p('payment.flow.' ++ (payload.paymentMethod default 'none'))]" />
<choice>
<when expression="#[isEmpty(vars.paymentFlow)]">
<raise-error type="APP:BAD_REQUEST"
description="#['Unsupported payment method: ' ++ (payload.paymentMethod default 'none')]" />
</when>
</choice>
<flow-ref name="#[vars.paymentFlow]" />
</flow>
O pix entrou como um sub-flow e uma linha de YAML. O roteador não mudou, e a configuração funciona também como lista de permitidos: um meio que não está configurado é rejeitado antes de qualquer coisa rodar.
Dois detalhes fáceis de errar no Mule 4, e que a primeira versão deste artigo errou:
p()dentro deset-variableprecisa deoutput application/java. Sem isso, o runtime não consegue escolher entre os media types Java e JSON em uso e falha com “Unable to infer a output media type”.payload.paymentMethodpode vir nulo.++ nullé erro no DataWeave, daí odefault 'none'.
A evidência
A suíte roda cartão, boleto e um meio desconhecido contra os dois roteadores, com resultados idênticos. Depois acrescenta o caso que importa: o pix é rejeitado pela versão antes e aceito pela versão depois — sem nenhuma mudança no roteador.
Os custos — e por que o encaixe é só parcial
- O Studio não valida um
flow-refdinâmico. Um erro de digitação no YAML só aparece em tempo de execução. - O MUnit precisa de um contorno. Ele só carrega os flows que consegue ver referenciados, então um flow referenciado dinamicamente fica de fora, a menos que algo o referencie de forma estática. A suíte faz isso num
before-suite. Funciona, mas é código de teste que existe por causa do design. - “Fechado” é relativo. Você continua editando um arquivo — o YAML. O ganho é que a edição fica nos dados, e não no flow pelo qual passa todo pagamento, e um meio novo não quebra um existente.
- Para uma lista curta e estável, o
choiceresolve. Se os meios de pagamento mudam uma vez por ano, o roteador explícito é mais fácil de ler e de depurar. Use roteamento dinâmico quando a lista realmente cresce, ou quando times diferentes são donos de implementações diferentes.
Checklist
- Acrescentar uma nova variação exige editar um flow pelo qual passam todas as variações existentes?
- Se há roteamento dinâmico, a configuração é a lista de permitidos e valores desconhecidos são rejeitados?
- Existe teste para cada implementação configurada, e não só para o roteador?
- A indireção extra se justifica pela frequência com que a lista muda?