Abstraction
Abstraction is a foundational concept in software design. The term is used as both a verb and a noun.
As a verb, abstraction is the process of distilling an element down to its essentials, stripping away incidental details that are not relevant to a particular purpose. This is similar to the concept of generalization.
As a noun, an abstraction is a simplification of something more complicated going on underneath. More concretely, an abstraction is a component that provides a simplified interface to one or more other components while hiding the implementation details. In other words, an abstraction provides a view to one or more components, where the view focuses on only the information that is relevant to one particular use case for the abstracted components.
An abstraction draws a line between the what and the how. On one side is the public surface – the interface and contract that callers depend on. On the other side is the implementation, which is free to change as long as the contract still holds.
The goal of abstraction is for building blocks to depend on abstractions of other building blocks, rather than the concrete implementations of those dependencies. The main purpose for doing this is to help people reason about a system by hiding complexity from them. By hiding details behind stable interfaces, abstractions allow a system to be understood component-by-component. This is the basis of modular design, which in turn supports decoupling and code reuse, allowing components to be more easily changed independently of one another, so improving the overall maintainability and extensibility of systems.
Abstraction is synonymous with information hiding and is related to, but distinct from, encapsulation. Abstraction is about choosing which details to expose. Encapsulation is about enforcing the boundary so that the hidden details stay hidden. The two work together. An abstraction defines the contract, and encapsulation protects it.
All programming languages provide mechanisms for abstraction, but methods of abstraction are strongest in object-oriented programming languages. Classes are the built-in mechanism for abstraction. A class hides its implementation details and exposes only the operations that callers need. The same idea underlies dependency inversion, which directs both high-level and low-level modules to depend on abstractions rather than on concrete implementations.
Abstractions exist at every level of a system architecture. Indeed, everything in information systems is an abstraction of some kind. SQL is an abstraction for relational databases. A layered system design consists of stacked abstractions, each layer presenting a simpler model to the one above.
Design principles and trade-offs
The strength of an abstraction lies in choosing the right details to keep and the right ones to drop. A good abstraction captures what is essential to the problem and discards what is merely incidental. It sits at a level that is useful to its callers — not so detailed that it leaks the implementation and not so coarse that it hides what callers need to know. An abstraction that serves one caller well can serve another poorly.
A related principle is to encapsulate what changes. This means to hide behind an interface the most volatile parts — elements that are likely to change in the future. This is how systems are protected from extensive refactorings when any one part of them changes.
Applying the principle of conceptual integrity keeps a system’s abstractions consistent with one another, so that each one feels like part of the same design language.
Abstraction is not free. Every layer of indirection pushes the running code one step further from the machine, adding overhead and making behavior harder to trace when something goes wrong. Abstractions hide the cost of the operations behind their interfaces. Stacked abstractions accumulate such costs. The general advise is to keep abstractions thin, just sufficient to serve their purpose.
There is also a cost in abstracting too early. An abstraction built before a problem is understood tends to capture the wrong essentials, locking incidental detail into a contract that callers come to depend on. YAGNI is the principle of deferring an abstraction until there is a real second case to generalize from.
As a general rule-of-thumb, abstractions are best discovered by factoring them out of working code, rather than designed them in ahead of time.
No abstraction is perfect. Every simplification discards some detail, and there are times when the discarded detail forces its way back to the surface, and the abstraction is said to become leaky.