I'm embarking on a new web app we want do do it RESTfully. Now it is time to begin designing the interactions, and something sorta basic about REST has me stumped. I am trying to figure out the best way to mediate the impedance mismatch between REST and OO without falling down the slippery slope of RPC. Let me give a (contrived) example.
Widgets can be created, modified, and then submitted for review.
To modify a widget with the id of 123, the user does a PUT to /myapp/widget/123 and the new form data. The server repackages all the form data as a POJO and hands it off to the logic layer for validation and subsequent persistence, invoking WidgetManager.update(widgetPojo).
To submit a widget for review, the user clicks a button, which also does a PUT to /myapp/widget/123, but now the form data just has has one field, a status of "submitted" (I don't send all the form data again, just the field I want to change). However, now the server needs to invoke a different business object, WidgetStateManager.updateState(123, "submitted"), which is going to do some other specialized processing in addition to updating the state.
So, in an attempt to be RESTful, I've modeled both the widget updates and the submit for review action as PUTs to the same URL, /myapp/widget/123. So now, in my server side code, I need to figure out what a particular PUT request means in terms of the business functions, and therefore which business function(s) to invoke.
But how can I reliably determine which function to invoke merely by inspecting the values in the form data? It is SOOO tempting to pass an "action" field along with the form data, with a value like "update" or "submit for review" in the PUT! Then the server could do a switch based on that value. But that of course is not RESTful and is nothing more than dressed up RPC.
It just doesn't seem safe or scalable to infer what button was clicked just by examining the form data with a bunch of if-then-e开发者_如何学Clses in the restlet. I can imagine dozens of different actions that could be taken on a widget, and therefore dozens of if-then-elses. What am I missing here? My gut tells me I haven't modeled my resources correctly, or I'm missing a particular resource abstraction that would help.
You aren't limited to mapping URLs to domain objects. A RESTful API has a small number of actions but a large number of resources to which the actions may be applied.
Create a widget:
POST to /rest/widget (returns "123")
("The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line.")
Validate widget 123:
POST to /restapi/validator/123
(The resource is a notional "validator" for widget 123.)
Update widget 123:
PUT to /restapi/widget/123
Submit widget 123 for review:
POST to /restapi/reviewqueue
(There is only one review queue, so no need for /123.)
Delete a widget:
DELETE to /restapi/widget/123
A few points;
The domain of the service interface != the implementation domain
The resources exposed by your service do not have to be directly implemented as objects in your code. The (service) interface is not the implementation.
Put & Post require all of the external state
You must provide all of the resource state when doing your updates (you said that you only provide the changes - that's a PATCH not a PUT).
Modelling state changes via collection resources
Sometimes it's best to model a state change as a collection resource. In your example, you really need to consider either a 'Review' resource with an associated 'Review-queue' or add a 'Needs review' attribute to the widget.
Approach 1. A container for the 'review-queue'
Having the 'Review' object would make it easy to list widgets for review, assign resources for review etc
GET /review-queue -- to list the widgets which need review
POST /review-queue -- create a new review entry (listing just the id, name of the widget and a url back to the widgets)
DELETE /review-queue/X -- delete from the queue when the widget has been reviewed
I'd use this approach if there was significant behaviour associated with the 'review-queue' e.g. permissions associated with adding a widget for review, multiple review queues etc
Approach 2. 'Needs-review' attribute
You may decide that a separate resource is over-kill for your needs. You can model the basic functionality with POST, PUT and GET.
When a widget is created, it's state include a s 'needs-review' attribute which is set to False. Obviously you need all of the external state in the POST
When a widget needs review, GET it and PUT it back with the 'needs-review' updated. Again, you need all of the external state in the PUT
When listing the widgets for review use
GET /widgets/?needs_review=true
Poor old RPC
You mention RPC in your last paragraph and although it's off-topic, I can't help but comment...
I think perhaps we're all guilty now of blaming RPC for the ills of the world. The real problem with RPC is that it aimed to make remote function calls transparent to the programmer, hiding failure scenarios and attempting to make a remote call equivalent in the implementation language as a standard function call. As an old CORBA (which suffered from the same problem) programmer, I can appreciate how REST corrects that failing.
Other points from your post
You can't determine which method to call without examining the new state and comparing it to the existing state.
You should validate the new state before doing anything else, passing any errors back to the submitter.
From you're last paragraph, I think you know this already - sorry.
Chris
It's hard to say without knowing more about the domain, but having a "to be reviewed" collection resource might be a good idea. As your resources flow through the process, you can move them from one list to the next. As a side benefit, you also get the option to do a GET on these lists to find out which resources are in that particular state.
精彩评论