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?