In his book Patterns of Software : Tales from the Software Community, Richard Gabriel gives his definition of software abstraction ..
Abstraction in programming is the process of identifying common patterns that have systematic variations; an abstraction represents the common pattern and provides a means for specifying which variation to use.
The two phrases which appear twice in the above definition are common pattern and variation - undoubtedly these are the hotspots which define what an abstraction is. At one point in time (may be even today), OO community had personalized the concept of abstractions with their own style of designing - separating the interface from the implementation was thought to be known as the only way of software abstraction around. I have seen lots of codebase where PublishedInterfaces were just mimicking the implementation classes. This is definitely not a good interface design where the interface has a strict coupling with the implementation. As Martin Fowler mentions in InterfaceImplementationPair, Interfaces should be designed around your clients' needs.
The main problem with abstraction in software arises when we take them too far. Gabriel observes
This (taking abstraction too far) results in code that is structured like a big web, in which groups of abstractions are tightly interwoven with others by means of interfacial glue.
A big abstraction loses its reusability, has traces of implementation knowledge sneaked into it. The big bloat is not Habitable and does not support Piecemeal Growth. One of the main forces behind designing abstractions is to identify the appropriate granularity, one which will make them composable and allow piecemeal growth of small and shallow class hierarchies.
One of the salient characteristics of good abstractions is extensibility. Bertrand Meyer professes the Open Closed Principle, where the abstractions should be open for extension but closed for invasive changes. Uncle Bob has a nice article on this. Many of the GOF design patterns support designing extensible abstractions.
- If you want to decouple an abstraction from the implementation and allow independent extensibility of both hierarchies, use the Bridge Design Pattern.
- If you want to decouple the algorithm from its user, go for the Strategy Pattern.
- If you want to encapsulate the state of an object and allow transparent access of the object to its clients, use the State Design Pattern.
Sometimes, though, extensibility may appear to be orthogonal to efficiency, as Martin D. Carroll and John F. Isner emphasizes when discussing the design of the C++ Standard Components ..
Classes can only be made extensible in certain directions, where each of these directions is consciously chosen (and programmed in) by the designer of the class. Class libraries which claim to be “fully extensible” are making an extravagant claim which frequently does not hold up in practice. . . . There is absolutely no reason to sacrifice efficiency for an elusive kind of “extensibility.” (Carroll and Isner 1992) - source Gabriel [PatternsOfSoftware].
It's a nice idea to have an interface published to clients, while you go on switching your implementations merrily. But what happens if the abstraction is found to be wrong - repairing the abstraction implies repair of all its uses. This can often have huge consequences, since your published abstraction is now littered all over. The software development community is split in this regard - whether to make interfaces immutable. The school of people who consider interfaces to be immutable make use of the Extension Object design pattern while designing interfaces. The other group of people have switched to abstract classes and provide default implementations for the newly added methods. In an interview with Bill Venners, Erich Gamma has the following observation to make ..
Since changing interfaces breaks clients you should consider them as immutable once you've published them. As a consequence when adding a new method to an interface you have to do so in a separate interface. In Eclipse we take API stability seriously and for this reason you will find so called I*2 interfaces like IMarkerResolution2 or IWorkbenchPart2 in our APIs. These interfaces add methods to the base interfaces IMarkerResolution and IWorkbenchPart. Since the additions are done in separate extension interfaces you do not break the clients. However, there is now some burden on the caller in that they need to determine at run- time whether an object implements a particular extension interface.
In the next entry of this series I will discuss how good abstractions can be turned into beautiful programming artifacts through the model of computation of a programming language. Plug in some of the design patterns to model the commonality and variability and you have the three pillars of software nirvana - compression, habitability and piecemeal growth !!