5 Steps to Mistake Proof Software Design

Images from Impossible Objects Catalog, based on Impossible Objects by Jacques Carelman

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:

  • faster implementation time for common tasks
  • faster integration of new developers in an existing team.

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

The Design Of Everyday Things

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.

– Donald Norman, “The Design of Everyday Things”

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.

– Donald Norman, “The Design of Everyday Things”

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?

baby-programmer
I bet your team’s Jarod is not that junior
  1. Tell him that it’s documented and point him to the right place to read
  2. Explain him how it’s done
  3. Look at the design and change it so that this problem cannot repeat

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 Craft 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.

Memory Card
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

Hint: This object was involved

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 shows FeatureEnvySmell 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.

Acknowledgments

Many thanks to Thomas Sundberg for proof-reading this blog post.

And, of course, many thanks to the Mozaic Works team for encouraging, supporting and pushing for the type of content that is a joy to write.

Photo sources: http://mitpress.mit.edu/sites/default/files/9780262525671_0.jpg, http://software.nhm.in/images/programmer.jpg, http://static1.squarespace.com/static/512e110ce4b025d112a30850/t/516a6aece4b048b5b3e4e0b7/1365928684826/Biz_Card_Back_Deming_Quote.png, http://p.playserver1.com/ProductImages/9/9/9/6/2/0/0/3/30026999_700x700min_2.jpg, http://www.peachriot.com/reading-normans-design-of-everyday-things-and-emotional-design/, http://impossibleobjects.com/catalogue.html

More from the Blog

6 thoughts on “5 Steps to Mistake Proof Software Design”

  1. Thanks for sharing these examples that do a good job of illustrating your point. When I first read title “3. Eliminate exceptions”, I wasn’t quite sure what I would find below. Now that I see what you mean, I call it “avoid temporal coupling” as I find it explicitly frames the problem. Would that phrasing cover 3 totally or are you addressing other concerns as well?

    Coordinates are excellent! An idea I get when reading is new Place(Top, Left); Using static imports for the enum. Of course that might not scale above 3 dimensions

    1. Alexandru Bolboaca

      Thank you Johann for reading and for your comment.

      re exceptions vs. temporal coupling: Interesting perspective.

      Looking back, I think this example is part of a larger principle: the fourth principle of simple design, “have fewer elements”. I’m cutting out things (such as exceptions) from the interface by changing the design. This results into a simpler design, because there are fewer things that can happen.

      Maybe in this particular case it’s the same as removing temporal coupling, but my intuition tells me it’s not always the case. I’ll have to think more about this to find examples when removing things doesn’t involve temporal coupling.

      re coordinates: Yes, I agree, this example is very easy to use :). It doesn’t play that well with changeability as it is. I thought about it when writing the code, and I left it like this because it would be very easy to refactor. The key thing is encapsulation; once coordinates are encapsulated, the Coordinate enum can easily change into a class that offers the One/Top values as constants. Increasing the board size will probably require a more general Coordinate class, with Design by Contract constraints. Changing to a different coordinate system, be it hexagonal (3 axes, 2 dimensions) or 3D is more complicated. The natural way is to use two different coordinate systems. The cleaner and more changeable way is to introduce classes such as Axis, Dimension and possibly Topology accessible through facades for orthogonal 2D, hexagonal 2D or 3D.
      I think you can easily see why I decided to leave this reasoning outside the article 🙂

      1. Interesting. The principle “have fewer elements” is useful to compare design options, whereas your idea of exceptions and “temporal coupling” are code smells and can be used to detect a problem even if a better solution isn’t obvious (I’m a bit puzzled that I can’t find a documented smell that corresponds to this). The latter two also better fit the subject of the article.

        I can see you have thought a lot about dimensions and topology in Tic Tac Toe 🙂

  2. Hi, very nice article.
    I suggest you to change the last example in the 4th point, because it violates the principle explained in the 3rd point: you can easily create a wrong flow if you add a player after a move, or adding a wrong number of players etc.

    1. Alexandru Bolboaca

      You are right, this is true. This also shows that I didn’t thoroughly explain that specific case.

      What I wanted to express was the situation when there should be at least two players and at most n players (let’s say 10). However, my issue is that there’s no way to specify in the constructor “it has to have two to ten players”. The only thing we can do in the constructor is to say “It has to have at least two players”. While this doesn’t solve the “at most 10 players” constraint, it does at least show very clearly that at least two players are needed.

      Thinking about it, there’s a (potentially) better solution that uses a smart data structure:

      Game(TwoToTenPlayersList twoToTenPlayersList)

      This would very clearly communicate intent.

      It does however get more complicated if the max constraint is dynamic. I’ve seen names like TwoToNPlayersList, but they only obfuscate things in my opinion. A better option might then be a fluent interface:

      Game.aGameWithMaxNPlayers(10).withPlayers(playersList)

      Thanks for pointing this out, it gives me lots of new ideas.

Leave a Comment

Your email address will not be published. Required fields are marked *

0
    0
    Your Cart
    Your cart is empty
      Apply Coupon
      Available Coupons
      individualcspo102022 Get 87.00 off
      Unavailable Coupons
      aniscppeurope2022 Get 20.00 off
      Scroll to Top