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).
Loose coupling can be achieved by having an object depend on an abstraction instead of a concrete type.
Correct.
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.
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 the sort
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 the List
interface which is about the “ordered, indexable collection” abstraction, and supports the Queue
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:
interface Comparable<T> {
public int cmp(T that);
public boolean lt(T that); // less than
public boolean le(T that); // less than or equal
public boolean eq(T that); // equal
public boolean ne(T that); // not equal
public boolean ge(T that); // greater than or equal
public boolean gt(T that); // greater than
}
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 as
trait Comparable[T] {
def cmp(that: T): Int
def lt(that: T): Boolean = this.cmp(that) < 0
def le(that: T): Boolean = this.cmp(that) <= 0
...
}
When a class then uses that trait, the other methods get added to the class definition:
// "extends" isn't different from Java's "implements" in this case
case class Inty(val x: Int) extends Comparable[Inty] {
override def cmp(that: Inty) = this.x - that.x
// lt etc. get added automatically
}
So Inty(4) cmp Inty(6)
would be -2
and Inty(4) lt Inty(6)
would be true
.
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.
interfaces are essential for single-inheritance languages like Java and C# because that's the only way in which you can aggregate different behaviors into a single class
(which leeds me to the comparison with C++, where interfaces are just classes with pure virtual functions).