Modèle de conception : KVO pour Key Value Observing. Apprenez à suivre vos objets

Sylvain Gamel

Le modèle KVO forme avec KVC l'un des principaux socles de l'environnement de développement Cocoa.

Si KVC se rapproche très largement des règles de codage définies en Java pour les Java Beans (1) , on peut considérer que le modèle KVO est le pendant du modèle d'événements en Java.

La similitude s'arrête là, car le modèle KVO est bien plus simple.

- Titre : Modèle de conception : KVO pour Key Value Observing. Apprenez à suivre vos objets

- Auteur : Sylvain Gamel

- Parution : 23 mai 2008

- Mise à jour : 23 avril 2020

- Licence : Le contenu de cet article est rédigé par Sylvain Gamel et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé. Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright ® 2013 Developpez.com.

Sommaire

- I. Principe général

- II. Protocoles

- III. Un exemple pratique

- IV. Mise en œuvre de l'exemple

- IV-A. La classe position

- IV-B. La classe ValueTracker, sans KVO

- IV-C. La classe ValueTracker, ajout de KVO

- IV-D. Tester KVO

- V. Un parallèle avec Java

- VI. En Conclusion

I - Principe général

Le modèle de conception Key Value Observing (2) permet de suivre les changements sur les valeurs des propriétés d'objets.

Si l'on parle ici de couple clef-valeur c'est pour une raison simple : pour son fonctionnement, le mécanisme KVO s'appuie largement sur le modèle KVC.

On considère au moins deux acteurs pour la mise en œuvre de ce modèle de conception :

1. L'objet observé qui expose un ensemble d'attributs sous la forme d'une liste de noms : les clefs. La valeur de ces attributs est donc assimilée à la valeur des clefs ;

2. L'objet observateur qui décide de suivre les changements intervenus sur un objet particulier.

II - Protocoles

Les responsabilités entre les deux objets sont donc définies comme suit.

Objet observé

1. Implémente le modèle de conception KVC. Les attributs seront accédés en utilisant le protocole NSKeyValueCoding.

2. Permet de s'abonner aux modifications de l'objet en respectant le protocole NSKeyValueObserving.

Objet observateur

1. S'abonne à un objet en utilisant le protocole NSKeyValueObserving.

2. Est notifié d'un changement par son implémentation du même protocole.

Encore un protocole à implémenter ?

En fait non, pas pour les objets observés. C'est la classe NSObject qui propose une implémentation du protocole NSKeyValueObserving.

Ainsi tous les objets dont la classe dérive de NSObject peuvent être suivis par d'autres objets observateurs. Pour s'abonner, un objet doit simplement en faire la demande en appelant la méthode suivante sur l'objet observé :

- (void) addObserver: (NSObject *) anObserver
forKeyPath: (NSString *) keyPath
options: (NSKeyValueObservingOptions) options
context: (void *) context

Pour ne plus recevoir les notifications, il suffit de résilier son abonnement avec la méthode suivante :

- (void) removeObserver: (NSObject *) anObserver
forKeyPath: (NSString *) keyPath

Seuls les observateurs doivent implémenter au moins une méthode du protocole, celle qui permet d'être averti d'un changement :

- (void) observeValueForKeyPath: (NSString *) keyPath
ofObject: (id) object
change: (NSDictionary *) change
context: (void *) context

III - Un exemple pratique

Si les exemples les plus classiques utilisent une interface graphique pour illustrer l'utilisation de KVO, je vais vous proposer un exemple plus simple sous la forme d'une application en ligne de commande.

Même si nous en sommes très loin, considérons le petit exemple que je vous propose comme les briques élémentaires d'un outil de GPS.

Nous allons implémenter deux classes.

- Une classe Position qui donne la position d'un objet dans l'espace par un triplet de coordonnées : latitude, longitude et altitude.

- Une classe de suivi ValueTracker (notre observateur) dont le rôle sera de suivre les déplacements d'un objet.

Pour faire une classe un peu générique, notre classe de suivi peut s'abonner aux évolutions de n'importe quel objet et ne se limite donc pas à l'utilisation avec un objet Position.

Un objet ValueTracker connaît :

- l'objet qu'il suit : la source ;

- le nom de la propriété à laquelle il est abonné : valueKey ;

- la dernière valeur en date : value.

Le corps de l'exemple va ensuite utiliser ses deux classes pour construire une position et un objet de suivi et montrer comment l'observation vient s'intégrer dans cet exemple.

IV - Mise en œuvre de l'exemple

