Diseño de API RESTful basada en funciones

8

Por favor, resuelva una discusión entre un amigo y yo.

Actualmente estamos diseñando una API de producto. Nuestra entidad de producto se ve así

{
    "Id": "",
    "ProductName": "",
    "StockQuantity": 0
}

Las ventas de productos son manejadas por un tercero y están obligadas a informarnos con la cantidad comprada para que el StockQuantitycampo se pueda disminuir.

Mi acercamiento:

PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }

El tercero es responsable de consultar el producto, hacer el cálculo en función de la StockQuantitycantidad actual y comprada y enviar una PUTsolicitud con el nuevo valor.

Mi amigo no quiere que el tercero haga el cálculo. Su acercamiento

PUT /api/Product/{Id}/DecreaseStock --data { "PurchasedQuantity": "{PurchasedQuantity}" }

Entonces podemos hacer el cálculo y actualizar el StockQuantity

No quiero crear puntos finales basados ​​en funciones y él no quiere confiar en terceros para hacer los cálculos.

¿Cuál sería la forma correcta de abordar este problema?

Sefa Ümit Oray
fuente
Recuerde que PUT debe ser (en teoría) idempotente. La opción 1 encajaría en la semántica. La opción 2 no lo haría. Si cumplir con REST es importante para ti. Intentaría mantener las llamadas PUT idempotentes, ya que le ahorrará muchos dolores de cabeza. Lo mismo para ELIMINAR. Para operaciones similares a comandos, sinceramente, le daría una oportunidad a Json o XML RPC. Ambas estrategias (REST y RPC) pueden vivir juntas dentro de la misma API web. Se transmite con el principio de CQRS (segregación de responsabilidad de consulta de comando :-)
Laiv

Respuestas:

19

Puede dejar que su tercero publique las ventas de su producto. Por ejemplo:

POST /product/{id}/sale { "Quantity": 3 }

Estoy de acuerdo con tu punto y el de tu colega. Esta es la lógica de negocios, y no debe dejarse al cliente de la API, pero también debe evitar tener "funciones" como puntos finales.

A veces, resolver estos problemas es tan fácil como llamarlo de manera diferente, es cierto que no siempre.

Robert Bräutigam
fuente
2
Esta. Además: parece que cada venta también necesita un objeto en la base de datos. Tener cada venta como objeto separado en db permite la trazabilidad. Piense, si algo salió mal y la cantidad de existencias final está mal y necesita fijar valores. Si solo tiene una columna de valor final, entonces no puede hacer mucho. Con suerte, hay registros útiles en el sistema para descubrir qué salió mal. Si tiene objetos de venta con marcas de tiempo, nombres de usuario y posiblemente una dirección IP adjunta, puede eliminar ciertos registros para corregir datos y rastrear de qué usuario / ubicación provienen.
Ski
Gracias por los aportes. La venta / pedido es un recurso de otro equipo, no es mi responsabilidad guardarlos o procesarlos. ¿Sabe esto, la creación de /salepunto final sigue siendo válida?
Sefa Ümit Oray
@ SefaÜmitOray: los puntos finales /saley /product/{id}/saleson completamente independientes y el hecho de que tengan nombres similares no implica de ninguna manera que se refieran al mismo recurso.
Bart van Ingen Schenau
@BartvanIngenSchenau Lo que quiero decir es saleque no está en mi dominio y no forma parte de él product. ¿Todavía tiene sentido crear /product/{id}/salemientras no representa ningún recurso real?
Sefa Ümit Oray
55
@ SefaÜmitOray Es completamente válido si representa algo significativo en su contexto. No tiene que significar lo mismo que en otros contextos y tampoco tiene que ser algo que se mantenga directamente en la base de datos. Dominio! = Tablas de base de datos, Recurso! = Tablas de base de datos.
Robert Bräutigam
3

No hay razón para que tú tampoco puedas hacerlo; o ambos.

