It fails randomly.
It breaks in strange places.
It has regression bugs.
And no one wants to change it (for fear that it will become their code!).
How did the web become a breeding ground for both simple interfaces and complicated code? To make things worse, this complicated code is located in a codebase that changes all the time! It is very hard to get an interface right in a one swift commit. It requires successive refinement based on customer feedback. Although this code changes all the time, it is often the hardest part to maintain.
Corey Haines wrote an awesome book called Understanding the Four Rules of Simple Design based on Kent Beck’s 4 rules of simple design, which I highly recommend. Inspired by this book, I tried to distill UI complexity and found 4 rules of simple UI components that should help us write better, simpler interfaces. This is nothing new, but somehow it seems to have been forgotten along the way.
Rule #1: Break it down into named components
Thou shalt not underestimate the power of good naming
This is the simplest, yet most powerful rule of software design. Whether it is to discuss with the client or to understand each other when saying “the thingy” broke, good naming has the virtue of bringing people together. Picking a good name today might even help your colleague better decouple code in 6 months. This is what Corey Haines calls behavior attractors in his book. It is easier to see where your new code should go when things are properly named.
For UI design, this boils down to sitting with the client and figuring out a name for each thing. What is this portion of the page called? This new kind of input, what should we call it? Name small things, agglomerate them, then name bigger things! If you want, you can follow principles from Brad Frost’s atomic design, which is a good starting point. Instead of saying “the blue bar in the viewer thingy is overlapping with the other side”, you could say that “the document toolbar in the patient history viewer is overlapping with the annotation explorer“. Once the vocabulary is agreed upon, this makes your code less prone to bugs and misunderstandings with the customers.
In order to name components, you must break them down to a manageable size. If not, you maybe end up with names that include multiple “and’s” or end with “Util”. Be careful to name components according to their level of abstraction. If you name something “Intervention Plan”, I expect it to be high level and be broken down into smaller components. Maybe an “Intervention Plan” is made up of “Observations” and “Directives”. Maybe “Observations” is made from an “Action Bar” and an “Observations Grid”. Notice how “Observations” and “Observation Grid” differ in their level of abstraction, this is important.
If you need to spark your imagination, I suggest you read this article from J.B. Rainsberger.
Rule #2: Do not assume your context
Thou shalt not care to know who thy DOM parent is
Now that you have components of decent sizes and with good names (don’t worry, this takes time to master), you might realize that there are far more opportunities for reusability than you ever envisioned. Read the remainder of this article to be ready for this!
DRY (don’t repeat yourself) is a good principle to adhere to, but components are not reusable by default; they must be made that way. The first thing you must be careful about is to make your components as context-agnostic as possible. After all, how reusable can a component be if it requires a specific context? Picture 40 components requiring 40 different contexts. Are you crying yet? If not, just know that in most cases, those contexts are neither obvious nor documented.
Do not worry, there is a simple way to prevent this! First of all, simply do not make use of your DOM parents or siblings! Imagine your components as having a “root” DOM node. You are not allowed to go upwards in the DOM past this point. Until web components are finalized and included in all major browsers, you may have to use discipline not to cross this boundary, but this can be done!
Now, this raises the question: how can we update a sibling or parent component? Think about it. It is OK for the parent to know about its immediate children, but a child should not know about its parent, yet it has to send a message to its parent. What pattern comes to mind?
Rule #3: Give complete privacy to your children
Thou shalt not covet thy children’s DOM
So far, we have named components that can be included anywhere in the DOM. Okily dokily! This is all fine and dandy, but chances are you will need components that are made from other lower level components (again, refer to atomic design for inspiration here!). That’s a good thing, but there is one strict rule: do not query your children’s DOM! I mean it. This is where most ripple effects start! You change one thing in that observations grid and somehow you end up breaking something else without noticing it. Or you do notice it and spend the next hour fixing all those places where they used that CSS class you just removed. Ouch!
This is fairly easy to avoid. However, as I said before, until web components are finalized, you may need to do this out of discipline. The reward you get in return is very high.
We now have clear boundaries for what a component can and cannot do. You are not allowed to explore the DOM upwards past your root node and you are not allowed to drill down in the DOM past your children’s root nodes.
This is nothing new. This is what encapsulation is all about! This concept has been around for as long as most of us can remember, yet we sometimes forget the consequences of not respecting encapsulation. Let the action toolbar know whether buttons are arranged with an <li> or a <div>. Instead, just call
.addButton(myButton) on it. Do not interfere with the action toolbar’s affairs and let it handle its own buttons: it is its job! Now you have no reason to have duplicated jQuery selectors! All ripple effects from changing the DOM structure should now be contained within the boundaries of the component that manages it.
Oh, one last thing before one of you tries to hack this! Creating a method
.getButtonElement() is not the way to go! Remember Tell, Don’t Ask? If you communicate with your parents using events, you communicate with your children using plain old message passing (i.e. method calls).
Rule #4: See the world from what you are given
Thou shalt assuage with what hast been given
Luckily, this is easily fixed in most codebases. Remember that your goal when refactoring is to work in small increments and keep the tests passing (you have tests, right?). If a component uses a global variable, just turn it into a method or constructor parameter instead. Then, whoever uses that component just passes in the global variable. Now fix those higher level components too. Rinse and repeat until you find the point of origin, then make an educated decision about where the object should reside.
This may seem complicated, but it is not. Now your components have explicit dependencies instead of requiring an implicit context. You improved the readability and reusability of your code, you should be proud! You will thank yourself in six months time.
What you should take home from all this
It sounds simple, doesn’t it? It should. Find good names, use events to decouple from your parents, delegate to your children and respect encapsulation, and make dependencies explicit. That’s it! This is not new nor ground breaking, but it should get you started on an happier path. Most of these principles are simply based on known best practices like SOLID principles or atomic design and none of this is web-specific. User interfaces have been around for ages and I think web developers should read more about the accumulated knowledge drawn from these years of experience.
Oh, also, here is one last bonus for you! You now have components that are trivial to test! Try it out, you might be amazed.