Abstraction is just hiding some complexity in implementation details behind a simpler interface. It offers benefits from reducing the need to deal with the full complexity everywhere else. It also has costs. The interface establishes a new concept, albeit a simpler one, that must also be understood and maintained wherever client code uses the abstraction. Moreover, if you need to understand or modify the detailed implementation later, there is now a barrier to doing so.
When we define an abstraction, hopefully we do so because the benefits outweigh the costs at that time. The simpler the interface relative to the complexity of the implementation it hides, the more likely this is to be true. However, that balance is inevitably subject to change as a program evolves and the relevance of the hidden details to different parts of the system changes.
So it feels like abstractions might be better characterised by whether they represent good value under the current circumstances. It is perfectly reasonable for an abstraction to be cost-effective at the time it is added, but to become more or less so as the context evolves. If it reaches a point where it is no longer cost-effective, it should be removed. Either the relevant parts of its implementation can then be inlined at each place that previously used it, or some new abstraction(s) can be defined that better reflect the relevance of different implementation details at that time.
I was under the impression this was about generalizing the abstraction to something reusable.
I.e. calculate_vat will always be used by the same thing and there will only ever be one of them.
The generalization would be:
interface SalesTaxCalculator {
func calculate_sales_tax
}
This kind of abstraction is more uncommon.What im guessing the article is struggling with is that making abstractions like salestax calculation os usually kind of useless, there is only 1-3 types of sales tax calculation and its best defined inline next to the proce calculation or put in a function like calculate_vat.
A lot of developers use abstractions to remove code duplication.
Its more fun using this kind of code than code where devs dont try to remove duplicstion but i just wish instead of seeing duplication as evil, they instead saw it a sign that their design is wrong.
If youre seeing duplication, there is an abstraction there, whether it is worth it to implement is usually more to do with if its just hiding code duplication or if it is creating a new concept that hides implementation details.
But I think there are lots of abstractions often missing from code that I have seen that don't need any sort of polymorphism. A common example is performing input validation all over the place instead of at the boundary with some sort of form object that only accepts valid params.
Input validation, and any abstraction that smells like a framework or library make great code folds for abstraction.
The question is all about cost. Does a single layer abstraction, at the boundaries, with clear inputs and outputs have much of a cost?
I don't think so.
Does forcing vat calculation into a new class far away from the widget price class have a cost? I think so, the cost is having the same concern separated for no material gain other than perhaps reducing one form of duplication for the cost of creating another.
I think you're right that it may not be necessary to extract "calculate_vat" into another class. It just really depends: if the VAT code is so complex that it should be properly unit tested, I think it deserves its own class. If it's fairly straightforward and only used inside of a single other price calculation class, then yeah, it can remain a private method or so.
But I don't think this is a question of whether to abstract or not, rather about what the right level of cohesion and coupling is for a given situation.
But in my daily work, I routinely see abstractions just come up very naturally all the time. Sometimes they turn out to be slightly wrong, but often also not. I need to perform some calculation (e.g. for billing)? That can be abstracted away. I need to parse some unstructured user request into something structured? That's an abstraction. And so on. A lot of these things are clear to me even before I start writing code.
I also tend to use (at least some amount of) DDD, to write small, composable functions with few side effects and to be as declarative as possible. All of this might help with coming up with lasting abstractions.
But I'm not denying that I'm running into lots of situations in my daily work where it turns out some abstraction was wrong. Just that I find that many more of them actually turn out to be correct or at least correct for the most part (it might be that something needs to receive an additional parameter or to returns some slightly different structure to account for error conditions or so, but it's still basically the same abstraction).