PurelyFunctional.tv Newsletter 413: Affordances in software

Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.

Issue 413 - February 09, 2021 · Archives · Subscribe

Design Tip 💡

Affordances in software

In the last issue, we talked about affordances, those features of a designed object that both allow for manipulation and communicate such to the user. In general, you want the affordance's usage to align with what it is communicating. For instance, if your UI button doesn't look clickable (even if it is), the user won't know they have the option to click it. That button is not an appropriate affordance. Likewise, a handle on a door communicates "pull me". If it is on the push side of the door, it's not a good affordance.

I also teased last week that we might do well to think of affordances when designing our abstractions, not just the UI. In this issue, I'd like to look at what affordances we would want in coffee shop software.

Who is going to need to use this coffee order abstraction, and for what? That will give us a list of use cases. Here is my list:

  • the barista will need to enter it on their cash register
  • the barista will need to follow it to build the ordered drink
  • the cash register will use it to calculate the cost of the drink
  • the customer will need to recognize their drink amongst many others
  • the manager will use it, in summarized form, to predict what they will sell next month (what flavors are popular, etc)

Okay, it's not a complete list, but we can begin to see some necessary affordances based on these use cases. Let's take a few of these.

Entering the order

As the customer states their order, it comes to the barista choppy, and with errors. "Grande. No. Tall." Or "dark roast. with milk. make that medium roast. And do you have soy milk instead? And can I have almond flavor?" The cashier must be able to enter in a sequence of descriptors as they come in. Some of the descriptors are additive, as in "almond flavor". But some are corrective, as in "make that medium roast". This suggests different types of properties. Some are single-arity, such as the roast or the size. Some are multi-arity, such as the additives (almond, mocha, whipped cream). Adding almond doesn't mean you also don't want whipped cream. We need operations to do these things. The point is, the operations---the affordances---should do the right thing and communicate what it will do.

Just a first sketch, but I imagine operations like:

  • set-size
  • set-roast
  • add-flavor
  • remove-flavor

Calculating the cost

We want to be able to calculate the cost of the drink as ordered. The actual formula for doing so is really up to the business. But all of the data needs to be there, including the size, roast, and the additions like espresso shots. It's probably just a matter of addition and multiplication. But here is the operation:

  • cost

Building the drink

On the assembly line that is a modern, chain coffee store, a drink is built. Perhaps the machine of your store is so well-oiled that you want to display a precise list of steps to perform and in what order, such as:

  1. Grab tall cup.
  2. Fill with medium roast to 75%.
  3. Add soy milk.
  4. Add almond flavor.
  5. Place lid.
  6. Call out "coffee for Eric".

But maybe you want to leave a little room for human thought and ingenuity, so you give a convenient form of the order:

  • For: Eric
  • Size: tall
  • Roast: medium
  • Milk: soy
  • Flavor: almond (1)

Either way, you need the data necessary to generate the desired form.

  • instructions

Equality operation

It might be nice, for programming purposes, to be able to tell when two orders are equal. For example, when writing a test, you often want to check that the expected order is equal to the actual order. This is kind of a meta-concern, but it will be a big help so we should have an affordance for equality.

  • =

Discussion

Notice that we are not concerned with what the code will look like. We are concerned with capturing facts about the domain. We are building a theory of how the store operates. And we haven't even begun to implement it.

This is what I mean when I say "build an interface around data". A lot of people were triggered by that statement. They shouted that it was a return to OOP! But that's certainly not what I meant. I should have said "figure out your affordances first".

In your final implementation, you might not have an operation you started with. For example, maybe you don't have a function called set-size, and instead you just (assoc order :size :tall). That's fine. What's important is that, during the design phase, you thought about it. set-size was a token of that needed affordance to remind you that it was different from add-flavor.

Because once you have those affordances, they constrain the design problem. They help you hone in on a representation of your data that does afford all of the operations you'll need in a straightforward way. Sometimes you learn of a needed operation later and it's hard to retrofit the data structure you chose. That happens. But it's not the worst thing.

The worst thing is when there is no way to correctly write the needed operation. For instance, what if you didn't allow for mixing different flavors? Or you can't correct an order? We want to get those important affordances in early so they can help shape the design.

What I find is that, if you get the enough complex affordances into the initial design, the underlying model becomes very robust. New affordances are easier to write. Why? I think it goes back to the idea of truth. The affordances constrained the problem so that it had to capture essential truths in your domain. In the next issue, we'll talk about which affordances help constrain the problem the most. Be sure to subscribe if you're not already.

Podcast episode🎙

This week on the podcast, I read the 1969 ACM Turing Award Lecture Form and Content in Computer Science by Marvin Minsky. Minsky was a founder in the field of Artificial Intelligence. Check it out!

Quarantine update 😷

I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.

I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let alone learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.

Also, if you just want to subscribe for a paid membership, I have opened them back up for the moment. Register here.

Stay healthy. Wash your hands. Wear a mask. Take care of loved ones.

Clojure Challenge 🤔

Last issue's challenge

Issue 413

Please do participate in the discussion at the submission links above. It's active and it's a great way to get comments on your code.

This week's challenge

Minimum steps to palindrome

A string is a palindrome if it is equal to its reverse.

(= "racecar" (str/reverse "racecar")) ;=> true, so palindrome

Your task is to write a function that adds a minimum of letters to the end of a string to make it a palindrome.

Examples

(->palindrome "race") ;=> "racecar"
(->palindrome "mad") ;=> "madam"
(->palindrome "mirror") ;=> "mirrorrim"

Note: the generated string does not have to be a real English word.

Please submit your design process as comments to this gist. Discussion is welcome.

Rock on!
Eric Normand