All programs exist within a larger environment, and typically have an interface to interact with the environment. That may be a hardware interface for doing low-level I/O, a “standard library” in languages like C or Java, or a set of built-in commands in an educational environment like Logo, Scratch or Greenfoot. Broadly, there’s two different approaches to take when designing an interface: I’ll call them minimalism and convenience. I’ll describe each in turn, with examples.
One way to design an interface is for minimalism: to find the smallest set of orthogonal commands that allow you to manipulate an interface. For example, in many environments (e.g. POSIX/C), the file-access interface consists of: open, close, seek, tell, read and write. This allows you to accomplish anything you want with files, and it’s hard to provide a simpler interface that can still accomplish all possible tasks. As a non-programming example, some calculators have a power operation (x^y) but no square root button, as the former subsumes the latter (x^0.5=) — and they do not offer a conversion between radians and degrees, just multiplication, division and a button for π.
The other way to design an interface is for convenience. The classic file interface is nice and orthogonal, but the most common way to write a file is just to write from a string over the top of a file. Some languages, like Haskell, have a function that does exactly that — you give a file path and a string, and it just writes that to the file (effectively doing open-for-overwrite, seek-to-begin, write, close). It overlaps with the classic operations, which are still provided, but it is easier to get done what you want. As another example, Excel provides an AVERAGE function for getting the mean of a range, even though it’s trivial to do given the SUM and COUNT functions.
These two different approaches are different ends of a spectrum, and each interface designer must make design decisions to veer towards one or the other. Some orthogonal interfaces can make it very hard to implement common cases (e.g. Java’s file API), while some convenience interfaces pack in so many similar methods and synonyms (e.g. Ruby’s array API) that it becomes a tangled mess (especially if, in Ruby’s case, you want to duck-type something to act like an array). I’m particularly interested in these decisions in the context of educational environments, such as in Greenfoot, where I have a hand in the design.
Education Example: Greenfoot
The original Greenfoot 1.* had these methods for movement: setLocation(x, y), getX() and getY(). It also had setRotation(r) and getRotation(), which rotated your image. This was design by minimalism: these methods are enough to implement any kind of movement you want: up/down/left/right Pacman-style movement, Asteroids style drifting, directional movement, and so on.
So there you are on your first day of teaching. You load up Greenfoot and want to show some novices how to get your character running across the screen in the direction in which it is pointed. Here’s the code, if you just used Greenfoot’s original built-in movement methods:
setLocation((int)(getX() + 5 * Math.cos(getRotation())),
(int)(getY() + 5 * Math.sin(getRotation())));
Ouch! Let’s see how many concepts are in that one line:
- Method calls with no parameters or multiple parameters
- Method calls with and without return values
- The general concept of accessors and mutators (aka getters and setters) and their combination
- Nested method calls
- Casting between numeric types
That’s a lot to take in. Compare this to our move method which we added more recently:
Now we’re down to just one method call, with one parameter and no return. (We also debated building in a no-parameter version, move(), that advanced a default amount, but that seemed too little gain over the one-parameter version.)
In fact, since version 2.0 of Greenfoot, we’ve slowly added more methods in the convenience category: move, turn, turnTowards, isTouching/removeTouching and a few more. Looking back, I think it has mainly been me arguing to add these methods, based on making our beginners’ workshop smoother. Now, designing the software differently purely to make a single workshop easier is a bit bizarre, but my thought was always been that the workshop mirrors most users’ journey into Greenfoot. Generally, allowing people to easily do what they commonly ask for when they start with Greenfoot seems like a good idea.
Everyone wants collision detection, so in 2.3.0 we made it shorter and simpler. In the beginners’ workshop, everyone always asks how to add a score, and my answer has always been that unfortunately it’s not as easy as you’d think. So in Greenfoot 2.4.0, we’re adding new methods so that it can be as easy as you’d think. There’s a limit to how easy we want to make things: at some point users need to learn more complicated concepts to perform more complicated tasks. But I think the right time for that should be a bit later than it was in version 1.*.
With the new methods, a learner’s journey can be better structured. In the upcoming version, if you want a score, you can add it and display it easily. If you want to eat things you run into, that’s also easier. Then, if you want to be able to shoot bullets that destroy asteroids and increase the player’s score, you already have an easy way to do the score and the destroying, but you must use object references (the bullet holds a reference to the rocket that fired it) to add to the score at the appropriate time. But at least it’s a more manageable pathway.
(Early versions of Greenfoot tried to have it both ways, by providing a minimal core interface, and then introducing convenience helper classes into beginners’ scenarios, so that the move() method would be in a per-scenario helper class. But ultimately this could cause confusion when beginners moved on and found these convenience methods were “missing” from their next scenario. So gradually, we have steered away from this approach to putting convenience methods into the core of Greenfoot.)
Education Example: Scratch
I find Scratch 1.* an interesting example because it had a couple of significant complications in its interface design. Scratch 1.* did not support user-defined methods (a restriction later remedied by Scratch 2.0 and BYOB/Snap). So users were not able to combine several simple commands into a larger command for re-use. This probably biased Scratch somewhat towards convenience, as the users could not add a convenience layer themselves. But at the same time, in the Scratch interface, all blocks are accessible in a visual menu down the side of the interface. Thus it is harder to hide a large set of methods compared to something like Greenfoot (where you could have a large list available through the documentation). So this might steer Scratch away from having too many blocks available.
You can run a critical eye over Scratch’s interface yourself, but I would say that Scratch mainly ended up with convenience methods. If you look at the list of motion blocks (shown on the left), technically this could be boiled down to a small number of orthogonal methods, but instead they have provided many similar variants. For example, turn anti-clockwise could have been left out (turn clockwise a negative amount), “set x to” and “set y to” are just special cases of “go to x: y:”, and so on. However, having all these blocks means beginners may need to think less about motion: you just search for a block that does almost exactly what you want, rather than having to think how to combine several smaller operations. (On the other hand, you could argue that having all these blocks makes it more overwhelming for the beginner.)
I can’t finish discussing Scratch without pointing out my favourite convenience block: the “say” block. This block displays a speech bubble coming from a character with the given text inside it. This single block enables entire classes of Scratch scenarios: stories, interactive text adventures and so on. I think it was probably the minor masterpiece in Scratch.
Interfaces can be designed for minimalism (with fewer, orthogonal methods) or convenience (with more methods to easily handle the common cases). In general software design, there’s no single right answer. However, I wonder if in educational software design, there are a different set of constraints that means that convenience is more often the right answer, to allow learners to get satisfying results earlier on with less cognitive demand. Of course, the meat of programming comes in the combination of methods, but via interface design, the creators of systems like Scratch and Greenfoot can choose at which point novices need to begin to combine methods, and how much they can done with built-in convenient primitive methods.
(The inspiration for this blog post came from John Maloney, one of the Scratch designers, who shared his observation about these two different ways of approaching interface design.)