← Blog

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 de set-variable precisa de output 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.paymentMethod pode vir nulo. ++ null é erro no DataWeave, daí o default '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-ref dinâ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 choice resolve. 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?