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
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?
I bet your team’s Jarod is not that junior
Tell him that it’s documented and point him to the right place to read
Explain him how it’s done
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.
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:
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.
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.
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
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 🙂
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 🙂
Luca Pucacco
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.
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:
Thanks for sharing great tips and its really such a nice post!
Comments are closed.
Manage Consent
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
Functional
Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes.The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.
var astra = {"break_point":"921","isRtl":"","is_scroll_to_id":"1","is_scroll_to_top":"1","is_header_footer_builder_active":"1","responsive_cart_click":"flyout","is_dark_palette":"","revealEffectEnable":"","ajax_url":"https://mozaicworks.com/wp-admin/admin-ajax.php","infinite_count":"2","infinite_total":"0","pagination":"infinite","infinite_scroll_event":"scroll","no_more_post_message":"No more posts to show.","grid_layout":{"desktop":"3","tablet":1,"mobile":1},"site_url":"https://mozaicworks.com","blogArchiveTitleLayout":"","blogArchiveTitleOn":"","show_comments":"Show Comments","enableHistoryPushState":"1","masonryEnabled":"","blogMasonryBreakPoint":"0","shop_infinite_count":"2","shop_infinite_total":"0","shop_total_products":"1","shop_pagination":"number","shop_infinite_scroll_event":"scroll","shop_no_more_post_message":"No more products to show.","shop_result_count_format":"Showing 1\u2013{count} of {total} results","checkout_prev_text":"Back to my details","checkout_next_text":"Proceed to payment","shop_quick_view_enable":"disabled","shop_quick_view_stick_cart":"","shop_quick_view_auto_height":"1","woo_cart_empty_featured_product":"","single_product_qty_ajax_nonce":"e93debc8af","quick_view_nonce":"849fc62228","single_product_ajax_add_to_cart":"1","single_product_ajax_url":"/?wc-ajax=astra_add_cart_single_product","is_cart":"","is_single_product":"","view_cart":"View cart","cart_url":"https://mozaicworks.com/cart","checkout_url":"https://mozaicworks.com/checkout","add_to_cart_options_single":"rt_add_to_cart","is_astra_pro":"1","shopRevealEffectEnable":""};
//# sourceURL=astra-theme-js-js-extra
document.addEventListener( 'wpcf7submit', e => turnstile.reset() );
//# sourceURL=cloudflare-turnstile-js-after
var astraAddon = {"is_elementor_active":"","sticky_active":"1","svgIconClose":"\u003Cspan class=\"ast-icon icon-close\"\u003E\u003C/span\u003E","hf_account_show_menu_on":"hover","hf_account_action_type":"link","hf_account_logout_action":"link","header_main_stick":"1","header_above_stick":"","header_below_stick":"","stick_header_meta":"","header_main_stick_meta":"","header_above_stick_meta":"","header_below_stick_meta":"","sticky_header_on_devices":"both","sticky_header_style":"none","sticky_hide_on_scroll":"1","break_point":"921","tablet_break_point":"768","mobile_break_point":"544","header_main_shrink":"1","header_animation_effect":"none","header_logo_width":"","responsive_header_logo_width":{"desktop":200,"tablet":"","mobile":150},"stick_origin_position":"","site_layout":"ast-full-width-layout","site_content_width":"1070","site_layout_padded_width":"1200","site_layout_box_width":"1024","header_builder_active":"1","component_limit":"10","single_product_sticky_product_image":"","single_product_sticky_product_image_top_offset":"0","cart_sticky_cart_totals":"","order_review_toggle_texts":{"toggle_show_text":"Show Order Summary","toggle_hide_text":"Hide Order Summary"},"check_user_exist_nonce":"cbcdd699d6","woocommerce_login_nonce":"e740847d90","is_logged_in":"","user_validation_msgs":{"error_msg":"Entered user detail is not a valid.","success_msg":"This user is already registered. Please enter the password to continue.","login_failed_msg":"Login failed. Please reload the page and try again."},"checkout_order_review_sticky":"","cartflows_version":"","is_registration_required":"1","is_complete_package":"1","is_header_builder_active":"1"};
//# sourceURL=astra-addon-js-js-extra
var xoo_wsc_params = {"adminurl":"https://mozaicworks.com/wp-admin/admin-ajax.php","wc_ajax_url":"/?wc-ajax=%%endpoint%%","qtyUpdateDelay":"500","notificationTime":"5000","html":{"successNotice":"\u003Cul class=\"xoo-wsc-notices\"\u003E\u003Cli class=\"xoo-wsc-notice-success\"\u003E\u003Cspan class=\"xoo-wsc-icon-check_circle\"\u003E\u003C/span\u003E%s%\u003C/li\u003E\u003C/ul\u003E","errorNotice":"\u003Cul class=\"xoo-wsc-notices\"\u003E\u003Cli class=\"xoo-wsc-notice-error\"\u003E\u003Cspan class=\"xoo-wsc-icon-cross\"\u003E\u003C/span\u003E%s%\u003C/li\u003E\u003C/ul\u003E"},"strings":{"maxQtyError":"Only %s% in stock","stepQtyError":"Quantity can only be purchased in multiple of %s%","calculateCheckout":"Please use checkout form to calculate shipping","couponEmpty":"Please enter promo code"},"nonces":{"update_shipping_method_nonce":"d738eabd81"},"isCheckout":"","isCart":"","sliderAutoClose":"1","shippingEnabled":"","couponsEnabled":"1","autoOpenCart":"yes","addedToCart":"","ajaxAddToCart":"yes","showBasket":"always_show","flyToCart":"yes","flyToCartTime":"1500","productFlyClass":"","refreshCart":"no","fetchDelay":"200"};
//# sourceURL=xoo-wsc-main-js-js-extra
var complianz = {"prefix":"cmplz_","debug":"","user_banner_id":"1","set_cookies":[],"block_ajax_content":"0","banner_version":"22","version":"7.5.6","store_consent":"","do_not_track_enabled":"1","consenttype":"optin","region":"eu","geoip":"","dismiss_timeout":"","disable_cookiebanner":"","soft_cookiewall":"","dismiss_on_scroll":"","cookie_expiry":"365","url":"https://mozaicworks.com/wp-json/complianz/v1/","locale":"lang=en&locale=en_US","set_cookies_on_root":"0","cookie_domain":"","current_policy_id":"26","cookie_path":"/","categories":{"statistics":"statistics","marketing":"marketing"},"tcf_active":"","placeholdertext":"Click to accept {category} cookies and enable this content","css_file":"https://mozaicworks.com/wp-content/uploads/complianz/css/banner-{banner_id}-{type}.css?v=22","page_links":{"eu":{"cookie-statement":{"title":"Cookie Policy ","url":"https://mozaicworks.com/cookie-policy-eu"},"privacy-statement":{"title":"Terms And Conditions","url":"https://mozaicworks.com/terms-and-conditions"},"impressum":{"title":"Private: Contact Old","url":"https://mozaicworks.com/?page_id=12523"}},"us":{"impressum":{"title":"Private: Contact Old","url":"https://mozaicworks.com/?page_id=12523"}},"uk":{"impressum":{"title":"Private: Contact Old","url":"https://mozaicworks.com/?page_id=12523"}},"ca":{"impressum":{"title":"Private: Contact Old","url":"https://mozaicworks.com/?page_id=12523"}},"au":{"impressum":{"title":"Private: Contact Old","url":"https://mozaicworks.com/?page_id=12523"}},"za":{"impressum":{"title":"Private: Contact Old","url":"https://mozaicworks.com/?page_id=12523"}},"br":{"impressum":{"title":"Private: Contact Old","url":"https://mozaicworks.com/?page_id=12523"}}},"tm_categories":"","forceEnableStats":"","preview":"","clean_cookies":"","aria_label":"Click to accept {category} cookies and enable this content","iapi_nav":"fallback","is_cookiepolicy_page":""};
//# sourceURL=cmplz-cookiebanner-js-extra
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
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 🙂
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 🙂
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.
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.
Thanks for sharing great tips and its really such a nice post!