Implementación del patrón de comando en una API RESTful

12

Estoy en el proceso de diseñar una API HTTP, con la esperanza de que sea lo más RESTANTE posible.

Hay algunas acciones cuya funcionalidad se extiende sobre unos pocos recursos, y en algún momento debe deshacerse.

Pensé para mí mismo, esto suena como un patrón de comando, pero ¿cómo puedo modelarlo en un recurso?

Introduciré un nuevo recurso llamado XXAction, como DepositAction, que se creará a través de algo como esto

POST /card/{card-id}/account/{account-id}/Deposit
AmountToDeposit=100, different parameters...

esto creará una nueva DepositAction y activará su método Do / Execute. En este caso, devolver un estado HTTP creado 201 significa que la acción se ha ejecutado con éxito.

Más tarde, si un cliente desea ver los detalles de la acción, puede

GET /action/{action-id}

Supongo que la actualización / PUT debería estar bloqueada, porque no es relevante aquí.

Y para deshacer la acción, pensé en usar

DELETE /action/{action-id}

que en realidad llamará al método Deshacer del objeto relevante y cambiará su estado.

Digamos que estoy contento con un solo deshacer, no necesito rehacer.

¿Está bien este enfoque?

¿Hay algún inconveniente, razones para no usarlo?

¿Se entiende esto desde el punto de vista de los clientes?

Mithir
fuente
Respuesta corta, eso no es DESCANSO.
Evan Plaice el
3
@EvanPlaice, ¿quieres explicarlo? Esa es exactamente la pregunta.
Mithir
1
Hubiera elaborado una respuesta, pero la respuesta de Gary ya cubre la mayoría / todo lo que agregaría. Digo que no es descanso porque se supone que los URI solo representan recursos (es decir, no acciones). Las acciones se manejan a través de GET / POST / PUT / DELETE / HEAD. Piense en REST como una interfaz OOP. El objetivo es hacer que la API se ajuste al patrón general y desacoplarla de los detalles específicos de la implementación como sea posible.
Evan Plaice
1
@EvanPlaice Ok, entiendo, gracias. Creo que es confuso aquí porque Deposit podría considerarse un sustantivo y un verbo ...
Mithir
En este caso, el URI debe representar una transacción donde debitar (tomar dinero) y acreditar (dar dinero) son acciones realizadas a través de solicitudes POST. POST se usa para ambos porque cada vez que el dinero se mueve en cualquier dirección representa una nueva transacción que se está creando. En su caso específico, las transacciones se realizan en la cuenta del titular de la tarjeta, por lo que el número de cuenta de la tarjeta es el URI del recurso.
Evan Plaice el

Respuestas:

13

Estás agregando una capa de abstracción que es confusa

Su API comienza muy limpia y simple. Una POST HTTP crea un nuevo recurso de depósito con los parámetros dados. Luego, se sale del camino introduciendo la idea de "acciones" que son un detalle de implementación en lugar de una parte central de la API.

Como alternativa, considere esta conversación HTTP ...

POST / card / {card-id} / account / {account-id} / Deposit

AmountToDeposit = 100, diferentes parámetros ...

201 CREADO

Ubicación = / tarjeta / 123 / cuenta / 456 / Depósito / 789

Ahora desea deshacer esta operación (técnicamente, esto no debería permitirse en un sistema de contabilidad equilibrado, pero qué bueno):

BORRAR / tarjeta / 123 / cuenta / 456 / Depósito / 789

204 SIN CONTENIDO

El consumidor de API sabe que está tratando con un recurso de Depósito y puede determinar qué operaciones están permitidas en él (generalmente a través de OPCIONES en HTTP).

Aunque la implementación de la operación de eliminación se lleva a cabo a través de "acciones" hoy en día, no hay garantía de que cuando migre este sistema de, por ejemplo, C # a Haskell y mantenga el front-end que el concepto secundario de una "acción" continuaría agregando valor , mientras que el concepto principal de Depósito ciertamente lo hace.

Editar para cubrir una alternativa a DELETE y Deposit

Para evitar una operación de eliminación, pero aún así eliminar efectivamente el Depósito, debe hacer lo siguiente (utilizando una Transacción genérica para permitir Depósito y Retiro):

POST / card / {card-id} / account / {account-id} / Transaction

Cantidad = -100 , diferentes parámetros ...

201 CREADO

Ubicación = / tarjeta / 123 / cuenta / 456 / Transición / 790

Se crea un nuevo recurso de transacción que tiene exactamente la cantidad opuesta (-100). Esto tiene el efecto de equilibrar la cuenta de nuevo a 0, negando la transacción original.

Puede considerar crear un punto final de "utilidad" como

POST / card / {card-id} / account / {account-id} / Transaction / 789 / Undo <- ¡MALO!

