web-dev-qa-db-fra.com

Quand utiliser les mixins et quand utiliser les interfaces dans Dart?

Je suis très familier avec les concepts d'interfaces et de classes abstraites, mais pas très familier avec les concepts de mixins.

À l'heure actuelle, dans Dart, chaque classe A définit une interface implicite, qui peut être implémentée par une autre classe B en utilisant le mot clé implements. Il n'y a aucun moyen explicite de déclarer des interfaces comme, par exemple, en Java, où une interface ne contient que des méthodes non implémentées (et éventuellement des variables statiques). Dans Dart, étant donné que les interfaces sont définies par des classes, les méthodes de l'interface A peuvent en fait déjà être implémentées, mais la classe qui implémente B doit toujours remplacer ces implémentations.

Nous pouvons voir cette situation à partir du code suivant:

class A {
  void m() {
    print("method m");
  }
}

// LINTER ERROR: Missing concrete implementation of A.m
// Try implementing missing method or make B abstract.
class B implements A {
}

Dans Dart, un mixin est également défini via des déclarations de classe ordinaires ...

... En principe, chaque classe définit un mixin qui peut en être extrait. Cependant, dans cette proposition, un mixin ne peut être extrait que d'une classe qui n'a pas de constructeurs déclarés. Cette restriction évite les complications qui surviennent en raison de la nécessité de transmettre les paramètres du constructeur dans la chaîne d'héritage.

Un mixin est fondamentalement une classe qui peut définir des méthodes non implémentées ou implémentées. C'est un moyen d'ajouter des méthodes à une autre classe sans avoir à utiliser logiquement l'héritage. Dans Dart, un mixin est appliqué à une super classe, qui est étendue via l'héritage "normal", comme dans l'exemple suivant:

class A {
  void m() {
    print("method m");
  }
}

class MyMixin {
  void f(){
    print("method f");
  }
}

class B extends A with MyMixin {
}

Dans ce cas, nous devons noter que B n'a pas à implémenter d'autres méthodes à la fois de A et MyMixin.

Il y a une distinction claire entre appliquer un mixin à une classe et hériter d'une classe, au moins dans un langage qui ne prend en charge que l'héritage monoparental, car, dans ce cas, nous pourrions appliquer de nombreux mixins à un mais une classe pourrait simplement hériter d'une autre classe.

Il existe également une distinction claire entre l'implémentation d'une interface et l'héritage d'une classe. La classe qui implémente une interface doit implémenter obligatoirement toutes les méthodes définies par l'interface.

