Don Norman’s "The Design of Everyday Things" is a classic design book, focused on the design of physical objects. Many of the principles that he discusses are also applicable to non-physical objects - like Swift! In this talk from try! Swift, Rachel Bobbins goes over some of these principles and how can they can be applied towards writing well-factored, easy-to-read Swift code.
The Design Of Everyday Things, by Don Norman (0:00)
My name is Rachel Bobbins and I am an iOS engineer at Stitch Fix in San Francisco. A few months ago, I went on vacation and I decided to reread a book, Don Norman’s “The Design of Everyday Things”. This book was influential in shaping how I think about applications and building delightful experiences for users: it is all about combining ideas from design, psychology and cognitive science to build a users-first focus when creating objects and experiences. The first version of the book was about physical objects; they recently updated to include digital interface design. I chose to reread this book while I was on vacation because I did not want to think about coding for a week, but as I was reading, I kept thinking about all the connections I could make between concepts in this book and my work as an iOS developer.
The Design Of Everyday Swift (2:15)
As an iOS developer, I felt I could learn from the ideas in this book. We want to do the right things for our users. We want them to be delighted when they interact with our app and with our code. As highly technical people, we want to write code that is readable, clean, easy to understand, and easy to augment.
Often, we look to technical resources for new ideas about how to do this. Applying the ideas of design thinking, though, we can learn to think more creatively about how we write and structure code. We can utilize design to achieve our goals of rendering our code as clean, readable, and testable as possible.
Who are our users? This book is known for raising the concept of user-focused design. However, if we are going to focus on users, we need to make sure we all have the same definition about who our users are.
As developers, when we talk about users, we think of:
- The people who download our apps from the App Store and use them on their phone.
- If we develop enterprise apps, our users are not necessarily the people who have downloaded them: they are the people who are required to use them. Those users are the people who are interacting with the interfaces and experiences that we have built; but, for the purposes of this talk, those people are not our users.
- As Swift developers, our users are also the people who use the code that we have written, perhaps other developers currently on our team. They are our future teammates, and we need to give those people as much thought and consideration as we give the people who download our apps from the App Store. We want to make sure that other developers and other people who are dealing with this code base have a delightful experience. They have a different set of goals, needs, and concerns from the people who download our apps from the App Store.
Let’s Build This! (5:03)
Here’s a quick psychology lesson based on some of the ideas in this book (it looks like a programming lesson, but do not let yourself be deceived).
I want to build a simple iOS application that has only one ViewController with no interaction. It will only have the word “Welcome” in both English and Japanese.
To get started, we hone in on our goal. We know that we want to have two lines of text: English, first; Japanese, second. We want the text to be centered horizontally and vertically on the screen. We want to use auto-layout: as the screen rotates or as the app is run on different devices, the words behave correctly.
Now, we can get specific. We want to use programmatic auto-layout to build this. In fact, we want to use the NSLayout Anchors that were introduced in iOS 9 (instead of the visual programming language). We also want to use localization.
With these specific goals in mind, iOS developers know how to build this screen. We write the code, which is a short ViewController (about 20 lines total). We think that when we press the play button, we will see the app build and run as expected.
Something goes wrong: we see warnings on the console. We can interpret these warnings, and they may tell us useful information. We compare those warnings with what we know about iOS development, and how the application looks. We know that we have not achieved our original goal, hence we set a new goal. Our new goal is about correcting the code that we have already written, not about building an app from scratch.
With this new goal, we go back into Xcode. I have an idea of how to fix the problem, since I realize I had a line that defaults to
true, which I had set to
false. We rebuild the app, and now it works, our problem is solved, and we get what we expected.
7 Stages of Action (8:18)
In that example, we went through what The Design of Everyday Things identifies as the “Seven Stages of Action”:
Actually, we went through the steps twice: the first time, I was explicit and ran through each of them purposefully. The second time was during the debugging process, in which I went through the steps quickly (and mostly subconsciously).
Going through these seven steps subconsciously is something that developers do thousands of times per day. However, as people who care about those who will continue to work on our code base in the future, we want to make it as easy as possible to go through these seven stages. We want to give clues that will help ease someone through these stages as they debug cryptic code, track down tricky bugs, and build new features.
7 Principles Of Design (9:26)
In The Design of Everyday Things, Norman also identified Seven Principles of Design, which correspond to the Seven Stages of Action:
- Conceptual model
These principles can be used to write cleaner, easier-to-read code. These principles are not mutually exclusive. For example, the feedback that you give to a user can affect the user’s conceptual model of a system. Alternatively, the presence or lack of signifiers can affect the discoverability of properties of the code base. As you write code, you should think about how these seven principles interact with each other.
In The Design of Everyday Things, Norman draws all of his examples from physical objects, e.g. teapots, doors or light switches. I tried to draw some physical examples from the interfaces that we have on apps, and then translate those ideas about interfaces to code and to programming.
On the Display and Brightness screen on the Settings app, I intuitively know which actions are available. I look at the screen, and I know that I can change properties related to brightness and how text is displayed on the screen. These actions are discoverable to me; I don’t need to think much about how to do them.
In the hypothetical app we wrote, there was a bit of an issue with discoverability. We did not know that a flag had to be set to false until we ran the app. We wish we had discovered sooner that this was a required setting.
For users who are current and future developers on our iOS applications, it is important to be able to figure out what features and properties exist in a system. Therefore, as Swift developers who are building code bases today, it is our responsibility to make sure that important properties of our code base are easily discoverable to these future developers.
As we write Swift, there is a few ways to make sure that important aspects of our systems are discoverable:
- We can make liberal use of the
internaldesignations for methods and properties: obvious way to indicate to someone what should and should not be discoverable.
- Tests: developers can read tests to learn about interactions between objects and about the expectations that one object might have for another’s behavior. Without tests, developers have to dig through your code to try to understand what properties might exist, including the ones that are not immediately obvious.
- Use sensible method and variable names, especially ones that intuitively trigger autocomplete. Out of the 30+ methods in the UITableView data source and delegate protocols, only two of them do not start with the phrase TableView. Every time I need to add more than one section to a UITableView, I find myself frustratingly scrolling through autocomplete, wondering what the name of that method is.
- Comments and documentation can help developers discover properties of your code base that might not be immediately obvious. For example, linking to a Stack Overflow to explain a workaround, or adding a comment about a build process or a tricky dependency. These are properties of your code base and its system that would not be obvious through code because they are not coding-related explicitly. But they are important to discover to developers who have to use your code base.
When I open the language settings on my iPhone and toggle it between English and Japanese, I get an alert asking me to confirm my choice. As a user who speaks no Japanese, I appreciate this alert because it prevents me from accidentally switching to Japanese. This has the added benefit of giving me positive feedback to let me know that the iPhone received my tap and is responding to it.
Feedback is about providing information to users about whether their actions have succeeded or failed. We should try to give developers as much feedback as possible while they interact with our code bases. Positive feedback tells a user that everything is okay. Negative feedback tells a user that something is not okay. The worst feedback is no feedback: a developer has to make their own assumption about whether things went right or wrong.
As developers, we should try to give automated feedback to the future developers of our code base:
- Compiler errors and warnings: they are an immediate red flag in Xcode that something is incorrect. It allows a developer to react and adjust their course accordingly.
- Test failures. When they all pass, they let the developer know that they have not broken anything that we expected to be working. When the test fails, it is a form of negative feedback: you have broken something.
- Runtime crashes are not a very useful type of feedback because we have to remember what conditions and what steps we went through in order to trigger that crash in the first place. Runtime crashes will only get triggered if we go through the right steps to trigger them.
There is also human feedback we can give, such as pair programming, code reviews,; bug reports, and App Store reviews. Pair programming is great in terms of getting feedback, but we do not have time machines - we cannot go pair with the future users of our code base.
Conceptual Model (16:52)
My favorite illustration of a conceptual model is when Steve Jobs introduced the iPhone in 2007. At the time, smartphones were not very popular but Steve introduced the iPhone in terms of something that people already knew. He told everyone that he was introducing three new devices that day: a widescreen iPod, a cellular phone and, in his words, an “Internet communicator device”. He repeated those three phrases over and over again and finally asking people: “Do you get it yet? It is all one device”.
It’s about explaining a new concept in terms that people already understand. Developers often think about code too logically (this function works, it returns the right thing). We do not think about the fact that other developers, the ones who are using this code base in the future, are going to form their own conceptual models about how our system works.
People’s conceptual models are influenced by things within our control (e.g. feedback and signifiers and discoverability), but also by things outside of our control (e.g. somebody’s background, their travels, their cultural experiences, etc.).
Affordances are relationships between users and objects, which allow people to take actions with those objects. On the Display and Brightness screen, I am afforded the ability to take action to make the screen more readable. Affordances were emphasized in the 1980’s version of Don Norman’s book, but he emphasized them less in the 2013 revised edition because digital interfaces tend to have fewer affordances than physical objects.
They are still applicable for our purposes as developers, though. We can think of the fact that methods afford calling. Variables afford getting and setting. Declaring
var variables also afford resetting. As developers, we can think about what abilities and what actions our code is affording the future developers of this code base. If we do not want them to reset a variable, for example, we should not give them an affordance to do that. We should declare it with a
Signifiers are perceptible signals about what can be done. On a brightness slider, I see at least three signifiers: 1) two icons, which signify the relative minimum and maximum values at each end of the slider; 2) it is a slider, and to me that signifies that this is a continuous spectrum, not a set of discrete values; 3) the current position of the slider, which signifies the relative value of brightness as it currently is set.
Swift is full of signifiers, which we can use to send messages to the future users of our code base.
internal are obvious signifiers about where a method is intended to be used, but others are less obvious.
For data structures, we have
class. I see
enums as a way of signifying that we have a known set of values that are roughly equivalent to each other.
struct is something you can pass around by value, copy it all over the place and it is fine. A
class signals that this is something that is passed by reference - the fact that it is passed by reference, I assume that it is important. If it is not important, then maybe a
class is the wrong data structure to be using.
Every choice you make and every line of code you write is some type of signal to future developers. We should try as hard as we can to make sure that we are sending the right signals.
Mappings are spatial and temporal relationships between controls and actions. In my company’s iOS app, we have a button to “continue check out” which has an arrow that points to the right. This takes advantage of the fact that time moves “forward” (at least for Westerners), and our conception is that the next thing is always to the right.
Be aware that mappings are culturally dependent. Think carefully, as you lay things out spatially, what those mappings might signify.
Think of how your
.xcodeproj file is organized. The way you group your files and how they relate to each other signifies something to the user, and it’s important to make those mappings make sense.
Constraints are about guiding and restricting actions. In the real world, for example, scissors have natural constraints that help you know how to use them. We are guided towards putting our thumb in the smaller hole, and the rest of the hand on the other part of the scissor. There is only one way to use a scissor.
The best example of constraints in Swift is the type system. If you try to do anything that goes against the type system, you will get a compiler warning (i.e. immediate negative feedback). The type system provides information about how methods can be called, and with what types of arguments. Swift’s type system forbids mistakes that were previously possible in Objective-C.
To recap, as developers, we want to write clean, readable and maintainable code. Too often, we only look to other developers and the software engineering discipline about how we can accomplish that. Design thinking can give us a fresh perspective and help us to think more creatively about how we write code.
Maybe you are not that interested in design theory, but look to your others fields and passions. I am sure there are philosophies, principles and ideas that you can apply from your other interests to software engineering. By cross pollinating your development practice with ideas from other fields, you will be able to be a more creative and more effective developer.