Applying design principles to software, Part III

Software design can be beautiful. It can bring joy to your work. Design is about usability. A beautiful object inspires good use. Good design teaches the user to use the object properly. A well designed library will bring joy to its users. It can instil a passion to learn more and improve skills. I love good design.
This is the third article in a series in which I explore how to apply principles of design to writing software libraries. The ideas in these articles were inspired while reading The Design of Everyday Things, a wonderful book by Donald Norman. There is one principal insight I am trying to explore: that the principles of design presented in the book can help us make software libraries that are intuitive to use.
In the last two articles, I explored the ideas of the Conceptual Model and Feedback. The Conceptual Model of the user is his/her understanding of the system. It’s what clues the user into how the system works. He/she builds the model from the System Image and through active experimentation, fueled by feedback. Feedback is when the library gives the user information about the effect of his/her actions.
In this article, I want to discuss the principle of constraints. Constraints limit actions. They can keep the user from breaking the system. Constraints can also guide the user to learn how to do what he/she needs to do. Constraints serve two purposes: preventing errors and helping the user figure out what actions he/she wants to take. Well designed constraints can vastly improve the usability of a library.
Constraints
Half of the ease of use of an object is in the object itself preventing errors, or at least making them undoable. If the object didn’t constrain them, the user would have to constrain him/herself from taking inappropriate actions.

Yes — constraints are a good thing. Well conceived constraints can give the user the freedom to explore. Preventing horrible errors makes it safe for learning by experimentation. It also gives the user confidence that he/she doesn’t have to worry much about unexpected errors happening. Constraints free the user from having to keep track of every possible thing that could go wrong. The system sets up boundaries within which the user can play and learn.
The other half of the ease of use is in helping the user decide how to do what they want to do. It is easier to choose between a small list of options than a large list. By using constraints, you can help whittle down the list of possible actions to just a few — freeing the mind of the user to think of other things. Constraints help the user figure out how to do what he/she wants.
In the design of physical objects, there are three kinds of constraints:
1. Physical constraints
Physical constraints use the physical properties of the object to limit the possible actions. These constraints are the strongest constraints. They absolutely limit the actions, and they are out of the control of the user.
Example:
Square pegs won’t fit in round holes.
Physical constraints might not literally exist in software, but there is an analogue in software. Compiler constraints limit actions before runtime. Lisp checks the number of arguments at compile time. Some languages check the types of the arguments at compile time. These constraints absolutely limit the possible actions the user can take.
Another type of physical constraint is runtime constraints — such as dynamic type checking — that limit actions, though you won’t know until you run your program. Runtime assertions can limit the actions allowed based on the values or types of the arguments, or based on the state of the system.
We’ll keep metaphorically calling these constraints “physical”. Physical constraints should be used in any case where an action should absolutely be avoided.

2. Cultural constraints
Cultural constraints use the cultural meaning attributed to objects to limit the possible actions. Cultural constraints are not as strong as physical constraints, but they are still important. While they don’t stop the user from performing an action, they do guide the user in deciding whether it is appropriate.
Example:
Green means go, red means stop. Coloring buttons this way makes it clear which to push when.
It’s possible to use the meanings embedded in function names to communicate more than just what the function does. It can also communicate when to use it, how to use it, and any context that surrounds it. For instance, the ABORT method on a transaction object implies that no more operations can be performed on it. The word “abort” connotes death, an absolute end. It would be difficult to imagine an operation that an aborted transaction could perform that wasn’t RESTART, RESURRECT, etc. Cultural constraints help the user filter out impossible or inappropriate actions.

Cultural constraints can be used instead of physical constraints or in addition to them. The cultural constraint helps the user figure out in which cases the action should be used. Well-named functions can make it intuitive how to use the library.
3. Logical constraints
Logical constraints limit the actions to what makes sense. Logical constraints are about as strong as cultural constraints. They don’t stop the user, but they can guide the user — especially when the user doesn’t know what to do next.
Example:
A logical constraint that we’re all familiar with is the process of elimination. If there’s only one choice left, it must be the right one.
The thing about programming is that it already relies on logic. Logical constraints form the basis of the whole enterprise. So it’s your job to make those constraints evident and clear. If something doesn’t make perfect sense, explain why — in the doc string or the error messages. The more your system makes sense to the user, the more natural it will feel.

