A reading from the Book of Design Patterns.
"18:3. And John sat and he programmed and he released. And was heard a great sound of huge joy. But lo the Customer was fickle and did then change the requirements and John, crestfallen, was made to program again. 'Customer,' he cried, 'Why have you forsaken our roadmap? Why must you with very wickedness remove the feature specified only to demand this architecture-dildo in its place?'
"Then the air filled with ferocity and light and the Customer thundered, 'I spoke unto you of decoupling yet you delivered mud. I spoke unto you of interface yet you delivered implementation. I spoke unto you of change yet you delivered rigidity most frozen. Lovin' the fixed-price contract yet, beeatch?"
This is the word of the Customer.
Thanks be to GoF.
Actually, section 1.6, page 18, paragraph 3 of the Gang-of-Four's Design Patterns reads:
"There are two benefits to manipulating objects solely in terms of the interface defined by abstract classes:
"1. Clients remain unaware of the specific types of objects they use, as long as the objects adhere to the interface that the clients expect.
"2. Clients remain unaware of the classes that implement those objects. Clients only know about the abstract class(es) defining the interface.
"This so greatly reduces implementation dependencies between subsystems that it leads to the following principle of reusable object-oriented design: Program to an interface, not an implementation."
(Hence the title, by the way.)
Here we find one of the most powerful, most well-respected principles of Java source-code dependency-management.
Why do we care about about source-code dependencies?
One of the reasons is ripple-effect: a change to one piece of software causes a change somewhere else, which in turn causes a change somewhere else, etc. All those changes incur costs. Poorly-structured systems obscure and complect dependencies, incubating ripple-effects and hence reducing the predictability of potential costs. Well-structured systems, with clear and manicured dependencies, aid this predictability.
Reducing the number of implementation dependencies contributes to reducing the probability of ripple-effects. Design Patterns above claims that programming to an interface and not an implementation reduces implementation dependencies, and thus it follows that ptainai reduces the probability of ripple-effects.
But is this true?
That old chestnut: an interface is the general term for the set of requests to which an entity can respond, whereas a Java interface (that's in italics, so if you see normal font there then you are going to be one confused puppy) is defined as a, " ... reference type, similar to a class, that can contain only constants, method signatures, and nested types. There are no method bodies."
Both interface and interface overlap, of course, yet enjoy sufficient distinction to allow a rephrasing of our central question: does an interface reduce the probability of ripple-effects more than an interface that is not an interface?
More concretely, let us say that we have two packages, p1
and p2
, and p1
has some source-code dependencies on p2
. Is p1
less prone to ripple-effects from p2
if it depends on a set of interfaces in p2
rather than depending instead on a set of classes in p2
?
To understand how ripple-effect might differentiate between the two we must examine the changes that can occur in p2
and might leak out into p1
. Five categories present themselves.
Firstly, some entirely new functionality might be implemented in p2
causing it to export a new set of method signatures either in the existing interfaces/classes or as a new interfaces/classes.
Clearly, whether the original interface was an interface or a class places no necessary restriction on this new functionality. To make use of it the client must change. So having p2
export an interface rather than a class does not reduce the probability of ripple-effect in this case.
Secondly, we might remove some functionality from p2
. Here, if p1
depends on the removed functionality, be it interface or class, then ripple-effect will generate cost. This case, then, also fails to distinguish between the two, though it well highlights why functionality should always expose a minimum interface.
Thirdly, the signature of a method in the existing p2
functionality might change. Yet again, whether this signature belongs to an interface or class makes no difference: the client that depends on the changed signature must change.
Fourthly, a method body in the existing p2
functionality might change. Perhaps here the interface can coast to victory given that interfaces, unlike classes, contain no method bodies and thus interfaces stand immune to an entire category of change to which classes lie susceptible.
Not so fast.
For methods in Java classes are procedurally abstracted, only accessible to clients via their signatures: a client cannot chose an entry point into a called method and execute an arbitrary operation therein, it can only invoke the signature. So if the change to the method body fails to change the signature then that change remains invisible to client methods.
Of course the change to the method body might cause the signature to change or indeed cause the method to crack open and disintegrate into a number of methods to which the client must then suffer exposure, but this then transforms the body-change into one of the other categories of change discussed above, none of which distinguishes between interface and class from the ripple-effect point-of-view.
Once more: a draw.
Lastly, and most interestingly, the method implementing a given signature within p2
may swap places with that of another implementing the same signature. Now, surely, the interface wins for it lacks all knowledge of the class implementing its method signatures whereas the implementing class necessarily encompasses such knowledge.
Again, not so fast. For although the client may have access to a method in a class it cannot be certain which class implements that method: an entire family of other classes might derive from the class to which the client has access and any one of those subclasses may override the method.
If we define an, "Implementation dependency," as one which entails knowledge of the implementing class or method then no necessary implementation dependency surfaces here. Java actually makes it quite difficult to program to an implementation, as a trivial example shows.
public void welcome(Person person) { System.out.println("Hello, " + person.getName()); }
In the snippet above, is Person
a class or an interface? And even if Person
is a class, is it the class that implements the getName()
method or is that method overridden and implemented in some other subclass? The client does not know. Java's prescient designers ensured that the client does not need to know. Invoking getName()
's signature in the Person
entity does not guarantee execution of getName()
's method in the Person
entity.
The glaring exceptions to all this, of course, are the non-overridable methods (for example, final methods) and the constructor.
If the client depends on any non-overridable method or constructor then the client has exact knowledge of the implementing method: that implementing method cannot swap with another without affecting the client; using an interface carries no such penalty. Here, finally, the interface gathers its velvet cloak, swivels and plonks its royal behind on its cushioned throne.
A slightly unimpressed audience notes, however, that no inherent property of the interface has forged this magical shield capable of repelling ripple-effects better than class's rusted armour. Rather it is the properties of the class that exist over and above those of interface that cause ripple-effect concerns.
Yet no one has mandated that these properties be expressed in the published interfaces between packages.
Final methods have their place but the very concerns discussed here have seduced many programmers into chosing flexibility instead, rendering final methods something of a niche market. Constructors, for their part, often find themselves farmed out to factories and system-external instantiation-engines, again precisely to avoid their appearing in published interfaces. Plain-ol' overridable methods overwhelmingly represent the means by which most functionality is invoked, so class-inheritance could - in theory - play precisely the role that interface-inheritance plays in the ripple-effect game.
Yet in practice it does not.
The interface's superiority stems, rather, from the crushing inheritance-restrictions that afflict the class.
A class may derive from - and inherit the methods of - only one superclass. Introduced to curb the chaos that was perceived to dog multiple inheritance, this straight-jacket has proved such a snug fit as to have practically eradicated implementation-inheritance from modern Java systems, most programmers instead favouring composition. interface-inheritance reigns because Java's designers hobbled class-inheritance (even if for all the right reasons). Yet it is inheritance - shared by interface and class alike and certainly no peculiarity of the interface - that offers the means of by-passing this one category of ripple-effect. Here, finally, lies the source of the interface's resistance to ripple-effect.
Pragmatists celebrate.
Programmers program.
Purists worry.
They worry that some misunderstand the nature of the interface, ascribing to it mythical powers of ripple-effect immunity that vastly outstretch its reach. interfaces prove themselves over classes in one category of change only and this a relatively minor one. Most interface-change explodes in the form of first- and second-category events with entire classes and methods popping into being and vanishing, or, worse still, shattering asunder leaving ragged clients to comb the rubble for lost functionality.
A skewed view of interface's advantages causes some programmers to focus on interfaces rather than on interface-focus, to consider an interface acceptable simply because it comprises only interfaces rather than because it offers a fanatically minimised, "Surface area," to its clients.
interface's remain one of the great weapons in the Java-programmer's arsenal and Design Patterns one of the great manuals of war. Misinterpretations of that Good Book - written in a pre-Java vocabulary - should not, however, lure programmer's into language-specific thinking when generality was its essence.
Should you program to an interface, not an implementation? Absolutely.
Should you program to an interface, not an implementation? Absolutely.
Does doing the latter equate with doing the former? Absolutely not.
ptainai ain't ptainai.
CC image Part of glass painting from around 1610 A.D. courtesy of arnybo on Flickr.
CC image Magic No Mystery 1 courtesy of plaisanter~ on Flickr.