En un contexto de punto de venta, el seguimiento de transacciones individuales tiene mucho sentido. Allí, la solución de Robert tiene mucho sentido.

En un contexto de stock / almacén, no necesariamente realiza un seguimiento de las transacciones sino de "hacer un inventario"; Tener un punto final que permita al cliente informar sus niveles de existencias

Tengo 10 unidades Tengo 7 unidades Tengo 3 unidades Tengo 20 unidades

Tiene mucho sentido.

Los niveles de existencias cambian por otras razones que no sean "ventas"; Sólo algo para tener en cuenta.

En teoría, el nivel de existencias debería ser computable a partir de los cambios; pero en algunos dominios esa es precisamente la suposición que desea verificar . Desearía poder calcular el nivel de existencias de dos maneras diferentes y verificar las discrepancias (también conocido como "contracción").

Por lo tanto, no creo que la semántica sea clara, según el contexto que haya proporcionado.

En cuanto a la parte HTTP; PUT [target-uri]tiene sentido semánticamente cuando reemplaza una representación de un documento con otra. Es un UPSERT- el segundo PUT a un recurso está pidiendo sobrescribir la representación existente.

PUT /sales { Quantity = 5 }
PUT /sales { Quantity = 2 }
PUT /sales { Quantity = 3 }

dice que la cantidad de unidades vendidas es 3, no 10.

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }

Eso es lo que 10parece

PUT /sales { Quantity : [5] }
PUT /sales { Quantity : [5,2] }
PUT /sales { Quantity : [5,2,3] }

Esa es otra forma de deletrear 10.

POST /sales { Quantity = 5 }
POST /sales { Quantity = 2 }
POST /sales { Quantity = 3 }

En lo que respecta a HTTP, esto también es aceptable. Sin embargo, no es una gran opción en una red poco confiable porque los mensajes a veces se duplican.

POST /sales { Quantity = 5 }
POST /sales { Quantity = 2 }
POST /sales { Quantity = 3 }
POST /sales { Quantity = 3 }

Es eso 13? o 10?

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }
PUT /sales/3 { Quantity = 3 }

Eso es inequívocamente 10

PUT /sales { Quantity : [5,2,3] }
PUT /sales { Quantity : [5,2,3] }

Eso es inequívocamente 10

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }
PUT /sales/4 { Quantity = 3 }

Eso es sin ambigüedad 13

PUT /sales { Quantity : [5,2,3] }
PUT /sales { Quantity : [5,2,3,3] }

Eso es sin ambigüedad 13

POST /sales { TransactionId = 1 , Quantity = 5 }
POST /sales { TransactionId = 2 , Quantity = 2 }
POST /sales { TransactionId = 3 , Quantity = 3 }
POST /sales { TransactionId = 3 , Quantity = 3 }

10

POST /sales { TransactionId = 1 , Quantity = 5 }
POST /sales { TransactionId = 2 , Quantity = 2 }
POST /sales { TransactionId = 3 , Quantity = 3 }
POST /sales { TransactionId = 4 , Quantity = 3 }

13

(Para ser justos, HTTP tiene soporte para solicitudes condicionales ; puede levantar algunos de los metadatos de su protocolo específico de dominio a los encabezados agnósticos de dominio para eliminar parte de la ambigüedad, si puede persuadir al cliente para que siga adelante).

Por supuesto, hay compensaciones: HTML no tiene soporte PUT nativo; si tiene la intención de que los clientes de su API sean navegadores, entonces necesita un protocolo basado en POST o necesita extensiones de código a pedido para convertir el envío del formulario de POST a PUT.

VoiceOfUnreason
fuente
1
No tiene que rastrear las ventas individuales, solo porque hay un punto final para ello. Es decir, no es necesario poder enumerar las llamadas de venta anteriores, solo porque puede PUBLICAR. Sin embargo, tiene razón en que puede haber otros casos de uso (no lo sabemos), y debe definir las llamadas idempotentes con llamadas condicionales o con algún otro medio.
Robert Bräutigam
2

