Article

All Code Is Designed — Sometimes Unintentionally

Tyler Johnson

May 4, 2018

The physical act of writing code is easy. It requires nothing more than a keyboard and a text editor. Of course if you want your program to do something, you must understand the most basic elements of the language you’re writing — this is not difficult. Yet software engineering is difficult. I know this because I have written code that I didn’t understand after just a few months. Multiply that by the ten people working on any given code base, and you’re left with a big mess that’s difficult to understand. If you’ve seen Rich Hickey’s talk on simplicity , this is the metaphorical elephant that is your code base. The problem is not that we don’t know how to write code, the problem is that no one ever taught us design.

All artificial things are designed. Whether it is the layout of furniture in a room, the paths through a garden or forest, or the intricacies of an electronic device, some person or group of people had to decide upon the layout, operation, and mechanisms.

Don Norman. “The Design of Everyday Things.”

Everything we interact with on a daily basis has been designed — this includes the code we write and maintain. When something has been designed well, it’s intuitive to work with. We make fewer mistakes as we use the thing effectively to accomplish our goals. On the other hand, when something has been poorly designed, mistakes are all too easy to make. We blame ourselves and often get frustrated. Poorly designed code is frustrating to work with because it is difficult to use correctly; if you want to write high quality code, it’s important to understand how to apply design to code.

Applying Design Concepts to Software

Two of the most important characteristics of good design are discoverability and understanding.

Don Norman. “The Design of Everyday Things.”

In “The Design of Everyday Things”, Don Norman breaks down design into six components. Each of the six components falls under one of two categories: discoverability and understanding. Let’s explore each of the design components by 1) Defining what it is; 2) Looking at a real world example; and 3) Looking at an example in code.

Discoverability

Understanding

Discoverability

Design Component 1: Affordances

“An affordance is a relationship between the properties of an object and the capabilities of the agent that determine just how the object could possibly be used.”

Don Norman. “The Design of Everyday Things.”

In the real world: A piece of glass affords the ability to see through it. A handle on that door affords the ability to pull the door open.

In code: Methods and properties are affordances. They help inform what is possible with the code that we interact with.

Example

Below is a snippet of the interface for UIImageView. The properties and methods available in the interface afford the ability to set a single image, or have the image view animate a collection of images.

class UIImageView : UIView {

/**
Returns an image view initialized with the specified image.
*/
public init(image: UIImage?)

/**
The amount of time it takes to go through one cycle of the images.
*/
var animationDuration: TimeInterval

/**
An array of UIImage objects to use for an animation.
*/
var animationImages: [UIImage]?

/**
The image displayed in the image view.
*/
var image: UIImage?
}

Design Component 2: Signifiers

“The term signifier refers to any mark or sound, any perceivable indicator that communicates appropriate behavior to a person.”

Don Norman. “The Design of Everyday Things.”

In the real world: When a door has a sign that instructs you to “pull”, that is a signifier.

In code: Signifiers come in the form of comments and modifiers. Some examples of signifiers in Swift are struct, class, enum, private, var, and let.

Example

Below is a property from UICollectionView.

/**
The value of this property is an array of NSIndexPath objects, each of which corresponds to a single selected item. If there are no selected items, the value of this property is nil.
*/
var indexPathsForSelectedItems: [IndexPath]? { get }

There are a few signifiers that give us context about how to use this property:

  • var indicates this property is mutable.
  • get indicates this is a read-only property.
  • ? indicates this property can be set to nil.
  • The comment for this property signifies when we can expect this property to be nil.

Design Component 3: Constraints

“Constraints are powerful clues, limiting the set of possible actions”

Don Norman. “The Design of Everyday Things.”

In the real world: At the gas station, the nozzle for pumping diesel gas is slightly bigger than the nozzle for pumping regular gas. This is a constraint that prevents people from accidentally putting diesel gas into a car that takes unleaded fuel.

In code: Dependency injection and strongly typed languages act as constraints.

Example

Below is the initializer for URLSession. URLSessionConfiguration allows you to customize features like the cache, timeouts, and the maximum number of connections per host. You are not required to customize any of these features, but you can’t create an instance of URLSession without providing a configuration instance.

init(configuration: URLSessionConfiguration)

Design Component 4: Mappings

