Software developers want to make abstractions, and understandably! It is an intellectual effort to do so, and a craft to elegantly implement. The reasoning behind the abstractions is to make life easier for other software developers – an admirable goal. But there are some things we tend to forget in this process as software developers.
The first thing is that learning abstraction is like learning a new language. Programming languages by themselves are abstractions that are composed. Creating components, functions, classes, etc. can be seen as a means of extending this programming language. They ought to be learned, and in teams, it is a real effort to disseminate and align the use of abstraction.
The next is that some abstractions do not scale with future use cases. An example could be to make a modal overlay that insists on getting a title, a dismiss callback, and a confirm callback as parameters. When writing it, it might seem like a good idea but becomes detrimental the second a developer is making a modal that does not fit that specification. One that does not require the confirm button. Instead, the modal should do only one thing – the modal, and take the entire contents as a child trusting the user of the component to be able to implement the design spec.
The third, and potentially most important, is that abstractions rarely embrace the way we add features, patch bugs, or generally change a codebase. When a developer sees an error on, eg. a website, the first step is to locate the error in the code. One was is to find some text close to the error and search through the codebase for an exact match. This is incredibly effective when the buggy code is also close to the found match. But when layers upon layers of abstractions are applied, it can quite some time locate the buggy code. Finding out that the code is re-used in 3 other places in the codebase can make it virtually impossible to fix without the risk of breaking the code in other places.
All this is not to say that we should not abstract. It is just to say, that we need to be careful when doing so. A good place to start is abstraction elements that have broad consensus. It is like writing an article. One is free to make up their own sections, but following the consensus of abstract, introduction, discussion, related works, conclusion just makes it easy for people to read.
Using well-known abstractions like models, views, services, components, and the like for specific frameworks equally makes things easier. And then, when in doubt, do not abstract, don’t be shy of repeating yourself, and equally don’t shy of refactoring when the right abstraction immerges.
And lastly, we should embrace the way we know that developers are changing the codebase, not the way we want them to change it. Developers seldomly have context on the entire code base, and as such, we should not create too great of an abstraction distance from the place bugs are realized to the place they are implemented.