Esto parece un diseño realmente malo, no importa cómo lo cortes. Nunca confiaría en que un tercero me diga mi inventario actual a menos que los haya contratado para administrar mi almacén.

Además, el enfoque de búsqueda de funciones no es RESTANTE en absoluto y está destinado a crear consternación entre sus consumidores.

Finalmente, no puedo imaginar un escenario en el que lo único que le importa de una venta es el inventario resultante que le queda después de que se haya realizado.

Es mucho mejor que el tercero le publique un recurso de Venta o Factura (con información como qué producto, cantidad, fecha, método de envío, información del cliente, etc.). Esto le permite hacer un análisis real y realizar un seguimiento de lo que está vendiendo, a quién, cuándo, etc. para que pueda actualizar su negocio.

Incluso si su tercero está haciendo el cumplimiento total del pedido, deseará realizar un seguimiento de las ventas para fines contables y demográficos de los clientes, si nada más.

Pablo
fuente
1

PUT / api / Product / {Id} / --data {"StockQuantity": "{NewStockQuantity}"}

Este tipo de diseño tiene un problema importante en el sentido de que si alguna vez desea que se ejecute más de un hilo de cliente en su API, está sujeto a lecturas / escrituras sucias. Es decir, entre el momento en que el cliente extrae la cantidad actual y calcula el nuevo valor, otro cliente puede extraer ese mismo valor anterior y calcular una respuesta diferente. La cantidad con la que termines será la que se actualice por última vez, pero ninguna de las dos es correcta. Por ejemplo, supongamos que su cantidad actual es 10. El cliente A quiere vender 5 artículos y extrae la cantidad actual. Al mismo tiempo, el cliente B quiere vender 6 artículos y extrae la cantidad actual. Ambos ven 10 artículos en stock. A calcula 5 elementos restantes. sicalcula 4 restantes. Ambos se actualizan. Ahora muestra 4 o 5 elementos restantes dependiendo de quién fue la última actualización registrada. Sin embargo, en realidad ha vendido más artículos que realmente tiene. Lo peor es que no hay una manera fácil de caminar y ver qué salió mal. Todo lo que tienes son dos incorrectos PUTsen tus registros para mirar.

En cualquier sistema de registro del mundo real, simplemente tener un total actual no es adecuado. Considere si va a una tienda y compra una cantidad de artículos. Solicita un recibo y el cajero solo le entrega un recibo con un total total. ¿Cómo mostrarías que el total es correcto de ese recibo? ¿Cómo mostrarías que compraste un artículo si quisieras devolver algo?

El enfoque de su amigo es mejor, pero sugeriría agregar una identificación de transacción en la mezcla. Esto aborda las preocupaciones reales que VoiceOfUnreason menciona sobre transacciones duplicadas. Una opción es proporcionar una POSToperación para crear una nueva transacción y luego PUTa esa transacción para confirmarla. En el momento de la confirmación, reduce el stock total o rechaza la solicitud porque no hay suficiente disponible.

JimmyJames
fuente
1

Dado que las ventas son manejadas por terceros, debe tener control sobre su inventario de productos al no permitirles actualizar el recuento de existencias.

Para uso interno, por ejemplo, con fines de recuento de existencias, puede tener su enfoque, es decir PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }.

Para uso externo, debe crear una interfaz separada, por ejemplo, /api/SalesOrder/que tome una lista de productos y cantidades, como:

POST /api/SalesOrder/ --data { [{"Id": 1, "Qty": 1}, {"Id": 2, "Qty": 3}] }

Según lo SalesOrderenviado por terceros, la cantidad de cada producto puede actualizarse y asignarse al pedido o puede rechazar el pedido si no hay suficiente producto disponible.

El procesamiento y el recuento de existencias es un proceso interno, los terceros solo requieren una interfaz para que puedan enviar sus pedidos al inventario. Básicamente, así SalesOrderes como se comunican las ventas, las finanzas y el almacén para completar una venta.

imel96
fuente