A Case Study In Design Tradeoffs: Usability vs Discoverability
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.
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?
Comments
28 responses