KVC-KVO avec MacRuby
Le KVC (Key Value Coding) et le KVO (Key Value Observing) sont des modèles que vous risquez de rencontrer très rapidement si vous faites du développement Cocoa. Le premier est un système générique d'accès aux propriétés des objets, dont l'utilité, dans le cadre de MacRuby, ne vaut d'être connu que si vous utilisez le second. Le KVO quant à lui est un mécanisme qui vous permet d'observer les changements ayant lieu sur les valeurs des attributs d'un objet.
Avant de rentrer dans les détails, je tiens à vous signaler que, pour ceux qui voudraient rester dans du Ruby standard, il est possible de mettre en place ces mécanismes via le module KeyValue de Melchior Brislinger.
KVC
Le principe du KVC veut que chaque attribut d'un objet soit identifiable par un nom. Il est alors possible d'accéder à cet attribut via ce nom. Pour cela, il est nécessaire de respecter certaines contraintes lors de la création de la classe. Imaginons une classe Person pour laquelle nous avons les attributs firstName et lastName. En Ruby, nous codons généralement cela de la manière suivante :
class Person
attr_accessor :firstname
attr_accessor :lastName
end
La présence d'attr_accessor indique à Ruby qu'il doit mettre en place les accesseurs en lecture et écriture pour les attributs firstName et lastName. Ceci se vérifie effectivement :
p = Person.new
p.firstname = "Greg"
puts p.firstname
# => Greg
Pour être conforme au modèle KVC de Cocoa, la documentation nous indique que les accesseurs doivent avoir la forme suivante :
p.setFirstName("Greg")
p.firstName
# => "Greg"
En ce qui conserve la lecture, c'est déjà le cas. Bonne nouvelle, c'est également vrai pour l'écriture. En effet, si vous testez les 2 lignes de codes ci-dessus, vous verrez que cela fonctionne parfaitement avec MacRuby.
Nous pouvons donc dire que nous sommes conformes au modèle KVC. En effet, nous pouvons accéder de façon générique à nos attributs en utilisant les méthodes setValue:forKey: et valueForKey: :
p.setValue("Muriel", forKey:"firstName")
p.valueForKey("firstName")
# => "Muriel"
"C'est tout ?
- Oui, c'est aussi simple que cela !
KVO
Le KVO est presque aussi simple. En effet, il s'agit simplement de mettre en place un observer sur chaque attribut dont nous souhaitons contrôler les modifications de valeur. Pour cela nous utiliserons deux méthodes :
- addObserver:forKeyPath:options:context: Cette méthode place sur l'objet (respectant le modèle KVC) un observer donné, pour un nom d'attribut donné. Nous spécifions également des options pour cet observer et (éventuellement) un contexte qui sera passé à la méthode d'observation.
- removeObserver: Cette méthode permet de supprimer l'observer mis en place via la méthode précédente.
En plus de ces deux méthodes, nous devrons implémenter la méthode observeValueForKeyPath:ofObject:change:context: dans notre classe d'observation.
Si nous reprenons l'exemple de notre classe Person, nous pouvons par exemple, mettre en place un observer sur l'attribut firstName de la façon suivante :
observer = Observer.new
p.addObserver(Observer.new, forKeyPath:"firstName", options:0, context:nil)
Par la suite, si nous n'en avons plus besoin, nous pourrons supprimer cet observer :
p.removeObserver(observer)
Il ne nous reste plus qu'à créer la classe Observer. Comme je l'ai indiqué, la seule contrainte de cette classe est d'implémenter la méthode observeValueForKeyPath:ofObject:change:context:. Cette dernière reçoit en paramètre le nom de l'attribut observé, l'objet auquel il appartient, les changements observés et le contexte (celui passé à addObserver:forKeyPath:options:context:).
class Observer
def observeValueForKeyPath(aKey, ofObject:anObject, change:ch, context:ctx)
puts "La valeur de #{aKey} vient de changer dans l'object #{anObject.class}"
end
end
Si nous voulons tester que tout cela fonctionne bien, il suffit de modifier la valeur de l'attribut firstName :
p.firstName = "Maia"
# =>
p.setFirstName("Colyne")
# => La valeur de firstName vient de changer dans l'object #<Class:0xXXXXXXXX>
Comme vous pouvez le voir, le KVO ne fonctionne pas si nous utilisons la syntaxe p.firstName = ..., il faut donc se contraindre à utiliser p.setFirstName( ... ) ou p.setValue(..., forKey:"firstName").
Avant de terminer, sachez que les informations de modifications (paramètre change:) que vous pouvez récupérer dans observeValueForKeyPath:ofObject:change:context: dépendant des options mises en place via addObserver:forKeyPath:options:context:. Il existe quatre valeurs possibles pouvant être combinées par un ou (|) :
- NSKeyValueObservingOptionNew = 0x01 indique que le dictionnaire des changements doit contenir la nouvelle valeur.
- NSKeyValueObservingOptionOld = 0x02 indique que le dictionnaire des changements doit contenir l'ancienne valeur.
- NSKeyValueObservingOptionInitial = 0x04 indique qu'une notification doit être envoyée à l'observer avant l'appel à la méthode d'enregistrement.
- NSKeyValueObservingOptionPrior = 0x08 indique qu'une notification doit être envoyée avant et après chaque changement. Dans le cas de na notification envoyée avant, le dictionnaire contiendra une entrée ayant comme clé notificationIsPrior positionnée avec la valeur true
Pour voir cela en action, modifier l'exemple de la façon suivante :
class Person
attr_accessor :firstName
attr_accessor :lastName
end
class Observer
def observeValueForKeyPath(aKey, ofObject:anObject, change:ch, context:ctx)
puts "La valeur de #{aKey} vient de changer dans l'object #{anObject.class}"
p ch
end
end
p = Person.new
p.setValue("Greg", forKey:"firstName")
puts p.valueForKey("firstName")
observer = Observer.new
opt = 0x01|0x02
p.addObserver(Observer.new, forKeyPath:"firstName", options:opt, context:nil)
p.setFirstName("Muriel")
Ceci donnera alors le retour suivant :
Greg
La valeur de firstName vient de changer dans l'object #<Class:0xXXXXXXXXX>
{"kind"=>1, "old"=>"Greg", "new"=>"Muriel"}