Versions used
- Django 2.1
- VueJS 2.5
- vuex 2.5
Yup. LEGO
This popular game has been around for quite a long time (the brick as we know it came to life around 1958 according to Wikipedia). It is (very objectively) both fun and serious [1]. We explore here what it may teach us, as software makers, in terms of Maintainability and Usability.
Let's play a game
LEGO boxes come with a plan: you may unpack small chunks of plastic, yet the way forward is clear for wondrous constructions. Let's suppose you've just offered your nephew Kevin [2] an impressive Stormtrooper squad. How does he get it built? Simple, there's a blueprint ready for him to follow. Baby steps, arrows, illustrations. Can't be easier.
Just stick to the plan Kevin. Alas:
- He likes to break rules
- He's had a thing for scuba diving lately
- As a requirement from his sister (aka the boss), he's had to include a swimming pool baseplate
So we end up with:
A a… a scuba troopers pool party? Well, after all troopers do fit in with their oversize helmets.
Unexpected but good job Kevin 👍.
What does it have to do with software?
Sounds childish? Or futile maybe?
Beware! Kevin is not only a future user of yours, he's also your next intern. And IMHO this story echoes the challenges we face in the typical timeline of software development:
- Phase 1 - The master plan
- We run user stories in our heads with all corner cases covered.
- From there we get it translated into code we are proud of.
- We iterate through these steps a few times and build confidence.
- Finally wish it a long and prosperous life as we ship it to production.
- Phase 2 - PEBCAK time
- Reality happens. No one cares about your damn instructions!
It can be harsh but really confronting your ideas and its implementation to reality is what makes this job great in my opinion. Sometimes you can even be surprised in a positive way by stuff you did not plan [3].
Back to the analogy with LEGO, it seems like it is a good framework to manage this kind of unexpected situation, hence ensure that your application is both usable and maintainable. That is what we will explore in the next sections, focusing on three of its major features:
But first, a few words on the tool we designed that is the source of these peculiar reflections.
Introducing FILIO
For what's relevant in this article, we (Polyconseil) are the technical leads behind some major car sharing and electric car charging infrastructures worldwide: BlueSG, Source London, Autolib or BlueIndy to name a few. From bootstrapping to monitoring interfaces, we provide the full spectrum of IT tooling they require.
One of the (many) challenges we face
We must enable operational teams to get the system back on tracks if anything goes wrong, or simply when human involvement / teams collaboration is required.
That being said, there are many variables to deal with:
- cars of various kinds
- multiple charging kiosks versions
- users, stakeholders and their own culture
- etc.
and yet common business use cases across all entities:
- handle vehicle accidents
- treat customer claims
- ensure infrastructure maintenance
- etc.
Those make a great case for flexible yet automated workflows that allow teams to solve issues without highly specialized knowledge.
To meet this need we created our own ticketing system / collaboration back-office: FILIO 🎉. Its design serves as a case study for this blog post.
Discoverability - The starting blocks
Picture yourself preparing a mission to conqu… ahem build the world. Chances are you'd first organize all your available pieces in such a fashion:
You would gather basic blocks and group them by similarity:
- All heads together
- All trunks together
- All legs together
And probably in that order since it respects a LEGO character anatomy. What about the other pieces? Well they're all accessories, or custom blocks, so you put them together. Yet you can still avoid your brain too much of a hassle with an additional classification:
- "Stuff that goes on head"
- "Stuff that goes in hand"
OK, that's the LEGO teaching. What did we need to apply it for?
Design challenge #1
We want a main navigation that targets both occasional and expert users, with very different focuses:
- Some just need to easily report problems (they "create a file" as we call it)
- Some need to create appropriate tasks for a file, dispatch and monitor them until resolved
- Some just need to know what task is assigned to them
- Some supervise the overall operations
- etc.
Long story short we came up with the following navigation:
It hopefully kinda makes sense for you even if you know nothing about the application. Custom blocks here are the "notifications".
What kind of notifications are there you may ask?
- There has been activity on a task of type "Repair a rental kiosk"
- Someone has created a file with type "Loss of car connectivity"
- A file of type "Customer claim" has been inactive for more than 10 minutes
- etc.
We did not find such a simple design before we succeeded to see the big picture and stopped taking each user needs individually. This step back is actually what showed us the need for notifications.
In the same movement, it made sense to introduce a proper Notification class in database models instead of the various hacks we were thinking of (having special flags inactive_for, unread_by, etc. on Task and / or File models).
Resulting Django models schematically look like:
from django.db import models
from django.utils import timezone
### BASIC BLOCKS ###
class File(models.Model):
title = models.CharField()
# Other attributes: created by, created at, etc.
class Task(models.Model):
file = models.ForeignKey('File', on_delete=models.CASCADE)
# Other attributes: assignee, type, etc.
### CUSTOM BLOCKS ###
class Notification(models.Model):
file = models.ForeignKey('File', on_delete=models.CASCADE)
task = models.ForeignKey('Task', on_delete=models.CASCADE)
type = models.CharField(
choices=(
('modified_task', 'Modified task'),
('inactive_file', 'Inactive file'),
('new_file', 'New file'),
),
)
# Other attributes: targeted user, last read at, etc.
The combination of fields file, task and type on Notification model is what makes it a custom piece generator. Just like a LEGO accessory could fit in the head or the hand (or both!?) and have a certain function (i.e. a drinking glass or a weapon), our notifications refer to a task or a file and highlight a certain property (this task has been modified, this file is inactive, etc.).
To recap, high-level thinking (or actually trying to stay at a "dumb" level 😏) enabled us to reframe our models into more universal ones, making them more discoverable. For users and maintainers alike.
And I cannot but make a reference to the virtual equivalent of LEGO, Minecraft:
Modularity - Another brick in the workflow
In the Filio presentation above, we talked about business workflows we have to support. Here is a concrete example of what needs to be done in case of a vehicle damage report:
.-----------------------. | Damage assessment | |-----------------------| No damage | A technician asseses |---------------------------------. | damages on the field | | '-----------------------' | | | | Highly damaged | | Slightly damaged | v v | .-----------------------. .-----------------------. | | Towing | | Transfer | | |-----------------------| |-----------------------| | | A towing company is | | Technician can move | | | called | | the car themselves | | '-----------------------' '-----------------------' | | | | | | | | v | | .-----------------------. | | | Repairing | Was slightly | | |-----------------------| damaged | | | Garage fixes the car |------------------------. | | '-----------------------' | | | Was | ^ Needs some | | | highly damaged | | fixing | | | v | v v | .-----------------------. .---------. | | Expert assessment | | Done! | | |-----------------------| Good to go | | '----------> | Insurance expert |---------------------> | 🎉 🙌 🍻 | | assesses damages | | | '-----------------------' '---------'
These processes are not cast in stone, our project managers did an incredible analysis to actually map and synthesize them in such diagrams. It was the base material for our reflections. Our first idea was to simply create models in database that would represent them. They would be editable by operational teams, of course, so that they could evolve. It does not feel totally satisfying, does it?
Design challenge #2
Regarding the mapping of business processes (aka workflows) in models, we face a dilemma:
- If it is not precise enough we won't help end users know what to do in situation A, B or C
- If our models are too strict, our application is adding a layer of rigidity. We simply cannot afford this since our primary goal is to solve unexpected issues!
So what we have are files that are created to solve an issue, e.g. handling a car damage. Those files must follow business workflows which consist of orchestrated tasks. Drawing an analogy with LEGO:
- A workflow is a blueprint
- Tasks are bricks
- Files are an assembly of tasks that may or may not follow the blueprint
So instead of looking at a workflow as a "thing" by itself (that needs to be broken down into tasks), we can take the opposite view. A task is the base unit, and a workflow organically stems from combining tasks:
Task == Brick ------------- .==========. <--- What is supposed to take place before? / (_) (_) /| /-========-/ | <--- What is it? What does it do? | |/ '-========-' <--- What is supposed to take place after? Follow workflow (== blueprint)? -------------------------------- .==========. .==========. .==========. / (_) (_) /| / (_) (_) /| / (_) (_) /| /-========-/ | /-========-/ |=====. /-========-/ | .==========. | |/| | |/ (_) /| | |/ / (_) (_) /| '-========-' / '-========-'====-/ | '-========-' /-========-/ | | |/ | |/ | |/ '-========-' '-========-' '-========-' YES ✔ KINDA ✔ NO (but we don't crash ✔)
Hence we went for task specifications that resemble a basic LEGO brick. Their definitions are database-stored JSON objects that conform to the following format:
# Schematic task definition example: CAR REPAIRING
# What is expected before this task
conditions:
# A damage assessment task
- damage_assessment
# What must be done
actions:
# Send garage a mail to request repairing
template: "…"
# What normally comes after
outcomes:
# If car was very damaged, insurance expert should examine it again
- next_task: expert_assessment
initial_damage: heavy
# Otherwise, nothing left to do!
- next_task: null
initial_damage: light
While the resulting workflows are flexible and non-blocking, the tasks themselves are supposed to be simple, predictable and rock solid. For this we use Cerberus schema validation library and ensure what users edit in these tasks definitions is actually valid.
I think that focusing on doing simple things that can be freely arranged is a key factor of success for many web applications (in particular business-oriented ones). Take Trello for instance. I find it impressive how differently people use it. It feels powerful yet at the core of it is just a bunch of virtual post-its.
Educational - Show how it's done
Remember Clippy?
Maybe it did not leave you such a great impression. You are not alone, it made it to TIME's worst 50 ideas. Yet helping users navigate your app is essential, which brings us to our third and last challenge:
Design challenge #3
How can we embed support in an application without making it an extra layer of complexity (cost and boredom) for users and maintainers?
A proposed answer lies in the question: help got to be fully embedded in the code so that it stays in line with it. For that purpose, Git is great. Let us suppose you have a feature branch on a project and you want to update it with the changes that were made on "master" branch since you forked:
me@mine:~/project$ git rebase master
Some of these changes between the two branches will be conflicting and Git will not be able to choose. It will not let you down though:
First, rewinding head to replay your work on top of it… Applying: break everything Using index info to reconstruct a base tree… M project/models.py Falling back to patching base and 3-way merge… Auto-merging project/models.py CONFLICT (content): Merge conflict in project/models.py error: Failed to merge in the changes. Patch failed at 0001 break everything The copy of the patch that failed is found in: .git/rebase-apply/patch When you have resolved this problem, run "git rebase --continue". If you prefer to skip this patch, run "git rebase --skip" instead. To check out the original branch and stop rebasing, run "git rebase --abort".
It tells you what went wrong, a possible cause and how to get out of the mess. An UI equivalent (spotted on Dribble):
Such cool empty states and 404 pages are "aesthetic sugar" around this "Show, don't tell" philosophy. Your application should be self-explanatory and not need a parallel communication channel (whether Clippy or long trainings, etc.).
That is for the usability side of the coin. Maintainability can also be enhanced by sticking to an "educational" and open design. Let us take a look at this "LEGO wall":
You see how it's built, there are no paintings over it to hide its structure, you can try to reproduce it with confidence. No last minute surprise with the blue brick.
Translating it into coding best practices:
- Comments are good, self-descriptive code that does not need them is even better
- Can you easily break the code bundle into small generic modules? Replace some parts?
- Can you modify it with confidence that there will be no side effects?
- Are affordances well designed? (aka "naming things", following conventions) so that you don't need to read the whole code to get a rough idea of what it does
A practical application
Let us try on an example and show how we dealt with globally managing user feedback in our VueJS code.
We use an API endpoint "Assign task" in two ways in our application:
- (1) You can self-assign by clicking an "Assign to me" button
- (2) You can fill a little form to assign someone else
In case of a permission error, we display a toast message for both (1) and (2):
In case of a validation error:
- It will just be a toast message for self-assignment button action (1):
- We need to embed the returned error in the form for an assignment through form (2):
For a unknown / server error, we display a toast message for both:
In this case it needs to be reported to our Sentry instance. Et caetera, et caetera.
Now imagine yourself with way more API endpoints and error types to handle. We need some kind of centralized error system management. Simplifying the problem to just handle validation errors and unknown errors, the code looks like:
- src/enums.js: the constants shared between vuex actions and components to define error handling behaviour:
const ERROR_TYPE_VALIDATION = 'validation'
const ERROR_TYPE_UNKNOWN = 'unknown'
const ERROR_HANDLING_NOTIFY = 'notify'
const ERROR_HANDLING_RAISE = 'raise'
// Error handling usual presets for a "button" action
const ERROR_PRESETS_BUTTON = {
[ERROR_TYPE_VALIDATION]: ERROR_HANDLING_NOTIFY,
[ERROR_TYPE_UNKNOWN]: ERROR_HANDLING_NOTIFY,
}
// Error handling usual presets for a "form" action
const ERROR_PRESETS_FORM = {
[ERROR_TYPE_VALIDATION]: ERROR_HANDLING_RAISE,
[ERROR_TYPE_UNKNOWN]: ERROR_HANDLING_NOTIFY,
}
- src/components/self-assign-button.vue: the "self-assign task" button. It calls the vuex action assignTask with button action presets:
this.$store.dispatch(
'assignTask',
{ data: ['me'], errorOptions: enums.ERROR_PRESETS_BUTTON },
)
// Ignore any error: it will be caught and trigger a global toast message
- src/components/assign-form.vue: the task assignment form. It calls the vuex action assignTask with form action presets:
try {
await this.$store.dispatch(
'assignTask',
{ data: ['Obiwan Kenobi'], errorOptions: enums.ERROR_PRESETS_FORM },
)
} catch (error) {
// Display validation error in the form!
}
- src/store/actions.js where our vuex actions live (of particular interest is decorateApi):
import Raven from 'raven-js'
import api from 'src/api'
import * as enums from 'src/enums'
// The action we call from "self-assign" button and form
async assignTask ({ dispatch }, { data, errorOptions } = {} ) {
dispatch(
'decorateApi',
{ endpoint: api.assignTask, args: data, errorOptions: errorOptions },
)
}
// Wrap every API call made by other actions
async decorateApi ({ dispatch }, { endpoint, args, errorOptions }) {
try {
result = await endpoint(...args)
return Promise.resolve(result)
} catch (error) {
// (1) Get API call error type
let type = null
switch (error.status) {
case 400:
type = enums.ERROR_TYPE_VALIDATION
break
case 401:
/// …
default:
type = enums.ERROR_TYPE_UNKNOWN
// Send the weird stuff to Sentry for analysis
Raven.captureException(error)
}
// (2) Notify and silence OR raise error as requested by calling component
if (errorOptions[type] === enums.ERROR_HANDLING_NOTIFY) {
return dispatch('notifyError', { error, type })
} else {
return Promise.reject(error)
}
}
}
// (1) Generate error messages
// (2) Store them in a "vuex state" queue
// Those messages will be processed by a global display component
async notifyError ({ commit }, { error, type }) {
// Implementation left as an exercise ;)
}
const actions = {
assignTask,
decorateApi,
notifyError,
}
So what's LEGO-inspired in this?
- The decorateApi action has a very simple role, making it easy to extract, test and modify
- You have presets for form and button "actions" respectively, so that it's a breeze to add new API endpoints and wire them into the app
- And nothing prevents you from overriding the proposed values in your component call (silence non critical errors completely for instance)
- You may refactor the API, vuex actions or components keeping the same interfaces. We actually did that a few times, switching from Fetch to Axios for example
That's it! Hope you enjoyed.
Take-aways
The LEGO universe offers actionable parables for designing discoverable, modular and educational systems. Together these properties naturally bring a unified flavour of usability and maintainability.
This is mostly because thinking in terms of little bricks forces us to convey very explicitly the mental models we rely on during conception. In software engineering like in many domains, communication is key! The original mix of playfulness and inherent rigidity at the heart of LEGOs may provide a healthy balance for that matter.
To be honest though, the "design philosophy" sketched here is more an afterthought than anything else. That's what happens when you're having fun. Also, it applies best to applications where empowering users in their activities is key as is the case with our collaboration tool.
On a personal note, using such an approach feels like it encompasses a lot of lessons you learn as you make mistakes in this job. It provides a sweet narrative to navigate among what I would call "developer common sense". And most of all it is enjoyable and inclusive.
A big thanks to my dear colleague Sébastien Nicolas for the help with the pictures!
[1] | There are even design workshops called "LEGO serious play". |
[2] | In case you wonder, Kevin does not exist. The author of this article did buy the box for himself. |
[3] | Not software related but I'm fond of ikeahackers. IKEA tried to shut it down at first. They soon realized such an initiative was actually building a community for free around their products. |