The triangle of abstractions
We as developers always look for the perfect abstractions. But this simply does not exist. It's always about trade-offs. Interface simplicity, reusability of the abstraction and the scope of the abstractions are the different goals that span a triangle. And as it has to be with triangles in software design, you can only pick 2 of the 3 factors.
iJS in Munic
Last week, the international JavaScript conference took place in Munic. I had the honor to give not only one, but two talks. One about the rules of React and how they changed with React's concurrent features, and the other was about software abstractions and how you can find the right one. While preparing this, thought a lot about what makes a good abstraction. So I tried to imagine the perfect abstraction that had a super simple interface so that it is very easy to understand and use, a very large scope so that it takes care of lots of complexities and is highly reusable for different kinds of use cases. And thus, the triangle of abstractions was created.
Examples of extremes
After I could not find any example for an abstraction that covers all 3 points, it was quite easy to find something for all combinations of 2 of the 3 dimensions.
- Large scope and reusable: A data grid component — Lots of config options; very large feature set; lots of complexities hidden from you; huge, horrible interfaces
- Highly reusable, simple interface: An icon component — Can already be very useful with only one config option: the icon to display. Can be used in all sorts of cases that need to display an icon. You could have hundreds of icon instances across your whole app
- Very simple interface and large scope: The App component or your main function — Mostly no config options at all. One function call, and suddenly you have Excel Online, or Figma, or Google Maps! But, using the
<GoogleMaps />
component will probably not help you building another Figma competitor, so the reusabilty is low.
So which location within the triangle is the best? As always, it depends. All examples are perfectly fine to have in your app. So depending on the use case, you as developers need to make decisions.
One simple decision with three options
That means, whenever you get a new ticket, that needs to increase the scope of a module (how cool would it be, if we got tickets more often that reduce scope?), you have 3 options:
Increase scope and decrease reusability: Maybe that's fine, because you don't need to reuse this and by moving this feature into your components, all your call sites suddenly get much more powerful.
Increase scope and maintain reusability by adding more config options: Could also be fine. Maybe you are building a library and this new use case came up that you simply forgot during the initial design and now you need to increase the surface area of your interface by adding one more flag.
Don't increase scope: Luckily, this is also an option. Instead of increasing the scope of one abstraction further and further, you can also say no because maybe it's time to create a separate function/module/component that handles this new use case so that you don't need to touch any existing modules.
No silver bullet
As always in software design, there is no best option. It's always about trade-offs. So when think about adding a feature to a library, I hope you think of the triangle of abstraction to make a conscious decision about the direction that you want to go.