TSM - 5 Steps to Mistake Proof Software Design

Alexandru Bolboacă - Agile Coach and Trainer, with a focus on technical practices

My previous blog posts have shown how to create better software design and defined the idea of Usable Software Design. Usable Software Design comes from the simple observation that the developer is the user of a software design. My thesis is that using principles and practices from Usability in software design will create two important economic benefits:

In this article, I will explore further usable software design starting from the simple idea that nobody likes to make mistakes but mistakes still happen. Since usable software design means software design that's a delight to use by developers, something has to be done to prevent mistakes. So, how about Mistake-Proofing your software design to make it more usable?

But first, you have to understand one important thing…

1. It's The System's Fault

In 1988, a cognitive scientist took upon himself to take a hard look at how we're designing everyday objects. Professor Donald Norman explored user-centric design in his book "The Design of Everyday Things", starting from psychology:

The vicious cycle starts: if you fail at something, you think it is your fault. Therefore you think you can't do that task. As a result, next time you have to do the task, you believe you can't, so you don't even try. The result is that you can't, just as you thought. You're trapped in a self-fulfilling prophecy.

and the solution:

It is time to reverse the situation: to cast the blame upon the machines and their design. […] It is the duty of machines and those who design them to understand people.

This means that…

2. Developer Mistakes Point To System Design Issues

Put yourself in this scenario: you find out that Jarod the Junior Programmer did a mistake when working on a task. You realize that other people have done this mistake before, and the solution has been documented. What is your reaction?

I bet your team's Jarod is not that junior

I used to do 1 or 2, and still do sometimes. Old habits take effort to change. Unlike 5 years ago, I understand now that this might create a vicious cycle. Here's how.

How will Jarod The Junior Programmer feel if your answer is 1 or 2? The involuntary psychological reaction is to feel guilty. Repeat the scenario a few times and he will stop challenging the design. Therefore, the vicious cycle.

What Mr. Norman taught me instead is that I should consider this situation as the fault of the system, not the fault of the developer. Figuring out how to improve the system so that the error doesn't repeat is the next important thing.

Here are three ways to improve your design to prevent mistakes.

3. Eliminate Exceptions

Sometimes, methods throw exceptions when called in a different order than they are supposed to. In certain cases, it's possible to completely remove the exceptions by redesigning the class. Here's an example.

At various Software Craftsmanship events where we practice coding techniques, we have used TicTacToe as a problem. We typically end up with a Game class that most developers design as follows:  

class Game{
    ...
    moveX();
    moveO();
    ...
}

This design leads to a potential mistake: nothing prevents me from writing the following code:

Game game = new Game();
game.moveO();
game.moveO();
game.moveO();

which is wrong according to the rules of TicTacToe. Player X should start, and then the game should continue with alternating moves.

The default answer of developers facing this issue is to change the implementation to something similar with:

void moveX(){
    if(currentPlayer != Player.X){
        throw new NotTheTurnOfThePlayerException();
    }
}

This still doesn't prevent me from writing the code above. It is a bit better because it warns me that I did something wrong. However, I would argue that finding out my mistakes at runtime is too late. Mistake-proofing means designing the system so that it's (almost) impossible to use it wrong.

I find the following design to have better mistake-proofing:

class Game{
    Game(Player playerX, Player playerO);
    move();
    ...
}

This design typically leads to code similar to:

Game game = new Game(playerX, playerO);
game.move();

I don't see any way to use this design any other way than it should. Not only it's easy to use, it's also easy to learn and mistake-proof.

The Game class can only be used in one way, the same way there's only a way to plug in a memory card

4. Pass Mandatory Arguments In Constructor

A common mistake is to create an object without all the mandatory parameters. When calling a method later on, an error appears.

For example, keeping the TicTacToe realm:

Game game = new Game();
game.move(); // players have not been added to the game

TicTacToe can only be played by two players, be they human or computer. There probably are TicTacToe games with more than two players, but I can't imagine a solitaire TicTacToe.

It's only natural to express this constraint in the constructor:

Game game = new Game(firstPlayer, secondPlayer);

Even if we later decide to implement the more-that-two players version of TicTacToe, it's easy:

game.addPlayer(thirdPlayer);
game.addPlayer(fourthPlayer); 
game.move();

5. Avoid Primitive Obsession

Primitive Obsession is a very common code smell, besides having a very suggestive name. It was also the source of a \$125 million loss in one of the rare occasions when we can measure losses caused by software issues.

Ward Cunningham's wiki discusses it:

The Smell: Primitive Obsession is using primitive data types to represent domain ideas. For example, we use a String to represent a message, an Integer to represent an amount of money, or a Struct/Dictionary/Hash to represent a specific object.

The Fix: Typically, we introduce a ValueObject in place of the primitive data, then watch like magic as code from all over the system showsFeatureEnvySmell and wants to be on the new ValueObject. We move those methods, and everything becomes right with the world.

In the case of TicTacToe, it's very tempting to write code like this:

game.move("A1");

or like this:

game.move(0, 0);

There are many problems with this design. Nothing prevents me from sending in bad coordinates such as game.move(-1, 2000) or game.move("Z9"). To avoid the problems, validations will have to be spread throughout the code. In the first case, string processing will be spread around code, and it's easy to introduce off-by-one errors when doing string processing. If the corner cases are validated with unit tests, you will need to repeat the unit tests for valid/invalid coordinates in each class that uses them.

There's a way to avoid all this: no matter how you input the coordinates, convert them immediately into a value object. In the case of TicTacToe, the domain of the problem can be easily described: the TicTacToe Board is formed of 9 Places that have Coordinates, each ranging from 1-3. So why not:

Place place = new Place(Coordinate.One, Coordinate.One);
game.move(place);

The user of this design cannot call the move() method with the wrong parameters anymore.

Recap

People using a poorly designed system tend to blame themselves instead of the system they're using. I postulate that this happens for software developers using existing software designs as much as for users of physical man-made objects. Donald Norman shows a way out: as a designer, understand that it's typically the system's fault and design your software with fault tolerance in mind. We've looked at three ways to improve the design of a class to be more mistake-proof: Eliminate Exceptions, Pass Mandatory Arguments In Constructor, Avoid Primitive Obsession. We've seen that the result is easier to learn, easier to use and avoids common mistakes at the same time.

Further Reading

When you cannot design your interfaces to prevent mistakes, Design By Contract comes to rescue. I recommend reading about it as another way to mistake-proof your designs.

This article focuses on how to mistake proof software design by using elements of software design. The reality is more complex: mistake-proofing software design certainly needs the fast feedback offered by pair programming, automated tests, continuous integration and IDE support.

What kind of mistakes do you make? What do you do to prevent them? Let me know in the comments.