Pour commencer, vous devez créer un projet de type « Foundation Tool » dans le groupe « Command Line Utility ». Cela vous construit un projet utilisable en ligne de commande comprenant au moins le fichier source principal avec une fonction main().

Vous pouvez récupérer directement le source de cet exemple dans le fichier archive joint avec cet article.

Archive du projet pour XCode 3

IV-A - La classe position

Commencez par implémenter la classe position en suivant l'interface indiquée ci-dessous :

Position.h

@interface Position : NSObject {
float latitude;
float longitude;
float altitude;
}

@property(assign, readwrite) float latitude;
@property(assign, readwrite) float longitude;
@property(assign, readwrite) float altitude;

- (NSString *) description;

@end

Dans votre implémentation, seule la méthode description devrait être à votre charge. Les accesseurs et modificateurs étant générés par le compilateur à l'aide de la directive @synthetize. (3)

Position.m

@implementation Position

@synthesize latitude, longitude, altitude;

- (NSString *) description
{
NSString * msg = [NSString
stringWithFormat:@"Position(lat=%f ; long=%f, alt=%f)",
latitude,
longitude,
altitude];
return msg;
}

@end

IV-B - La classe ValueTracker, sans KVO

Nous allons commencer par le comportement fonctionnel de cette classe en définissant les propriétés suivantes :

1. source est une référence sur l'objet suivi ;

2. valueKey est le nom de la propriété à suivre dans l'objet source ;

3. value est la valeur de la propriété valueKey dans l'objet source.

Pour l'implémentation de ces propriétés, nous allons utiliser là encore les fonctionnalités d'Objective-C 2.0 pour limiter le code à écrire.

Pour faciliter la création des objets, une méthode de classe createValueTrackerForKey:onObject va également être utile pour créer un ValueTracker sur une propriété particulière d'un objet spécifique.

L'interface à implémenter est donc la suivante :

ValueTracker.h

@interface ValueTracker : NSObject {
@private
id source;
NSString * valueKey;
id value;
}

@property(retain, readwrite) id source;
@property(assign, readwrite) id value;
@property(retain, readwrite) NSString * valueKey;

+ (ValueTracker *) createValueTrackerForKey: (NSString *) aKey onObject: (id) anObject;

IV-C - La classe ValueTracker, ajout de KVO

L'implémentation du modèle KVO implique deux étapes :

1. La gestion de l'abonnement aux changements ;

2. L'implémentation du suivi des modifications.

Pour la première partie, nous allons définir une méthode pour activer l'abonnement et une autre pour supprimer l'abonnement. Leur signature est à ajouter dans le fichier d'interface :

ValueTracker.h

- (void) enableObserving;
- (void) disableObserving;

Pour l'implémentation elle suit les spécifications de l'interface.

ValueTracker.m

+ (ValueTracker *) createValueTrackerForKey: (NSString *) aKey
onObject: (id) anObject
{
ValueTracker * tracker = [[ValueTracker alloc] init];

tracker.valueKey = aKey;
tracker.source = anObject;
tracker.value = [anObject valueForKey: tracker.valueKey];

return tracker;
}

- (void) enableObserving
{
if ( nil != source ) {
[source addObserver: self
forKeyPath: valueKey
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context: NULL];
}
}

- (void) disableObserving
{
if ( nil != source ) {
[source removeObserver:self];
}
}

La seconde étape est de fournir une méthode appelée pour chaque modification. Son interface est définie dans l'interface et le fichier en-tête de notre classe peut simplement se contenter de le rappeler. Son implémentation est triviale :

ValueTracker.m

- (void) observeValueForKeyPath: (NSString *) keyPath
ofObject: (id) object
change: (NSDictionary *) change
context: (void *) context
{
NSLog(@"Value %@ changed! Here is the changeset: %@", keyPath, change);
value = [change valueForKey:NSKeyValueChangeNewKey];
}

IV-D - Tester KVO

Pour tester nos deux classes, nous allons procéder par étapes :

1. Création des objets ;

2. Premier test en modifiant la position sans activer l'observation ;

3. Second test en activant l'observation d'une propriété puis en effectuant des modifications ;

4. Troisième test en activant l'observation d'une autre propriété.

Le test est réalisé dans la méthode main() du fichier source créé par le modèle de projet.

Création des objets

Position* pos = [[Position alloc] init];
pos.latitude = 45.0;
pos.longitude = 12.0;
pos.altitude = 30.0;

ValueTracker * tracker = [ValueTracker createValueTrackerForKey:@"latitude" onObject:pos];