Donc, en résumé, le concept d'implémentation d'une interface consiste davantage à établir un contrat avec la classe qui implémente l'interface, et le concept de mixins (comme son nom l'indique) concerne davantage la réutilisation de code (sans revenir à une hiérarchie d'héritage).

Quand utiliser les mixins et quand utiliser les interfaces dans Dart? Existe-t-il des règles générales pour au moins des modèles récurrents spéciaux lors de la conception de logiciels où il serait préférable de définir un mixage et de l'appliquer à une super classe plutôt que de faire en sorte que notre classe implémente une interface? J'apprécierais des exemples concrets de décisions de conception dans un contexte où les interfaces et les mixins pourraient être utilisés, mais l'un est utilisé par rapport à l'autre (pour une raison quelconque).

28
nbro

Mixins concerne la façon dont une classe fait ce qu'elle fait, elle hérite et partage une implémentation concrète. Les interfaces sont tout au sujet de ce qu'est une classe, c'est la signature abstraite et promet que la classe doit satisfaire. C'est un type.

Prenez une classe implémentée comme class MyList<T> extends Something with ListMixin<T> .... Vous pouvez utiliser cette classe comme MyList<int> l = new MyList<int>(); ou List<int> l = new MyList<int>(), mais vous ne devez jamais écrire ListMixin<int> l = new MyList<int>(). Vous pouvez, mais vous ne devriez pas, car cela traite ListMixin comme un type, et ce n'est vraiment pas prévu comme tel. C'est la même raison pour laquelle vous devez toujours écrire Map m = new HashMap(); et non HashMap m = new HashMap(); - le type est Map, c'est un détail d'implémentation que c'est un HashMap.

Si vous mixez dans une classe (ou plutôt, le mixin dérivé d'une classe), alors vous obtenez tous les membres concrets de cette classe dans votre nouvelle classe mixin. Si vous implémentez une classe (ou plutôt l'interface implicite d'une classe), vous n'obtenez aucun membre concret, mais la signature abstraite devient une partie de votre interface.

Certaines classes peuvent être utilisées comme les deux, mais vous ne devriez jamais utiliser une classe comme mixage que si elle est destinée à être utilisée comme mixage (et documentée comme telle). Il y a beaucoup de changements qu'un auteur de classe peut faire à une classe qui briserait leur utilisation en tant que mixage. Nous ne voulons pas interdire de tels changements, qui pourraient être des changements parfaitement raisonnables pour une classe non-mixin, donc l'utilisation d'une classe non-mixin comme mixin est fragile et susceptible de se casser à l'avenir.

D'un autre côté, une classe destinée à être utilisée comme mixage concerne généralement l'implémentation, il est donc probable qu'une interface similaire soit également déclarée, et c'est ce que vous devez utiliser dans la clause implements.

Donc, si vous voulez implémenter une liste, vous pouvez soit implémenter la classe List et faire toute l'implémentation vous-même, soit mélanger dans la classe ListMixin pour réutiliser certaines fonctionnalités de base. Vous pouvez toujours écrire implements List<T>, Mais vous obtenez cela par héritage de ListMixin.

Les mixins ne sont pas un moyen d'obtenir un héritage multiple au sens classique. Mixins est un moyen d'abstraire et de réutiliser une famille d'opérations et d'états. Il est similaire à la réutilisation que vous obtenez en étendant une classe, mais il est compatible avec l'héritage unique car il est linéaire. Si vous avez plusieurs héritages, votre classe a deux (ou plus) superclasses, et vous devez gérer les conflits entre elles, y compris l'héritage de diamant, d'une manière ou d'une autre.

Les mixins dans Dart fonctionnent en créant une nouvelle classe qui superpose l'implémentation du mixin au-dessus d'une superclasse pour créer une nouvelle classe - ce n'est pas "sur le côté" mais "sur le dessus" de la superclasse, donc il n'y a aucune ambiguïté dans comment résoudre les recherches.

Exemple:

class Counter {
  int _counter = 0;
  int next() => ++_counter;
}
class Operation {
  void operate(int step) { doSomething(); }
}
class AutoStepOperation extends Operation with Counter {
  void operate([int step]) {
    super.operate(step ?? super.next());
  }
}

Ce qui se passe vraiment, c'est que vous créez une nouvelle classe "Operation with Counter". C'est équivalent à:

Exemple:

class Counter {
  int _counter = 0;
  int next() => ++_counter;
}
class Operation {
  void operate(int step) { doSomething(); }
}
class $OperationWithCounter = Operation with Counter;
class AutoStepOperation extends $OperationWithCounter {
  void operate([int step]) {
    super.operate(step ?? super.next());
  }
}

L'application mixin de Counter à Operation crée une nouvelle classe, et cette classe apparaît dans la chaîne de superclasse de AutoStepOperation.

Si vous faites class X extends Y with I1, I2, I3, Vous créez quatre classes. Si vous ne faites que class X extends Y implements I1, I2, I3, Vous ne créez qu'une seule classe. Même si I1, I2 Et I3 Sont des interfaces abstraites complètement vides, utiliser with pour les appliquer équivaut à:

class $X1 extends X implements I1 {}
class $X2 extends $X1 implements I2 {}
class $X3 extends $X2 implements I3 {}
class X extends $X3 {}

Vous n'écririez pas cela directement, vous devriez donc l'écrire en utilisant with soit

16
lrn

Les langages tels que Java et C # utilisent des interfaces pour avoir un héritage multiple de type au lieu d'un héritage d'implémentation multiple. Il existe des compromis complexes que les langages avec héritage d'implémentation multiple (comme Eiffel, C++ ou Dart) doivent faire face à ce que les concepteurs de Java et C # ont choisi d'éviter.

Cependant, une fois que vous avez l'héritage d'implémentations multiples, il n'est pas vraiment nécessaire de prendre en charge séparément l'héritage d'interfaces multiples, car une interface devient alors juste un cas spécial d'une classe abstraite sans variables d'instance et seules les méthodes abstraites et l'héritage d'interface sont identiques à l'héritage d'une telle classe.

Exemple:

abstract class IntA {
  void alpha();
}

abstract class IntB {
  void beta();
}

class C extends IntA with IntB {
  void alpha() => print("alpha");
  void beta() => print("beta");
}

void main() {
  var c = new C();
  IntA a = c;
  IntB b = c;
  a.alpha();
  b.beta();
}

Dart possède un héritage d'implémentation multiple (via des mixins), il n'a donc pas besoin d'héritage d'interfaces multiples en tant que concept séparé, ni de moyen de définir séparément les interfaces en tant qu'entités autonomes. Les interfaces implicites (via la clause implements) permettent de documenter ou de vérifier qu'une classe implémente au moins la même interface qu'une autre. Par exemple, Int8List met en oeuvre List<int>, bien que l'implémentation sous-jacente soit complètement différente.

L'utilisation d'héritage/mixins et d'interfaces implicites obtenues via implements est généralement orthogonale; vous les utiliserez très probablement en conjonction plutôt qu'à la place les uns des autres. Par exemple, vous souhaiterez peut-être utiliser implements Set<int> pour décrire l'interface souhaitée d'une implémentation de jeu de bits, puis utilisez les clauses extends et/ou with pour extraire l'implémentation réelle de cette interface. La raison en est que votre ensemble de bits ne partagera aucune implémentation réelle avec Set<int>, mais vous voulez toujours pouvoir les utiliser de manière interchangeable.

La bibliothèque de collection fournit pour nous un mixage SetMixin qui ne nécessite que d'implémenter nous-mêmes quelques routines de base et fournit le reste du Set<T> implémentation basée sur ceux-ci.

import "Dart:collection";

class BitSetImpl {
  void add(int e) { ...; }
  void remove(int e) { ...; }
  bool contains(int e) { ...; }
  int lookup(int e) { ...; }
  Iterator<int> get iterator { ...; }
  int get length { ...; }
}

class BitSet extends BitSetImpl with SetMixin<int> implements Set<int> {
  BitSet() { ...; }
  Set<int> toSet() { return this; }
}
5
Reimer Behrends

Les interfaces Dart, comme un autre langage, définissent un contrat à n'importe quelle classe à implémenter, ce contrat son obligatoire implémente ses propriétés et méthodes publiques

mixin c'est juste une autre façon d'ajouter des fonctionnalités à votre classe car dans Dart il n'existe pas de multi-extensions.

1
Javier González