Pourquoi les interfaces sont-elles plus utiles que les superclasses pour réaliser un couplage lâche?

15

( Aux fins de cette question, quand je dis «interface», je veux dire la construction du langageinterface , et non une «interface» dans l'autre sens du mot, c'est-à-dire les méthodes publiques qu'une classe propose au monde extérieur pour communiquer avec et le manipuler. )

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.

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 s'adaptent tous à l'abstraction. De nouveaux types de béton peuvent également être ajoutés ultérieurement sans avoir à modifier le code dépendant existant.

Par exemple, considérons une classe Caret deux sous Volvo- classes et Mazda.

Si votre code dépend de a Car, il peut utiliser a Volvoou a Mazdapendant l'exécution. Plus tard, des sous-classes supplémentaires pourraient être ajoutées sans qu'il soit nécessaire de modifier le code dépendant.

En outre, Car- qui est une abstraction - est moins susceptible de changer que Volvoou Mazda. Les voitures sont généralement les mêmes depuis un certain temps, mais Volvos et Mazdas sont beaucoup plus susceptibles de changer. C'est-à-dire que les abstractions sont plus stables que les types en béton.

Tout cela devait montrer que je comprends ce qu'est le couplage lâche et comment il est réalisé en fonction des abstractions et non des concrétions. (Si j'ai écrit quelque chose d'inexact, veuillez le dire).

Ce que je ne comprends pas, c'est ceci:

Les abstractions peuvent être des superclasses ou des interfaces.

Dans l'affirmative, pourquoi les interfaces sont-elles particulièrement appréciées pour leur capacité à permettre un couplage lâche? Je ne vois pas en quoi c'est différent que d'utiliser une superclasse.

Les seules différences que je vois sont les suivantes: 1- Les interfaces ne sont pas limitées par l'héritage unique, mais cela n'a pas grand-chose à voir avec le sujet du couplage lâche. 2- Les interfaces sont plus «abstraites» car elles n'ont aucune logique d'implémentation. Mais je ne vois toujours pas pourquoi cela fait une si grande différence.

Veuillez m'expliquer pourquoi les interfaces sont réputées pour permettre un couplage lâche, contrairement aux superclasses simples.

Aviv Cohn
la source
3
La plupart des langages (par exemple Java, C #) qui ont des «interfaces» ne prennent en charge que l'héritage unique. Comme chaque classe ne peut avoir qu'une seule superclasse immédiate, les superclasses (abstraites) sont trop limitées pour qu'un seul objet prenne en charge plusieurs abstractions. Découvrez les traits (par exemple Scala ou Perl's Roles ) pour une alternative moderne qui évite également le « problème des diamants » avec héritage multiple.
amon
@amon Vous dites donc que l'avantage des interfaces sur les classes abstraites lorsque vous essayez de réaliser un couplage lâche n'est-il pas limité par l'héritage unique?
Aviv Cohn
Non, je voulais dire coûteuse en termes de compilateur a plus à faire quand il gère une classe abstraite , mais cela pourrait être probablement négligé.
pâteux
2
On dirait que @amon est sur la bonne voie, j'ai trouvé ce post où il est dit que: 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(ce qui me conduit à la comparaison avec C ++, où les interfaces ne sont que des classes avec des fonctions virtuelles pures).
pâteux
Veuillez dire qui dit que les superclasses sont mauvaises.
Tulains Córdova

Réponses:

11

Terminologie: je ferai référence à la construction du langage interfacecomme 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 sortroutine 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' sortabstraction) 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.LinkedListprend en charge à la fois l' Listinterface qui concerne l'abstraction «collection indexée ordonnée» et prend en charge l' Queueinterface 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:

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

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

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 cmpet 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' Comparableinterface 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 -2et 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.

amon
la source
Bonne réponse, mais j'ajouterais que les langages à héritage unique peuvent tromper l'héritage multiple en utilisant des interfaces avec la composition.
4

Ce que je ne comprends pas, c'est ceci:

Les abstractions peuvent être des superclasses ou des interfaces.

Dans l'affirmative, pourquoi les interfaces sont-elles particulièrement appréciées pour leur capacité à permettre un couplage lâche? Je ne vois pas en quoi c'est différent que d'utiliser une superclasse.

Premièrement, le sous-typage et l'abstraction sont deux choses différentes. Le sous-typage signifie simplement que je peux substituer des valeurs d'un type à des valeurs d'un autre type - aucun type ne doit être abstrait.

Plus important encore, les sous-classes dépendent directement des détails d'implémentation de leur superclasse. C'est le type de couplage le plus solide qui soit. En fait, si la classe de base n'est pas conçue en tenant compte de l'héritage, les modifications de la classe de base qui ne changent pas son comportement peuvent toujours casser les sous-classes, et il n'y a aucun moyen de savoir a priori si une rupture se produira. Ceci est connu comme le problème de la classe de base fragile .

L'implémentation d'une interface ne vous couple à rien sauf à l'interface elle-même, qui ne contient aucun comportement.

Doval
la source
Merci de répondre. Pour voir si je comprends: lorsque vous voulez qu'un objet nommé A dépende d'une abstraction nommée B au lieu d'une implémentation concrète de cette abstraction nommée C, il est souvent préférable que B soit une interface implémentée par C, au lieu d'une superclasse étendue par C. C'est parce que: C la sous-classe B couple étroitement C à B. Si B change - C change. Cependant C implémentant B (B étant une interface) ne couple pas B à C: B n'est qu'une liste de méthodes que C doit implémenter, donc pas de couplage étroit. Cependant en ce qui concerne l'objet A (le dépendant), peu importe si B est une classe ou une interface.
Aviv Cohn
Correct? ..... .....
Aviv Cohn
Pourquoi considéreriez-vous qu'une interface soit couplée à quelque chose?
Michael Shaw
Je pense que cette réponse le cloue sur la tête. J'utilise beaucoup C ++, et comme cela a été indiqué dans l'une des autres réponses, C ++ n'a pas tout à fait d'interfaces, mais vous le faîtes en utilisant des superclasses avec toutes les méthodes laissées comme "purement virtuel" (c'est-à-dire implémentées par des enfants). Le fait est qu'il est facile de créer des classes de base qui font quelque chose avec des fonctionnalités déléguées. Dans beaucoup, beaucoup, beaucoup de cas, mes collègues et moi-même constatons qu'en faisant cela, un nouveau cas d'utilisation arrive et invalide ce bit de fonctionnalité partagé. Si des fonctionnalités partagées sont nécessaires, il est assez facile de créer une classe d'assistance.
J Trana
@Prog Votre ligne de pensée est généralement correcte, mais encore une fois, l'abstraction et le sous-typage sont deux choses distinctes. Lorsque vous dites you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named Cque vous supposez que les classes ne sont en quelque sorte pas abstraites. Une abstraction est tout ce qui cache les détails de l'implémentation, donc une classe avec des champs privés est tout aussi abstraite qu'une interface avec les mêmes méthodes publiques.
Doval
1

Il existe un couplage entre les classes parent et enfant, car l'enfant dépend du parent.

Disons que nous avons une classe A et que la classe B en hérite. Si nous entrons dans la classe A et changeons les choses, la classe B change aussi.

Disons que nous avons une interface I et que la classe B l'implémente. Si nous changeons l'interface I, bien que la classe B ne puisse plus l'implémenter, la classe B reste inchangée.

Michael Shaw
la source
Je suis curieux de savoir si les downvoters avaient une raison, ou s'ils passaient simplement une mauvaise journée.
Michael Shaw
1
Je n'ai pas déçu, mais je pense que cela peut avoir à voir avec la première phrase. Les classes enfants sont couplées aux classes parents, et non l'inverse. Le parent n'a besoin de rien savoir de l'enfant, mais l'enfant a besoin de connaissances intimes sur le parent.
@JohnGaughan: Merci pour vos commentaires. Modifié pour plus de clarté.
Michael Shaw