Maison > Java > javaDidacticiel > le corps du texte

Implémentation de mixins (ou traits) dans Kotlin à l'aide de la délégation

Mary-Kate Olsen
Libérer: 2024-10-18 20:11:03
original
1019 Les gens l'ont consulté

Implementing Mixins (or Traits) in Kotlin Using Delegation

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

  • Objectif
    • Définition du motif « Mixins »
    • Caractéristiques et contraintes
  • Mise en œuvre
    • Approche naïve par composition
    • Utilisation de l'héritage
    • Délégation pour contenir l’État des Mixins
    • Mise en œuvre finale
  • Limitations
  • Exemples
    • Auditable
    • Observables
    • Entité / Identité
  • Conclusion

Objectif

Définition du modèle « Mixins »

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.

Caractéristiques et contraintes

Une implémentation du modèle qui correspond étroitement à ces définitions doit répondre aux contraintes suivantes :

  • On peut ajouter plusieurs mixins à une classe. Nous sommes dans un contexte orienté objet, et si cette contrainte n'était pas respectée, le motif n'aurait que peu d'intérêt par rapport à d'autres possibilités de conception comme l'héritage.
  • La fonctionnalité Mixin peut être utilisée en dehors de la classe. De même, si l’on fait abstraction de cette contrainte, le motif n’apportera rien que l’on ne puisse réaliser avec une simple composition.
  • Ajouter un mixin à une classe ne nous oblige pas à ajouter des attributs et des méthodes dans la définition de la classe. Sans cette contrainte, le mixin ne pourrait plus être vu comme une fonctionnalité de « boîte noire ». Nous pourrions non seulement nous appuyer sur le contrat d’interface du mixin pour l’ajouter à une classe, mais il faudrait comprendre son fonctionnement (par exemple via la documentation). Je veux pouvoir utiliser un mixin comme j'utilise une classe ou une fonction.
  • Un mixin peut avoir un état. Certains mixins peuvent avoir besoin de stocker des données pour leur fonctionnalité.
  • Les mixins peuvent être utilisés comme types. Par exemple, je peux avoir une fonction qui prend n'importe quel objet comme paramètre tant qu'elle utilise un mixin donné.

Mise en œuvre

Approche naïve par composition

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

        // ...
    }
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

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.

Utilisation de l'héritage

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")
    }
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cette approche est plus satisfaisante que la précédente pour plusieurs raisons :

  • La classe utilisant le mixin n'a pas besoin d'implémenter le comportement du mixin grâce aux méthodes par défaut
  • Une classe peut utiliser plusieurs mixins car Kotlin permet à une classe d'implémenter plusieurs interfaces.
  • Chaque mixin crée un type qui peut être utilisé pour manipuler des objets en fonction des mixins inclus par sa classe.

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.

Délégation pour contenir l’État du Mixin

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

        // ...
    }
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Mise en œuvre finale

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")
    }
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Limites

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 :

  • Toutes les méthodes de mixage doivent être publiques. Certains mixins contiennent des méthodes destinées à être utilisées par la classe utilisant le mixin et d'autres qui ont plus de sens si elles sont appelées de l'extérieur. Puisque le mixin définit ses méthodes sur une interface, il est impossible de forcer le compilateur à vérifier ces contraintes. Il faut alors s'appuyer sur de la documentation ou des outils d'analyse de code statique.
  • Les méthodes Mixin n’ont pas accès à l’instance de la classe utilisant le mixin. Au moment de la déclaration de délégation, l'instance n'est pas initialisée et nous ne pouvons pas la transmettre au mixin.
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...
    }
}
Copier après la connexion
Copier après la connexion

Si vous l'utilisez dans le mixin, vous faites référence à l'instance de classe Holder.

Exemples

Pour améliorer la compréhension du pattern que je propose dans cet article, voici quelques exemples réalistes de mixins.

Auditable

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

        // ...
    }
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Observable

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")
    }
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

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

Entité / Identité

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...
    }
}
Copier après la connexion
Copier après la connexion

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.

Conclusion

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.


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

  2. Héritage basé sur Mixin ↩

  3. Cours et Mixins ↩

  4. 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!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal