Substituição de Liskov em MuleSoft: implementações que se substituem
Código deste artigo:
src/main/mule/lsp· contrato:lsp-order-after.raml· testes:lsp-test-suite.xml
O princípio de Barbara Liskov diz que, se um código funciona com um tipo, ele precisa continuar funcionando com qualquer subtipo desse tipo. O Mule não tem classes nem subtipos, então é tentador concluir que o LSP não se aplica. Ele se aplica — mas é preciso procurá-lo no lugar certo.
Onde o LSP aparece em integração
A primeira versão deste artigo associava o LSP a “tipos de pedido”: pedidos padrão, expresso e internacional devolvendo respostas consistentes. Isso é, na verdade, uma regra de consistência de contrato, e força o princípio. A situação em que a substituição acontece de fato no Mule é esta: várias implementações ficam atrás de um mesmo contrato, e o consumidor não deveria saber qual delas respondeu.
- Uma process API que lê pedidos de um ERP antigo e, depois de uma migração, de um novo.
- Uma system API com um backend de produção e um stub usado nos testes.
- Dois sistemas regionais expostos pela mesma experience API.
Liskov também insistia que o contrato trata de comportamento, não só de assinaturas. Para uma API isso significa três coisas: o formato dos dados, o significado dos valores e a forma de reportar erros.
O contrato
#%RAML 1.0 DataType
type: object
properties:
orderId: string
status:
enum: [PENDING, SHIPPED]
totalPrice: number
# E, no comportamento: pedido inexistente é 404, nunca um 200 vazio.
Um flow consumidor se apoia nele para decidir se o pedido pode ser cancelado:
<sub-flow name="lsp-after-summarise-order">
<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>
Antes: um adaptador que parece compatível e não é
O adaptador do ERP legado traduz os códigos dele (P, S) para o contrato. O adaptador do ERP novo foi escrito às pressas: o sistema novo já usa nomes legíveis, então ele copia os valores como vêm.
<sub-flow name="lsp-before-provider-new-erp">
<!-- o ERP novo devolve { id, state: "pending", amount: "120.00" } -->
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
orderId: payload.id,
status: payload.state,
totalPrice: payload.amount
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</sub-flow>
Todos os nomes de campo batem, então uma checagem de schema só pelos nomes passaria. O comportamento, não:
statusvem como"pending", ecanCancelvira false para um pedido que pode ser cancelado.totalPriceé o texto"120.00", não um número.- Um pedido inexistente devolve um 200 vazio em vez de 404.
Nada falha. O consumidor devolve uma resposta confiante e errada — o tipo mais caro de bug em integração.
Depois: os dois adaptadores respeitam o contrato inteiro
<sub-flow name="lsp-after-provider-new-erp">
<ee:transform>...</ee:transform> <!-- chama o ERP novo -->
<choice>
<when expression="#[payload == null]">
<raise-error type="APP:NOT_FOUND" description="Order not found" />
</when>
</choice>
<ee:transform>
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{
orderId: payload.id,
status: upper(payload.state),
totalPrice: payload.amount as Number
}]]></ee:set-payload>
</ee:message>
</ee:transform>
</sub-flow>
Agora o adaptador traduz vocabulário, tipos e erros. Chame GET /lsp/after/orders/ORD-1001?provider=legacy e ?provider=new: as respostas são idênticas.
A evidência
A suíte MUnit faz as mesmas perguntas aos dois provedores. Na versão antes, o provedor novo devolve um canCancel diferente, um preço em texto e um 200 para um pedido inexistente — cada um verificado explicitamente. Na versão depois, o pedido pendente, o pedido enviado e o pedido inexistente dão exatamente o mesmo resultado nos dois provedores. Isso é substituição, testada.
Duas nuances
- Acrescentar um campo opcional costuma ser seguro. Consumidores feitos para tolerar o desconhecido ignoram campos novos. O que quebra a substituição é remover, renomear ou mudar o tipo de um campo — ou manter o nome e mudar o significado, como acima.
- A entrada também conta. Uma implementação que exige um campo obrigatório a mais na requisição não é substituível, mesmo que as respostas sejam perfeitas.
Checklist
- Existe um contrato escrito contra o qual todas as implementações são testadas?
- Ele cobre os vocabulários de valores (enums, unidades, formatos), e não só os nomes dos campos?
- Todas as implementações reportam “não encontrado” e entrada inválida da mesma forma?
- Dá para trocar uma implementação por outra na configuração e obter os mesmos resultados de teste?