Terminologie: je ferai référence à la construction du langage interface
comme interface , et à l'interface d'un type ou d'un objet comme surface (faute d'un meilleur terme).
Un couplage lâche peut être obtenu en faisant dépendre un objet d'une abstraction plutôt que d'un type de béton.
Correct.
Cela permet un couplage lâche pour deux raisons principales: 1 - les abstractions sont moins susceptibles de changer que les types en béton, ce qui signifie que le code dépendant est moins susceptible de se casser. 2 - différents types de béton peuvent être utilisés lors de l'exécution, car ils correspondent tous à l'abstraction. De nouveaux types de béton peuvent également être ajoutés ultérieurement sans avoir à modifier le code dépendant existant.
Pas tout à fait correct. Les langages actuels ne prévoient généralement pas qu'une abstraction changera (bien qu'il existe certains modèles de conception pour gérer cela). Séparer les spécificités des choses générales est l' abstraction. Cela se fait généralement par une couche d'abstraction . Cette couche peut être modifiée en quelques autres spécificités sans casser le code qui s'appuie sur cette abstraction - un couplage lâche est obtenu. Exemple non OOP: une sort
routine peut être modifiée de Quicksort dans la version 1 à Tim Sort dans la version 2. Le code qui ne dépend que du résultat trié (c'est-à-dire s'appuie sur l' sort
abstraction) est donc découplé de l'implémentation de tri réelle.
Ce que j'ai appelé la surface ci-dessus est la partie générale d'une abstraction. Il arrive maintenant dans la POO qu'un objet doit parfois prendre en charge plusieurs abstractions. Un exemple pas tout à fait optimal: Java java.util.LinkedList
prend en charge à la fois l' List
interface qui concerne l'abstraction «collection indexée ordonnée» et prend en charge l' Queue
interface qui (en gros) concerne l'abstraction «FIFO».
Comment un objet peut-il prendre en charge plusieurs abstractions?
C ++ n'a pas d'interfaces, mais il a plusieurs héritages, méthodes virtuelles et classes abstraites. Une abstraction peut alors être définie comme une classe abstraite (c'est-à-dire une classe qui ne peut pas être instanciée immédiatement) qui déclare, mais ne définit pas de méthodes virtuelles. Les classes qui implémentent les spécificités d'une abstraction peuvent alors hériter de cette classe abstraite et implémenter les méthodes virtuelles requises.
Le problème ici est que l'héritage multiple peut conduire au problème du diamant , où l'ordre dans lequel les classes sont recherchées pour une implémentation de méthode (MRO: ordre de résolution de méthode) peut conduire à des «contradictions». Il y a deux réponses à cela:
Définissez un ordre sain d'esprit et rejetez les ordres qui ne peuvent pas être linéarisés sensiblement. Le C3 MRO est assez sensible et fonctionne bien. Il a été publié en 1996.
Suivez la voie facile et rejetez l'héritage multiple tout au long.
Java a pris cette dernière option et a choisi l'héritage comportemental unique. Cependant, nous avons toujours besoin de la capacité d'un objet à prendre en charge plusieurs abstractions. Par conséquent, des interfaces doivent être utilisées qui ne prennent pas en charge les définitions de méthode, uniquement les déclarations.
Le résultat est que le MRO est évident (regardez simplement chaque superclasse dans l'ordre), et que notre objet peut avoir plusieurs surfaces pour un nombre quelconque d'abstractions.
Cela s'avère plutôt insatisfaisant, car assez souvent un peu de comportement fait partie de la surface. Considérez une 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
}
C'est très convivial (une belle API avec de nombreuses méthodes pratiques), mais fastidieux à mettre en œuvre. Nous aimerions que l'interface inclue cmp
et implémente automatiquement les autres méthodes en fonction de la seule méthode requise. Les mixins , mais plus important encore les Traits [ 1 ], [ 2 ] résolvent ce problème sans tomber dans les pièges de l'héritage multiple.
Cela se fait en définissant une composition de traits afin que les traits ne finissent pas réellement par participer au MRO - au lieu de cela, les méthodes définies sont composées dans la classe d'implémentation.
L' Comparable
interface pourrait être exprimée en Scala comme
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
...
}
Lorsqu'une classe utilise ensuite ce trait, les autres méthodes sont ajoutées à la définition de classe:
// "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
}
Il en Inty(4) cmp Inty(6)
serait ainsi -2
et Inty(4) lt Inty(6)
serait true
.
De nombreuses langues prennent en charge certains traits, et toute langue dotée d'un «protocole de métaobjet (MOP)» peut y être ajoutée. La récente mise à jour de Java 8 a ajouté des méthodes par défaut qui sont similaires aux traits (les méthodes dans les interfaces peuvent avoir des implémentations de secours de sorte qu'il est facultatif pour les classes d'implémentation d'implémenter ces méthodes).
Malheureusement, les traits sont une invention assez récente (2002), et sont donc assez rares dans les plus grandes langues dominantes.