NSLog(@"Tracker (1): %@", tracker);

Si vous exécutez le projet dans cet état, vous aurez les traces suivantes (4)

2008-04-28 13:22:26.805 SimpleKVO[799:10b] *** Exemple sans observation

2008-04-28 13:22:26.808 SimpleKVO[799:10b] Tracker (1): Tracking latitude=45

test sans observation

pos.latitude += 5.0;
pos.longitude += 5.0;
pos.altitude += 10.0;

NSLog(@"Tracker (2): %@", tracker);

Les traces devraient montrer que la valeur de la propriété n'a pas été mise à jour dans l'objet tracker.

2008-04-28 13:22:26.809 SimpleKVO[799:10b] Tracker (2): Tracking latitude=45

test avec observation

[tracker enableObserving];

pos.latitude += 3.0;
pos.altitude += 7.0;

En revanche, dès lors que l'observation est activée la méthode de notification est immédiatement appelée au moment d'une modification et les traces reflètent ce suivi des modifications :

2008-04-28 13:22:26.809 SimpleKVO[799:10b] *** Activation de l'observation

2008-04-28 13:22:26.811 SimpleKVO[799:10b] Value latitude changed! Here is the changeset: {
kind = 1;
new = 53;
old = 50;
}

Il est possible d'aller plus loin en obligeant l'objet observateur de suivre une propriété même s'il ne l'a pas prévu initialement. Le programme de test peut ainsi forcer le suivi de la propriété d'altitude.

Test avec observation supplémentaire

[pos addObserver: tracker
forKeyPath: @"altitude"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context: NULL];

pos.latitude += 3.0;
pos.altitude += 2.0;

NSLog(@"Tracker (3): %@", tracker);

Les traces montrent alors que ce sont bien les deux propriétés latitude et altitude qui sont suivies par notre objet tracker

2008-04-28 13:22:26.812 SimpleKVO[799:10b] *** Activation de l'observation de l'altitude

2008-04-28 13:22:26.812 SimpleKVO[799:10b] Value latitude changed! Here is the changeset: {
kind = 1;
new = 56;
old = 53;
}

2008-04-28 13:22:26.813 SimpleKVO[799:10b] Value altitude changed! Here is the changeset: {
kind = 1;
new = 49;
old = 47;
}

2008-04-28 13:22:26.814 SimpleKVO[799:10b] Tracker (3): Tracking latitude=49

V - Un parallèle avec Java

Le mécanisme d'observation est très similaire à celui de gestion des évènements sur les Java Beans.

Cependant Java va un cran plus loin en proposant un modèle générique de gestion d'évènements.

Le seul modèle KVO correspond simplement à la gestion des évènements PropertyChange. Cependant là où le modèle Cocoa est directement supporté au niveau de la classe de base, Java impose de définir :

- une classe d'évènement PropertyChangeEvent ;

- une interface pour les observateurs PropertyChangeListener proposant une méthode propertyChange() ;

- l'objet émetteur doit pour sa part fournir l'ensemble d’outils techniques pour gérer la liste des observateurs de l'évènement en proposant des méthodes de gestion d'abonnement addPropertyChangeListener(), removePropertyChangeListener() ainsi que d'envoi de notification.

Si Java propose une architecture potentiellement plus riche et puissante, elle est également beaucoup plus complexe à mettre en œuvre et fait intervenir bien plus de classes.

VI - En Conclusion

Le modèle KVO propose un mécanisme simple et minimaliste pour qu'un objet puisse suivre les variations de valeur d'une propriété d'un autre objet.

C'est un atout indiscutable lorsqu'il est question de construire des interfaces graphiques où un seul objet modèle est susceptible d'impliquer la mise à jour de plusieurs composants graphiques. C'est ainsi que KVO permet la mise en œuvre de la liaison de données entre modèle de données et présentation.

C'est également grâce au modèle KVO que Cocoa peut proposer le framework Core Data pour la persistance des données.

Les atouts combinés des modèles KVC et KVO permettent à Cocoa de proposer une large palette de services sans sacrifier à la simplicité des classes de base.

(1)POJO et non Enterprise Java Beans.

(2)Ce qui en français pourrait se traduire par Observation des paires de clef-valeurs.

(3)Ce qui suppose que vous utilisez au minimum Léopard et les fonctionnalités de la version 2 du langage Objective-C.

(4)Il est nécessaire d'afficher la console pour voir le résultat des NSLog en utilisant les touches Majuscule+Commande+R.