para obtener el mismo efecto. Sin embargo, esto rompe la semántica de un URI como un identificador al introducir un verbo. Es mejor atenerse a los sustantivos en los identificadores y mantener las operaciones restringidas a los verbos HTTP. De esa manera, puede crear fácilmente un enlace permanente a partir del identificador y utilizarlo para GET, etc.

Gary Rowe
fuente
3
+1 "técnicamente esto no debería permitirse en un sistema de contabilidad equilibrado". Alguien sabe contar frijoles. Esa declaración es absolutamente correcta, la forma de revertir sería crear otra transacción que acredite la devolución de los fondos. Las entradas del libro mayor siempre deben considerarse inmutables y permanentes una vez que se completa una transacción.
Evan Plaice el
Entonces, si cambio, en mis preguntas, en lugar de Eliminar / acción / ... a Eliminar / depositar / ... ¿está bien?
Mithir
2
@Mithir Estaba describiendo la regla de contabilidad. En un sistema de contabilidad estándar de doble entrada, nunca se eliminan las transacciones. La historia una vez comprometida se considera inmutable para mantener a la gente honesta. En su caso, aún podría usar una acción ELIMINAR, pero en el back-end (por ejemplo, la tabla de la base de datos del libro mayor) agregaría otra transacción que representa acreditar (es decir, devolver) el dinero al usuario. No soy contador de frijoles (es decir, contador), pero es una de las prácticas estándar que se enseñan en un curso de "Principios de contabilidad I".
Evan Plaice
2
(cont) Los registros de la base de datos utilizan las transacciones de manera similar. Es por eso que es posible replicar y / o reconstruir un conjunto de datos utilizando solo los registros. Siempre que las transacciones se reproduzcan cronológicamente, debería ser posible reconstruir el conjunto de datos desde cualquier punto de su historial. Eliminar la mutabilidad de la ecuación asegura la consistencia.
Evan Plaice
1
Bastante justo, solo cámbiele el nombre a Transacción.
Gary Rowe
1

La razón principal de la existencia de REST es la resistencia frente a errores de red. Para lo cual todas las operaciones deben ser idempotentes .

El enfoque básico parece razonable, pero la forma en que describe la DepositActioncreación no parece ser idempotente, lo que debería solucionarse. Al hacer que el cliente proporcione una identificación única que se utilizará para detectar solicitudes duplicadas. Entonces la creación cambiaría a

PUT /card/{card-id}/account/{account-id}/Deposit/{action-id}
AmountToDeposit=100, different parameters...

Si se realiza otro PUT a la misma URL con el mismo contenido que anteriormente, la respuesta aún debería ser 201 createdsi el contenido es el mismo y un error si el contenido es diferente. Esto permite que el cliente simplemente retransmita la solicitud cuando falla, ya que el cliente no puede saber si la solicitud o la respuesta se perdieron.

Tiene más sentido usar PUT, porque solo escribe el recurso y es idempotente, pero el uso de POST tampoco causaría ningún problema.

Para ver los detalles de la transacción, el cliente tendrá GETla misma URL, es decir

GET /card/{card-id}/account/{account-id}/Deposit/{action-id}

y para deshacerlo, puede BORRARLO. Pero si realmente tiene algo que ver con el dinero, como sugiere la muestra, sugeriría PONERLO con banderas "canceladas" agregadas, en cambio, para la rendición de cuentas (que queda rastro de transacción creada y cancelada).

Ahora debe elegir un método para crear la identificación única. Tienes varias opciones:

  1. Emita un prefijo específico del cliente anteriormente en el intercambio que debe incluirse.
  2. Agregue una solicitud POST especial para obtener una ID única en blanco del servidor. Esta solicitud no tiene que ser idempotente (y no puede, realmente), porque las ID no utilizadas realmente no causan ningún problema.
  3. Simplemente use UUID. Todos los usan y nadie parece tener ningún problema con los basados ​​en MAC ni con los aleatorios.
Jan Hudec
fuente
2
Por lo que sé, POST no es idempotente. en.wikipedia.org/wiki/POST_(HTTP)#Affecting_server_state
Mithir
@Mithir: no se supone que POST sea idempotente; todavía puede ser. Pero es cierto que dado que se supone que todas las operaciones REST son idempotentes, POST básicamente no tiene lugar en REST.
Jan Hudec
1
Estoy confundido ... el contenido que he leído y la implementación existente con la que estoy familiarizado (ServiceStack, API web ASP.NET), todo sugiere que POST tiene un lugar en REST.
Mithir
3
En REST, la idempotencia se asigna al recurso, no al protocolo o sus códigos de respuesta. Por lo tanto, en REST sobre HTTP, los métodos GET, PUT, DELETE, PATCH, etc. se consideran idempotentes, aunque sus códigos de respuesta pueden variar para llamadas posteriores. POST es idempotente en el sentido de que cada llamada crea un nuevo recurso. Vea Fielding's Está bien usar POST .
Gary Rowe el
1
Las operaciones que no son idempotentes están permitidas en reposo. Esa afirmación es totalmente errónea.
Andy