A Case Study In Design Tradeoffs: Usability vs Discoverability

asp.net, code, asp.net mvc 0 comments suggest edit

Usability and Discoverability (also referred to as Learnability) are often confused with one another, but they really are distinct concepts. In Joel Spolsky’s wonderful User Interface Design for Programmers (go read it!), Joel provides an metaphor to highlight the difference.

It takes several weeks to learn how to drive a car. For the first few hours behind the wheel, the average teenager will swerve around like crazy. They will pitch, weave, lurch, and sway. If the car has a stick shift they will stall the engine in the middle of busy intersections in a truly terrifying fashion. \ If you did a usability test of cars, you would be forced to conclude that they are simply unusable.

Scary
Driver

This is a crucial distinction. When you sit somebody down in a typical usability test, you’re really testing how learnable your interface is, not how usable it is. Learnability is important, but it’s not everything. Learnable user interfaces may be extremely cumbersome to experienced users. If you make people walk through a fifteen-step wizard to print, people will be pleased the first time, less pleased the second time, and downright ornery by the fifth time they go through your rigamarole.

Sometimes all you care about is learnability: for example, if you expect to have only occasional users. An information kiosk at a tourist attraction is a good example; almost everybody who uses your interface will use it exactly once, so learnability is much more important than usability.

Rick Osborne in his post, Usability vs Discoverability, also covers this distinction, while Scott Berkun points out in his post on The Myth of Discoverability that you can’t have everything be discoverable.

These are all exmaples of the principle that there is no such thing as a perfect design. Design always consists of trade-offs.

Let’s look at an example using a specific feature of ASP.NET Routing that illustrates this trade-off. One of the things you can do with routes is specify constraints for the various URL parameters via the Constraints property of the Route class.

The type of this property is RouteValueDictionary which contains string keys mapped to object values. Note that by having the values of this dictionary be of type object, the value type isn’t very descriptive of what the value should be. This hurts learnability, but let’s dig into why we did it this way.

One of the ways you can specify the value of a constraint is via a regular expression string like so:

Route route = new Route("{foo}/{bar}", new MyRouteHandler());
route.Constraints = 
  new RouteValueDictionary {{"foo", "abc.*"}, {"bar", "\w{4}"}};
RouteTable.Routes.Add(route);

This route specifies that the foo segment of the URL must start with “abc” and that the bar segment must be four characters long. Pretty dumb, yeah, but it’s just an example to get the point across.

We figure that in 99.9% of the cases, developers will use regular expression constraints. However, there are several cases we identified in which a regular expression string isn’t really appropriate, such as constraining the HTTP Method. We could have hard coded the special case, which we originally did, but decided to make this extensible because more cases started cropping up that were difficult to handle. This is when we introduced the IRouteConstraint interface.

At this point, we had a decision to make. We could have changed the the type of the Constraints property to something where the values are of type IRouteConstraint rather than object in order to aid discoverability. Doing this would require that we then implement and include a RegexConstraint along with an HttpMethodConstraint.

Thus the above code would look like:

Route route = new Route("{foo}/{bar}", new MyRouteHandler());
route.Constraints = 
  new RouteConstraintDictionary {{"foo", new RegexConstraint("abc.*")}, 
    {"bar", new RegexConstraint("\w{4}")}};
RouteTable.Routes.Add(route);

That’s definitely more discoverable, but at the cost of usability in the general case (note that I didn’t even include other properties of a route you would typically configure). For most users, who stick to simple regular expression constraints, we’ve just made the API more cumbersome to use.

It would’ve been really cool if we could monkey patch an implicit conversion from string to RegexConstraint as that would have made this much more usable. Unfortunately, that’s not an option.

So we made the call to favor usability in this one case at the expense of discoverability, and added the bit of hidden magic that if the value of an item in the constraints dictionary is a string, we treat it as a regular expression. But if the value is an instance of a type that implements IRouteConstraint, we’d call the Match method on it.

It’s not quite as discoverable the first time, but after you do it once, you’ll never forget it and it’s much easier to use every other time you use it.

Making Routing with MVC More Usable

