Propriétés déléguées à Kotlin

1. Introduction

Le langage de programmation Kotlin prend en charge nativement les propriétés de classe.

Les propriétés sont généralement sauvegardées directement par les champs correspondants, mais il n'est pas toujours nécessaire que ce soit comme ça - tant qu'elles sont correctement exposées au monde extérieur, elles peuvent toujours être considérées comme des propriétés.

Ceci peut être réalisé en gérant cela dans les getters et les setters, ou en tirant parti de la puissance des délégués.

2. Que sont les propriétés déléguées?

En termes simples, les propriétés déléguées ne sont pas sauvegardées par un champ de classe et délèguent l'obtention et la définition à un autre morceau de code. Cela permet d'extraire les fonctionnalités déléguées et de les partager entre plusieurs propriétés similaires - par exemple, stocker les valeurs de propriété dans une carte au lieu de champs séparés.

Les propriétés déléguées sont utilisées en déclarant la propriété et le délégué qu'elle utilise. Le mot clé by indique que la propriété est contrôlée par le délégué fourni au lieu de son propre champ.

Par exemple:

class DelegateExample(map: MutableMap) { var name: String by map }

Cela utilise le fait qu'un MutableMap est lui-même un délégué, vous permettant de traiter ses clés comme des propriétés.

3. Propriétés déléguées standard

La bibliothèque standard Kotlin est livrée avec un ensemble de délégués standard prêts à être utilisés.

Nous avons déjà vu un exemple d'utilisation d'un MutableMap pour sauvegarder une propriété mutable. De la même manière, vous pouvez sauvegarder une propriété immuable à l'aide d'une carte , ce qui permet d'accéder à des champs individuels en tant que propriétés, sans jamais les modifier.

Le délégué paresseux permet à la valeur d'une propriété d'être calculée uniquement lors du premier accès, puis mise en cache. Cela peut être utile pour les propriétés qui peuvent être coûteuses à calculer et dont vous pourriez ne jamais avoir besoin - par exemple, être chargées à partir d'une base de données:

class DatabaseBackedUser(userId: String) { val name: String by lazy { queryForValue("SELECT name FROM users WHERE userId = :userId", mapOf("userId" to userId) } }

Le délégué observable permet à un lambda d'être déclenché à chaque fois que la valeur de la propriété change , par exemple en permettant des notifications de modification ou la mise à jour d'autres propriétés associées:

class ObservedProperty { var name: String by Delegates.observable("") { prop, old, new -> println("Old value: $old, New value: $new") } }

Depuis Kotlin 1.4, il est également possible de déléguer directement à une autre propriété. Par exemple, si nous renommons une propriété dans une classe d'API, nous pouvons laisser l'ancienne en place et déléguer simplement à la nouvelle:

class RenamedProperty { var newName: String = "" @Deprecated("Use newName instead") var name: String by this::newName }

Ici, chaque fois que nous accédons à la propriété name , nous utilisons effectivement la propriété newName à la place.

4. Création de vos délégués

Il y aura des moments où vous voudrez écrire vos délégués, plutôt que d'utiliser ceux qui existent déjà. Cela repose sur l'écriture d'une classe qui étend l'une des deux interfaces - ReadOnlyProperty ou ReadWriteProperty.

Ces deux interfaces définissent une méthode appelée getValue - qui est utilisée pour fournir la valeur actuelle de la propriété déléguée lorsqu'elle est lue. Cela prend deux arguments et renvoie la valeur de la propriété:

  • thisRef - une référence à la classe dans laquelle se trouve la propriété
  • property - une description de réflexion de la propriété déléguée

L' interface ReadWriteProperty définit en outre une méthode appelée setValue qui est utilisée pour mettre à jour la valeur actuelle de la propriété lorsqu'elle est écrite. Cela prend trois arguments et n'a pas de valeur de retour:

  • thisRef - Une référence à la classe dans laquelle se trouve la propriété
  • property - Une description de réflexion de la propriété déléguée
  • value - La nouvelle valeur de la propriété

Depuis Kotlin 1.4, l' interface ReadWriteProperty étend en fait ReadOnlyProperty. Cela nous permet d'écrire une classe de délégué unique implémentant ReadWriteProperty et de l'utiliser pour les champs en lecture seule dans notre code. Auparavant, nous aurions dû écrire deux délégués différents - un pour les champs en lecture seule et un autre pour les champs modifiables.

À titre d'exemple, écrivons un délégué qui fonctionne toujours en ce qui concerne une connexion à une base de données au lieu des champs locaux:

class DatabaseDelegate(readQuery: String, writeQuery: String, id: Any) : ReadWriteDelegate { fun getValue(thisRef: R, property: KProperty): T { return queryForValue(readQuery, mapOf("id" to id)) } fun setValue(thisRef: R, property: KProperty, value: T) { update(writeQuery, mapOf("id" to id, "value" to value)) } }

Cela dépend de deux fonctions de niveau supérieur pour accéder à la base de données:

  • queryForValue - cela prend du SQL et des liaisons et renvoie la première valeur
  • update - cela prend du SQL et des liaisons et le traite comme une instruction UPDATE

Nous pouvons ensuite l'utiliser comme n'importe quel délégué ordinaire et faire sauvegarder automatiquement notre classe par la base de données:

class DatabaseUser(userId: String) { var name: String by DatabaseDelegate( "SELECT name FROM users WHERE userId = :id", "UPDATE users SET name = :value WHERE userId = :id", userId) var email: String by DatabaseDelegate( "SELECT email FROM users WHERE userId = :id", "UPDATE users SET email = :value WHERE userId = :id", userId) }

5. Délégation de la création de délégués

Une autre nouvelle fonctionnalité que nous avons dans Kotlin 1.4 est la possibilité de déléguer la création de nos classes déléguées à une autre classe. Cela fonctionne en implémentant l' interface PropertyDelegateProvider , qui a une seule méthode pour instancier quelque chose à utiliser en tant que délégué réel.

Nous pouvons l'utiliser pour exécuter du code autour de la création du délégué à utiliser - par exemple, pour enregistrer ce qui se passe. Nous pouvons également l'utiliser pour sélectionner dynamiquement le délégué que nous allons utiliser en fonction de la propriété pour laquelle il est utilisé. Par exemple, nous pouvons avoir un délégué différent si la propriété est nullable:

class DatabaseDelegateProvider(readQuery: String, writeQuery: String, id: Any) : PropertyDelegateProvider
     
       { override operator fun provideDelegate(thisRef: T, prop: KProperty): ReadWriteDelegate { if (prop.returnType.isMarkedNullable) { return NullableDatabaseDelegate(readQuery, writeQuery, id) } else { return NonNullDatabaseDelegate(readQuery, writeQuery, id) } } }
     

Cela nous permet d'écrire du code plus simple dans chaque délégué car ils n'ont qu'à se concentrer sur des cas plus ciblés. Dans ce qui précède, nous savons que NonNullDatabaseDelegate ne sera jamais utilisé que sur des propriétés qui ne peuvent pas avoir de valeur nulle , nous n'avons donc pas besoin de logique supplémentaire pour gérer cela.

6. Résumé

La délégation de propriété est une technique puissante, qui vous permet d'écrire du code qui prend le contrôle d'autres propriétés et aide cette logique à être facilement partagée entre différentes classes. Cela permet une logique robuste et réutilisable qui ressemble et se sent comme un accès normal à la propriété.

Un exemple pleinement fonctionnel de cet article est disponible à l'adresse over sur GitHub.