Building and Testing 50 UI States with Decision Tables

Data Structures, TypeScript, UI,

Illustration: Hachibu

Introduction

Decision tables are a useful tool for modeling complex logic. And when you’re building user interfaces, things can get very complicated – e.g., show a pop-up every other Monday between Midnight UTC+5:30 and Noon UTC+12:45 to trial customers born on a Leap Year.

In this post, I’m going tell you a story about how I lost my cool trying to build and test 50 different UI states and how decision tables saved my project. By the end of this post, you’ll have a problem solving tool that is generalizable across other programming domains besides UI programming.

What Are Decision Tables?

Decision tables are a visual way to map conditions to outcomes in a tabular format. Organizing information in a tabular format is nothing new. For example, Boolean Algebra uses truth tables to map inputs to outputs for logical operations such as AND and OR.

AND (&&)FalseTrue
FalseFalseFalse
TrueFalseTrue
OR (||)FalseTrue
FalseFalseTrue
TrueTrueTrue

Since it’s just a table, you get all of the benefits associated with organizing your information in a tabular format.

  • Missing outcomes are visibly obvious so you’re forced to immediately address any gaps in your logic and ensure that you’ve considered every case.
  • They can be used as a design document when communicating with your peers; a reference when you’re writing code; and a complete specification when you’re testing code.

The Setup

This story took place at a SaaS startup that helped its customers collect and manage product feedback. They also had a Machine Learning algorithm that could automatically categorize feedback by topic and sentiment. For example, the feedback “Constant rendering issues” would be tagged with “BUGS” and given a negative sentiment by the ML algorithm. Customers could also create their own tags to match specific words and phrases. These were called text match tags.

I was teamed up with a designer and a backend engineer to add 2 checkboxes to the UI which would allow customers to add tighter constraints to their text match tags. The goal of this feature was to improve the results of text match tags by letting customers define rules around how their tags would be applied to feedback. These 2 checkboxes would allow text match tags to be aware of more information like feedback metadata such as filters and the tags that were applied by the ML algorithm.

The Confrontation

This project appeared to be straight-forward. I just had to build and connect 2 checkboxes. What could be easier? So, I just started coding, and I opened a pull request for code review shortly thereafter. It wasn’t until I entered QA testing in our staging environment that my troubles began.

Every time my code got reviewed, my teammates would keep finding new bugs and regressions. This went on for days as I attempted to brute-force my way through the project. I knew that I didn’t have a complete understanding of what I was supposed to be building, but I soldiered on out of desperation.

My frustration was growing, and I could sense that I was approaching my limit. So, I put the project on pause so I could sleep on it. After sleeping on it, I came to the realization that there were too many states for me to keep track of. I would need to write down and enumerate every single state so that I could confidently implement and test this feature.

Each hide or show decision relied on the value of 2 variables: the parent tag mode and the child tag mode. This reminded me of something I had studied years ago while implementing logic gate functions: Boolean Algebra Truth Tables. With truth tables in mind, I eventually stumbled upon decision tables. Working together with the backend engineer for this project, we created the following decision tables.

Once we finished the decision tables, I breathed a sigh of relief because all of the logic was now crystal clear. But I was also shocked to discover that this seemingly simple feature had 50 different UI states. A parent tag has 5 options, and a child tag has 5 options. So each checkbox has 5 * 5 combinations: 25 states. And with 2 checkboxes, that’s 25 + 25 combinations: 50 total states.

The Resolution

I was finally ready to start programming but first I would need to decide how to translate a decision table into a data structure. Building your programs around well designed data structures is very important, which reminds me of a quote by Linus Torvalds (the creator of Linux and Git).

git actually has a simple design, with stable and reasonably well-documented data structures. In fact, I'm a huge proponent of designing your code around the data, rather than the other way around, and I think it's one of the reasons git has been fairly successful […] I will, in fact, claim that the difference between a bad programmer and a good one is whether he considers his code or his data structures more important.

Linus Torvalds