Keep in mind that Routing is a separate feature from ASP.NET MVC. So what I’ve covered applies specifically to Routing.

When we looked at how Routing was used in MVC, we realized we had room for improving the usability. Pretty much every time you define a route, the route handler you’ll use is MvcRouteHandler it was odd to require users to always specify that for every route. Not only that, but once you got used to routing, you’d like a shorthand for defining defaults and constraints without having to go through the full collection initializer syntax for RouteValueDictionary.

This is when we created the set of MapRoute extension methods specific to ASP.NET MVC to provide a façade for defining routes. Note that if you prefer the more explicit approach, we did not remove the RouteCollection’s Add method. We merely layered on the MapRoute extensions to RouteCollection to make defining routes simpler. Again, a trade-off in that the arguments to the MapRoute methods are not as discoverable as using the explicit approach, but they are usable once you understand how they work.

Addressing Criticisms

We spent a lot of time thinking about these design decisions and trade-offs, but it goes without saying that it will invite criticisms. Fortunately, part of my job description is to have a thick skin. ;)

In part, by favoring usability in this case, we’ve added a bit of friction for those who are just starting out with ASP.NET MVC, just like in Joel’s example of the teenager learning to drive. However, after multiple uses, it becomes second nature, which to me signifies that it is usable. Rather than a flaw in our API, I see this more as a deficiency in our documentation and Intellisense support, but we’re working on that. This is an intentional trade-off we made based on feedback from people building multiple applications.

But I understand it won’t please everyone. What would be interesting for me to hear is whether these usability enhancements work. After you struggle to define constraints the first time, was it a breeze the next time and the time after that, especially when compared to the alternative?

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

