Object-oriented – Why are interfaces more helpful than superclasses in achieving loose coupling

designinterfacesobject-oriented

(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).

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:

  1. 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.

  2. 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.