Composing a Cup of Coffee from Components

You can learn a lot about software design by thinking about your first drink of the day.

Anyone heading in to work isn’t themselves without that first infusion of caffeine. Whether it’s at the drive-thru or the counter, you make your order, then grip, sip, and go.

What does that have to do software design?

Follow me down this line of questions, I will show you how composable design can improve your software.

First, how do you use a cup of coffee? Typically, you grip the cup, then take a sip.

Hand-drawn image of a cup labeled “Drink”.

What if it’s a really hot cup of coffee? You’re server may give you a cup with a sleeve. And how do you take a drip of hot coffee? Grip, then sip.

Hand-drawn image of a cup with a sleev labeled “HOT Drink”.

What about when you’re on the move? In that case, your server can give you a cup with a lid. And how do you drink your coffee when you’re late and moving fast to the office? Grip, then sip.

Hand-drawn image of a cup with a lid labeled “MOVING Drink”.

What if I’m not drinking coffee? Juice, water, soft-drinks, the rules are still the same. Grip, then sip.

So the rules around using a cup — for coffee, juice, or anything else you drink in the morning — involve two steps: grip and sip.

In software terms, we would say that the interface for a drink has two methods: grip and sip.

public interface Drink {
public void grip();
public void sip();
}

But I need to drink my half-caff, no-foam, one-pump, extra-hot machi-latte while I’m running late!

Hand-drawn image of a cup with a sleeve and lid labeled “HOT MOVING Drink”.

Grip, then sip.

It doesn’t matter whether your language supports interfaces, protocols, or duck-types. What matters is that when someone asks for a drink, they expect to grip and sip.

That’s easy enough for you. But what about your barista? How can they keep up with all of these different requests.

How would that look in software?

Well, a regular cup is simple enough. We could use some popular OO language to implement the coffee this way.

public class Cup implements Drink {

private String liquid;

public Cup(String liquid) {
System.out.println("Pouring your new " + liquid);
this.liquid = liquid;
}

public void grip() {
System.out.println("Gripping " + this.liquid);
}

public void sip() {
System.out.println("Sipping " + this.liquid);
}

}

You could buy a bunch of different cups: regular cups, lidded cups , sleeved cups, and sleeved-and-lidded cups. This solves the problem, but now you’ve got so many combinations to think about.

The manufacturer has to decide if your sleeved, lidded cups is more similar to the sleeved cups, or to the lidded cups.

public class Cup implements Drink {} // just like before

public class HotCup extends Cup {

public HotCup(String liquid) {
super(liquid);
}

public void grip() {
System.out.println("Protecting your hands.");
super.grip();
}
}

public class LiddedCup extends Cup {

public LiddedCup(String liquid) {
super(liquid);
}

public void sip() {
System.out.println("Protecting your shirt.");
super.sip();
}
}

// TODO -- which way should it be?
public class HotLiddedCup extends LiddedCup {}
public class LiddedHotCup extends HotCup {}

The barista also has to get it right the first time.

public enum OrderType {
HOT,
FAST;
}

public Drink processOrder(String liquid, OrderType... types) {
List<OrderType> ots = Arrays.asList(types);
Drink d;
if (ots.contains(OrderType.HOT) && ots.contains(OrderType.FAST)) {
d = new HotLiddedCup(liquid);
} else if (ots.contains(OrderType.HOT)) {
d = new HotCup(liquid);
} else if (ots.contains(OrderType.FAST)) {
d = new LiddedCup(liquid);
} else {
d = new Cup(liquid);
}
return d;
}

If you change your mind, you have to go back to the counter and ask for a different type of cup. Are you sure you want to upset your barista that way? Besides, you’re running late.

A Different Approach

Luckily, the coffee store found a company that makes cups, sleeves and lids that all work together.

Hand-drawn image of a cup, sleeve, and lid.

The barista reads your mind, puts together the right parts, and hands you your drink. That always work well.

Again, the manager comes to the rescue by putting out some extra lids and sleeves near the cream and sugar. So you’re set.

This is the gist of composing your solution from small parts that work together.

What could that look like in code?

public class Cup implements Drink {} //same as before

public class SleevedDrink implements Drink {
private Drink drink;

public SleevedDrink(Drink d) {
this.drink = d;
}

public void grip() {
System.out.println("Protecting your hands.");
this.drink.grip();
}

public void sip() {
this.drink.sip();
}
}

public class LiddedDrink implements Drink {
private Drink drink;

public LiddedDrink(Drink d) {
this.drink = d;
}

public void grip() {
this.drink.grip();
}

public void sip() {
System.out.println("Protecting your shirt.");
this.drink.sip();
}
}

// Making a drink becomes much simpler....
public Drink processOrder(String liquid, OrderType... types) {
List<OrderType> ots = Arrays.asList(types);
Drink d = new Cup(liquid);
if (ots.contains(OrderType.HOT)) {
d = new SleevedDrink(d);
}
if (ots.contains(OrderType.FAST)) {
d = new LiddedDrink(d);
}
return d;
}

What if I’m experience frost-bite, and even the simple hot coffee is too much? Composition has got you covered here, too. Add a second sleeve to your drink.

Hand-drawn image of a cup with a lid and two sleeves labeled “EXTRA HOT MOVING Drink”.
Drink hotLiddedDrink = b.processOrder("extra-hot coffee", OrderType.HOT, OrderType.FAST);
c.enjoy(hotLiddedDrink);
System.out.println("...and if it's still too hot...");
hotLiddedDrink = new SleevedDrink(hotLiddedDrink);
hotLiddedDrink.grip()
hotLiddedDrink.sip();

What do all of these lids and sleeves mean for my software design?

First lets make sure we understand what we are comparing. The first design showed a hierarchy of many types of cups. The second design showed components that could be assembled into different types of cups.

The design based on a hierarchy has some disadvantages:

  • its impossible to change your mind about the type of cup later.
  • the logic the barista followed in processOrder is more complex.
  • the logic for handling a hot drink is implemented in multiple places, both HotCup and HotLidded cup. Or was it LiddedHotCup?

The design based on composition had several advantages:

  • The design is flexible. both the customer and the barista could build out the perfect drink.
  • The barista’s steps in processOrder were, therefore, much simpler.
  • You could even add two sleeves, which was not possible with the hierarchical design.
  • The manager could just count cups, rather than guessing which customers need which kind of customization.
  • The logic for sleeves is in one place, the SleevedDrink.

Both designs solved the problem, providing protection for your hands and your shirt, while letting you grip and sip.

This example is a metaphor, and somebody could say completely contrived. Yet it shows the trade-offs involved when you think about how to break up your code.

There are trade-offs for sure, and you can tell which side I lean towards.

Composable design makes your software simpler and more adaptable. Your code becomes simpler. You avoid duplication. Your software can adapt to situations you may not have imagined up front.

Consider using a composable design, and see how simple and adaptable your software can be.

You can find the simulation source code on GitHub.


Posted

in

by

Tags: