(Lire cet article en français sur mon site)
En programmation orientée objet, un Mixin est un moyen d'ajouter une ou plusieurs fonctionnalités prédéfinies et autonomes à une classe. Certains langages offrent cette fonctionnalité directement, tandis que d'autres nécessitent plus d'efforts et de compromis pour coder les Mixins. Dans cet article, j'explique une implémentation de Mixins dans Kotlin utilisant la délégation.
Le modèle mixin n'est pas défini aussi précisément que d'autres modèles de conception tels que Singleton ou Proxy. Selon le contexte, il peut y avoir de légères différences dans la signification du terme.
Ce motif peut aussi être proche des "Traits" présents dans d'autres langages (ex. Rust), mais de même, le terme "Trait" ne signifie pas forcément la même chose selon le langage utilisé1.
Cela dit, voici une définition tirée de Wikipédia :
En programmation orientée objet, un mixin (ou mix-in) est une classe qui contient des méthodes utilisées par d'autres classes sans avoir besoin d'être la classe parent de ces autres classes. La manière dont ces autres classes accèdent aux méthodes du mixin dépend du langage. Les mixins sont parfois décrits comme étant « inclus » plutôt que « hérités ».
Vous pouvez également trouver des définitions dans divers articles sur le thème de la programmation basée sur le mixin (2, 3, 4). Ces définitions apportent également cette notion d'extension de classe sans la relation parent-enfant (ou is-a) fournie par l'héritage classique. Ils sont en outre liés à l'héritage multiple, ce qui n'est pas possible en Kotlin (ni en Java) mais est présenté comme l'un des intérêts de l'utilisation des mixins.
Une implémentation du modèle qui correspond étroitement à ces définitions doit répondre aux contraintes suivantes :
La manière la plus triviale d'ajouter des fonctionnalités à une classe est d'utiliser une autre classe comme attribut. Les fonctionnalités du mixin sont alors accessibles en appelant les méthodes de cet attribut.
class MyClass { private val mixin = Counter() fun myFunction() { mixin.increment() // ... } }
Cette méthode ne fournit aucune information au système de types de Kotlin. Par exemple, il est impossible d’avoir une liste d’objets avec Counter. Prendre un objet de type Counter en paramètre n'a aucun intérêt car ce type ne représente que le mixin et donc un objet probablement inutile au reste de l'application.
Un autre problème avec cette implémentation est que les fonctionnalités du mixin ne sont pas accessibles depuis l'extérieur de la classe sans modifier cette classe ou rendre le mixin public.
Pour que les mixins définissent également un type utilisable dans l'application, il faudra hériter d'une classe abstraite ou implémenter une interface.
Utiliser une classe abstraite pour définir un mixin est hors de question, car cela ne nous permettrait pas d'utiliser plusieurs mixins sur une seule classe (il est impossible d'hériter de plusieurs classes dans Kotlin).
Un mixin sera ainsi créé avec une interface.
interface Counter { var count: Int fun increment() { println("Mixin does its job") } fun get(): Int = count } class MyClass: Counter { override var count: Int = 0 // We are forced to add the mixin's state to the class using it fun hello() { println("Class does something") } }
Cette approche est plus satisfaisante que la précédente pour plusieurs raisons :
Cependant, il reste une limitation importante à cette implémentation : les mixins ne peuvent pas contenir d'état. En effet, si les interfaces de Kotlin peuvent définir des propriétés, elles ne peuvent pas les initialiser directement. Chaque classe utilisant le mixin doit ainsi définir toutes les propriétés nécessaires au fonctionnement du mixin. Cela ne respecte pas la contrainte selon laquelle nous ne voulons pas que l'utilisation d'un mixin nous oblige à ajouter des propriétés ou des méthodes à la classe qui l'utilise.
Il va donc falloir trouver une solution pour que les mixins aient un état tout en gardant l'interface comme seul moyen d'avoir à la fois un type et la possibilité d'utiliser plusieurs mixins.
Cette solution est légèrement plus complexe pour définir un mixin ; cependant, cela n’a aucun impact sur la classe qui l’utilise. L'astuce consiste à associer chaque mixin à un objet pour contenir l'état dont le mixin pourrait avoir besoin. Nous utiliserons cet objet en l'associant à la fonctionnalité de délégation de Kotlin pour créer cet objet à chaque utilisation du mixin.
Voici la solution de base qui répond néanmoins à toutes les contraintes :
class MyClass { private val mixin = Counter() fun myFunction() { mixin.increment() // ... } }
On peut encore améliorer l'implémentation : la classe CounterHolder est un détail d'implémentation, et il serait intéressant de ne pas avoir besoin de connaître son nom.
Pour y parvenir, nous utiliserons un objet compagnon sur l'interface mixin et le modèle "Factory Method" pour créer l'objet contenant l'état du mixin. Nous utiliserons également un peu de magie noire Kotlin pour ne pas avoir besoin de connaître le nom de cette méthode :
interface Counter { var count: Int fun increment() { println("Mixin does its job") } fun get(): Int = count } class MyClass: Counter { override var count: Int = 0 // We are forced to add the mixin's state to the class using it fun hello() { println("Class does something") } }
Cette implémentation des mixins n'est pas parfaite (aucun ne pourrait être parfait sans être supporté au niveau du langage, à mon avis). Il présente notamment les inconvénients suivants :
interface Counter { fun increment() fun get(): Int } class CounterHolder: Counter { var count: Int = 0 override fun increment() { count++ } override fun get(): Int = count } class MyClass: Counter by CounterHolder() { fun hello() { increment() // The rest of the method... } }
Si vous l'utilisez dans le mixin, vous faites référence à l'instance de classe Holder.
Pour améliorer la compréhension du pattern que je propose dans cet article, voici quelques exemples réalistes de mixins.
Ce mixin permet à une classe d'"enregistrer" les actions effectuées sur une instance de cette classe. Le mixin fournit une autre méthode pour récupérer les derniers événements.
class MyClass { private val mixin = Counter() fun myFunction() { mixin.increment() // ... } }
Le modèle de conception Observable peut être facilement implémenté à l'aide d'un mixin. De cette façon, les classes observables n'ont plus besoin de définir la logique d'abonnement et de notification, ni de maintenir elles-mêmes la liste des observateurs.
interface Counter { var count: Int fun increment() { println("Mixin does its job") } fun get(): Int = count } class MyClass: Counter { override var count: Int = 0 // We are forced to add the mixin's state to the class using it fun hello() { println("Class does something") } }
Il y a cependant un inconvénient dans ce cas précis : la méthode notifyObservers est accessible depuis l'extérieur de la classe Catalog, même si nous préférerions probablement la garder privée. Mais toutes les méthodes mixin doivent être publiques pour être utilisées depuis la classe utilisant le mixin (puisque nous n'utilisons pas l'héritage mais la composition, même si la syntaxe simplifiée par Kotlin fait ressembler à de l'héritage).
Si votre projet gère des données métiers persistantes et/ou que vous pratiquez, au moins en partie, le DDD (Domain Driven Design), votre application contient probablement des entités. Une entité est une classe avec une identité, souvent implémentée sous la forme d'un identifiant numérique ou d'un UUID. Cette caractéristique s'accorde bien avec l'utilisation d'un mixin, et en voici un exemple.
interface Counter { fun increment() fun get(): Int } class CounterHolder: Counter { var count: Int = 0 override fun increment() { count++ } override fun get(): Int = count } class MyClass: Counter by CounterHolder() { fun hello() { increment() // The rest of the method... } }
Cet exemple est un peu différent : on voit que rien ne nous empêche de nommer différemment la classe Holder, et rien ne nous empêche de passer des paramètres lors de l'instanciation.
La technique mixin permet d'enrichir les classes en ajoutant des comportements souvent transversaux et réutilisables sans avoir à modifier ces classes pour s'adapter à ces fonctionnalités. Malgré quelques limitations, les mixins permettent de faciliter la réutilisation du code et d'isoler certaines fonctionnalités communes à plusieurs classes de l'application.
Les mixins sont un outil intéressant dans la boîte à outils du développeur Kotlin, et je vous encourage à explorer cette méthode dans votre propre code, tout en étant conscient des contraintes et des alternatives.
Fait amusant : Kotlin a un mot-clé trait, mais il est obsolète et a été remplacé par interface (voir https://blog.jetbrains.com/kotlin/2015/05/kotlin-m12-is-out/#traits -sont-maintenant-interfaces) ↩
Héritage basé sur Mixin ↩
Cours et Mixins ↩
Programmation orientée objet avec Saveurs ↩
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!