Use interface, not leaky abstraction.
The key to program modularity and extensibility is interface, not leaky abstraction. Software must be either a kernel or a component in the form of plugins, modules, libraries or packages.
To illustrate, I will give examples of interface:
- operating system – interface to the physical machine
- hypervisor, container (Docker, Solaris Zones) – interface to the operating system
- SQL – interface to relational databases
- NPM (Node Package Manager) – module interface in Node
- jQuery – interface for DOM, AJAX, etc
- composition – interface inheritance (see composition vs implementation inheritance below)
Examples of leaky abstraction
- ORM (object/relational mapping) – leaks to SQL
- .NET, JVM – leaks to operating system
- implementation inheritance – ask the Go language designers
The problem with leaky abstraction is that they only provide a facade instead of a well-designed interface to the underlying software.
There are two issues with this scenario.
- the underlying software is a moving target so the leaky abstraction must keep up with the changes, so now you have to track and maintain two different sets of software
- backward incompatibility is a fact of life in the software world but that is not the point. The issue is, leaky abstraction won’t solve all use cases so for edge cases, you have no option but to dig the underlying software
But leaky abstractions are not the same as tight-coupling though they are related.
The direction of “eventual functionality” between these two are just polar opposites.
For leaky abstraction, its eventual functionality is downward.
To illustrate, when a leaky abstraction cannot provide the functionality you want and it does not offer a way to write an extension, you are forced to dig deeper below the layer of abstraction. For example, when an ORM does not cover your use case, you will eventually resort to SQL, so why not just use SQL in the first place? The notion of abstracting away many relational databases is a fallacy. That is not going to happen because your application is tightly coupled to your data store. In case of NoSQL, why not just use its native query
language? The choice of data storage software is a high-level architectural decision that is left to those who know what they are doing.
Another example of leaky abstractions are client-side frameworks (CSF) like Angular and Ember. Ok, there are extensibility avenues but plugins designed for one framework are not going to work with another. You have to port them. When there is porting involved, there are two issues:
1. lock-in – now you are tightly coupled not just programmatically but mentally as well
2. interoperability – when plugins are not interoperable, turf wars are inevitable
In contrast, consider the elegant solution of interface compared with leaky abstraction.
For interface, its eventual functionality is upward.
I will an example why an interface trumps leaky abstraction.
CommonJS as implemented in NPM
The community came up with two major specifications:
According to Addy Osmani, AMD adopts a browser-first approach to tackle the problems of module system and dependency management. CommonJS on the other hand takes the server-first approach.
Node.js adopted the CommonJS specification and implemented it with NPM (Node Package Manager). NPM solves the problem elegantly and has an ever growing number of modules (called packages). At the time of writing, the NPM repository has over 90,000 packages. The fact that Node included NPM as a built-in module says a lot about its architecture and implementation.
On the other hand, these are the experiences of those using AMD.
The list goes on and on. As Einstein said,
In theory, theory and practice are the same. In practice, they are not.
Well, he is damned right about AMD.
Let’s dissect the real issues.
First, dependency management is hard. You should not do it manually. Let the algorithm do it for you automatically using a well-defined interface (syntax) and a few rules
Second, compare NPM repository with Jam
Third, consider the case of using the actual module. If there is too much ceremony as Tom Dale pointed out, do not count me in
Now, consider how NPM solved those issues intuitively.
- NPM is a package manager with a few rules
- NPM has a bustling repository
- NPM has a simple syntax when importing modules
So back to the issue of interface versus leaky abstraction.
Both CommonJS and AMD are specifications. NPM and RequireJS are implementations of CommonJS and AMD respectively.
So what’s the problem?
The problem is that AMD is a leaky abstraction because dependency management is based on configuration.
With NPM, you can segment your business logic into folders under node_modules and it will resolve the dependencies automatically.
So how can you use those NPM modules in the browser?
Simple. Through Browserify!
You see, Browserify does not have to reinvent the 3-in-1 awesomeness of NPM (package manager, repository and clean interface).
Since Browserify implements the Node.js module resolve algorithm, you can use the same NPM modules
in the server and export them for use in the browser.
This is what I mean by interface. An interface is not concerned with the underlying implementation. If the syntax or interface being used by NPM modules works in the server, it should also work in the browser. And that’s what Browserify does.
Composition vs implementation inheritance
Excerpt from Golang:
Object-oriented programming provides a powerful insight: that the behavior of data can be generalized independently of the representation of that data. The model works best when the behavior (method set) is fixed, but once you subclass a type and add a method, the behaviors are no longer identical. If instead the set of behaviors is fixed, such as in Go’s statically defined interfaces, the uniformity of behavior enables data and programs to be composed uniformly, orthogonally, and safely.
Composition lends itself to better interfaces rather than dependency injection.