“Mapping is a technical term, borrowed from mathematics, meaning the relationship between the elements of two sets of things.”

Don Norman. “The Design of Everyday Things.”

In the real world: In some cars, the controls to adjust the seat are laid out to look like the seat itself.

In code: Mapping clarifies the intent of our code and, when used properly, reduces the amount of knowledge required to interact with our code.

  • We can map a set of classes to a particular design pattern (i.e. Model-View-Controller).
  • We can map our models to a real world object (i.e. Person).
  • We can map our code to align with a particular algorithm we’re implementing.
Example

Here’s an excerpt from the documentation for HomeKit. It's a good example of mapping a real-world concept into software:

* Homes (HMHome) are the top level container, and represent a structure that a user would generally consider to be a single home. Users might have multiple homes that are far apart, such as a primary home and a vacation home. Or they might have two homes that are close together, but that they consider different homes—for example, a main home and a guest cottage on the same property.

* Rooms (HMRoom) are optional parts of homes, and represent individual rooms in the home. Rooms don't have any physical characteristics-size, location, etc. They’re simply names that are meaningful to the user, such as "living room" or "kitchen". Meaningful room names enable commands like, "Siri, turn on the kitchen lights."

Design Component 5: Feedback

“Poor feedback can be worse than no feedback at all, because it is distracting, uninformative, and in many cases irritating and anxiety-provoking.”

Don Norman. “The Design of Everyday Things.”

In the real world: If you insert a credit card with a chip into a payment terminal at your local retailer, the terminal will eventually beep at you when it’s time to remove your card. That’s feedback.

In code: The most common forms of feedback include returning errors from methods or asserts that crash the code early in the development cycle if certain conditions aren’t met.

Example

If you’ve ever used a UICollectionView or UITableView before, you've likely encountered this error:

Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘attempt to insert row into section 0, but there are only 0 rows in section 0 after the update’

This is a good example of UIKit providing feedback. UIKit is designed to crash your app as soon as possible to let you know that you've made an assumption about the structure of your data that is not compatible with the way a UICollectionView or UITableView works.

Understanding

Design Component 6: Conceptual Models

“A conceptual model is an explanation, usually highly simplified, of how something works.”

Don Norman. “The Design of Everyday Things.”

In the real world: If I asked someone to explain how the postal service works, I could expect to hear many different explanations. If you didn’t know how the postal service works, you might guess that the mailman picks up mail from your address and drives it directly to the recipient. While this model is incorrect, it is still a valid conceptual model. Your conceptual model can change over time as you learn more about the system with which you’re interacting.

In code: Design patterns and algorithms are the main ways conceptual models are applied in code. Naming types according to the design patterns and algorithms we’re using provide hints to other software engineers about the intent of our code.

Every time you interact with an API in code, you start with a set of expectations for how the code will work. This is your conceptual model. As you interact with the code more, you update your conceptual model to reflect your new understanding. For example, passing a nil parameter to a method might cause your code to crash. If that happens, your conceptual model changes to accommodate the idea that nil is not a valid parameter to pass to the method you're interacting with. Notice in this example, when the code crashes (i.e. feedback) we update our conceptual model. Likewise, we use each of the other design components (affordances, signifiers, constraints, mappings, and feedback) to change our conceptual models over time.

Example

`NotificationCenter` in iOS and macOS utilizes the observer pattern.

That is a terse statement that easily conveys a lot of information to anyone who knows what the observer pattern is. Even though the documentation for NotificationCenter doesn’t explicitly state that it uses the observer pattern, the exposed methods provide clues to anyone who has worked with the observer pattern.

/**
Adds an entry to the notification center's dispatch table with an observer and a notification selector, and an optional notification name and sender.
*/
func addObserver(_ observer: Any, selector aSelector: Selector, name aName: NSNotification.Name?, object anObject: Any?)
/**
Removes all entries specifying a given observer from the notification center's dispatch table.
*/
func removeObserver(_ observer: Any)

Design is Not Just for Pretty UI’s

The design concepts in this article don’t definitively tell us what makes good or bad code. Instead, they provide a foundation for understanding what meaning we are providing to others with the code that we write. Every character in our code base can be characterized under one or more of these design components and conveys meaning to the person interacting with the code. Without a solid understanding of design, we may not be conveying the intended message to future maintainers of our code — including ourselves!