Where and how to use abstraction
Abstraction is one if the first things a developer learns when starting programming. It's repeated again and again from "Clean Code" to every other introduction book. It's so ingrained, that we don't really question why.
As most developers I also started to use abstraction everywhere. Abstraction as a goal in and of itself. After a while I ran into more and more problems though. The problems got bigger and bugs harder to solve. I remember one case where it took us 4 hours to find out why an action could be performed in one case where it shouldn't and another day to be be sure that changing it didn't break anything else using the validation.
As an overreaction (what I only now realize it was), I went in the opposite direction. Using abstraction as little as possible. The problems got smaller and the bugs way easier and faster to solve. The downside though was that refactoring became way more work.
Way to late I realized that you could think about abstraction like you can about every other concept in development. Meaning: There are no silver bullets and every concept has advantages and disadvantages. So let's see what they are for this concept called "abstraction":
Advantages when done right:
- Less repetition and therefore less surface for errors.
- Encapsulation of logic which means there is less code to understand at once.
Disadvantages when done right:
- Increases complexity due to abstraction overhead.
- When changing the abstraction you must be sure that there are no unintended side effects.
As you might already know from experience the "when done right" suffix is very important here. When done wrong, only "less repetition" might be left but the code might not be easier to understand, the complexity gives you headaches and changing the abstraction breaks the usages in some places you didn't touch.
So if there aren't simple advantages and disadvantages that are always realized, maybe it's more a question on where and how to use it. The conditions that come to mind which must apply at least:
- Independent from where it's used.
- Only changes rarely, if ever.
- If changed, the change is relevant everywhere it's used.
Examples that come to mind where that's the case:
- A value object for an email address. The email address is always validated on construction and an email address must be the same throughout the system. If the validation for a correct email address changes, it must not be different anywhere in the system.
- A validation for valid HTML elements that can be used within a CMS. There only would need to be one instance where a script tag is suddenly allowed to be a problem.
- An input element for an email address where the input element would only return a valid email address or null. Again the email address validation must be streamlined throughout the system. Additionally stripping whitespaces from the wrapped text input prevents the need for the using component to applying those transformations only to see if the input is filled or not.
Examples where that's not the case:
- Business logic to see if someone is able to perform an action. The conditions for that might change often and using the same abstraction in multiple places means you have to check all other cases, as there might not be any connection between the separate processes using the validation.
- Application helpers that encapsulate logic by moving the part that is repeated without separating the concerns.