(For the purpose of this question, when I say 'interface' I mean the language construct interface
, and not an 'interface' in the other sense of the word, i.e. the public methods a class offers the outside world in order to communicate with and manipulate it.)
Loose coupling can be achieved by having an object depend on an abstraction instead of a concrete type.
This allows for loose coupling for two main reasons: 1- abstractions are less likely to change than concrete types, which means the dependent code is less likely to break. 2- different concrete types can be used at runtime, because they all fit the abstraction. New concrete types can also be added later with no need to alter the existing dependent code.
For example, consider a class Car
and two subclasses Volvo
and Mazda
.
If your code depends on a Car
, it can use either a Volvo
or a Mazda
during runtime. Also later on additional subclasses could be added with no need to change the dependent code.
Also, Car
– which is an abstraction – is less likely to change than Volvo
or Mazda
. Cars have been generally the same for quite some time, but Volvos and Mazdas are far more likely to change. I.e. abstractions are more stable than concrete types.
All of this was to show that I understand what loose coupling is and how it is achieved by depending on abstractions and not on concretions. (If I wrote something inaccurate please say so).
What I don't understand is this:
Abstractions can be superclasses or interfaces.
If so, why are interfaces specifically praised for their ability to allow loose coupling? I don't see how it's different than using a superclass.
The only differences I see are: 1- Interfaces aren't limited by single inheritance, but that doesn't have much to do with the topic of loose coupling. 2- Interfaces are more 'abstract' since they have no implementation logic at all. But still, I don't see why that makes such a big difference.
Please explain to me why interfaces are said to be great in allowing loose coupling, while simple superclasses are not.
Best Answer
Terminology: I'll refer to the language construct
interface
as interface, and to the interface of a type or object as surface (for a lack of a better term).Correct.
Not quite correct. Current languages do not generally anticipate that an abstraction will change (although there are some design patterns to handle that). Separating specifics from general things is abstraction. This is usually done by some layer of abstraction. This layer can be changed to some other specifics without breaking code that builds upon this abstraction – loose coupling is achieved. Non-OOP example: A
sort
routine might be changed from Quicksort in version 1 to Tim Sort in version 2. Code that only depends on the result being sorted (i.e. builds upon thesort
abstraction) is therefore decoupled from the actual sorting implementation.What I termed surface above is the general part of an abstraction. It now happens in OOP that one object must sometimes support multiple abstractions. A not-quite optimal example: Java's
java.util.LinkedList
supports both theList
interface which is about the “ordered, indexable collection” abstraction, and supports theQueue
interface which (in rough terms) is about the “FIFO” abstraction.How can an object support multiple abstractions?
C++ doesn't have interfaces, but it has multiple inheritance, virtual methods, and abstract classes. An abstraction can then be defined as an abstract class (i.e. a class that cannot be immediately instantiated) that declares, but not defines virtual methods. Classes that implement the specifics of an abstraction can then inherit from that abstract class and implement the required virtual methods.
The problem here is that multiple inheritance can lead to the diamond problem, where the order in which classes are searched for a method implementation (MRO: method resolution order) can lead to “contradictions”. There are two responses to this:
Define a sane order and reject those orders that can't be sensibly linearized. The C3 MRO is fairly sensible and works well. It was published 1996.
Take the easy route and reject multiple inheritance throughout.
Java took the latter option and chose single behavioral inheritance. However, we still need the ability of an object to support multiple abstractions. Therefore, interfaces have to be used which do not support method definitions, only declarations.
The result is that the MRO is obvious (just look at each superclass in order), and that our object can have multiple surfaces for any number of abstractions.
This turns out to be rather unsatisfactory, because quite often a bit of behavior is part of the surface. Consider an
Comparable
interface:This is very user-friendly (a nice API with many convenient methods), but tedious to implement. We would like the interface to only include
cmp
, and implement the other methods automatically in terms of that one required method. Mixins, but more importantly Traits [1],[ 2] solve this problem without falling into the traps of multiple inheritance.This is done by defining a trait composition so that the traits don't actually end up taking part in the MRO – instead the defined methods are composed into the implementing class.
The
Comparable
interface could be expressed in Scala asWhen a class then uses that trait, the other methods get added to the class definition:
So
Inty(4) cmp Inty(6)
would be-2
andInty(4) lt Inty(6)
would betrue
.Many languages have some support for traits, and any language that has a “Metaobject Protocol (MOP)” can have traits added to it. The recent Java 8 update added default methods which are similar to traits (methods in interfaces can have fallback implementations so that it's optional for implementing classes to implement these methods).
Unfortunately, traits are a fairly recent invention (2002), and are thus fairly rare in the larger mainstream languages.