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) <= 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 blocoson-errorem 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?