← Blog

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.