← Blog

Responsabilidade Única em MuleSoft: um motivo de mudança por flow

Código deste artigo: src/main/mule/srp · testes: srp-test-suite.xml

O Princípio da Responsabilidade Única costuma ser resumido como “faça uma coisa só”. A formulação do próprio Robert Martin é mais útil: um módulo deve ter um único motivo para mudar. Depois ele refinou ainda mais — um único ator que pede a mudança. A diferença importa em integração, porque “pequeno” e “responsabilidade única” não são a mesma coisa. Um flow de dez linhas pode misturar três motivos de mudança.

O que significa numa aplicação Mule

A unidade é o flow ou o sub-flow, e o “motivo de mudança” normalmente é uma pessoa ou um time:

  • As regras de validação mudam quando o negócio muda o que é um pedido válido.
  • O cálculo de preço muda quando o financeiro muda a forma de calcular o total.
  • A chamada ao ERP muda quando o ERP, o endpoint ou o payload mudam.

Se um único flow contém os três, cada um desses pedidos edita o mesmo XML, e cada release desse flow precisa ser testado de novo para os três assuntos.

Antes: um flow, três motivos de mudança

<flow name="srp-before-process-order">
    <http:listener config-ref="http-listener-config" path="/srp/before/orders" allowedMethods="POST">
        <http:response statusCode="#[vars.httpStatus default 200]" />
    </http:listener>
    <choice>
        <when expression="#[isEmpty(payload.orderId)]">
            <raise-error type="APP:BAD_REQUEST" description="orderId is required" />
        </when>
    </choice>
    <choice>
        <when expression="#[(payload.quantity default 0) &lt;= 0]">
            <raise-error type="APP:BAD_REQUEST" description="quantity must be greater than zero" />
        </when>
    </choice>
    <ee:transform doc:name="Price, send to ERP and build response">
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
var total = payload.price * payload.quantity
---
{
    orderId: payload.orderId,
    totalPrice: total,
    erpReference: "ERP-" ++ payload.orderId,
    status: "SENT_TO_ERP"
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
    <logger level="INFO" message="#['Order ' ++ payload.orderId ++ ' sent to ERP']" />
</flow>

Funciona. O problema é o transform do meio: preço e resposta do ERP estão fundidos num único script DataWeave, então uma mudança de preço e uma mudança no ERP viram a mesma edição.

Repare que a validação usa raise-error. Um erro comum — que eu cometi na primeira versão deste artigo — é “validar” com set-payload "Order ID is missing". Isso não interrompe o flow: o pedido inválido segue para o cálculo e para o ERP.

Depois: o flow orquestra, os sub-flows são donos das mudanças

<flow name="srp-after-process-order">
    <http:listener config-ref="http-listener-config" path="/srp/after/orders" allowedMethods="POST">
        <http:response statusCode="#[vars.httpStatus default 200]" />
    </http:listener>
    <flow-ref name="srp-after-validate-order" />
    <flow-ref name="srp-after-calculate-total" />
    <flow-ref name="srp-after-send-to-erp" />
</flow>

<sub-flow name="srp-after-calculate-total">
    <ee:transform>
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    orderId: payload.orderId,
    totalPrice: payload.price * payload.quantity
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
</sub-flow>

O flow principal agora se lê como a descrição do processo. Cada sub-flow tem um dono: o financeiro pode mudar srp-after-calculate-total sem que o diff toque a validação ou a chamada ao ERP. O arquivo completo está em srp-after.xml.

A evidência

A suíte MUnit roda os mesmos três casos contra as duas versões — pedido válido, orderId ausente e quantidade zero — e as duas passam. É esse o ponto: SRP é uma refatoração, não uma mudança de comportamento. O que muda é o custo da próxima mudança e a precisão com que ela pode ser testada: cada sub-flow pode ser chamado direto num teste com flow-ref.

Onde o princípio deixa de ajudar

  • Não divida só por dividir. Um sub-flow com um logger que existe “por causa do SRP” aumenta a navegação e não acrescenta clareza. Divida onde os donos são diferentes.
  • Sub-flow não é reuso entre aplicações. Para compartilhar lógica entre apps Mule, publique — como Mule plugin ou módulo XML SDK no Exchange, ou como uma API própria. Domain projects (os “shared resources” do Anypoint) compartilham configurações de conectores, não flows.
  • Tratamento de erro também é uma responsabilidade. Deixe num único error handler global, como o exemplo faz no global.xml, em vez de copiar blocos on-error em cada flow.

Checklist

  • Você consegue nomear o único time ou regra que faria este flow mudar?
  • O flow principal se lê como uma sequência de passos, e não como uma mistura de detalhes?
  • Cada passo pode ser testado isoladamente?
  • A validação interrompe o flow quando falha?