The value of abstractions
In software systems, maintenance quickly becomes harder as more components are added. When a component deteriorates, it should be possible to refactor or replace it without affecting many other parts of the system.
Following this principle, each component needs a clear separation of concern with a clear interface, and without leaky abstractions.
Minimize the cost of change
Over time, implementations and underlying dependencies will change, but the interfaces should hardly change. Adhering to the Interface Segregation Principle ensures that changes to a component's implementation have minimal impact on other components in the system.
An interface should be more dependent on the code that calls it than the code that implements it.
Let's take an example component "C" in our system. The deeper C is everywhere in the system, the more often it is used, the more important its interface becomes. It may help to ask ourselves questions like this about C:
- How hard is it to refactor C without affecting other components?
- How hard is it to replace C within the structure later on?
- How hard is it to replace C's external dependency?
- How to build the next feature with an alternative of C?
The harder it is to do these things, the more coupled the components are to C, and the harder it is to maintain the project. As such, the more value it has to add the right abstraction for C.
The more expensive it is to refactor or replace a component in the system, the more value it has to design an interface to abstract the implementation away.
The hard part is to figure out what components in the system need an abstraction, and to design their interfaces. Although this requires research and preparation upfront, it pays off in the long run by reducing maintenance complexity and facilitating system evolution.
Even with great interfaces and abstractions, other developers may be unaware of them, or simply decide not to use them. Code linters can be very effective here.
ESLint's no-restricted-imports and the @nrwl/nx/enforce-module-boundaries rule to enforce boundaries in Nx projects prevent direct imports of modules or dependencies with different interfaces. If a dependency (external or internal) is banned, the linter will yell when a developer tries to import it directly.
When properly configured, tools like this effectively encourage developers to think about the situation, look for better solutions, and maintain component decoupling.
Here are some other articles about related programming principles: