The Paradox of Abstraction: When Good Code is Bad Code

I felt like a freaking genius.

My code had transcended bits and bytes.

Now it was truly a work of art, worthy of awards and accolades...

...or so I thought.

My path to greatness

I was building a set of components: a Menu, a Dropdown, and an Autocomplete component.

Here is what they look like:

As I was building them I realized that they shared a lot of functionality.

They all had this "popover" element, which contained a list of items and was positioned beneath the button or input that launched it. It was the same styling across all 3 components, and had the same functionality and accessibility concerns.

And that was just the start.

I realized that about 80% of the functionality of these components could be shared across all of them. So I set out to create a whole collection of components, carefully orchestrated to minimize code duplication and to maximize code reuse.

I don't usually like to brag, but... my creation was beautiful.

It felt like I had unlocked God-mode, if only for the briefest of moments.

It all comes crashing down

I started to use these components in production. Other developers started using them too.

Soon these components were being used all over the place, in all sorts of features. It was saving developers time and frustration, and they were thankful for my brilliance (at least that's what I kept telling myself).

But this isn't a Disney movie with a fairy tale ending.

The first bug report came in.

Whenever the items displayed on the Menu changed, it would force the Menu open. Not the way it should work at all.

This happened because of some logic I had to add in order to support the Autocomplete.

It was a 5 minute fix though. Easy.

But now more and more bugs were being reported.

You couldn't type spaces into the Autocomplete component, because we needed to open the Menu when the spacebar was hit in order to make it accessible.

Okay, not too bad to fix this one either.

Then you couldn't tab out of the Menu at all. It would stay open and cover part of the page. And this behaviour affected the Autocomplete and Dropdown components too.

As more of these bugs kept coming in, I had a gut-wrenching thought:

Was this beautiful abstraction I created actually worth it?

My ego had more "important" things to worry about:

Maybe I'm not such a genius after all...

What's more, I realized my interface was wrong.

Instead of using declarative, component-based interface to define the things that go inside of the menu, I had gone with an object-based approach.

Gross.

But too late to change it now. It was being used all over production.

Asking myself the hard questions

Now I had several questions I needed to answer:

This abstraction was incredibly valuable, but was it worth all of the pain it caused?

If it wasn't, was there a better to do it?

If it was worth it, how do we deal with the problems it caused?

After ruminating on this for awhile I uncovered something interesting.

The Paradox

So here's the paradox:

The more valuable an abstraction is, the more damage it can cause.

Any abstraction increases in value when you're able to use it more frequently. It saves you time and effort because you don't have to write extra code.

This usefulness is correlated with how general an abstraction is. The more generic it is, the more applicable it is to a wider variety of situations. So it becomes more valuable.

But at the same time as you are multiplying your effort, you're also amplifying any mistakes that you've made.

If you accidentally introduced bugs into the implementation, those bugs appear everywhere the abstraction is used.

If you got the interface wrong and wrote the wrong abstraction, the headaches it causes are amplified across your codebase.

We can't have valuable abstractions without the risk that those abstractions will cause damage.

So what do we do about it?

Dealing with the paradox

I've only started to think about solutions, but this is my best advice here:

Be extremely careful when trying to generalize your code.

Think twice before creating a generic component to handle multiple use cases. It will cost you more than you think it will.