CRUD is not the only user interaction metaphor, thank you
Update: This post takes some liberties with the context and intent of the quotes within it. Most of this was unintentional, but all of it was thoughtless of me; they deserve better. The post itself is not all that interesting, but the comments within have some really awesome back-and-forth over the semantics of web applications.
Update 2: Evan Weaver has written a post which makes a much better set of points than I have, but he agrees with me, so I’ll pretend like I had it all worked out ahead of time but just didn’t feel like typing it all up. ;-)
With that in mind…
There’s been a huge push lately to integrate RESTful web services into the core of Rails, and I think that’s awesome. Anything that advances the cause of light-weight, human-readable web services strikes me as a good idea. The Rails approach is a good one, not just because it has characteristically beautiful code, but also because it looks fantastically easy to work with.
That said, I think some people are taking it a little too far, like this article on habtm, “if your models aren’t namespaced, why should your controllers be?”:
The New CRUD is about using your controllers to expose your models and various other things as objects. If your models aren’t namespaced, why should your controllers be?
With this new approach, you start thinking about how to move your controllers over to a flat namespace and still have your separate backend layouts for editing actions.
First, the reason that your models are not modularized is because the unit testing framework dies a horrible death when you use modularized models, not because it’s a design decision that makes any sense. Thus the flat namespace for Rails models is not a solid base from which to extrapolate the design of the controllers.
Second, namespaced controllers fit just fine with the latest CRUD push by rails-core. Rails 1.1.3 breaks the default routing for modularized controllers, but that’s fixed for 1.1.4. A bug in a revision release shouldn’t determine your architecture plans, and in the meantime you can work around this issue by either freezing Rails to the stable branch or adding explicit routes to your modularized controllers:
map.connect 'admin/users/:action/:id', :controller => 'admin/users'
Finally, and most importantly: CRUD is not the only user interaction metaphor. You should choose an interface (and thus, a controller’s structure) based on what your users’ goals are, not what your system metaphor ideology dictates.
When people use modularized controllers, it’s usually because they realize that they have two or more groups of people using their application who have overlapping yet mutually exclusive needs. For an online store, the information returned by a Read action for customers will be different than that of an action for store employees. To try to shoehorn these two needs into the same interface is bad design, plain and simple.
A flat namespace for your controllers only makes sense when your users are all doing roughly the same thing. That’s not always the case, and to overlook that possibility in order to stuff your application domain into some cool new paradigm is not responsible software development. Don’t take this to mean I don’t like RESTful systems–I do–or that I think the latest CRUD push in Rails is bad–I think it’s awesome–but the idea that CRUD is the one, true pattern of user interaction… well, that just seems a bit silly.
Speaking of silly, search does not work with a CRUD metaphor, despite what people may think:
Instead, consider using a Search model. Create a search resource that holds all the options for the query. Then you can execute it whenever you want. POST /searches (post data contains query options) would redirect to GET /searches/42 or GET /people/?searches=42. You could even make it so that searches are unbound until execution, like blocks: GET /searches/42?on=people.
*sigh*
Why is this a good idea? So a single HTTP transaction can be replaced with two? Because the POST verb just doesn’t get enough love these days? A search session is a Read operation, end of story. Instead of saying “give me the record whose ID is 4,” you’re saying “give me the records which have ‘frankenberry’ in the description.” Not, “create a search session with these parameters, then read it and the magic results you have appended to the end of it without my say-so, then finally destroy it.”
So why would people want to do this?
Now, I just do a quickie insert of the pertinent info into our DB, then apply the search to the engine (totally different middleware). Boom! Instant ’search history’. To find out why someone did something ‘downstream’ in the app, looking up their most recent search is trivial.
Shorter version: “I confuse searching with logging.”
Solution: Rename your model LoggedSearch.
Consider this: have you ever wanted to:
- Know what your users are searching for?
- Allow your users to save their search criteria for later use?
- Define a “google alert” type feature in your app?
If you wanted to do any of these, having a search model object would save your bacon. Imagine having an Alert object that has a 1-many relationship with your Search object.
Shorter version: “I confuse searching with logging, preferences, and alerts.”
Solution: Make LoggedSearch, SearchSetting, and SearchAlert models.
This is the kind of crap which buried XML-RPC, folks: overengineering a solution because you’ve got a shiny, new hammer and the world is your nail. The first thing one should do when modeling an application domain is to figure out what people actually want to do with it. If that lines up with CRUD, awesome. If not, don’t force it. It just adds complexity where none is needed, which is exactly the thing RESTful web services were designed to avoid.
Moral of the story? No single metaphor fully expresses the richness of human-object interaction, and any totalist ideology will result in a poorer user experience. CRUD is a solution, not the solution.
37 comments »