← Blog

Segregação de Interfaces em MuleSoft: uma API para cada consumidor

Código deste artigo: src/main/mule/isp · contratos: isp-order-views.raml · testes: isp-test-suite.xml

O Princípio da Segregação de Interfaces diz que nenhum cliente deve ser obrigado a depender de métodos que não usa. Várias interfaces pequenas são melhores que uma grande, porque um cliente acoplado a uma interface grande é afetado por toda mudança nela — inclusive nas partes que ele nunca usa.

Onde se encaixa no Mule, e onde não

Dentro de uma aplicação Mule não há interfaces para segregar. Dividir um flow em sub-flows é Responsabilidade Única, não ISP; a primeira versão deste artigo misturava os dois.

No nível do contrato de API, o encaixe é excelente. Uma especificação de API é exatamente o tipo de interface de que Martin falava, e os consumidores — um app mobile, um sistema financeiro, um parceiro — são os clientes. Em termos de API-led, o ISP é o motivo de existirem as experience APIs.

Antes: um endpoint para todos

<flow name="isp-before-get-order">
    <http:listener config-ref="http-listener-config" path="/isp/before/orders/{orderId}" allowedMethods="GET">
        <http:response statusCode="#[vars.httpStatus default 200]" />
    </http:listener>
    <flow-ref name="isp-load-order" />
</flow>

GET /isp/before/orders/ORD-1001 devolve o registro completo: rastreamento, contato do cliente, endereço, itens, faturamento — e um bloco internal com preço de custo, fornecedor e margem.

Daí vêm dois problemas:

  • Acoplamento. Quando o financeiro precisa de um novo campo de faturamento, o contrato do app mobile muda junto. Todo consumidor é testado de novo por uma mudança que só um deles pediu.
  • Exposição. O app mobile passa a receber dados pessoais e o custo interno, que ele nunca mostra. Esses dados vão parar em logs, caches e na memória do aparelho de qualquer forma. Em integração, esse é o argumento mais forte para o ISP — é um problema de segurança e de proteção de dados antes de ser um problema de design.

Depois: uma interface por consumidor

<flow name="isp-after-get-mobile-order">
    <http:listener config-ref="http-listener-config" path="/isp/after/mobile/orders/{orderId}" allowedMethods="GET">
        <http:response statusCode="#[vars.httpStatus default 200]" />
    </http:listener>
    <flow-ref name="isp-load-order" />
    <ee:transform>
        <ee:message>
            <ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
    orderId: payload.orderId,
    status: payload.status,
    estimatedDelivery: payload.estimatedDelivery
}]]></ee:set-payload>
        </ee:message>
    </ee:transform>
</flow>

O financeiro tem o seu próprio /isp/after/finance/orders/{orderId}, com nota fiscal, total, imposto e status do pagamento. O bloco de custo interno não é exposto a ninguém. Cada recurso traz o {orderId} a que se refere — algo que os endpoints /orders/stock e /orders/payment-status da primeira versão esqueceram.

A evidência

Os testes verificam o conjunto exato de campos que cada consumidor recebe. A visão mobile é exatamente orderId, status e estimatedDelivery; a visão financeira é exatamente os seus cinco campos de faturamento. O teste da versão antes verifica o oposto, de propósito: o e-mail do cliente e o custo interno aparecem para todo mundo.

Sem exagero

  • Uma API por tela não escala. Segregue por tipo de consumidor com necessidade diferente, não por cada página de cada app.
  • Considere seleção de campos quando os consumidores são muitos e parecidos. Parâmetros para escolher conjuntos de campos, ou GraphQL, dão a cada consumidor uma visão estreita sem criar uma API nova a cada vez.
  • Mantenha uma única fonte atrás das visões. O exemplo tem um só isp-load-order; as visões moldam os dados, não duplicam a busca.

Checklist

  • Algum consumidor recebe campos que não usa?
  • Uma mudança pedida por um consumidor altera o contrato de outro?
  • Dados pessoais ou comercialmente sensíveis só chegam aos consumidores que precisam deles?
  • As visões moldam uma única fonte, em vez de cada uma reimplementar a busca?