Published Apr 6, 2024

Resist the Urge to Abstract

Categories: Good Practices

Tags: #Habits , #WET , #DRY

A lot of programming involves abstractions—a lot of abstractions: classes, traits, wrapper functions and components...

Hi, my name is Elliot, and I'm a recovering abstractaholic (is that a thing?)

Early Abstractions

Like some other devs, I've gotten into the habit of abstracting early, just because I get an idea in my head of how I think an ideal abstraction for a thing I'm making will work.

This can lead to some issues:

  • Extra abstractions that each end up having only 1 dependent
  • Shared abstractions that become bloated too quickly
  • You determine an abstraction was way wrong—a little early, but also late enough that you now have to refactor both it and its dependents

A good solution I've heard about lately: Write Everything Twice (WET). It's fine if the same thing has been written only a couple of times. After the second or third time, it becomes much clearer what a good, shared abstraction might be. The implementation becomes better, and issues that may have been present the first time had more chances to be worked out.

Layers of Inheritance

In languages that object-oriented programming is the norm (like PHP), defaulting to classes is easy, and so is extending them.

Due to this, it's also easy to create a huge inheritance "tree" of deeply-nested classes. This is something that can draw complaints from both OOP and Functional programming crowds (though the latter tends to be fully against OOP-like programming anyway).

Shared behaviors are helpful in a lot of ways, but more nesting can just produce more clutter and unnecessarily tight coupling. Luckily, there are ways to trim this down to a more maintainable degree:

  • Avoiding early abstraction: This side effect wasn't listed above, but early abstraction can result in unnecessary extra nesting.
  • Use traits: Traits can be used to add behavior without extending. If a shared solution can exist fully in a trait, multiple classes can just use that trait, sidestepping more nesting or reducing ancestor bloat. You may have heard the phrase "composition over inheritance". This refers to composing class behavior using traits rather than extending classes.
  • Use interfaces: Maybe you have multiple classes that need some similar methods, but they don't need a shared implementation of those methods. Create an interface instead and have these classes implement it. Then those methods will have to exist on the classes, and only the classes will have to worry about how the methods actually work.

Overly-abstract Abstractions

This has some shared elements and pitfalls of the other 2. When abstracting, you're usually trying to encapsulate common/shared behaviors. Sometimes that can go too far.

For example, I'm sure all front-end devs have written wrapper components in a framework like React or Vue. In those, it's common to write wrapper components that add extra behaviors or even just some common defaults.

In doing this, every now and then I've found myself making my wrappers too generalized. Doing this can get you very close to reinventing the thing you're wrapping—a vanilla HTML element or even a third-party UI library component.

I'm not really sure what antidotes exist for this. But I remember the last time I did this, I undid some of my wrapper components and just wrote the HTML—basically a reset. The front-end component I was writing had the logic it needed anyway, so it was fine letting it worry about what it needed to render.


When tempted to reach for an abstraction, just wait a little longer to see if something better comes along. You'll end up having to refactor something later anyway, but it'll be simpler to refactor basic code patterns that have repeated a couple of times rather than abstractions that were incorrect—especially if those bad abstractions have become pervasive in a short time.

Thanks for reading!

Please confirm whether you would like to allow tracking cookies on this website, in accordance with its privacy policy.