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 :
- 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 ;
- 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é
- Implémente le modèle de conception KVC. Les attributs seront accédés en utilisant le protocole NSKeyValueCoding.
- Permet de s'abonner aux modifications de l'objet en respectant le protocole NSKeyValueObserving.
Objet observateur
- S'abonne à un objet en utilisant le protocole NSKeyValueObserving.
- 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.
IV-A. La classe position▲
Commencez par implémenter la classe position en suivant l'interface indiquée ci-dessous :
@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)
@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 :
- source est une référence sur l'objet suivi ;
- valueKey est le nom de la propriété à suivre dans l'objet source ;
- 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 :
@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 :
- La gestion de l'abonnement aux changements ;
- 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 :
-
(
void
) enableObserving;
-
(
void
) disableObserving;
Pour l'implémentation elle suit les spécifications de l'interface.
+
(
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 :
-
(
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 :
- Création des objets ;
- Premier test en modifiant la position sans activer l'observation ;
- Second test en activant l'observation d'une propriété puis en effectuant des modifications ;
- 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.
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
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
[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.
[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.