Yes, you’re right. So we’ll need to deal with that.
Preventing and Fixing Errors
Obviously, limiting the number of errors the user makes will make the system feel easier to use.
Let’s talk about two kinds of errors: program errors and usage errors.
Program errors are problems in the program: syntax errors, type errors, wrong number of arguments, etc. Some of these are checked by the compiler, some are checked at runtime. Program errors are usually absolutely clear and can be caught. When a program error occurs, it means the action the user tried to perform did not occur.
Usage errors are when you accidentally do the wrong thing. You delete the wrong file or you store the answer in the wrong variable. Something wrong happens in usage errors, whereas usually nothing happens with program errors. Usage errors can be destructive — something happened, but not what the user wanted.
Program errors are easier to constrain. Syntax is checked by the compiler. Many type errors are checked at runtime, etc. One of your jobs, as a library designer, is to set up those constraints. These can usually be done with a physical constraint to absolutely prevent the action.
Usage errors are harder to constrain. They happen when the user does something he/she could do, but by his/her own reconning shouldn’t do. Imagine the usage error of setting your clock an hour too slow. It’s only a usage error because you don’t want to be late for work. If you were moving to a different time zone, on the other hand, it wouldn’t be an error. The clock can’t know what the user is up to, so it shouldn’t constrain. But that leaves room for errors. If these can be prevented, they are prevented through use of logical and cultural constraints. However, sometimes they are unpreventable. In these cases, the only recourse is to allow the action to be undone.
But let’s look first a how we can limit the damage caused by usage errors as much as possible. One way to limit usage errors is to constrain them with cultural constraints or logical constraints.
Let’s look again at the cultural constraint we talked about before. The ABORT method implies that no more operations can be performed on the transaction object. And let’s assume that it’s still possible to call methods on the transaction even after the ABORT. As a cultural constraint, it does its job pretty well of preventing an error.
Another way to reduce the number of usage errors is to turn them into program errors. Then the system can deal with it by setting up a physical constraint.
Even though the ABORT method is well-named, nothing would stop the user from accidentally calling a subsequent method, causing an error. But if we instead throw an exception when a subsequent method is called, we prevent an error from happening: we’ve created a physical constraint. The cultural constraint is still there — it still guides the user to choose correct actions — but there’s a safety net now.
You can do this to a lot of your functions — throw exceptions as soon as you know they will result in errors. Those exceptions can inform the client code (with appropriate restarts) and the programmer (with informative error messages). We’ll talk about that later.
Finally, errors will happen anyway, no matter how easy it is to use. What you can do in this case is to make them visible and correctable. Provide query functions, as in the last article, to discover the result of the actions. Also provide functions that are complementary to each other. One does, the other undoes, and vice versa. And make it clear that they can be used that way.
For instance, the macros PUSH and POP are complementary. If you POP something, and then decide you shouldn’t have, you can then PUSH it back on, and the state has returned to where it was. No destruction — the error is fixed.
Learning from errors

Program errors are inevitable — no one writes perfect code. But fortunately, they can be learning experiences. The library can teach the user how to fix the error.In the last article we talked about feedback. There’s an important kind of feedback that I didn’t describe in much depth — exception messages. In addition to constraining behavior, exceptions also communicate to the user through their type and through the message they contain. This makes exceptions part of the System Image, and therefore a good way to teach the user how to use the system — how to fix the error and how to prevent it later.
If the error is a result of the user’s misunderstanding, you can explain how the user can fix his/her conceptual model in the message. Take, for example, if the user enters this at the REPL:
CL-USER> (+ "Hello" 8 )
It would be easy to tell the user “Argument of wrong type”.
With that message, the user knows that one of the arguments is of the wrong type, but not which one. We can do better — and the message is usually not this bad.
When I type the above code at my REPL, SBCL throws a TYPE-ERROR condition, with the message The value "Hello" is not of type NUMBER.
Well, putting the type of the condition together with the message, the user can deduce that the function wants a NUMBER, and “Hello” is not one of them. But notice that it does require a bit of a leap. Imagine if you were new to Lisp and didn’t think to look at the type of the condition, only at the message. What would you think? I know I would think “Of course “Hello” is not of type NUMBER, it’s a string. Tell me something I don’t know.” It told me something obvious — it’s nearly a truism that “Hello” is not a number. I have to put the pieces together to figure out why it’s telling me this.
We can do even better than SBCL’s default message. Yes, that message does communicate a lot, but there is a lot more that could be communicated, and in a clearer fashion. What if the function gave this message:
"+ represents the mathematical operation of addition, and therefore only accepts arguments of type NUMBER. The value "Hello" is of type STRING. Perhaps you meant to concatenate two strings. In that case, read the documentation for CONCATENATE by entering (documentation 'concatenate 'function) at the REPL. Perhaps you wanted to add the length of the string to a number. The function LENGTH returns the length of a string."
What does this give me? While it’s still not perfect, I would much rather see this error than the previous one. It tries to teach me when to use + and explain why a STRING value is inappropriate. It also suggests another function that I might have meant instead. It is broadening my understanding of the system. Errors are a great opportunity to teach the user.
Conclusion
Remember, you want to set up boundaries within which the user can play and explore. So what can you do to do that?
Ask yourself these questions:
Does the library limit wrong actions, either through compile-time or runtime checks?
Check the arguments and throw exceptions with helpful messages. Check the type and value of arguments to functions at runtime, as soon as it is clear that they will cause an error.
Does the library let you undo mistakes?
Provide query functions for the state, as described in the feedback article. Also, for every function, provide another function that is the complementary action like PUSH and POP so that when the user determines that the action was an error, he/she can undo it.
Do the names of my functions and macros and their arguments imply how and when they should be used?
Think about the meanings of the names of your functions. The subtle meanings can communicate a lot to the user.

If you’ve been following the articles, you’ve learned some powerful concepts: Conceptual Models, Feedback, and Constraints. You’ve also learned about keeping knowledge out of the head of the user, and developing a good system image. These concepts can help you define the experience of the users of your library. That concludes the design principles article. Perhaps in the future I will expound more on the process of design. Thanks for reading. I hope to hear all about how you’ve used these principles in your own software.
Popularity: 4% [?]
Filed under Programming Practices |2 Responses to “Applying design principles to software, Part III”
Leave a Reply

Is that
turned into a smiley on purpose on your (+ “hello” example?
Thanks, Ryan, I fixed it.