What kind of data structure could represent a table? Given that a table is comprised of rows and columns, it seemed obvious to use a 2-dimensional data structure like an array of arrays or a map of maps.

I chose a map of maps, because it would allow me to get my answer quickly by 2-dimensionally indexing on parent tag mode and child tag mode. The outer map would represent the rows which are the parent tag modes; the inner map would represent the columns which are the child tag modes; and the cells would represent the hide or show decision as a boolean.

The following code is written in TypeScript which should seem familiar to you if you’ve written JavaScript before. That’s because the syntax of TypeScript and JavaScript are closely related. The biggest difference between TypeScript and JavaScript is that the former layers additional syntax for types on top of the latter.

type TagMode = 'none' | 'manual' | 'text_match' | 'smart' | 'managed'

type Tag = {
  mode: TagMode
}

type DecisionTable = Record< TagMode, Record< TagMode, boolean > >

class ManageTagsController {
  showUseFeedbackFilters(parent: Tag, child: Tag): boolean {
    const table: DecisionTable = {
      none:       { none: false, manual: false, text_match: true, smart: true, managed: true },
      manual:     { none: false, manual: false, text_match: true, smart: true, managed: true },
      text_match: { none: false, manual: false, text_match: true, smart: true, managed: true },
      smart:      { none: false, manual: false, text_match: true, smart: true, managed: true },
      managed:    { none: false, manual: false, text_match: true, smart: true, managed: true }
    }
    return table[parent.mode][child.mode]
  }

  showScopeByParent(parent: Tag, child: Tag): boolean {
    const table: DecisionTable = {
      none:       { none: false, manual: false, text_match: false, smart: false, managed: false },
      manual:     { none: false, manual: false, text_match: false, smart: false, managed: false },
      text_match: { none: false, manual: false, text_match: true,  smart: true,  managed: true  },
      smart:      { none: false, manual: false, text_match: true,  smart: true,  managed: true  },
      managed:    { none: false, manual: false, text_match: true,  smart: true,  managed: true  }
    }
    return table[parent.mode][child.mode]
  }
}

After adding those functions to the AngularJS controller for that page, all I had to do was bind those functions to the HTML view. For that, AngularJS has a built-in directive called ng-show which shows or hides an HTML element based on an expression.

<div ng-show="showFeedbackFilters(parentTag, childTag)">
  <label>
    <input ng-model="child.scope_by_feedback_filters" type="checkbox">
    <span>Scope tag using feedback filters (Advanced)</span>
  </label>
</div>
...
<div ng-show="showScopeByParent(parentTag, childTag)">
  <label>
    <input ng-model="child.scope_by_parent" type="checkbox">
    <span>Scope By Parent</span>
  </label>
</div>

Finally, the code was ready to be deployed to staging for another round of QA testing. This time would be different. Decision tables in hand, the QA testers were able to systematically reproduce each case and verify that the code was behaving correctly. The code moved through QA testing quickly, and it was deployed to production later that day.

Lastly, I think it’s worth mentioning that while finalizing this post months after writing the original code, I realized that I could have written these two functions as fairly simple one-liners. Even with the benefit of hindsight, I don’t know which solution I prefer; I could make arguments for and against each one, but that’s beyond the scope of this post.

function showUseFeedbackFilters(parent: Tag, child: Tag): boolean {
  return child.mode === 'text_match'
      || child.mode === 'smart'
      || child.mode === 'managed'
}

function showScopeByParent(parent: Tag, child: Tag): boolean {
  return (parent.mode === 'text_match' || parent.mode === 'smart' || parent.mode === 'managed')
      && (child.mode  === 'text_match' || child.mode  === 'smart' || child.mode  === 'managed')
}

Conclusion

I wish I had known about decision tables earlier in my career. I’m a visual learner and thinker and being able to visually map out states into a table would have helped me out so much in the past. When you enumerate the states of your program into a table, the logic becomes obvious and it can also be validated by anyone on your team because tables are everywhere in daily life.

Thanks to my wife, Marín Alcaraz and Michael Geraci for editing.
If you liked this post, you might also like Be Your Own Password Generator.