28 responses

  1. Avatar for Robb Allen
    Robb Allen November 6th, 2008

    I am so going to steal that picture.

  2. Avatar for Torkel
    Torkel November 6th, 2008

    Why not provide something like a fluent interface, that would create a more usable & discoverable API:
    www.codinginstinct.com/.../...e-in-mvccontrib.html

  3. Avatar for Simone Chiaretta
    Simone Chiaretta November 6th, 2008

    Seems like part of your job for the next few months will be explaining each design decision you made on the framework, as someone will always question the decision you did :)
    Don't get me wrong, it's good to have critics, as they help improve the product, but sometimes someone is just too critic :)

  4. Avatar for josh
    josh November 6th, 2008

    Let's caption the picture. I''l start..
    Harold and Kumar go to Wayne's World
    (that might only make sense to me though)

  5. Avatar for Liam McLennan
    Liam McLennan November 6th, 2008

    "we’ve added a bit of friction for those who are just starting out and have trouble using Google"
    Apart from being slightly insulting towards a respeced member of the .NET community I think this statement is incorrect. I have found Google nearly useless when it comes to Asp.Net Mvc. There is very little information out there and most of tends to be out of date. I have better luck directly searching Scott Gu / Haacked / Hanselman. I look forward to the official release so that all the books will finally ship.

  6. Avatar for Thomas Eyde
    Thomas Eyde November 6th, 2008

    A dumb question, couldn't you solve this with overloading?
    This also remind me of the CommandArgument property, which is a string, while the event handler providing the actual value defines it as an object. It can never be anything but a string, yet we have to downcast it every time. No one bothered to explain this decision as far as I know.

  7. Avatar for haacked
    haacked November 6th, 2008

    @Liam it was a joke. I've known Ayende for a long time now and he probably has a thicker skin than I do. If *he* finds it insulting, I will retract it immediately, because I like the guy. Apparently a subsequent commenter on his blog post found the answer via Google.
    @Thomas Not really. Unless we broke up the constraints into two dictionaries, one that is string, string, and one that is string, IRouteConstraint. Is that what you mean?

  8. Avatar for haacked
    haacked November 6th, 2008

    @Torkel looking at that page, I see this:
    .WithConstraints(new { id="^[0-9]+$" })
    Isn't the argument to "WithConstraints" an object? That's exactly the discoverability problem that Ayende talked about in his blog post, which I addressed in this post.

  9. Avatar for Ayende Rahien
    Ayende Rahien November 6th, 2008

    I didn't get the joke, I am afraid.
    As for the API discoverability issue.
    Change IRouteConstraint to AbstractRouteConstraint (you like to do that anyway) and define an implicit convertion operator from string to AbstractRouteConstraint.
    That will let you define constraints as an enumerable AbstractRouteConstraint. so you can write it like:
    new AbstractConstraint[] { @"\d4", new MyCustomConstraint()}

  10. Avatar for Erik van Brakel
    Erik van Brakel November 6th, 2008

    @ Ayende:
    That's what I was thinking as well. I guess the API now is a result of thinking in interfaces too much, while neglecting the fact that using an abstract base class still IS a valid option in a lot of cases?

  11. Avatar for Ryan E
    Ryan E November 6th, 2008

    Christ, when did "ease of development" in the form of terseness outweigh code clarity and intent. I'd much prefer fluent interfaces or even your second code snippet.
    Your code would never make it to production in my world. I have developers of all skill levels that need to maintain my codebase, and I ensure you I'd waste resources as they tried to figure out what your code was doing.
    If I have to use Google to understand your intent, you fail.

  12. Avatar for Neil Mosafi
    Neil Mosafi November 6th, 2008

    Firstly, why is constraints not a read only property? Is there a semantic difference between constraints being null or an empty collection?
    Secondly, I don't get why you can't use overloads - one which takes an IRouteConstraint and one which takes a string. In the string version you can convert the string to an IRouteConstraint and call the other overload.

  13. Avatar for haacked
    haacked November 6th, 2008

    @Ayende I sent you a personal email regarding the joke.
    After all the grief I've received for defending occasional uses of Abstract Base Classes, you're suggestion is to add one! :P
    Kidding aside, we've already shipped RouteValueDictionary and IRouteConstraint as part of the Framework, so we obviously can't get rid of those or change them. However, I do like the idea of adding AbstractRouteConstraint and perhaps a strongly typed overload for MapRoute.
    The only problem is that really belongs in the core framework and I can't touch that right now.
    Let me noodle on this a bit.

  14. Avatar for Andrei Rînea
    Andrei Rînea November 6th, 2008

    Offtopic as usual :P but who's on the passenger seat? It somehow resembles Jeff Atwood (that would be titled "Driving Horror" :P ), that's why I'm asking.

  15. Avatar for haacked
    haacked November 6th, 2008

    @Andrei that's my younger brother. :)

  16. Avatar for Rob Conery
    Rob Conery November 6th, 2008

    I'm going to guess that was Phil's brother on their roadtrip a few years back... am I right Phil?
    Ayende I have a hard time thinking you're offended by anything :) and I mean that in a nice way.
    Phil maybe it's time to invent the AbstractInterface and use it for everything.

  17. Avatar for haacked
    haacked November 6th, 2008

    @Rob, that's right. Here's the photo set on Flickr.

    As for the insult, Ayende and I smoothed things over via private email and it's all good.
    To make amends, next time he sees me, he's going to shoot me in the foot. But only the left foot, not my good foot. ;)

  18. Avatar for Brannon
    Brannon November 6th, 2008

    I don't understand what all the fuss is about. It took me a few seconds and one Google query the first time I needed to add a constraint. I agree with Phil in that 99% of the time I'm going to want a simple regex constraint, but I appreciate the ability to add a more complex one if necessary.
    If intellisense had shown me I needed to supply an IRouteConstraint instance, it would have taken just as long to Google which classes implemented it, or worse I might have started implementing a RegexRouteConstraint myself!
    Also, how would an implicit string conversion to an abstract class be any more discoverable? Everyone would just use the abstract class directly, defeating the ease of use.
    Ultimately the problem is lack of documentation, which is understandable considering the code just made beta.

  19. Avatar for Torkel Ödegaard
    Torkel Ödegaard November 6th, 2008

    hm.. stupid me, my fluent interface for routing does not solve the discoverability aspect of constraints. But one could fix that by extending it with:
    MvcRoute
    .MappUrl("questions/{id}/{urlName}")
    .WithRegexConstraint("id", "^[0-9]+$")
    .WithRouteConstraint("urlName", new MyCustomRouteConstraint())
    .ToDefaultAction<questionscontroller>(x => x.ViewQuestion(0))
    .AddWithName("QuestionsById", routes);

  20. Avatar for Rob S
    Rob S November 6th, 2008

    I think Ryan E. nails it. I'd also question whether applying a user interface guideline is appropriate for an application programming interface.

  21. Avatar for meisinger
    meisinger November 6th, 2008

    you have to be kidding me... right?
    how many routes are you guys building?

  22. Avatar for glompix
    glompix November 6th, 2008

    I think the implementation as-is is absolutely perfect. It's terse and understandable. You can just look at a route and understand "oh, this state/province parameter only accepts 2 letters."
    I've built three MVC apps so far, (2 of them in production, one still in early development) and have maybe had to use 2 or 3 constraints, tops. Even then, I think Hanselman's videos explained all of this a long, long time ago. It seemed completely intuitive to me, given that anonymous objects are used to define default values as well.
    Great job on this routing stuff, Haack. The only qualm I have really is with IIS6, but that really has nothing to do with MVC itself.

  23. Avatar for dhasenan
    dhasenan November 9th, 2008

    meisinger: It's rare that you will add a route constraint. It's okay, then, if you need to write more code than you would like, as long as it's obvious how to do this. Either well documented or using static typing as a hint.

  24. Avatar for Jeff Atwood
    Jeff Atwood November 10th, 2008

    This is the best photo in any blog entry, ever!

  25. Avatar for haacked
    haacked November 10th, 2008

    @atwood Tell Joel that he should use it in the next edition of his usability book. :)

  26. Avatar for LD
    LD November 12th, 2008

    If I have a bookDetails.aspx page and I use MVC Routing to make the url friendly. But how can I construct the web.sitemap file to let my sitemappath control knows it the details page of xxx book and shows the boos's title at the end of the path

  27. Avatar for Dave
    Dave December 9th, 2008

    I understand that ASP.NET MVC is not ready for prime-time, but as a developer just getting into the framework now (beta), I have to express my disappointment that you're going forward with the anonymous-classes-née-JavaScript-dictionaries strategy.
    Let me explain, I've done my fair share of JavaScript development using script to do the heavy-lifting in UI view developement. It has a place there. Clientside Web UI code tends to be tightly bound in a works-just-here-and-dont-mind-the-magic kind of way.
    MVC and Routing are not the same. You're building this technology in an environment where companies pay real money for developers to use Visual Studio and it's spectacular Intellisense capabilities. The ability to "type three characters and press up, up, up, down, enter" is an astounding productivity boost. However, in MapRoutes(), I'm lost without spending my afternoon searching StackOverflow, your blog, other people's blogs about older preview versions, source code, and Red Gate's Reflector before I can understand sorta-kinda what I'm supposed to pass and how it will interact with other routes for an even slightly complex case (e.g., "/foo/1/bar/2/baz").
    I hope that you seriously reconsider this dynamic-language-work-alike hack (in the clever hacker sense of the word) before releasing publicly. Yes, it's a neat use of the language. Yes, one-liner's are sweet. But, No, I won't type less than in a designed-for-Intellisense API. And, No, it's not easy to grok. And for goodness sakes, please consider the affects of this coding style on the wider community of developers.
    I'm sure you've studied how C# for each loops are implemented? With a brute force duck typing sort of approach? Back in 2007 [1], Krzysztof Cwalina was expressing a desire for a notation to support the concept. How about instead of dumping this onto the general C#/VB.NET web development community as a legitimate technique ("but Microsoft releases supported APIs that do it this way!"), you work with the Framework Guidelines team to get a verifiable notation down.
    I know it'd be C# 4.0, I can wait.
    [1] blogs.msdn.com/.../DuckNotation.aspx

  28. Avatar for meglio
    meglio August 22nd, 2014

    Now compare to routing in dynamic languages (e.g. PHP):

    route('GET|POST /dashboard', 'some\namespace\Class::method');

    Intuitive, learnable, discoverable, short, easy to read.