One of the earliest and most fundamental principle one learns while coding is programming by interface.
Definition
Interface-based programming defines the application as a collection of components, in which Application Programming Interface (API) calls between components may only be made through abstract interfaces, not concrete classes. Instances of classes will generally be obtained through other interfaces using techniques such as the Factory pattern.
https://en.wikipedia.org/wiki/Interface-based_programming
For example, the following signature is not compliant as it uses a concrete class:
<T> ArrayList<T> foo();
The compliant signature should be:
<T> List<T> foo();
The rationale behind this design is that the caller shouldn’t be concerned about the concretion, but only about the contract. Hence, the provider implementation can change without impacting the caller code. Programming by interface helps avoid the ripple effect, where a codebase change in a specific place will require a change in a lot of other places.
The tip of the iceberg
I teach it in my courses, and I’m among the first to point it out in code reviews. Recently, however, I started to ponder if it should be as ubiquitous as it is.
Getting back to the previous example:
if the signature references List
, the implementing code may return ArrayList
, LinkedList
or any implementation that conforms to List
for that matter.
It for sure will compile in all those cases.
However, switching the returned implementation might have other side-effects.
You might know that ArrayList
is backed by an array, while LinkedList
is backed by Node
objects being referenced by one another.
Both offer different performance results, depending on the operation being performed.
Data structure |
Time complexity |
|||||||
---|---|---|---|---|---|---|---|---|
Average |
Worst |
|||||||
Access |
Search |
Insertion |
Deletion |
Access |
Search |
Insertion |
Deletion |
|
Array |
θ(1) |
θ(n) |
θ(n) |
θ(n) |
O(1) |
O(n) |
O(n) |
O(n) |
Doubly-Linked List |
θ(n) |
θ(n) |
θ(1) |
θ(1) |
O(n) |
O(n) |
O(1) |
O(1) |
Hence, the exact concretion returned might be an implementation detail - or not.
Not a mere paper cut
It’s easy to discard the previous issue: it’s pretty straightforward to look at the code and check the implementation.
And yet, this check doesn’t imply that the dependency won’t change. Worse, the above example only brushes at the top of the iceberg. Another case involves core principles of OOP:
Both guidelines naturally lead to the Decorator pattern:
As an example, let’s use the Scalar
hierarchy from the Cactoos library:
This design follows the above principles.
It makes it very simple to compose different Scalar
objects e.g.:
Scalar scalar = new StickyScalar<String>(
new RetryScalar<String>(
new Scalar<String>() {
@Override
public String getValue() {
// Get the value from a network call
}
}
, 3)
);
All those nested decorator layers make it quite difficult to understand properties about the value.
For example, in the above snippet, the StickyScalar
keeps the value once it’s read successfully once.
Underneath, there are 3 attempts to read from the network.
Just knowing a variable contains a Scalar
won’t probably be enough in most cases:
some return the same value over time, while others do not, etc.
Conclusion
It’s considered a good practice to handle abstractions over concretions.
In system-GUI, the most used example is to return a Window
reference, knowing it will be a UnixWindow
on Unix systems and a WindowsWindow
on Windows ones.
In that case - and some other, it’s the right thing to do.
In other cases, however, it’s not. The right type to manage is not the interface but a more concrete type.