Inversão de Dependência em MuleSoft: flows de negócio donos das suas abstrações
Código deste artigo:
src/main/mule/dip· testes:dip-test-suite.xml
O Princípio da Inversão de Dependência tem duas partes: módulos de alto nível não devem depender de módulos de baixo nível; os dois devem depender de abstrações, e abstrações não devem depender de detalhes. A segunda metade é a que costuma ser ignorada, e é ela que faz o princípio funcionar.
O que é “invertido”
Sem DIP, a lógica de negócio depende do sistema: ela conhece os nomes de campo, os códigos e as manias do ERP. Com DIP, a lógica de negócio define o modelo de que precisa — aqui, um pedido canônico { orderId, status, totalPrice } — e a integração com o ERP é escrita para se ajustar a ele. A seta de dependência passa a apontar do detalhe para a abstração, e a abstração pertence ao lado do negócio.
No Mule, essa é a ideia por trás da conectividade API-led: a lógica de processo depende do contrato de uma system API, e a system API esconde o backend. O exemplo mostra a mesma coisa dentro de uma única aplicação, para rodar sem rede.
Antes: a regra de negócio fala a língua do ERP
<flow name="dip-before-get-order">
<http:listener config-ref="http-listener-config" path="/dip/before/orders/{orderId}" allowedMethods="GET">
<http:response statusCode="#[vars.httpStatus default 200]" />
</http:listener>
<ee:transform doc:name="Call legacy ERP (simulated)">...</ee:transform>
<ee:transform doc:name="Business rule mixed with ERP mapping">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
var status = payload.STS_CD match {
case "P" -> "PENDING"
case "S" -> "SHIPPED"
else -> "UNKNOWN"
}
---
{
orderId: payload.ORD_NO,
status: status,
totalPrice: payload.TOT_AMT as Number,
canCancel: payload.STS_CD == "P"
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</flow>
A regra de cancelamento está escrita como STS_CD == "P". Trocar o ERP, acrescentar um cache ou simular o ERP num teste significa editar a regra de negócio.
Depois: a regra depende de um “repositório de pedidos”
<flow name="dip-after-get-order">
<http:listener config-ref="http-listener-config" path="/dip/after/orders/{orderId}" allowedMethods="GET">
<http:response statusCode="#[vars.httpStatus default 200]" />
</http:listener>
<flow-ref name="${orders.repository}" />
<flow-ref name="dip-after-apply-order-rules" />
</flow>
<sub-flow name="dip-after-apply-order-rules">
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
payload ++ { canCancel: payload.status == "PENDING" }]]></ee:set-payload>
</ee:message>
</ee:transform>
</sub-flow>
Duas implementações devolvem o pedido canônico: dip-after-order-repository-legacy-erp, que concentra todos os detalhes do ERP, e dip-after-order-repository-in-memory. A propriedade orders.repository, no config/app.yaml, escolhe uma delas. Como a propriedade é resolvida quando a aplicação sobe, a referência é estática — o Studio e o MUnit a enxergam, ao contrário do roteamento dinâmico do exemplo de Aberto/Fechado.
A evidência
As mesmas verificações — pedido pendente, pode ser cancelado, total 120; pedido inexistente, 404 — passam nos flows antes e depois. Dois testes a mais chamam direto o adaptador do ERP e o repositório em memória e verificam que os dois devolvem o mesmo pedido canônico. É essa a propriedade que o DIP entrega: as implementações mudam enquanto a regra e os testes dela ficam onde estão. (O artigo sobre Liskov trata do que essas implementações precisam respeitar para serem trocadas com segurança.)
O que DIP não é
A primeira versão deste artigo listava três técnicas que são boas práticas, mas não são inversão de dependência:
- Externalizar configuração (
config-ref="${db.config}", endpoints em propriedades). Isso muda com qual banco você fala, não de que o flow depende — o flow continua falando SQL com uma tabela. - Endpoints HTTP dinâmicos. Mesma coisa: um endereço configurável para uma dependência concreta.
- Guardar configuração no Object Store. Object Store serve para estado em tempo de execução, não para configuração.
Ela também mostrava uma query montada como WHERE order_id = #[attributes.queryParams.orderId], que concatena a entrada do usuário no SQL. No Mule 4, passe os valores como parâmetros:
<db:select config-ref="orders-db-config">
<db:sql>SELECT * FROM orders WHERE order_id = :orderId</db:sql>
<db:input-parameters>#[{ orderId: attributes.uriParams.orderId }]</db:input-parameters>
</db:select>
Checklist
- A regra de negócio está escrita nos termos do negócio, ou nos nomes de campo e códigos de um sistema?
- Quem é dono do modelo de que o flow depende — a lógica de negócio ou o backend?
- Dá para rodar a lógica de negócio contra uma implementação simulada num teste?
- As queries ao banco são parametrizadas?
Fechando a série
Três princípios se aplicam quase diretamente: Responsabilidade Única para flows, Segregação de Interfaces para contratos de API e Inversão de Dependência para a relação entre a lógica de negócio e os sistemas. A Substituição de Liskov se aplica por completo quando se olha para as implementações atrás de um contrato, e não para herança. O Aberto/Fechado é o de encaixe mais fraco: útil quando o conjunto de variações realmente cresce, e um custo quando não cresce.
Cada afirmação desta série tem um teste em mulesoft-solid-examples. Clone, quebre alguma coisa e veja qual teste percebe.