271 52 8MB
English Pages XXV, 427 Seiten: Diagramme [495] Year 2009;2010
Référence
Les design patterns de
Cocoa
Erik M. Buck Donald A. Yacktman
Réseaux et télécom Programmation Développement Web Sécurité Système d’exploitation
Pearson Education France a apporté le plus grand soin à la réalisation de ce livre afin de vous fournir une information complète et fiable. Cependant, Pearson Education France n’assume de responsabilités, ni pour son utilisation, ni pour les contrefaçons de brevets ou atteintes aux droits de tierces personnes qui pourraient résulter de cette utilisation. Les exemples ou les programmes présents dans cet ouvrage sont fournis pour illustrer les descriptions théoriques. Ils ne sont en aucun cas destinés à une utilisation commerciale ou professionnelle. Pearson Education France ne pourra en aucun cas être tenu pour responsable des préjudices ou dommages de quelque nature que ce soit pouvant résulter de l’utilisation de ces exemples ou programmes. Tous les noms de produits ou marques cités dans ce livre sont des marques déposées par leurs propriétaires respectifs.
Publié par Pearson Education France 47 bis, rue des Vinaigriers 75010 PARIS Tél. : 01 72 74 90 00 www.pearson.fr
Titre original : Cocoa Design Patterns Traduit de l’américain par Hervé Soulard, avec la contribution technique de Renaud Pradenc ISBN original : 978-0-321-53502-3 Copyright © 2010 Pearson Education, Inc. All rights reserved
ISBN : 978-2-7440-4131-0 Copyright © 2010 Pearson Education France Tous droits réservés Édition originale publiée par Pearson Education www.pearsoned.com Aucune représentation ou reproduction, même partielle, autre que celles prévues à l’article L. 122-5 2° et 3° a) du Code de la propriété intellectuelle ne peut être faite sans l’autorisation expresse de Pearson Education France ou, le cas échéant, sans le respect des modalités prévues à l’article L. 122-10 dudit code. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.
Table des matières Avant-propos .....................................................................................................................
IX
Préface ............................................................................................................................... XI Qu’est-ce qu’un design pattern ? ................................................................................... XI Pourquoi se focaliser sur les design patterns ? .............................................................. XIII Principes directeurs de la conception ............................................................................ XIV Public du livre................................................................................................................ XVI Organisation du livre ..................................................................................................... XVIII Conventions typographiques ......................................................................................... XVIII Votre avis ....................................................................................................................... XIX Télécharger le code........................................................................................................ XIX Remerciements ..................................................................................................................
XXI
À propos des auteurs ........................................................................................................ XXIII Partie I – Un pattern omniprésent 1 Modèle-Vue-Contrôleur ............................................................................................. 1.1 MVC dans Cocoa ............................................................................................... 1.2 En résumé ...........................................................................................................
3 5 19
2 Analyse et application de MVC ................................................................................. 2.1 Conception non MVC ........................................................................................ 2.2 Conception MVC ............................................................................................... 2.3 En résumé ...........................................................................................................
21 21 26 32
Partie II – Patterns fondamentaux 3 Création en deux étapes ............................................................................................. 3.1 Motivation .......................................................................................................... 3.2 Solution .............................................................................................................. 3.3 Exemples dans Cocoa......................................................................................... 3.4 Conséquences .....................................................................................................
35 35 37 45 49
IV
Table des matières
4 Patron de méthode ...................................................................................................... 4.1 Motivation .......................................................................................................... 4.2 Solution .............................................................................................................. 4.3 Exemples dans Cocoa......................................................................................... 4.4 Conséquences .....................................................................................................
51 52 52 55 59
5 Création dynamique ................................................................................................... 5.1 Motivation .......................................................................................................... 5.2 Solution .............................................................................................................. 5.3 Exemples dans Cocoa......................................................................................... 5.4 Conséquences .....................................................................................................
61 61 62 69 70
6 Catégorie ...................................................................................................................... 6.1 Motivation .......................................................................................................... 6.2 Solution .............................................................................................................. 6.3 Exemples dans Cocoa......................................................................................... 6.4 Conséquences .....................................................................................................
71 72 72 78 83
7 Type anonyme et Conteneur hétérogène .................................................................. 7.1 Motivation .......................................................................................................... 7.2 Solution .............................................................................................................. 7.3 Exemples dans Cocoa......................................................................................... 7.4 Conséquences .....................................................................................................
87 87 88 95 95
8 Énumérateur ............................................................................................................... 8.1 Motivation .......................................................................................................... 8.2 Solution .............................................................................................................. 8.3 Exemples dans Cocoa......................................................................................... 8.4 Conséquences .....................................................................................................
97 97 98 109 110
9 Exécution de sélecteur et Exécution retardée ........................................................... 9.1 Motivation .......................................................................................................... 9.2 Solution .............................................................................................................. 9.3 Exemples dans Cocoa......................................................................................... 9.4 Conséquences .....................................................................................................
111 112 112 117 119
10 Accesseur ..................................................................................................................... 10.1 Motivation .......................................................................................................... 10.2 Solution .............................................................................................................. 10.3 Exemples dans Cocoa......................................................................................... 10.4 Conséquences .....................................................................................................
121 122 123 134 137
Table des matières
V
11 Archivage et désarchivage .......................................................................................... 11.1 Motivation .......................................................................................................... 11.2 Solution .............................................................................................................. 11.3 Exemples dans Cocoa......................................................................................... 11.4 Conséquences .....................................................................................................
139 140 140 143 150
12 Copie 12.1 12.2 12.3 12.4
153 153 156 158 164
............................................................................................................................ Motivation .......................................................................................................... Solution .............................................................................................................. Exemples dans Cocoa......................................................................................... Conséquences ..................................................................................................... Partie III – Patterns qui favorisent le découplage
13 Singleton ...................................................................................................................... 13.1 Motivation .......................................................................................................... 13.2 Solution .............................................................................................................. 13.3 Exemples dans Cocoa......................................................................................... 13.4 Conséquences .....................................................................................................
167 167 168 176 177
14 Notification .................................................................................................................. 14.1 Motivation .......................................................................................................... 14.2 Solution .............................................................................................................. 14.3 Exemples dans Cocoa......................................................................................... 14.4 Conséquences .....................................................................................................
179 180 181 187 193
15 Délégué ......................................................................................................................... 15.1 Motivation .......................................................................................................... 15.2 Solution .............................................................................................................. 15.3 Exemples dans Cocoa......................................................................................... 15.4 Conséquences .....................................................................................................
195 195 199 209 210
16 Hiérarchie .................................................................................................................... 16.1 Motivation .......................................................................................................... 16.2 Solution .............................................................................................................. 16.3 Exemples dans Cocoa......................................................................................... 16.4 Conséquences .....................................................................................................
211 211 212 224 225
17 Outlet, cible et action .................................................................................................. 17.1 Motivation .......................................................................................................... 17.2 Solution .............................................................................................................. 17.3 Exemples dans Cocoa......................................................................................... 17.4 Conséquences .....................................................................................................
227 228 229 237 241
VI
Table des matières
18 Chaîne de répondeurs ................................................................................................. 18.1 Motivation .......................................................................................................... 18.2 Solution .............................................................................................................. 18.3 Exemples dans Cocoa......................................................................................... 18.4 Conséquences .....................................................................................................
243 243 244 254 255
19 Mémoire associative .................................................................................................... 19.1 Motivation .......................................................................................................... 19.2 Solution .............................................................................................................. 19.3 Exemples dans Cocoa......................................................................................... 19.4 Conséquences .....................................................................................................
257 257 257 261 265
20 Invocation .................................................................................................................... 20.1 Motivation .......................................................................................................... 20.2 Solution .............................................................................................................. 20.3 Exemples dans Cocoa......................................................................................... 20.4 Conséquences .....................................................................................................
267 267 267 280 280
21 Prototype ...................................................................................................................... 21.1 Motivation .......................................................................................................... 21.2 Solution .............................................................................................................. 21.3 Exemples dans Cocoa......................................................................................... 21.4 Conséquences .....................................................................................................
281 281 282 284 288
22 Poids mouche ............................................................................................................... 22.1 Motivation .......................................................................................................... 22.2 Solution .............................................................................................................. 22.3 Exemples dans Cocoa......................................................................................... 22.4 Conséquences .....................................................................................................
291 291 291 292 295
23 Décorateur ................................................................................................................... 23.1 Motivation .......................................................................................................... 23.2 Solution .............................................................................................................. 23.3 Exemples dans Cocoa......................................................................................... 23.4 Conséquences .....................................................................................................
297 298 299 300 302
Partie IV – Patterns qui masquent la complexité 24 Bundle .......................................................................................................................... 24.1 Motivation .......................................................................................................... 24.2 Solution .............................................................................................................. 24.3 Exemples dans Cocoa......................................................................................... 24.4 Conséquences .....................................................................................................
305 306 306 309 312
Table des matières
VII
25 Regroupement de classes ............................................................................................ 25.1 Motivation .......................................................................................................... 25.2 Solution .............................................................................................................. 25.3 Exemples dans Cocoa......................................................................................... 25.4 Conséquences .....................................................................................................
313 314 314 319 331
26 Façade .......................................................................................................................... 26.1 Motivation .......................................................................................................... 26.2 Solution .............................................................................................................. 26.3 Exemples dans Cocoa......................................................................................... 26.4 Conséquences .....................................................................................................
333 334 334 337 342
27 Mandataire et Renvoi ................................................................................................. 27.1 Motivation .......................................................................................................... 27.2 Solution .............................................................................................................. 27.3 Exemples dans Cocoa......................................................................................... 27.4 Conséquences .....................................................................................................
343 343 344 357 358
28 Gestionnaire ................................................................................................................ 28.1 Motivation .......................................................................................................... 28.2 Solution .............................................................................................................. 28.3 Exemples dans Cocoa......................................................................................... 28.4 Conséquences .....................................................................................................
359 359 359 366 367
29 Contrôleur ................................................................................................................... 29.1 Motivation .......................................................................................................... 29.2 Solution .............................................................................................................. 29.3 Exemples dans Cocoa......................................................................................... 29.4 Conséquences .....................................................................................................
369 370 370 392 393
Partie V – Outils d’application des patterns 30 Modèles de Core Data ................................................................................................. 30.1 Rôle du sous-système Modèle............................................................................ 30.2 Terminologie de Core Data ................................................................................ 30.3 Collaboration des patterns dans Core Data ........................................................ 30.4 Limites et avantages de Core Data .....................................................................
399 400 401 402 413
31 Vues Application Kit ................................................................................................... 31.1 Rôle du sous-système Vue.................................................................................. 31.2 Collaboration des patterns dans Application Kit................................................ 31.3 Limites et avantages d’Application Kit..............................................................
415 416 416 430
VIII
Table des matières
32 Bindings et contrôleurs ............................................................................................... 32.1 Rôles des bindings et des contrôleurs................................................................. 32.2 Limites et avantages des bindings et des contrôleurs.........................................
433 434 444
Annexe A Ressources ..................................................................................................... A.1 Documentation d’Apple ..................................................................................... A.2 Ouvrages............................................................................................................. A.3 Listes de diffusion .............................................................................................. A.4 Groupes d’utilisateurs......................................................................................... A.5 Groupes en ligne................................................................................................. A.6 Conférences ........................................................................................................
445 445 446 447 447 447 448
Index ..................................................................................................................................
449
Avant-propos Dans notre culture moderne, les vieux bonshommes grincheux sont bien souvent représentés sans complaisance. Ils sont montrés en train de lancer des objets et de beugler des phrases du genre : "Hé, les gosses, foutez le camp de mon jardin !" En réalité, ils ont plutôt des paroles sensées comme : "Les enfants, vous devriez diversifier votre portefeuille, on ne sait jamais." En tant que développeur d’applications Cocoa et Objective-C de longue date, je fais souvent office de vieux grincheux. Des programmeurs Cocoa débutants viennent me voir pour me dire : "Voici mon programme. Il fonctionne parfaitement. Voulez-vous examiner les sources ?" J’étudie le code et grogne alors : "C’est vrai, il fonctionne, mais ce n’est pas ainsi qu’il faut faire. Nous, les vieux programmeurs Cocoa grincheux, avons conçu un système et tu ne suis pas le système." Le jeune programmeur répond alors : "Très bien, mais pourquoi votre système est-il si formidable ?" Je bougonne : "Heu... eh bien... c’est comme ça ! Tais-toi et sors de mon bureau." Cet ouvrage répond à deux questions importantes : n
Comment les vieux programmeurs Cocoa grincheux travaillent-ils ?
n
Pourquoi est-ce la bonne manière de faire ?
En bataillant contre les mauvaises solutions, les vieux programmeurs Cocoa grincheux ont trouvé des solutions réellement bonnes à des problèmes de conception communs. Grâce à ce livre, vous n’aurez pas à souffrir des difficultés par lesquelles nous sommes passés. Erik M. Buck et Donald A. Yacktman ont gagné leur statut de vieux programmeurs Cocoa grincheux. Ils ont eu suffisamment de réussites et d’échecs à leur actif pour reconnaître une bonne conception Cocoa. En plus de présenter ces idiomes et ces techniques, Erik et Donald expliquent également les raisons de l’émergence de ces patterns dans la gabegie que représentait la programmation Objective-C il y a une dizaine d’années.
X
Les design patterns de Cocoa
La prochaine fois qu’un programmeur débutant se tiendra sur le pas de ma porte pour me demander d’examiner son code, je lui lancerai ce livre. Dommage que l’édition reliée n’existe pas.
Aaron Hillegass Big Nerd Ranch, Inc. Atlanta, Géorgie
Préface La plupart des technologies incluses par Apple dans Cocoa existent commercialement depuis 1988, mais cette maturité n’empêche pas Cocoa de rester révolutionnaire. Ces technologies ont changé plusieurs fois de nom, notamment NeXTSTEP, OPENSTEP, Rhapsody et Yellow Box. Il s’agit d’un ensemble de composants logiciels réutilisables qui contiennent des objets et d’autres ressources pour le développement d’applications bureautiques et mobiles destinées à Mac OS X. Ces dernières années, Apple a énormément étendu Cocoa et a ajouté de nouveaux outils de développement pour améliorer la productivité du programmeur, au-delà des niveaux reconnus que Cocoa permettait déjà d’atteindre. Lorsque les programmeurs font leurs premières armes avec Cocoa, ils sont souvent déroutés par son étendue et sa sophistication. Cocoa apporte effectivement un grand nombre de fonctionnalités, mais il se révèle étonnamment cohérent, grâce à une conception fondée sur des patterns1. La parfaite compréhension de ces patterns permet une utilisation efficace des frameworks et ils serviront de guide lors du développement des applications. Cet ouvrage décrit les design patterns orientés objet sur lesquels se fonde Cocoa. Ils ne sont pas uniques à Cocoa, mais se retrouvent dans de nombreuses bibliothèques logicielles réutilisables et disponibles dans tout environnement de développement de logiciels. Les design patterns identifient des problèmes logiciels récurrents, ainsi que les meilleures pratiques pour les résoudre. Le premier objectif de ce livre est d’expliquer la conception et la logique de Cocoa, mais, grâce à ces informations, vous serez en mesure de réutiliser dans vos propres logiciels ces patterns testés et approuvés, même si vous ne choisissez pas Cocoa.
Qu’est-ce qu’un design pattern ? Les design patterns décrivent des solutions pratiques de grande qualité à des problèmes de programmation récurrents. Il s’agit non pas d’astuces de programmation extraordi1. N.d.T. : dans cet ouvrage, nous conservons le terme "design pattern", parfois traduit "patron de conception" ou "motif de conception" (http://fr.wikipedia.org/wiki/Patron_de_conception), car il est couramment employé dans la littérature.
XII
Les design patterns de Cocoa
naires, mais d’une boîte à outils de solutions et de bonnes pratiques qui ont été affinées au fil des années pour prendre une forme succincte. Ils définissent un vocabulaire que les programmeurs peuvent employer pour expliquer un logiciel complexe à d’autres personnes. Les design patterns ne décrivent pas des algorithmes ou des structures de données particulières, comme les listes chaînées ou les tableaux de longueur variable, qui sont généralement mis en œuvre par des classes individuelles. Les design patterns de cet ouvrage ne décrivent pas des conceptions particulières pour des applications, bien que des exemples soient donnés. Les patterns établissent une carte de la conception de Cocoa afin que vous puissiez vous y aventurer. Ils montrent comment et pourquoi l’un des meilleurs logiciels réutilisables jamais créés a été conçu de cette manière. Chaque pattern est présenté par, au moins, les quatre éléments suivants : n
son nom ;
n
une courte description des motivations à l’origine du pattern et du problème qu’il résout ;
n
une description détaillée du pattern et des exemples dans Cocoa ;
n
les retombées de l’utilisation du pattern.
Les Parties II à IV de cet ouvrage constituent un catalogue de design patterns. Chaque chapitre du catalogue présente un design pattern et fournit les informations permettant d’identifier et de réutiliser ce pattern. Le nom du pattern permet aux développeurs de communiquer efficacement. En utilisant des noms connus, il leur est plus facile d’expliquer un système à des collègues ou de documenter la conception. Les patterns nommés clarifient les idées. Les implications de la conception, comme sa logique, peuvent être présentées avec quelques mots. Les programmeurs qui connaissent les patterns comprennent immédiatement les utilisations et les limitations des objets qui composent un pattern désigné, ainsi que la conception générale choisie et ses conséquences. La documentation d’Apple emploie occasionnellement des noms de design patterns dans la description des classes et les guides du programmeur, mais elle n’explique pas toujours ce que sont les patterns ni ce qu’ils signifient pour le développeur. Par ailleurs, Apple choisit souvent ses propres noms à la place de ceux couramment employés par l’industrie. Dans certains cas, les différences de terminologie sont dues à la découverte simultanée du pattern, mais de manière indépendante. Dans d’autres cas, les patterns ont tout d’abord été identifiés dans Cocoa, ou son prédécesseur NeXTSTEP, et l’industrie en a changé le nom. Les patterns décrits dans cet ouvrage sont nommés selon la terminologie d’Apple et, lorsque c’est possible, les noms retenus par l’industrie sont indiqués de manière que vous fassiez le lien.
Préface
XIII
Chaque design pattern décrit le ou les problèmes concernés, ainsi que les motivations de sa mise en œuvre. Certains recensent également une liste d’indicateurs qui doivent vous amener à envisager leur utilisation. Puisque Cocoa se fonde sur de nombreux patterns applicables dans diverses situations, nous les avons organisés de manière que les problèmes semblables se posant dans des contextes différents soient rapides à identifier. Dans certains cas, les patterns connexes à éviter sont également signalés. Enfin, chaque pattern précise les retombées de son utilisation. Les conséquences et les compromis des conceptions alternatives sont essentiels au choix des patterns à utiliser dans un cas précis.
Pourquoi se focaliser sur les design patterns ? Lorsqu’on aborde une technologie logicielle aussi étendue que Cocoa, il est facile de perdre de vue l’architecture globale et sa logique. De nombreux programmeurs ont indiqué se sentir perdus dans la multitude de classes, fonctions et structures de données proposées par Cocoa. Ils ne peuvent pas voir la forêt car ils sont focalisés sur chaque arbre. Les patterns employés dans Cocoa établissent une structure et une organisation qui permettent aux programmeurs de retrouver plus facilement leur chemin. Ils leur montrent comment réutiliser des groupes de classes coopérantes, même lorsque les relations entre les classes ne sont pas réellement expliquées dans leur documentation. La programmation orientée objet a pour but d’améliorer la productivité du programmeur en réduisant le temps de développement du logiciel et ses coûts de maintenance. Pour cela, la principale technique employée est la réutilisation d’objets. En réutilisant un objet, le programmeur gagne du temps car il ne doit plus le réimplémenter dans chaque nouveau projet. Par ailleurs, lorsque de nouvelles fonctionnalités sont requises ou lorsque des bogues sont identifiés, la réutilisation permet d’effectuer les modifications sur un nombre d’objets réduit, tout en profitant aux autres projets fondés sur ces objets. Plus important encore, grâce à la réutilisation des objets, la résolution d’un nouveau problème exige un nombre de lignes de code inférieur et, par conséquent, une maintenance moindre. Les design patterns identifient des stratégies éprouvées qui autorisent une réutilisation à plus grande échelle, non limitée à des objets individuels. Les patterns et tous les objets qu’ils impliquent ont été validés et réutilisés de nombreuses fois. Leur utilisation cohérente dans Cocoa contribue au niveau élevé de productivité constaté chez les programmeurs Cocoa. Les design patterns servent l’art de la programmation orientée objet. Les patterns employés dans Cocoa seront des guides pour la conception de différents types d’applications. Cocoa contient certains des meilleurs ensembles de classes jamais conçus, et le respect des patterns sous-jacents fera de vous un meilleur programmeur, même si vous n’utilisez pas Cocoa.
XIV
Les design patterns de Cocoa
Cet ouvrage devrait satisfaire votre curiosité intellectuelle. Les design patterns répondent aux questions "pourquoi", "quoi" et "comment". En sachant comment les patterns sont appliqués et, plus important encore, pourquoi ils améliorent la productivité, votre travail de programmeur n’en sera que plus agréable.
Principes directeurs de la conception Les design patterns décrits dans cet ouvrage partagent plusieurs propriétés. L’objectif de chacun d’eux est de résoudre un problème, de manière générale et réutilisable. Plusieurs principes directeurs permettent de garantir des patterns flexibles et applicables dans de nombreux contextes. Les mêmes stratégies de conception s’appliquent aux objets individuels et aux design patterns. En réalité, les bénéfices d’une bonne conception orientée objet sont plus importants dans le cas de patterns qui impliquent de nombreux objets que dans le cas de systèmes plus simples. Les patterns sont créés pour que la productivité obtenue par leur utilisation soit supérieure à celle obtenue par l’utilisation d’objets individuels – le tout est supérieur à la somme des parties. Réduire le couplage La conception doit avoir pour objectif global de réduire le couplage entre les classes. Le couplage fait référence aux dépendances entre des objets. Lorsque ces dépendances existent, elles diminuent les opportunités de réutilisation des objets. Le couplage concerne également les sous-systèmes dans des systèmes d’objets plus vastes. Il est important de rechercher des conceptions qui évitent tout couplage. Tous les design patterns de Cocoa servent en partie à limiter ou à éviter le couplage. Par exemple, le pattern MVC (Modèle-Vue-Contrôleur), très répandu, décrit dans la Partie I de cet ouvrage, est employé dans Cocoa pour organiser des sous-systèmes de classes et il est appliqué à la conception d’applications. Le premier objectif du pattern MVC est de partitionner un système complexe d’objets en trois sous-systèmes principaux, tout en réduisant le couplage entre ces sous-systèmes. Concevoir pour l’évolution Il est important de définir des conceptions qui prennent en compte les évolutions du système logiciel. Les conceptions trop rigides finissent par limiter les opportunités de réutilisation. Dans le pire des cas, aucune réutilisation n’est possible car il est plus facile de revoir la conception et la mise en œuvre d’un système que d’apporter à la conception existante les modifications nécessaires. Certaines formes de modification peuvent être anticipées et prises en compte dans la conception. Par exemple, le pattern Délégué de Cocoa fournit un mécanisme qui permet
Préface
XV
à un objet de modifier et de contrôler le comportement d’un autre objet sans introduire de couplage entre les deux. Cocoa comprend plusieurs objets qui peuvent être contrôlés par des délégués facultatifs. Le point clé de ce pattern réside dans le fait que les objets qui jouent le rôle de délégués peuvent ne pas encore avoir été imaginés au moment où Cocoa a été conçu. Tous les design patterns de Cocoa existent en partie pour prendre en charge l’évolution. Il s’agit de l’une des raisons de la grande flexibilité de Cocoa. Privilégier les interfaces aux implémentations Les interfaces établissent une sorte de contrat métaphorique entre un objet et ses utilisateurs. L’interface d’un objet indique au programmeur les possibilités de l’objet, sans préciser comment il les met en œuvre. Dans le cadre des frameworks réutilisables, comme Cocoa, les interfaces des objets doivent rester cohérentes d’une version à l’autre du framework. Dans le cas contraire, un logiciel écrit pour une version du framework risque de ne plus fonctionner correctement avec la suivante. Un contrat est donc nécessaire pour que les programmeurs soient enclins à réutiliser les objets du framework. Toutefois, quiconque a déjà essayé de créer un contrat réellement flexible sait qu’il s’agit d’une tâche difficile. Lorsque des détails d’implémentation s’immiscent dans le contrat entre un objet et ses utilisateurs, les développeurs du framework ont plus de mal à améliorer les objets sans remettre en cause la rétrocompatibilité. Rechercher la granularité optimale Les design patterns que l’on trouve dans Cocoa n’opèrent pas tous au même niveau de granularité. Par exemple, le pattern MVC est généralement appliqué à de grands soussystèmes de classes coopérantes et à des applications entières, tandis que le pattern Singleton est employé de manière à créer une seule instance d’une classe et pour donner accès à cette unique instance. L’objectif des patterns est d’améliorer la réutilisation, et leur granularité peut avoir un impact important sur les opportunités de réutilisation. Certains problèmes sont mieux résolus par de petits patterns faisant intervenir seulement quelques classes, tandis que d’autres trouvent une solution via de grands patterns globaux. L’essentiel est de trouver le bon équilibre. En général, les grands patterns conduisent à des gains de productivité plus élevés que les petits patterns, mais, si un pattern est trop grand ou trop général pour résoudre un problème spécifique, il ne peut pas être employé. Par exemple, le pattern MVC est très intéressant pour la plupart des applications, mais certaines applications spécifiques peuvent ne pas en tirer bénéfice, ce qui lui ôte alors toute valeur. À l’opposé, des patterns comme Type anonyme, Conteneur hétérogène, Énumérateur, Poids mouche et Singleton sont petits et apportent une valeur à chaque application. Cocoa emploie toute la gamme de patterns. La description de certains d’entre eux pose les problèmes de granularité et explique les choix effectués par Cocoa.
XVI
Les design patterns de Cocoa
Préférer la composition à l’héritage Nous ne le répéterons jamais assez, le couplage est l’ennemi. Il est assez paradoxal que l’héritage soit simultanément l’un des outils les plus puissants de la programmation orientée objet et aussi la principale source de couplage. En réalité, aucune relation ne conduit à un couplage aussi étroit que les liens entre les classes et leurs super-classes. De nombreux patterns décrits dans cet ouvrage sont conçus pour éviter les sous-classes. En règle générale, s’il existe une alternative à l’héritage, il est préférable d’opter pour l’alternative.
Public du livre Ce livre est destiné aux programmeurs Mac OS X qui utilisent ou envisagent d’utiliser les frameworks Cocoa d’Apple. La majorité des informations qu’il contient sont également valables pour le projet open-source GNUstep, disponible pour Linux et Windows. Qui doit lire cet ouvrage ? Les programmeurs Objective-C, C, C++ et Java doivent lire cet ouvrage. Il est impératif de connaître les principes généraux de la conception orientée objet pour comprendre les design patterns présentés et en tirer profit. Plusieurs design patterns de Cocoa exploitent les caractéristiques du langage Objective-C, qui n’est pas détaillé ici ; consultez le document The Objective-C 2.0 Programming Language fourni avec les outils Xcode (http:// developer.apple.com/documentation/Cocoa/Conceptual/ObjectiveC). Des connaissances en Objective-C sont nécessaires pour comprendre l’implémentation de Cocoa, mais les programmeurs expérimentés peuvent les acquérir progressivement au cours de la lecture de ce livre. Néanmoins, il ne remplacera pas un ouvrage de référence sur ce langage, comme The Objective-C 2.0 Programming Language, même si les caractéristiques du langage qui participent aux design patterns de Cocoa sont expliquées lors de leur description. INFO Pour vous initier au langage Objective-C, vous pouvez également consultez le guide de survie Objective-C 2.0 de Pejvan Beigui, aux éditions Pearson (http://www.pearson.fr).
Prérequis Pour profiter du contenu de cet ouvrage, il est inutile d’être un expert en programmation. Les patterns employés dans la conception de Cocoa sont identifiés et expliqués, en partie pour démystifier la technologie. Les programmeurs novices en Cocoa trouveront
Préface
XVII
un intérêt dans les idées et le bon sens mis en œuvre dans Cocoa, tout autant que les programmeurs expérimentés. Toutefois, si vous débutez totalement dans la programmation avec C ou les langages dérivés de C, vous rencontrerez des difficultés à suivre l’analyse détaillée du fonctionnement des patterns. Vous devez maîtriser les concepts de la programmation orientée objet, comme les classes, les instances, l’encapsulation, le polymorphisme et l’héritage. Sans de solides bases en développement de logiciels orientés objet, les descriptions parfois élaborées des avantages, des conséquences et des compromis risquent d’être difficiles à comprendre. Ce livre suppose que vous connaissiez C, C++ ou Java et que vous soyez familier du développement de logiciels orientés objet. Nous l’avons mentionné précédemment, la maîtrise d’Objective-C permettra de profiter au mieux du contenu, mais l’apprentissage de ce langage peut se faire au fur et à mesure. Vous devez également disposer d’un système Mac OS X sur lequel les outils Xcode d’Apple sont installés. Si ces outils ne sont pas disponibles sur votre système, vous pouvez les obtenir de deux manières : n
n
Si vous avez acheté un nouveau Mac ou une version de Mac OS X, les outils Xcode se trouvent sur le DVD d’installation, dans le dossier Optional Installs. n
Pour Mac OS X Leopard (version 10.5), examinez le dossier Xcode Tools et double-cliquez sur le fichier XcodeTools.mpkg pour installer Xcode.
n
Pour Mac OS X Snow Leopard (version 10.6), double-cliquez sur le fichier Xcode.mpkg pour démarrer l’installation de Xcode.
La dernière version des outils Xcode est disponible sur le site ADC (Apple Developper Connection) à l’adresse http://developer.apple.com. Pour la télécharger, vous devez tout d’abord vous inscrire au Mac Developer Program ; c’est gratuit. Toutefois, n’oubliez pas que la taille du paquetage à télécharger approche 1 Go et qu’une connexion rapide est donc fortement conseillée. INFO
Si vous développez pour l’iPhone ou l’Ipod Touch, vous devez tout d’abord vous inscrire au iPhone Developer Program (http://developer.apple.com/iphone/program), puis télécharger et installer le SDK (Software Development Kit) de l’iPhone depuis le centre de développement pour l’iPhone (http://developer.apple.com/iphone). La version 3.0 du SDK pour l’iPhone exige un Mac à base de processeur Intel. Autrement dit, il ne pourra pas fonctionner sur des Mac plus anciens équipés d’un processeur PowerPC, comme les versions G3, G4 ou G5. Le SDK est disponible pour Mac OS X Leopard (version 10.5) et pour Mac OS X Snow Leopard (version 10.6).
XVIII Les design patterns de Cocoa
Cet ouvrage a été écrit pour Mac OS X (version 10.5), mais vous finirez par exploiter les design patterns de Cocoa dans le développement d’applications pour n’importe quelle version de Mac OS X, de l’iPhone, de l’iPod Touch ou de Windows et Linux avec GNUstep.
Organisation du livre Cet ouvrage s’articule autour de cinq parties : n
La Partie I décrit le pattern MVC (Modèle-Vue-Contrôleur), qui fournit la structure et l’organisation générales de Cocoa, ainsi que de la plupart des applications fondées sur ces frameworks.
n
La Partie II identifie les patterns de Cocoa à partir desquels tous les autres patterns sont construits.
n
La Partie III concerne les patterns qui permettent de contrôler et d’étendre des objets sans introduire un couplage inutile.
n
La Partie IV explique les patterns qui masquent la complexité et les détails d’implémentation pour que les programmeurs puissent se focaliser sans crainte sur la résolution de leurs problèmes.
n
La Partie V présente des applications pratiques du pattern MVC, avec des exemples extraits des frameworks Cocoa.
L’Annexe A fournit des références supplémentaires sur l’utilisation et la compréhension de Cocoa et les design patterns.
Conventions typographiques Cet ouvrage emploie plusieurs styles pour le texte afin de mettre en exergue les différentes informations. La police italique est utilisée pour les noms de fichiers, les noms de dossiers et les mots nouveaux ou importants. La police à chasse constante est utilisée pour le code. Les mots affichés à l’écran, par exemple dans les menus ou les boîtes de dialogue, apparaissent dans le texte en PETITES CAPITALES. ATTENTION Les avertissements signalent des actions à éviter.
Préface
XIX
ASTUCE Ces encadrés présentent des astuces.
INFO Ces notes proposent des informations supplémentaires sur le sujet en cours.
Votre avis L’avis de nos lecteurs étant toujours le bienvenu, n’hésitez pas à nous faire part de vos commentaires. Pour cela, rendez-vous sur la page dédiée à cet ouvrage sur le site web Pearson (http://www.pearson.fr) et cliquez sur le lien RÉAGIR. Si vous souhaitez proposer la publication d’un titre dont vous avez besoin ou si vous souhaitez devenir auteur, ouvrez la page CONTACTS sur le site web Pearson, remplissez le formulaire présenté et envoyez-le au service EDITORIAL. Malgré tout le soin apporté à la rédaction du contenu de cet ouvrage, des erreurs sont toujours possibles. Si vous en rencontrez, que ce soit dans le texte ou dans le code, merci de nous les indiquer en allant sur la page CONTACTS du site web Pearson et en envoyant le formulaire rempli au service GÉNÉRAL.
Télécharger le code Les fichiers des exemples de code sont disponibles depuis le site web Pearson (http:// www.pearson.fr), en suivant le lien CODES SOURCES sur la page dédiée à ce livre. Ils contiennent les instructions permettant de les employer.
Remerciements De Erik M. Buck Les design patterns de Cocoa n’existerait pas sans les réflexions créatrices et avantgardistes répertoriées dans l’ouvrage Design patterns : catalogue des modèles de conception réutilisables, de Erich Gamma, Richard Helm, Ralph Johnson et John M. Vlissides. Les design patterns de Cocoa n’aurait aucune raison d’exister sans l’incroyable réussite, au niveau de la conception et de l’ingénierie, des frameworks Cocoa. De NeXTSTEP 0.8, en 1988, à OPENSTEP Enterprise 4.2 pour Windows NT, Solaris et HPUX, en 1997, en passant par Mac OS X 10.0, en 2001, et les iPhones, en 2007, les créateurs de Cocoa continuent à faire avancer l’état de l’art, tout en fixant des normes d’élégance et de cohérence toujours plus élevées. Les design patterns de Cocoa n’aurait aucun public sans le dévouement et la camaraderie extraordinaires de la communauté des développeurs Cocoa, représentés par les abonnés à la liste de diffusion Cocoa-dev d’Apple, les nombreux blogs d’information sur Cocoa et les développeurs d’applications tierces, petites ou grandes.
De Donald A. Yacktman Je souhaite remercier ma famille pour sa patience et son soutien, ainsi que les membres de la communauté qui m’ont aidé à apprendre au cours de toutes ces années.
À propos des auteurs Erik M. Buck a créé, en 1993, la société EMB & Associates, Inc., devenue leader dans le secteur du logiciel de divertissement et l’aérospatial grâce à l’exploitation des technologies de NeXT/Apple, qui se trouvent à présent dans les frameworks Cocoa. Erik Buck a également travaillé dans le bâtiment, a enseigné les sciences aux collégiens de quatrième, a exposé des portraits à l’huile et a développé des véhicules à carburants alternatifs. Il a vendu son entreprise en 2002 et est aujourd’hui cadre supérieur chez Northrop Grumman CorporationLes design patterns de Cocoa. Il a obtenu son baccalauréat en informatique de l’université de Dayton en 1991. Ses contributions aux listes de diffusion sur Cocoa et aux forums techniques sont fréquentes. Donald A. Yacktman utilise de manière professionnelle Cocoa et les technologies antérieures, OPENSTEP et NeXTSTEP, depuis 1991. Il a coécrit le livre Cocoa Programming et a contribué au site web Stepwise, à la fois en tant qu’auteur et éditeur. Par le passé, il a travaillé pour Verio/iServer et illumineX. Aujourd’hui, il est consultant indépendant dans le domaine de la conception et de la mise en œuvre d’applications Cocoa et pour l’iPhone. Donald Yacktman a obtenu un baccalauréat et une maîtrise en génie électrique et informatique à l’université Brigham Young, en 1991 et 1994.
Partie I Un pattern omniprésent L’intégralité de Cocoa est organisée selon les principes du design pattern MVC (Modèle-Vue-Contrôleur). Les outils et les frameworks d’Apple encouragent, voire imposent dans certainsLes design patterns de Cocoa cas, l’utilisation de ce pattern. Les chapitres de la Partie I présentent le pattern MVC, justifient son existence et expliquent comment il s’applique à la programmation Cocoa. Voici les chapitres de cette partie du livre : 1. Modèle-Vue-Contrôleur ; 2. Analyse et application de MVC.
1 Modèle-Vue-Contrôleur Au sommaire de ce chapitre U MVC dans Cocoa U En résumé
MVC (Modèle-Vue-Contrôleur) est l’un des design patterns logiciels les plus anciens et dont la réutilisation a été couronnée de succès. Il est arrivé dans les années 1970 avec le langage Smalltalk. Le pattern MVC définit l’architecture générale des frameworks Cocoa. Il s’agit d’un pattern de haut niveau destiné à l’organisation de groupes d’objets coopérant en sous-systèmes distincts : le Modèle, la Vue et le Contrôleur. Pour comprendre le rôle de chaque sous-système dans le pattern MVC, analysons les possibilités et les comportements des applications classiques. La plupart des applications enregistrent, récupèrent et présentent des informations à l’utilisateur. Elles lui permettent également de les modifier et, de manière générale, de les manipuler. Dans une application orientée objet, les informations ne sont pas uniquement des octets ; les objets encapsulent des informations, avec des méthodes permettant de les utiliser. Chaque objet d’une application doit appartenir à l’un des sous-systèmes suivants : n
Modèle. Le sous-système Modèle comprend les objets qui mettent en œuvre les caractéristiques uniques d’une application et l’enregistrement de ses informations. Le modèle établit les règles de traitement des données de l’application. Il s’agit du sous-système qui représente la valeur de l’application. Il est extrêmement important que le modèle soit autonome, sans aucune dépendance avec les sous-systèmes Vue ou Contrôleur.
n
Vue. Le sous-système Vue affiche les informations collectées à partir du modèle et donne aux utilisateurs un moyen d’interagir avec ces informations. Pour bien comprendre ce sous-système, il faut accepter l’existence potentielle d’une multitude de vues. Par exemple, il est possible de créer une vue pour l’interface graphique, une
4
Les design patterns de Cocoa
vue pour les rapports imprimés, une vue pour la ligne de commande, une vue de type web et une vue de type langage de script, toutes interagissant avec le même modèle. n
Contrôleur. Le sous-système Contrôleur existe pour découpler le modèle et les vues. Les interactions de l’utilisateur avec une vue sont transformées en requêtes sur le contrôleur, qui, à son tour, peut demander une modification des informations au modèle. Le contrôleur prend également en charge la conversion et la mise en forme des données pour leur présentation à l’utilisateur. Par exemple, le modèle peut enregistrer les données en pouces, mais, conformément aux préférences de l’utilisateur, le contrôleur peut les convertir en centimètres. Le modèle peut enregistrer des objets dans une collection non ordonnée, mais le contrôleur peut trier les objets avant de les transmettre à une vue pour leur affichage à l’utilisateur.
Le premier objectif du pattern MVC est de découpler le modèle et les vues afin que ces sous-systèmes puissent évoluer de manière indépendante. Le sous-système Contrôleur permet ce découplage (voir Figure 1.1). Au cours d’une interaction classique, l’utilisateur manipule un curseur ou tout autre objet de l’interface. Le curseur envoie un message au contrôleur pour lui signaler la modification de sa valeur (étape 1). Le contrôleur identifie les objets du modèle qui doivent être mis à jour en fonction de cette nouvelle valeur (étape 2). Il envoie des messages à ces objets pour leur demander d’effectuer leur mise à jour. Les objets du modèle réagissent à ces messages et peuvent contraindre les valeurs actualisées dans les limites définies par l’application ou effectuer d’autres validations. La logique de l’application est appliquée aux valeurs actualisées et d’autres objets du modèle peuvent être modifiés en conséquence. Le modèle indique ensuite au contrôleur qu’il a été modifié (étape 3). Enfin, le contrôleur envoie un message à la vue afin qu’elle reflète les changements du modèle (étape 4). Plusieurs parties de la vue peuvent être concernées. Vous pourriez être tenté de négliger le sous-système Contrôleur, car il est souvent difficile à concevoir et semble ajouter une complexité inutile. En effet, puisque le flux des informations a finalement lieu entre le modèle et les vues, pourquoi introduire une couche supplémentaire ? Simplement parce que les vues ont tendance à changer beaucoup plus souvent que les modèles. Outre le fait que les vues peuvent être nombreuses, il est dans la nature des interfaces utilisateurs d’évoluer en fonction des retours du client et des standards de présentation. Par ailleurs, il est quelquefois important de modifier le modèle sans affecter les vues. Le contrôleur place une couche d’isolation entre le modèle et les vues. La Figure 1.1 souligne l’importance de l’utilisation des messages pour réduire le couplage. Idéalement, le modèle et les vues ne doivent présenter aucune dépendance avec le contrôleur. Par exemple, les objets de la vue emploient souvent le design pattern Out-
Chapitre 1
Modèle-Vue-Contrôleur
5
let, cible et action (voir Chapitre 17) pour éviter d’avoir à détenir des informations sur les objets du contrôleur qui reçoivent les messages envoyés suite aux interactions de l’utilisateur avec les objets de l’interface. Les objets du modèle se servent souvent du pattern Notification (voir Chapitre 14) pour signaler la modification du modèle aux objets anonymes concernés, qui peuvent se trouver dans le contrôleur. Figure 1.1 Le contrôleur permet le découplage entre le modèle et les vues.
40 1 4 4
Contrôleur
Modèle
2
Valeur
3
1.1
MVC dans Cocoa
L’organisation de Cocoa suit assez librement les sous-systèmes Modèle, Vue et Contrôleur (voir Figure 1.2). Core Data simplifie le développement des modèles dans les applications. Application Kit (ou AppKit) contient des objets utilisés dans les vues et les contrôleurs. Le framework Foundation apporte des classes employées dans les trois sous-systèmes. Il ne fournit pas directement les fonctionnalités propres aux vues ou aux contrôleurs, mais donne accès aux services du système d’exploitation, à la classe de base NSObject, à la prise en charge des scripts et à d’autres caractéristiques permettant l’implémentation des modèles, des vues et des contrôleurs. Figure 1.2 L’organisation MVC générale de Cocoa.
Modèle Core Data
Vue
Contrôleur Application Kit
Foundation
Sur la page http://developer.apple.com/documentation/Cocoa/Reference/Foundation/ ObjC_classic/Intro/IntroFoundation.html, Apple fournit un diagramme complet des classes du framework Foundation.
6
Les design patterns de Cocoa
INFO Les modèles sont souvent développés directement en C ou en C++. Certaines applications se fondent sur un modèle multiplateforme qui ne dépend pas de Cocoa ou d’une autre technologie propre à une plateforme. La conception MVC a pour avantage d’éviter les dépendances entre le modèle et les autres sous-systèmes. Le contrôleur et les vues sont donc peu concernés par le langage ou la technologie employés pour le modèle. Toutefois, les frameworks Foundation et Core Data de Cocoa contiennent les ensembles de classes parmi les plus puissants, flexibles et extensibles. La construction du modèle à l’aide d’une technologie Cocoa est un bon choix lorsque des caractéristiques multiplateformes ne sont pas essentielles.
Hormis l’organisation MVC générale de Cocoa, des sous-systèmes importants reproduisent le design pattern MVC à plus petite échelle, à l’instar de l’architecture de gestion du texte qui regroupe des classes connexes sous les rôles de modèle, de vue et de contrôleur. De même, la gestion des documents est architecturée conformément aux composants MVC. D’autres technologies de Mac OS X, qui ne font pas strictement partie de Cocoa, utilisent également ce pattern. Les panneaux de Préférences Système, QuickTime Kit et Quartz Composer, séparent leurs composants secondaires selon les rôles du pattern MVC. Apports de Core Data aux modèles La technologie Core Data de Cocoa facilite le développement des modèles et résout deux défis classiques de l’implémentation : enregistrement des informations persistantes et gestion des relations entre les objets. Pratiquement tous les modèles ont besoin d’un mécanisme pour enregistrer des informations et les recharger ultérieurement. Il existe plusieurs manières de mettre en œuvre ces comportements. Certaines applications utilisent des fichiers binaires, d’autres, des fichiers textuels lisibles, et d’autres encore, une base de données relationnelle. Core Data implémente le chargement et l’enregistrement à l’aide d’une technique nommée persistance d’objets. L’idée est d’enregistrer les objets du modèle eux-mêmes, avec leurs informations et leurs relations à d’autres objets. Core Data est capable de charger et d’enregistrer les objets persistants en utilisant trois formats de fichiers : fichiers XML lisibles par l’homme, fichiers binaires à plat et bases de données SQLite. Que Core Data soit utilisé ou non, la conception du mécanisme d’enregistrement des informations persistantes doit satisfaire plusieurs critères. Doit-il servir à un échange simple d’informations entre différentes applications ? Dans l’affirmative, un format bien défini lisible par l’homme, comme XML, constitue le meilleur choix. La rapidité du chargement et de l’enregistrement est-elle un facteur important ? Les formats binaires affichent généralement les meilleures performances.
Chapitre 1
Modèle-Vue-Contrôleur
7
Core Data se fonde sur une infrastructure réutilisable qui masque les détails des formats d’enregistrement et vous permet de vous concentrer sur la conception des autres aspects du modèle. À tout moment du développement, vous pouvez changer le format d’enregistrement utilisé par Core Data avec votre modèle et même activer simultanément les trois formats proposés. Pratiquement tous les modèles doivent gérer les relations entre les objets. Core Data prend en charge les relations de cardinalité un-à-un et un-à-plusieurs. Chaque relation peut être facultative ou obligatoire. Par exemple, supposons que les membres d’une famille soient représentés par des objets. Chaque membre a une relation facultative avec un conjoint, mais a toujours précisément deux parents biologiques. Chaque membre peut avoir un nombre quelconque d’enfants. Core Data permet de préciser des relations, d’identifier des contraintes, de fournir des valeurs par défaut et de valider des relations. Bien que les relations puissent être établies dans le code, les outils Xcode d’Apple incluent un modélisateur qui permet de définir graphiquement les objets du modèle, appelés entités, et leurs relations. La classe NSManagedObject prend en charge la gestion des relations d’un objet et interagit avec un NSManagedObjectContext pour la persistance. Lorsque le modèle est conçu dans Xcode, vous pouvez utiliser directement des instances de NSManagedObject. Cette classe se fonde sur le pattern Mémoire associative, qui permet d’ajouter des relations et des informations, appelées attributs, dans les instances, sans passer par des sous-classes. Vous pouvez également créer vos propres sous-classes de NSManagedObject directement dans l’outil de modélisation. Core Data enregistre nativement les attributs dans des instances de NSNumber, NSData, NSString et NSSet. L’utilisation des sous-classes vous apporte une maîtrise maximale sur la manière dont les attributs sont enregistrés et vous permet d’utiliser des objets ou des structures C en guise d’attributs. Par ailleurs, cela vous permet d’ajouter une logique applicative aux entités du modèle. Core Data est détaillé au Chapitre 30. La gestion des relations, la modification des attributs et la persistance s’intègrent naturellement aux fonctionnalités annuler-rétablir (undo-redo) standard de Cocoa. Toute modification apportée à un attribut ou à une relation peut être annulée. Core Data automatise la validation des attributs et des relations afin que vous puissiez signaler aux utilisateurs les modifications incohérentes ou invalides des objets du modèle. Apports d’Application Kit aux vues Application Kit contient les classes utilisées pour les sous-systèmes Vue et Contrôleur. La Figure 1.3 souligne les classes les plus importantes pour les vues. Elles servent à la présentation des informations et aux interactions avec l’utilisateur. Apple fournit un diagramme complet des classes d’Application Kit à l’adresse http://developer.apple.com/ documentation/Cocoa/Reference/ApplicationKit/ObjC_classic/Intro/Intro-AppKit.html.
8
Les design patterns de Cocoa
NSWindow
NSPanel
NSSavePanel
NSOpenPanel
NSColorPanel NSFontPanel
NSApplication
NSTextField
NSTokenField
NSStepper
NSSecureTextField
NSSlider
NSSearchField
NSScroller
NSComboBox
NSSplitView
NSLevelIndicator
NSScrollView
NSTableView
NSRulerView
NSDatePicker
NSOpenGLView
NSSegmentedControl
NSMenuView
NSBrowser
NSBox
NSMatrix
NSClipView
NSImageView
NSControl
NSButton
NSTabView
NSColorWell
NSOutlineView
NSForm
NSMenu NSObject
NSResponder
NSView
NSPopUpButton
NSProgressIndicator NSTableHeaderView NSText
NSTextView
NSTextAttachementCell NSBrowserCell NSCell
NSLevelIndicatorCell
NSImageCell
NSButtonCell
NSMenuItemCell
NSActionCell
NSStepperCell
NSTokenFieldCell
NSSegmentedCell
NSSearchFieldCell
NSTextFieldCell
NSComboBoxCell
NSSliderCell
NSTableHeaderCell
NSDatePickerCell
NSSecureTextFieldCell
NSPopUpButtonCell
Figure 1.3 Les principales classes de Cocoa pour les vues.
Les classes NSMenu, NSWindow, NSApplication et NSView forment le noyau des interfaces utilisateurs graphiques de Cocoa. La quasi-totalité des informations affichées par une application Cocoa se trouve dans un menu ou une fenêtre. Chaque application Cocoa se fonde sur une instance de la classe NSApplication pour communiquer avec le système d’exploitation de manière à recevoir les actions de l’utilisateur, insérer une icône dans le Dock, présenter une barre des menus et afficher des fenêtres. Les classes dérivées de NSView implémentent les éléments standard d’une interface utilisateur, comme les boutons, le texte, les onglets, les indicateurs de progression et les images, qui peuplent les fenêtres. NSApplication, NSView et NSWindow sont des sous-classes de NSResponder, qui se trouve au cœur de la conception d’Application Kit ; elle encapsule la gestion des événe-
Chapitre 1
Modèle-Vue-Contrôleur
9
ments issus des actions de l’utilisateur et met en œuvre le design pattern Chaîne de répondeurs (voir Chapitre 18) pour que les événements et les messages soient reçus par les objets appropriés. La Figure 1.3 recense les nombreuses sous-classes standard de NSView employées dans les interfaces utilisateurs. Vous pouvez également ajouter des fonctionnalités spécifiques à l’interface d’une application en créant vos propres classes dérivées de NSView. Certains frameworks d’Apple, comme Web Kit, QuickTime Kit et Quartz Composer, fournissent des sous-classes spécialisées de NSView pour l’affichage et la modification de leurs types de médias respectifs. NSView met en œuvre le pattern Hiérarchie (voir Chapitre 16) et permet d’élaborer des interfaces constituées de vues imbriquées. Une vue peut contenir un nombre arbitraire de vues secondaires. Interface Builder facilite la création d’une hiérarchie de vues, mais vous pouvez également la construire dans le code avec les méthodes de NSView, comme -(void)addSubview:(NSView *)aView, -(void)removeFromSuperview et -(void) replaceSubview:(NSView *)oldView with:(NSView *) newView.
Certaines sous-classes Cocoa de NSView agencent visuellement leurs vues secondaires. Par exemple, la classe NSBox peut dessiner un cadre autour des vues secondaires de manière à les regrouper visuellement. La classe NSTabView utilise la métaphore visuelle des onglets pour sélectionner et afficher de manière mutuellement exclusive une vue parmi plusieurs vues secondaires. NSSplitView se sert d’une barre graphique pour séparer ses vues secondaires, que ce soit horizontalement ou verticalement. Les utilisateurs font glisser la barre de séparation à l’aide de la souris de manière à rendre visible une partie plus ou moins importante de chaque vue secondaire. NSScrollView repositionne ses vues secondaires en fonction du déplacement des barres de défilement effectué par l’utilisateur. De nombreux composants d’une interface utilisateur Cocoa sont des classes dérivées de NSControl, qui joue un rôle essentiel dans les patterns Cible, Action et Chaîne de répondeurs décrits aux Chapitres 17 et 18. Par exemple, lorsqu’un utilisateur sélectionne une date par l’intermédiaire d’un objet NSDatePicker (sélecteur de date), un message d’action est envoyé à la cible du sélecteur de date. Si aucune cible particulière n’est précisée, l’objet qui finit par recevoir le message est déterminé grâce au pattern Chaîne de répondeurs. La classe NSCell (cellule) met en œuvre le pattern Poids mouche (voir Chapitre 22), qui permet d’optimiser le temps d’exécution et la consommation mémoire. Les instances de NSControl utilisent des sous-classes de NSCell en vue de leur optimisation et pour ajouter la flexibilité. NSTableView est un bon exemple de contrôle qui utilise les cellules. Des instances séparées de NSCell déterminent la manière dont les données sont présen-
10
Les design patterns de Cocoa
tées dans chaque colonne. Il est inutile de créer une sous-classe de NSTableView simplement pour contrôler la présentation des informations. À la place, il suffit de configurer la classe NSTableView standard avec des cellules différentes. Apports d’Application Kit aux contrôleurs La classe Cocoa NSController et les classes connexes, comme NSArrayController, assurent un rôle de "médiateur" entre les objets d’une vue et ceux d’un modèle. Les médiateurs contrôlent le flux des informations et, dans certains cas, fournissent des valeurs par défaut. Par exemple, si une vue affiche des données en fonction de la sélection courante de l’utilisateur, un médiateur peut fournir des données par défaut qui seront utilisées lorsque la sélection est vide. Application Kit inclut les classes NSController, NSObjectController, NSArrayController, NSUserDefaultsController et NSTreeController qui gèrent le flux de données en utilisant les bindings Cocoa (voir Chapitre 32). Les bindings établissent des relations entre des objets et sont définis par programmation ou à l’aide d’Interface Builder. Lorsqu’un binding existe entre deux objets, les modifications apportées à l’un des objets pendant l’exécution conduisent à des mises à jour automatiques sur l’autre. Des bindings peuvent être établis directement entre des objets d’un modèle et ceux d’une vue ou entre des objets du même sous-système, par exemple entre deux objets d’une vue. Toutefois, les bindings directs entre des objets d’une vue et d’un modèle conduisent à des problèmes identiques à ceux des dépendances entre d’autres sous-systèmes. Il faut utiliser NSController et ses sous-classes pour établir un médiateur entre le modèle et la vue. INFO Les bindings Cocoa simplifient le développement des sous-systèmes Contrôleur et remplacent potentiellement le code écrit manuellement par des connexions créées dans Interface Builder. Cependant, même si vous n’utilisez pas les bindings, vous devez toujours employer des contrôleurs de médiation dans vos applications. Les programmeurs Cocoa créent souvent des sous-classes simples de NSObject pour jouer le rôle de contrôleur médiateur et implémentent manuellement des méthodes de synchronisation entre le modèle et la vue. Ces contrôleurs médiateurs personnalisés remplissent l’objectif de découplage du modèle et de la vue défini par MVC.
Hormis la médiation du flux de données entre le modèle et la vue, le contrôleur s’occupe également du contrôle général du comportement de l’application. Lorsqu’il existe plusieurs sous-systèmes Vue, des objets du sous-système Contrôleur sont chargés de déterminer les vues à présenter aux utilisateurs. Par exemple, si un script extrait des données à partir d’un modèle, il est probable qu’aucune vue graphique ne doive être affichée. Le contrôleur est l’endroit idéal où placer la logique pour déterminer si une
Chapitre 1
Modèle-Vue-Contrôleur
11
vue graphique doit être chargée et présentée. Le modèle ne peut pas s’en charger car il n’est pas supposé connaître l’existence des vues et les différentes vues ne doivent pas dépendre l’une de l’autre. Application Kit propose plusieurs classes pour le contrôle du comportement de l’application. L’architecture de document de Cocoa, décrite plus loin dans ce chapitre, met en avant les classes NSDocumentController, NSViewController et NSWindowController, qui contrôlent, respectivement, les documents, les vues et les fenêtres de l’application. Architecture pour la gestion du texte dans Cocoa Les classes NSText et NSTextView (voir Figure 1.4) apportent la partie visible de l’architecture MVC pour la gestion du texte dans Cocoa. La classe NSTextStorage fournit un modèle pour l’enregistrement et le traitement du texte. Lorsque la disposition du texte est un point important de la logique de l’application, la classe NSTextContainer, qui mémorise la forme géométrique d’un bloc de texte, fait également partie du modèle de l’architecture du texte. Par exemple, un programme de dessin qui contraint le texte dans une zone circulaire peut avoir besoin de stocker cette forme dans le modèle afin de la restaurer lorsque le modèle est sauvegardé puis rechargé. Toutefois, pour la plupart des applications, une disposition rectangulaire de texte suffit et aucun NSTextContainer explicite n’est requis. Figure 1.4
Modèle
Les composants MVC de l’architecture du texte dans Cocoa.
NSAttributedString
NSMutableAttributedString NSTextStorage
NSResponder
NSView
Vue NSObject
NSText
NSTextView
Contrôleur NSLayoutManager
La classe NSLayoutManager joue le rôle de contrôleur médiateur entre la vue et le modèle. Chaque instance de NSTextView demande à l’instance de NSLayoutManager associée de lui fournir le texte qui doit être affiché. L’instance de NSLayoutManager accède alors aux instances de NSTextStorage et de NSTextContainer pour obtenir ce texte. Au cours du processus, NSLayoutManager convertit les caractères Unicode en glyphes (représentation graphique des caractères), qui dépendent de la police de caractères en cours d’utilisation, du soulignement et des autres attributs du texte. Conformément au pattern MVC, la classe NSTextStorage est indépendante de la présentation du texte. Dans le modèle, de nombreux traitements différents peuvent être appliqués au texte. Par exemple, les attributs du texte peuvent être changés, le texte lui-même
12
Les design patterns de Cocoa
peut être modifié, une recherche peut être effectuée sur le texte, le texte peut être envoyé sous forme de pages web au travers du réseau et le texte peut être enregistré ou chargé dans une application de traitement par lots qui ne dispose d’aucune interface utilisateur. L’architecture de prise en charge du texte de Cocoa apporte une solution complète qui correspond aux besoins de la plupart des applications. Très souvent, des instances de NSTextView sont simplement déposées dans l’interface depuis Interface Builder. Apple propose un didacticiel qui explique comment procéder (http://developer.apple.com/ documentation/Cocoa/Conceptual/TextArchitecture/Tasks/TextEditor.html). Si vous souhaitez modifier le traitement du texte, la conception MVC de l’architecture vous permet de focaliser votre travail sur le sous-système approprié. Si vous souhaitez enregistrer des attributs non standard avec le texte, utilisez NSTextStorage ou sa super-classe, NSMutableAttributedString, fournie par le framework Foundation. Puisque NSMutableAttributedString utilise le pattern Mémoire associative (voir Chapitre 19), vous pouvez faire pratiquement tout ce que vous souhaitez sans passer par l’héritage. Si vous devez implémenter des possibilités d’agencement exotiques du texte, partez de NSLayoutManager. Si vous avez besoin d’un contrôle précis sur la saisie de l’utilisateur ou si vous souhaitez afficher des attributs de texte personnalisés, utilisez la sous-classe NSTextView. Architecture pour la gestion des documents dans Cocoa Les applications d’affichage ou de modification des informations adoptent souvent la métaphore des "documents" présentés à l’écran dans des fenêtres. C’est notamment le cas des tableurs, des traitements de texte, des navigateurs web et des logiciels de dessin. Les classes Cocoa données à la Figure 1.5 implémentent une architecture de document MVC réutilisable. Figure 1.5
Modèle
Les composants MVC de l’architecture de document dans Cocoa.
NSManagedObjectContext NSFileWrapper
Vue NSObject
NSResponder
NSWindow NSApplication
Contrôleur de vue NSWindowController NSDocumentController
Contrôleur de modèle NSDocument
NSPersistentDocument
Chapitre 1
Modèle-Vue-Contrôleur
13
L’architecture de document ajoute un nouvel aspect au pattern MVC. Le sous-système Contrôleur est divisé en deux parties : contrôleur de modèle et contrôleur de vue. Les classes présentes dans le contrôleur de modèle chargent, enregistrent et accèdent aux données du modèle. Les classes du contrôleur de vue accèdent aux données déjà chargées pour permettre leur affichage dans la vue. Cette séparation existe en partie pour simplifier la création fréquente des sous-classes de NSDocument. Cette classe est une parfaite illustration du pattern Patron de méthode (voir Chapitre 4). Puisqu’il s’agit d’une classe abstraite, vous devez créer des classes dérivées et implémenter quelques méthodes indispensables pour prendre en charge le chargement et l’enregistrement des données du modèle, qui sont spécifiques à votre application. Une autre raison de cette séparation tient dans la création dynamique de certaines parties du contrôleur. Par exemple, il est possible de charger et d’interagir avec un document sans nécessairement afficher une quelconque fenêtre associée à ce document. Un script peut ouvrir un document, y lire des informations et les copier ailleurs sans avoir besoin d’ouvrir des fenêtres de document. Les contrôleurs de vue, NSDocumentController et NSWindowController, s’intercalent entre la vue et le modèle de manière à accéder aux informations qui doivent être affichées ou modifiées. Il existe une seule instance de NSDocumentController dans une application basée sur les documents. À tout moment, il existe des instances séparées de NSDocument pour chaque document ouvert. Zéro, une ou plusieurs instances de NSWindowController peuvent être associées à chaque document ouvert. L’utilisation d’un nombre varié d’instances de différentes classes est une autre raison de la division du contrôleur en deux parties. Sans cette distinction, un même objet devrait contrôler toutes les fenêtres qui représentent chaque document, ainsi que le chargement et l’enregistrement des documents. Dans Mac OS X 10.5, Apple a ajouté la classe NSViewController, dont le rôle est comparable à celui de la classe NSWindowController. Les instances de NSViewController se placent entre les objets chargés depuis les fichiers .nib et les objets qui se trouvent hors de ces fichiers. Interface Builder facilite la création de bindings Cocoa qui comprennent des instances de NSViewController. Les classes NSWindowController et NSViewController simplifient la gestion de la mémoire utilisée pour les objets chargés à partir des fichiers .nib. La Figure 1.6 illustre les collaborations entre les classes de l’architecture de document dans Cocoa. Dans une application basée sur les documents, une instance de NSDocumentController reçoit et traite les messages de création de nouveaux documents, de chargement de documents et de rappel aux utilisateurs d’enregistrer les documents avant de fermer l’application. NSDocumentController gère également le contenu du menu DOCUMENTS RÉCENTS. Les applications Cocoa graphiques disposent d’une instance de la classe NSApplication pour la prise en charge des menus et des fenêtres, comme
14
Les design patterns de Cocoa
l’a décrit la section "Apports d’Application Kit aux vues". L’instance de NSDocumentController reçoit les messages de délégation envoyés par l’instance de NSApplication. Si vous fournissez un objet délégué différent, il recevra les messages qui vous permettent de contrôler la gestion des documents sans avoir à créer une sous-classe de NSDocumentController. Cette possibilité d’éviter, dans certains cas, l’héritage est un avantage du pattern Délégué (voir Chapitre 15). Figure 1.6 NSManagedObjectContext
NSApplication
NSDocumentController
1
délégué
Les collaborations entre les objets dans l’architecture de document de Cocoa.
NSPersistentDocument
0..*
NSWindowController
1..*
NSWindow
0..*
NSWindowController
1..*
NSWindow
0..*
0..*
Sous-classe de NSDocument
NSFileWrapper
En général, une instance de la classe NSWindowController est utilisée pour contrôler chaque fenêtre associée à un document. Si nécessaire, un même document peut être représenté par plusieurs fenêtres. Par exemple, une fenêtre peut afficher le modèle du document sous forme de données XML et HTML brutes, pendant qu’une autre fenêtre présente simultanément ces mêmes informations sous forme d’une page web mise en forme. Ou bien supposons qu’une application enregistre chacune des réunions planifiées dans un seul document. Une fenêtre peut contenir une table qui recense tous les participants à la réunion, tandis qu’une autre affiche les informations de contact concernant la personne sélectionnée dans la table. Chaque instance de NSDocument conserve une liste des NSWindowController associés et chaque contrôleur de fenêtre sait à quel document il appartient. Cette communication bidirectionnelle permet aux contrôleurs de fenêtres d’accéder au modèle via l’instance de NSDocument. De même, lorsque l’utilisateur masque ou ferme un document, l’instance de NSDocument signale l’opération aux contrôleurs de fenêtres. Chaque instance de NSWindowController possède un outlet vers la fenêtre qu’elle contrôle. Habituellement, cet outlet est connecté à l’aide d’Interface Builder. Vous pouvez créer vos propres classes dérivées de NSWindowController et ajouter des outlets et des actions supplémentaires. Les outlets et les actions sont ensuite connectés aux objets du sous-système Vue, comme des boutons et des champs de saisie, pour que le contrôleur de vue ait un accès direct à ces contrôles. Une autre solution, tout aussi valide d’un point de vue conception, se fonde sur les bindings Cocoa (voir Chapitre 32) pour configurer les objets de l’interface utilisateur de manière qu’ils reflètent en permanence les données du modèle obtenues via une instance de NSDocument.
Chapitre 1
Modèle-Vue-Contrôleur
15
Le modèle de l’application contient les données propres à cette application. Si vous utilisez les technologies Core Data de Cocoa pour mettre en œuvre le modèle, une instance de NSManagedObjectContext permet d’accéder aux informations et aux relations du modèle, ainsi qu’au système de stockage persistant, quel que soit le format employé (XML, binaire ou base de données). Apple fournit la classe NSPersistentDocument, une sous-classe de NSDocument, pour l’enregistrement et le chargement à l’aide d’un NSManagedObjectContext associé. Avec la solution Core Data, vous pouvez souvent éviter la création d’une classe dérivée de NSDocument. Si vous n’utilisez pas Core Data, vous devez créer une sous-classe de NSDocument et mettre en œuvre certaines méthodes, comme (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError et (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName -error:(NSError **)outError. Elles implémentent la logique et les conversions de données nécessaires à l’enregistrement et au chargement du modèle à partir du type de fichier employé et son emplacement sur le système de fichiers. Les informations de type de fichier permettent de mettre en place les conversions de type du document lors de l’enregistrement et sont nécessaires au code pour qu’il puisse passer d’un type à un autre lors du chargement. Le paramètre error: permet au code de signaler les erreurs qui se sont produites lors de l’enregistrement ou du chargement et qui doivent être affichées aux utilisateurs. Lorsque Core Data est utilisé, il existe souvent une correspondance directe entre les fichiers sur le disque et les documents. Autrement dit, toutes les informations d’un document sont enregistrées dans un fichier. Toutefois, si vos besoins de stockage sont plus complexes, la classe NSFileWrapper s’occupe des détails de l’utilisation de plusieurs fichiers par document. Chaque instance de NSFileWrapper gère un répertoire complet de fichiers dont l’emplacement est précisé par l’URL passée à -readFromURL: ofType:error:. Grâce à NSFileWrapper, l’enregistrement et le chargement de modèles complexes sont facilités. Au premier abord, l’architecture de document MVC de Cocoa peut sembler complexe. Toutefois, en pratique, cette architecture est rarement manipulée directement. Elle fournit tous les comportements standard attendus par les utilisateurs et nécessite très peu de travail de la part du développeur. Grâce à la conception MVC, vous pouvez adapter le comportement des applications multidocuments sans avoir à intervenir sur tous les composants qui collaborent à la mise en œuvre de la solution complète. L’outil Xcode inclut un modèle (template) de projet Cocoa pour les applications basées sur les documents. Il crée automatiquement une sous-classe de NSDocument et les autres composants requis, comme l’instance de NSDocumentController. Le modèle fournit même un fichier Interface Builder qui contient une fenêtre servant de point de départ à la définition de la vue du document. Il existe un modèle semblable pour les applications Core Data. Il utilise automatiquement la classe NSPersistentDocument.
16
Les design patterns de Cocoa
Scripts dans Cocoa Une interface de script est comparable à n’importe quelle autre interface utilisateur. Les auteurs de scripts supposent certaines conventions, tout comme il existe des conventions sur l’apparence des interfaces graphiques. Avec la conception MVC, une interface de script n’est qu’un autre des nombreux sous-systèmes Vue qu’une application peut inclure et, comme toutes les vues, ces interfaces interagissent normalement avec le contrôleur, sans accéder directement au modèle. Lors de la mise en place d’une interface de script, il ne faut pas oublier que, pour les auteurs de scripts, la meilleure façon d’interagir avec une application diffère très souvent de la manière d’utiliser une interface graphique. Bien qu’il soit possible d’écrire des scripts qui ouvrent des fenêtres et simulent des clics sur les boutons ou des sélections dans les menus, cette approche est rarement efficace. Dans l’idéal, une interface de script doit être opérationnelle même si l’application n’affiche aucune information à l’écran. Les scripts automatisent des tâches complexes et fastidieuses et s’exécutent en temps différé. Cocoa fournit automatiquement les bases des interfaces de script. L’objet NSApplication utilisé dans toute application Cocoa graphique accepte des messages interprocessus et des commandes appelées AppleEvent. Les AppleEvent se fondent sur un format standard de Mac OS X pour les commandes, les arguments et les valeurs de retour. Le langage de script le plus répandu et le plus populaire qui permet d’envoyer des AppleEvent est AppleScript. Toutefois, les langages Python et Ruby multiplateformes et livrés avec Mac OS X sont également capables d’envoyer de tels AppleEvent. Il est même possible de les générer à partir d’applications Cocoa écrites en C ou Objective-C. Depuis Mac OS X 10.5, Apple propose une technologie, nommée Scripting Bridge, qui simplifie et standardise l’intégration des interfaces de script avec les applications Cocoa. Outre NSApplication, d’autres classes du sous-système Contrôleur prennent en charge les scripts. Les classes NSDocument et NSDocumentController répondent aux AppleEvent associés à la sélection, au chargement et à l’enregistrement d’un document. L’architecture de gestion du texte de Cocoa traite les AppleEvent standard de manipulation du texte, notamment pour les opérations d’insertion, de suppression, de remplacement du texte et de la recherche. Les possibilités de l’application sont exposées au travers de nouvelles commandes fournies dans un "dictionnaire" de script enregistré comme une ressource applicative et chargé par la classe NSApplication. Le dictionnaire de script correspond généralement à un fichier XML qui précise comment les objets de l’application sont sélectionnés ou identifiés et quelles commandes peuvent être employées avec les objets. Les auteurs de
Chapitre 1
Modèle-Vue-Contrôleur
17
scripts utilisent le dictionnaire pour déterminer les commandes à envoyer depuis les scripts. Les outils de développement de scripts, comme AppleScript Studio et Automator pour Mac OS X, lisent le dictionnaire pour valider les scripts et détecter les erreurs avant l’exécution. Architecture pour les panneaux de préférences dans Cocoa L’application Préférences Système intégrée à Mac OS X est extensible. Elle possède une architecture de plugins grâce à laquelle de nouvelles interfaces utilisateurs, nommées "panneaux", peuvent être ajoutées. Lors de son démarrage, l’application examine des emplacements standard du système de fichiers pour trouver et charger les panneaux complémentaires disponibles. Elle affiche ensuite une fenêtre dans laquelle les utilisateurs activent les panneaux de préférences chargés. L’application se fonde sur le pattern MVC et les panneaux que vous ajoutez doivent également suivre cette méthode de conception. Le modèle de Préférences Système est constitué de fichiers dans lesquels sont enregistrées les préférences concernant le système et celles de chaque utilisateur. On y trouve notamment des informations comme la vitesse de répétition des touches, l’image d’arrière-plan du bureau et la méthode par défaut de connexion à Internet. Le modèle de Préférences Système est encapsulé par l’interface Preference Services de Core Foundation, que toutes les applications et le système d’exploitation emploient pour manipuler les préférences. Chaque panneau de préférences possède son propre contrôleur créé à partir d’une sousclasse de NSPreferencePane. NSPreferencePane prend en charge une grande partie de l’interface avec l’application Préférences Système. Par exemple, dès que votre panneau de préférences est sélectionné par un utilisateur, mais avant qu’il ne soit affiché, le message -(void)willSelect est envoyé à votre contrôleur. Immédiatement après l’affichage de la vue, Préférences Système envoie le message -(void)didSelect à votre contrôleur. Dès que l’utilisateur désélectionne votre panneau, que ce soit en choisissant un panneau différent, en fermant la fenêtre des préférences ou en quittant l’application Préférences Système, le message -(NSPreferencePaneUnselectReply)shouldUnselect est envoyé à votre contrôleur. Selon la valeur retournée par votre implémentation de -shouldUnselect, votre panneau peut retarder l’action de désélection. Par exemple, il peut implémenter -shouldUnselect de manière à afficher un message d’erreur qui signale tout problème avec les préférences définies et signaler qu’elles ne seront pas sauvegardées. Enfin, chaque panneau de préférences fournit son propre sous-système Vue pour que les utilisateurs puissent manipuler les préférences qu’il propose. Vous pouvez utiliser Interface Builder pour construire la vue et la connecter au contrôleur avec le pattern Outlet, cible et action (voir Chapitre 17) ou avec les bindings (voir Chapitre 29).
18
Les design patterns de Cocoa
N’oubliez pas que, dans une conception MVC, il est extrêmement important que les valeurs des préférences soient enregistrées dans le modèle, non dans l’interface utilisateur du panneau. Si une valeur est enregistrée uniquement dans l’interface utilisateur, elle pourrait aussi bien ne pas exister car le système et les autres applications n’y auront pas accès. Architecture de Quartz Composer L’application Quartz Composer fait partie des outils de développement gratuits fournis par Apple. Elle se fonde sur la technologie Quartz Core Imaging de Mac OS X pour créer des compositions visuelles à l’aide d’opérations graphiques appelées "patches". Un groupe de patches interconnectés et une source de données, comme des images, des couleurs et du texte, forment le modèle. Le modèle équivant à une recette qui explique comment créer des compositions visuelles. QCView est une classe dérivée de NSView capable d’afficher des compositions terminées. La classe QCPatchController joue le rôle de médiateur entre le modèle et QCView. Lorsque vous intégrez des compositions Quartz à vos applications Cocoa, vous pouvez connecter les contrôles de l’interface utilisateur à un QCPatchController pour agir sur les compositions affichées. Par exemple, un groupe de patches peut employer une variable qui précise le niveau d’opacité de la composition résultante. Votre application peut utiliser des actions ou des bindings avec l’instance de QCPatchController pour que la valeur de la variable d’opacité soit fixée à l’aide d’un curseur. Architecture de QTKit QTKit est un framework Objective-C qui manipule et affiche du contenu QuickTime. Le modèle utilisé par QTKit est mis en œuvre par la classe QTMovie, qui encapsule des vidéos, des flux audio, des animations et d’autres formats définis par la norme internationale MPEG-4 et acceptés par QuickTime. QTMovieView est une sous-classe de NSView et affiche le contenu QuickTime. Pour utiliser les classes QTMovie et QTMovieView dans une application Cocoa, vous devez généralement implémenter votre propre contrôleur. Il crée des instances de QTMovie et charge le contenu à partir de fichiers ou au travers du réseau. Il envoie ensuite des messages à l’instance de QTMovieView pour lui indiquer quel QTMovie lire. Les opérations de lecture, de pause, d’avance et de recul rapide, etc. sont réalisées en envoyant des messages à QTMovieView.
Chapitre 1
1.2
Modèle-Vue-Contrôleur
19
En résumé
Le pattern MVC réduit le couplage au sein des applications, mais, dans certains cas, il augmente également leur complexité. Une séparation claire des sous-systèmes se révèle bénéfique sur le long terme car elle réduit les coûts de maintenance et facilite les améliorations progressives. Le design pattern MVC présentera le plus grand intérêt si vous envisagez une future version 2.0 de l’application en cours de développement. Par ailleurs, plus l’application est volumineuse, plus l’intérêt du pattern MVC est notable. Il faut également prendre en compte le fait qu’il est généralement plus facile de tester un modèle directement qu’au travers d’une interface utilisateur. En effet, lorsque le test passe par l’interface utilisateur, un travail supplémentaire est nécessaire pour déterminer si l’échec est dû à un bogue dans la logique de l’application, dans l’interface utilisateur ou dans les deux. De plus, le modèle et l’interface utilisateur sont souvent développés par des équipes différentes. Les compétences requises pour l’établissement du modèle peuvent être très différentes de celles demandées pour la construction de l’interface utilisateur parfaite. Malgré tous ses avantages, la conception MVC ne convient pas forcément à tous les projets logiciels. À une extrémité, l’application web qui s’exécute sur un serveur et affiche des informations dans une page web fait le candidat idéal pour une conception MVC. Il existe déjà une séparation claire entre la vue implémentée par le navigateur web et le modèle défini sur le serveur. À l’autre extrémité, nous trouvons les pilotes de périphériques du système d’exploitation et les longs programmes de calcul intensif. L’application de configuration des pilotes de périphériques peut adopter la conception MVC, mais les pilotes eux-mêmes doivent se conformer aux interfaces du système d’exploitation et, en général, ils ne présentent aucune donnée aux utilisateurs. Les longs programmes de calcul s’exécutent souvent de manière différée et n’offrent aucune interface utilisateur. Entre ces deux extrêmes, la conjugaison de MVC et de Cocoa peut prouver toute sa valeur dans une grande variété d’applications, notamment les logiciels de dessin, les tableurs, les jeux, les traitements de texte et n’importe quel autre logiciel d’affichage ou de modification d’informations.
2 Analyse et application de MVC Au sommaire de ce chapitre U Conception non MVC U Conception MVC U En résumé
Ce chapitre présente une petite application de calcul des salaires et en propose deux implémentations distinctes. La conception de la première version est simple, mais naïve, et ne met pas en jeu l’approche MVC. La seconde souligne les améliorations obtenues par l’emploi du design pattern MVC. INFO Les deux versions de l’application sont l’occasion de faire un tour d’horizon des technologies Cocoa et des outils de développement. L’exemple illustre les possibilités, mais ce chapitre n’est pas assez long pour expliquer l’intégralité de la création de l’application par glisserdéposer dans Interface Builder et Xcode. Il permet d’avoir une vue d’ensemble sur une application triviale construite avec Cocoa. À partir de la Partie II, ce livre introduit les patterns qui servent de fondation aux technologies employées dans ce chapitre.
2.1
Conception non MVC
Pour déterminer le montant du salaire d’un employé, l’application de calcul multiplie le taux horaire de l’employé par le nombre d’heures travaillées au cours de la période concernée. Pour que l’exemple soit plus intéressant, les heures supplémentaires de certains employés sont payées à un taux 1,5 fois supérieur. Les autres employés ne sont pas sujets à ces heures supplémentaires mieux payées et reçoivent le même montant quel que soit le nombre d’heures de travail. La Figure 2.1 présente l’interface utilisateur de cette application simple.
22
Les design patterns de Cocoa
Figure 2.1 L’interface utilisateur de l’application de calcul des salaires.
L’application affiche le nom de l’employé. L’utilisateur peut saisir le taux horaire de cet employé, le nombre d’heures travaillées au cours de la période concernée et le nombre normal d’heures pour cette période. Il peut également indiquer si les heures supplémentaires de l’employé sont payées. Une fois toutes les informations entrées, un clic sur le bouton CALCULER affiche le montant du salaire. L’interface utilisateur est construite à l’aide de l’outil Interface Builder, fourni par Apple avec les outils gratuits du développeur. Les contrôles de l’interface utilisateur sont pris dans des palettes d’objets réutilisables et déposés dans des fenêtres (voir Figure 2.2). Ils sont ensuite configurés et interconnectés dans Interface Builder, sans écrire une ligne de code. La page http://developer.apple.com/documentation/DeveloperTools/Conceptual/IBTips/IBTips.html propose plusieurs didacticiels et documents sur Interface Builder. INFO Interface Builder permet de manipuler des objets dynamiques. Les opérations de glisserdéposer copient des objets pleinement opérationnels, qui peuvent être utilisés et testés dans Interface Builder. Ils sont configurés dans l’état initial souhaité et enregistrés dans un fichier. Ensuite, lorsque l’application Cocoa lit ce fichier, tous les objets présents sont restaurés dans l’état où ils ont été enregistrés. Interface Builder met en œuvre le design pattern Archivage et désarchivage (voir Chapitre 11).
La logique de l’application doit être mise en œuvre par du code. Dans cet exemple très simple, une seule méthode de classe est nécessaire. L’interface de la classe PayCalculator déclare des outlets pour chaque objet de l’interface utilisateur et une action pour calculer le montant à payer :
Chapitre 2
Analyse et application de MVC
23
Figure 2.2 Un bouton est déposé depuis la bibliothèque d’objets d’Interface Builder dans une fenêtre en construction. #import @interface { IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet }
PayCalculator : NSObject NSTextField NSFormCell NSFormCell NSFormCell NSTextField NSButton
*employeeNameField; *hourlyRateField; *hoursWorkedField; *standardHoursInPeriodField; *payAmountField; *employeeIsExemptButton;
- (IBAction)calculatePayAmount:(id)sender; @end
Les outlets équivalent à des pointeurs sur d’autres objets, tandis que les actions sont des messages envoyés d’un objet à un autre. Si ces termes ne vous sont pas familiers, pas de panique, car le Chapitre 17 détaillera cette technologie. Les outlets sont connectés à
24
Les design patterns de Cocoa
d’autres objets, comme les champs de saisie, à l’aide de l’outil Interface Builder (voir Figure 2.3). De manière similaire, une connexion entre le bouton CALCULER et l’action -calculatePayAmount: stipule que le message calculatePayAmount: doit être envoyé suite au clic sur le bouton. Figure 2.3 Une connexion entre un outlet et un champ de saisie est établie dans Interface Builder.
La classe PayCalculator implémente -calculatePayAmount: de manière à fixer la valeur du champ payAmountField d’après les informations saisies par l’utilisateur. #import "PayCalculator.h" @implementation PayCalculator - (IBAction)calculatePayAmount:(id)sender { if(NSOnState == [employeeIsExemptButton state]) { // Payer le taux horaire multiplié par le nombre d’heures normales, // quel que soit le nombre d’heures travaillées. [payAmountField setFloatValue:[hourlyRateField floatValue] * [standardHoursInPeriodField floatValue]]; }
Chapitre 2
Analyse et application de MVC
25
else { // Payer le taux horaire multiplié par le nombre d’heures travaillées. float payAmount = [hourlyRateField floatValue] * [hoursWorkedField floatValue]; if([hoursWorkedField floatValue] > [standardHoursInPeriodField floatValue]) { // Les heures supplémentaires sont augmentées de 50%. float overtimePayAmount = 0.5f * [hourlyRateField floatValue] * ([hoursWorkedField floatValue] [standardHoursInPeriodField floatValue]); payAmount = payAmount + overtimePayAmount; } [payAmountField setFloatValue:payAmount]; } } @end
Analyse de la version non MVC Toutes les informations nécessaires au calcul du montant à verser sont enregistrées dans les objets de l’interface utilisateur. Cette conception est illustrée à la Figure 2.4. Lorsque l’utilisateur clique sur le bouton CALCULER, le message -calculatePayAmount: est envoyé à un objet PayCalculator (étape 1). L’implémentation de la méthode -calculatePayAmount: de PayCalculator extrait des différents objets de l’interface utilisateur les informations requises sous forme de valeurs réelles (étape 2). La méthode -calculatePayAmount: utilise le résultat du calcul pour fixer la valeur du contrôle payAmount (étape 3). Figure 2.4 employeeNameField
Les relations entre les différents objets dans la conception non MVC.
hourlyRateField 2 2
hoursWorkedField
2 PayCalculator
2
standardHoursInPeriodField
2 3 1
employeeIsExemptButton
payAmountField
calculateButton
26
Les design patterns de Cocoa
La mise en œuvre de cette conception ne pose pas de difficultés ; vous en trouverez une version dans l’archive des codes sources de cet ouvrage. Toutefois, même une application aussi simple que celle-ci révèle plusieurs problèmes de conception sérieux. Tout d’abord, aucun mécanisme ne permet d’enregistrer les informations saisies. Chaque fois que l’utilisateur souhaite calculer le salaire, il doit entrer à nouveau toutes les informations. Ensuite, si l’utilisateur souhaite afficher ou saisir des informations pour plusieurs employés à la fois, il faut reproduire l’intégralité de l’interface utilisateur pour chaque employé, car les informations sur chaque employé sont enregistrées dans les objets de l’interface. À partir de là, les problèmes s’aggravent. Même si Cocoa permet aux utilisateurs d’imprimer l’interface telle qu’elle apparaît à l’écran, une présentation plus concise serait plus appropriée. Voilà donc déjà le besoin d’afficher les informations sous deux formes : une interface utilisateur graphique et une version imprimée. Cependant, puisque toutes les informations sont enregistrées dans les objets de l’interface, ceux-ci doivent être créés même si le résultat du calcul est simplement imprimé, sans jamais être affiché à l’écran. Que se passe-t-il si l’utilisateur signale que le champ standardHoursInPeriodField n’est pas utile dans l’interface ? Il est possible que la durée de travail normale sera toujours de 35 heures et que les utilisateurs demandent donc la suppression de ce champ. Cette simple modification de l’interface impose un changement dans le calcul du salaire à verser, par exemple pour placer le nombre d’heures normales dans une constante ou ailleurs. Le design pattern MVC a été créé pour éviter les situations dans lesquelles la logique de l’application, comme calculer le montant du salaire, doive être changée en raison d’une modification de l’interface utilisateur.
2.2
Conception MVC
Le pattern MVC est habituellement appliqué aux grandes applications et chacun des trois sous-systèmes est généralement constitué de nombreux objets. Néanmoins, rien n’empêche de l’appliquer à notre application simple de calcul des salaires. En réalité, Cocoa fournit les objets et la technologie qui permettent de mettre en œuvre notre application en utilisant le design pattern MVC sans pour cela demander une quantité de code beaucoup plus importante que la version non MVC. La Figure 2.5 dévoile l’interface utilisateur de la version MVC de notre application. La nouvelle interface utilisateur est développée avec Interface Builder et ne requiert aucun code. Cet outil encourage le développement de vues indépendantes de deux manières. Tout d’abord, il est tellement facile de construire des interfaces utilisateurs que plusieurs versions peuvent être créées et évaluées. Chacune des interfaces est une vue séparée, ce qui donne naturellement l’habitude aux développeurs de créer des appli-
Chapitre 2
Analyse et application de MVC
27
Figure 2.5 L’interface utilisateur de la version MVC de l’application de calcul des salaires.
cations manipulant plusieurs vues. Ensuite, les objets proposés dans les palettes d’Interface Builder sont réutilisables et ne peuvent donc pas présenter des dépendances avec des données spécifiques à une application. En déposant simplement des objets réutilisables, vous mettez naturellement en place un sous-système Vue découplé. En réalité, l’interconnexion des objets dans la version non MVC de l’application demande plus de travail que dans la version MVC, car Interface Builder n’est pas réellement adapté au développement d’applications non MVC. INFO Les outils de type Interface Builder sont très pratiques pour développer des applications qui se fondent sur le design pattern MVC, mais vous ne devez pas confondre les outils et l’approche. Le pattern MVC existe depuis longtemps et peut être employé pour tout type de développement de logiciels, quel que soit l’outil. Ceux fournis par Apple et Cocoa lui-même encouragent l’utilisation de ce pattern.
Pour appliquer une conception MVC à l’application, la modification importante concerne la création d’un modèle approprié. Dans le cas présent, le modèle n’a besoin que d’un seul type d’objet, MYEmployee. À l’aide de cette classe, nous pouvons placer
28
Les design patterns de Cocoa
dans des instances séparées de MYEmployee les informations de chaque employé. Un nombre d’instances suffisant est créé pour enregistrer les informations de tous les employés. L’enregistrement et le chargement se font en enregistrant et en chargeant des instances de MYEmployee dans un fichier. Puisque la logique de calcul du salaire est encapsulée dans la classe MYEmployee, cette opération est réalisée dans un seul endroit, quel que soit le nombre de vues créées. Avant d’examiner le code de la classe MYEmployee, voici la liste partielle des fonctionnalités obtenues grâce à la conception MVC de l’application : n
L’annulation et le rétablissement des modifications apportées aux informations d’un employé sont automatiquement pris en charge.
n
L’enregistrement et le chargement des informations concernant un employé sont automatiquement pris en charge.
n
La modification des objets des employés est automatiquement prise en charge.
n
Le calcul du montant à payer est automatique ; le bouton CALCULER est inutile.
n
Les informations concernant les différents employés peuvent être affichées et modifiées sans avoir à dupliquer les composants de l’interface utilisateur.
La classe PayCalculator de l’approche non MVC n’est plus nécessaire et le code de la nouvelle classe MYEmployee est moins long que celui de PayCalculator. Plusieurs points sont à remarquer dans la nouvelle version. Tout d’abord, la classe MYEmployee n’a pas besoin d’outlets pour accéder aux objets de l’interface utilisateur. En réalité, si elle disposait de ces outlets, elle ne constituerait pas un très bon modèle car elle serait couplée à une vue : @interface MYEmployee : NSManagedObject { } - (NSNumber *)payAmount; @end
Par ailleurs, signalons que MYEmployee dérive de NSManagedObject, qui est détaillée au Chapitre 30. Il s’agit d’une classe Cocoa qui sert essentiellement à encapsuler des données persistantes et à simplifier la création du modèle pour de nombreuses applications. NSManagedObject se fonde sur le pattern Mémoire associative (voir Chapitre 19) pour donner accès aux données enregistrées en mémoire, dans un fichier sur le disque ou dans une base de données relationnelle. Le rôle de NSManagedObject est d’encapsuler le mécanisme de stockage sous-jacent afin que les développeurs n’aient pas à en connaître les détails de mise en œuvre.
Chapitre 2
Analyse et application de MVC
29
La classe MYEmployee est conçue dans l’outil de modélisation de Core Data fourni avec Xcode. À l’instar d’Interface Builder, Xcode est un composant standard des outils de développement d’Apple. La Figure 2.6 illustre la conception de la classe MYEmployee.
Figure 2.6 La conception de la classe MYEmployee dans Xcode.
Tous les attributs nécessaires à l’application de calcul des salaires sont définis dans la classe MYEmployee : hourlyRate, hoursWorked, isExempt, name, payAmount et standardHours. La manière d’enregistrer les attributs pour chaque instance de la classe MYEmployee n’a pas vraiment d’importance ; grâce à NSManagedObject, le mécanisme de stockage ne nous concerne pas. Il suffit simplement de mettre en œuvre la logique de l’application dans la classe MYEmployee pour terminer le modèle : #import "MYEmployee.h" @implementation MYEmployee + (NSSet *)keyPathsForValuesAffectingPayAmount { // Retourner les noms des attributs qui affectent payAmount. return [NSSet setWithObjects:@"isExempt", @"hourlyRate", @"hoursWorked", @"standardHours", nil]; }
30
Les design patterns de Cocoa
- (NSNumber *)payAmount { // Retourner un salaire calculé d’après les autres attributs. float calculatedPayAmount; float hourlyRate = [[self valueForKey:@"hourlyRate"] floatValue]; float standardNumberOfHours = [[self valueForKey:@"standardHours"] floatValue]; if([[self valueForKey:@"isExempt"] boolValue]) { // Payer le taux horaire multiplié par le nombre d’heures standard // quel que soit le nombre d’heures travaillées. calculatedPayAmount = hourlyRate * standardNumberOfHours; } else { // Payer le taux horaire multiplié par le nombre d’heures travaillées. float numberOfHoursWorked = [[self valueForKey:@"hoursWorked"] floatValue]; calculatedPayAmount = hourlyRate * numberOfHoursWorked; if(numberOfHoursWorked > standardNumberOfHours) { // Les heures supplémentaires sont augmentées de 50%. float overtimePayAmount = 0.5f * hourlyRate * (numberOfHoursWorked - standardNumberOfHours); calculatedPayAmount = calculatedPayAmount + overtimePayAmount; } } return [NSNumber numberWithFloat:calculatedPayAmount]; } @end
La méthode +(NSSet *)keyPathsForValuesAffectingPayAmount de la classe MYEmployee indique à Cocoa que l’attribut payAmount dépend de la valeur d’autres attributs de MYEmployee. Par conséquent, chaque fois que ces autres attributs sont modifiés, l’attribut payAmount doit être recalculé. La capacité d’accéder aux attributs et aux relations par leur nom provient de la classe NSManagedObject ; les attributs disponibles étant précisés dans Xcode. La possibilité de créer des dépendances entre des attributs est due au mécanisme d’observation clé-valeur (Key Value Observing) de Cocoa (voir Chapitre 32). La méthode -payAmount est comparable à la méthode -calculatePayAmount: de la version non MVC. Elle utilise des expressions comme [self valueForKey:@"hourlyRate"] pour accéder aux valeurs nécessaires au calcul du salaire. La classe MYEmployee peut calculer le montant sans accéder à des objets hors du modèle. Puisque la nouvelle version de l’application dispose d’une vue et d’un modèle, il faut à présent examiner le contrôleur. Dans le cas d’une application aussi simple, une seule
Chapitre 2
Analyse et application de MVC
31
instance de la classe Cocoa NSArrayController suffit. Elle est créée et configurée avec Interface Builder de manière à donner accès à toutes les instances de MYEmployee présentes au cours de l’exécution de l’application. Aucun code n’est requis. L’instance de NSArrayController peut même prendre en charge le tri des informations présentées à l’utilisateur (voir Figure 2.5). La table des employés peut être triée en fonction de la colonne sélectionnée, par exemple pour que l’utilisateur les obtiennent par ordre décroissant de salaire. La version MVC de l’application de calcul des salaires est disponible dans l’archive des codes sources. La seule partie du code non montrée ici concerne le code standard généré par le template Xcode qui a servi à créer le projet. Analyse de la version MVC Les objets de l’interface utilisateur sont connectés à l’aide d’Interface Builder de manière qu’ils communiquent uniquement avec l’instance de NSArrayController (voir Figure 2.7). Les objets de la vue n’ont pas connaissance du modèle ni du stockage et de la logique sous-jacents. Ils sont configurés dans Interface Builder de manière à rester synchronisés avec la propriété arrangedObjects ou selectedObject du contrôleur de tableau. Si l’implémentation de la vue est amenée à évoluer, seule l’instance de NSArrayController doit être reconfigurée ; le modèle n’est pas affecté. Modèle
Vue
MYEmployee MYEmployee MYEmployee
ar
ra
ng
ed
Ob
jec
ts
contenu
Contrôleur
’a sd
cti
on
v en
oy
és
ge sa es Objects m selected
NSArrayController
Figure 2.7 Les relations entre les différents objets dans la conception MVC.
32
Les design patterns de Cocoa
L’implémentation du modèle est presque intégralement réalisée par les composants Core Data de Cocoa. L’exemple utilise Xcode pour concevoir l’objet MYEmployee et pour enregistrer le modèle dans un fichier de configuration chargé automatiquement par Core Data au démarrage de l’application. Core Data et NSArrayController fonctionnent parfaitement ensemble. Le NSArrayController de tableau peut accéder à un nombre quelconque d’instances de MYEmployee, car il est configuré dans Interface Builder pour demander à Core Data de les lui fournir. Il répond aux messages d’action -add:, -remove: et -fetch: en demandant à Core Data d’ajouter, de supprimer ou d’obtenir des instances de MYEmployee.
2.3
En résumé
Cocoa est lui-même conçu selon les principes du pattern MVC et vous êtes encouragé à les utiliser dans vos propres applications. Les outils de développement d’Apple ont un fonctionnement optimal lorsqu’ils sont employés pour des conceptions MVC. Dans certains cas, il est difficile de les utiliser d’une autre manière. En comprenant la logique qui sous-tend la séparation des sous-systèmes de MVC, vous comprendrez plus aisément la meilleure façon d’utiliser Cocoa. Même les applications Cocoa de petite taille, comme notre calculatrice des salaires, bénéficient de fonctionnalités sophistiquées lorsqu’elles se conforment à la conception implicite de Cocoa.
Partie II Patterns fondamentaux Les patterns décrits dans cette partie sont au cœur de l’architecture des frameworks Cocoa. En un sens, il est impossible d’utiliser Cocoa sans employer ces patterns. Toutefois, leur bas niveau et leur omniprésence peuvent conduire le programmeur à les oublier rapidement. Voici les chapitres de cette partie du livre : 3. Création en deux étapes ; 4. Patron de méthode ; 5. Création dynamique ; 6. Catégorie ; 7. Type anonyme et Conteneur hétérogène ; 8. Énumérateur ; 9. Exécution de sélecteur et Exécution retardée ; 10. Accesseur ; 11. Archivage et désarchivage ; 12. Copie.
3 Création en deux étapes Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Pour l’allocation et l’initialisation de nouvelles instances de ses classes, Cocoa se fonde sur des conventions établies par la classe de base NSObject. La création des instances est un aspect essentiel des langages orientés objet et l’utilisation de conventions pour sa mise en œuvre peut, au premier abord, sembler problématique, mais, en pratique, cela fonctionne parfaitement. Plusieurs patterns connexes sont employés pour garantir une allocation et une initialisation correctes des instances. De nombreux langages, comme Java, C++, Ruby et Smalltalk, se servent d’une méthode "new" pour allouer et initialiser de nouvelles instances. Bien que la classe NSObject dispose d’une méthode +new, les développeurs Cocoa l’utilisent rarement, voire jamais. Le pattern Création en deux étapes sépare la première phase, l’allocation de l’objet en mémoire, de la seconde, l’initialisation de l’objet. Pour utiliser efficacement Cocoa, ce pattern doit être employé.
3.1
Motivation
La création en deux étapes donne aux programmeurs la maîtrise de l’allocation des objets en mémoire et apporte simultanément une flexibilité pour l’initialisation des instances. Ce pattern simplifie l’initialisation des instances de classes dérivées des classes Cocoa et fournit les méthodes permettant de créer et d’initialiser des objets temporaires.
36
Les design patterns de Cocoa
Ce chapitre décrit le pattern Création en deux étapes de Cocoa et explique comment il permet d’atteindre les objectifs suivants : n
Permettre l’utilisation d’initialiseurs quelle que soit la manière dont la mémoire est allouée.
n
Éviter l’implémentation d’un trop grand nombre d’initialiseurs lors de la création de sous-classes.
n
Simplifier la création et l’utilisation d’instances temporaires.
Un petit historique permettra de comprendre pourquoi Cocoa utilise la création en deux étapes. Les très anciennes versions des bibliothèques de classes Objective-C qui sont devenues Cocoa utilisaient des méthodes de classe pour prendre en charge l’allocation et l’initialisation. INFO Les méthodes de classe opèrent sur des objets classe, non sur des instances de la classe. Les classes Objective-C sont elles-mêmes des objets qui héritent conceptuellement de toutes les méthodes de la classe de base. Les méthodes de classe Objective-C sont comparables aux fonctions membres statiques du langage C++ et aux méthodes statiques de Java. Contrairement aux fonctions membres statiques de C++, les méthodes de classe Objective-C ont accès à l’argument self qui fait référence à l’objet classe lui-même. Par ailleurs, les méthodes de classe sont totalement polymorphes (http://developer.apple.com/documentation/Cocoa/ Conceptual/CocoaFundamentals/).
Si l’allocation et l’initialisation se font dans une même méthode, celle-ci doit être une méthode de classe car, jusqu’à ce que l’allocation soit terminée, l’instance n’existe pas. Voici comment implémenter une méthode de classe qui alloue et initialise une instance : + (id)circleWithCenter:(NSPoint)aPoint radius:(float)radius { // L’allocation et l’initialisation partielle sont apportées // par la super-classe. id newInstance = [[super new] autorelease]; if(nil != newInstance) // Vérifier que la nouvelle instance a été créée. { [newInstance setCenter:aPoint]; [newInstance setRadius:radius]; [newInstance setLabel:@”default”]; } return newInstance; }
L’initialisation des instances au sein des méthodes de classe présente plusieurs inconvénients. La combinaison de l’allocation et de l’initialisation conduit à une explosion du
Chapitre 3
Création en deux étapes
37
nombre de méthodes nécessaires à la prise en charge des différentes manières d’allouer et d’initialiser des objets. Prenons l’exemple d’une classe hypothétique, MYImage, dont les objets peuvent être initialisés à partir du contenu d’un fichier, d’informations obtenues via une URL (Uniform Resource Locator), de données binaires quelconques, du presse-papiers de l’utilisateur ou être vides avec une taille indiquée. Considérons également que le stockage des images peut se faire dans une zone de mémoire normale ou dans la mémoire de la carte graphique. La classe MYImage doit donc fournir l’ensemble des méthodes suivantes : +imageFromRegularMemoryWithContentsOfFile: +imageFromRegularMemoryWithContentsOfURL: +imageFromRegularMemoryWithData: +imageFromRegularMemoryWithPasteboard: +imageFromRegularMemoryWithSize: +imageFromGraphicsMemory:(MYCardID)aCard withContentsOfFile: +imageFromGraphicsMemory:(MYCardID)aCard withContentsOfURL: +imageFromGraphicsMemory:(MYCardID)aCard withData: +imageFromGraphicsMemory:(MYCardID)aCard withPasteboard: +imageFromGraphicsMemory:(MYCardID)aCard withSize:
Imaginons à présent qu’il soit également possible d’enregistrer les images dans une mémoire spéciale partagée entre les processus ou dans une zone de l’espace d’adressage virtuel du processeur, mais allouée uniquement lorsqu’elle est requise. Il peut également exister d’autres manières d’obtenir les données de l’image, par exemple par copie d’une image existante ou par capture de l’écran. Lorsque les méthodes qui correspondent aux différentes combinaisons possibles seront créées, la classe proposera des dizaines, voire des centaines, de méthodes différentes. Le problème lié au trop grand nombre de méthodes de création d’instances initialisées surgit lorsque vous voulez créer une sous-classe. Cette classe dérivée risque d’avoir à réimplémenter chacune des méthodes de création d’instance de sa super-classe et fournir elle-même de nouvelles variantes pour prendre en compte les paramètres supplémentaires utilisés dans la création des instances de la nouvelle classe.
3.2
Solution
La classe Cocoa de base NSObject fournit deux méthodes qui allouent la mémoire pour les nouvelles instances, +(id)alloc et +(id)allocWithZone:(NSZone *)aZone. Elles sont héritées par toutes les autres classes Cocoa et sont rarement redéfinies. L’implémentation de la méthode +alloc invoque la méthode +allocWithZone: en précisant un argument de zone par défaut. Les zones sont brièvement expliquées à la section suivante. Les méthodes +alloc et +allocWithZone: retournent un pointeur sur le nouveau bloc de mémoire alloué, dont la taille est suffisante pour contenir une instance de la classe qui a exécuté la méthode. La mémoire allouée contient des zéros, excepté pour une variable d’instance,
38
Les design patterns de Cocoa
isa, que tous les objets Objective-C doivent posséder. La variable isa est automatiquement initialisée de manière à pointer sur l’objet classe qui a alloué la mémoire et constitue le lien avec le moteur d’exécution du langage Objective-C qui permet à l’instance de recevoir des messages, comme -init, pour terminer l’initialisation.
Zones Les zones de mémoire sont une caractéristique de Cocoa dont l’objectif est d’améliorer les performances de l’application en essayant de regrouper dans l’espace d’adressage de l’ordinateur les blocs de mémoire des objets utilisés ensemble. Pour comprendre comment l’emplacement des objets en mémoire affecte les performances, il est nécessaire d’expliquer ce qui se produit lorsque l’application a besoin d’une quantité de mémoire supérieure à celle de la mémoire physique disponible. Chaque application Cocoa dispose d’une très grande quantité de mémoire adressable. Lorsqu’une application alloue dynamiquement de la mémoire, le système d’exploitation la lui fournit même si l’intégralité de la mémoire physique de l’ordinateur est occupée. Pour satisfaire la demande d’allocation, le système d’exploitation copie le contenu d’une partie de la mémoire physique sur le disque dur au cours d’une opération appelée pagination (paging) ou échange (swapping). La mémoire physique qui contenait les données écrites sur le disque est rendue disponible à l’application qui en avait besoin. Lorsque le contenu de la mémoire qui a été copié sur le disque est à nouveau requis, le système d’exploitation copie une autre partie de la mémoire physique sur le disque et replace le contenu demandé dans cette zone libérée. Le système d’exploitation fournit de façon transparente le même espace d'adressage pour la zone, qu’elle réside en mémoire vive ou sur le disque dur. Cette caractéristique du système d’exploitation se nomme mémoire virtuelle. L’utilisation de la mémoire virtuelle affecte les performances car l’échange du contenu de la mémoire physique avec le disque prend du temps. Lorsque les échanges sont nombreux, les performances du système se dégradent par un phénomène d’emballement ou d’écroulement. L’emplacement de la mémoire allouée aux instances est important car, si plusieurs objets utilisés ensemble sont enregistrés dans des parties éloignées de la mémoire, la probabilité d’emballement augmente. Étudions un exemple de fonctionnement. Supposons l’utilisation d’un objet dont la mémoire allouée se trouve sur le disque. Elle est donc lue depuis le disque dur et placée en mémoire physique. L’objet accède ensuite à un autre objet qui ne se trouve pas dans la mémoire physique. Un autre échange avec le disque a donc lieu. Dans le pire des cas, le chargement de la mémoire du second objet déclenche la pagination de la mémoire du premier. Puisque les objets se font mutuellement référence, un emballement se produit.
Chapitre 3
Création en deux étapes
39
Les zones permettent de garantir que les parties de la mémoire allouées aux objets utilisés ensemble sont proches l’une de l’autre. Lorsque l’un des objets est requis, l’autre le sera très certainement. Puisque les objets se trouvent dans la même zone, il est fort probable qu’ils seront chargés en mémoire au même moment et, lorsqu’ils ne seront plus nécessaires, ils seront également placés sur le disque ensemble. Le type Cocoa NSZone correspond à une structure C qui identifie une zone de mémoire. La méthode +allocWithZone: accepte un argument NSZone et alloue la mémoire pour cette zone. Cocoa fournit plusieurs fonctions, comme NSDefaultMallocZone(), NSCreateZone() et NSRecycleZone(), pour la gestion des zones de mémoire. Pour de plus amples informations, consultez la section Core Library > Cocoa > Objective-C Language > Foundation Framework Reference > Functions dans la documentation de Xcode ou sur le site http:// developer.apple.com/. INFO Les zones sont un aspect de très bas niveau et Apple décourage leur utilisation explicite dans le code. Les zones sont employées automatiquement par Cocoa. Puisque les ordinateurs sont équipés d’une mémoire physique de plus en plus grande et puisque le système d’exploitation offre des fonctions d’allocation de la mémoire de plus en plus sophistiquées, l’utilisation des zones est de moins en moins justifiée. Avec Objective-C 2.0 sous Mac OS X 10.5, si votre application utilise le ramasse-miettes facultatif de Cocoa, les zones indiquées à +allocWithZone: sont ignorées par les frameworks.
Initialiser la mémoire allouée Après que la mémoire d’une nouvelle instance a été allouée, elle est initialisée en invoquant une méthode d’instance. Ces méthodes d’instance sont appelées initialiseurs et, par convention, leur nom commence par le mot init et elles retournent un id. Le Chapitre 7 présente quelques avantages de l’utilisation du type id. L’allocation et l’initialisation sont presque toujours combinées sur une seule ligne de code conformément au motif suivant : [[UneClasse alloc] init]. Les classes peuvent fournir autant d’initialiseurs qu’elles le souhaitent et les différents initialiseurs peuvent chacun accepter des arguments différents. Lorsqu’il existe plusieurs initialiseurs, l’un d’eux correspond généralement à l’initialiseur désigné. L’initialiseur désigné de la classe NSObject est -(id)init, tandis que celui de la classe NSView est -(id)initWithFrame:(NSRect)aFrame. Chacun des initialiseurs fournis peut être l’initialiseur désigné, mais il doit être clairement documenté. Il s’agit généralement de celui qui accepte le plus grand nombre d’arguments. L’implémentation des autres initialiseurs appelle alors l’initialiseur désigné.
40
Les design patterns de Cocoa
INFO Lorsque vous consultez la documentation des classes Cocoa fournie par Apple, gardez un œil sur les références à l’initialiseur désigné de chaque classe. Il est important de connaître la méthode qui sert d’initialiseur désigné lors de la création de classes dérivées des classes Cocoa. Bien qu’elles soient rares, certaines classes Cocoa, comme NSCell, disposent de plusieurs initialiseurs désignés.
Outre l’initialiseur désigné, la plupart des classes Cocoa fournissent une méthode -(id)initWithCoder:(NSCoder *)aCoder. La signification de -initWithCoder: est donnée au Chapitre 11. Implémenter l’initialiseur désigné L’initialiseur désigné de chaque classe doit invoquer l’initialiseur désigné de sa superclasse. Voici l’exemple de la classe MYCircle simple qui dérive de NSObject : @interface MYCircle : NSObject { NSPoint center; float radius; } // Initialiseur désigné. - (id)initWithCenter:(NSPoint)aPoint radius:(float)aRadius; @end @implementation MYCircle // Initialiseur désigné. - (id)initWithCenter:(NSPoint)aPoint radius:(float)aRadius { self = [super init]; if(nil != self) { center = aPoint; radius = aRadius; } return self; } @end
La méthode -(id)initWithCenter:(NSPoint)aPoint radius:(float)aRadius affecte tout d’abord à la variable locale implicite self le résultat de l’invocation de l’initialiseur désigné de la super-classe. Cette étape est cruciale car certains initialiseurs retournent un objet différent de celui qui a reçu le message. Cela peut se produire lorsqu’il est impossible d’initialiser le récepteur ou lorsqu’une instance déjà existante est retournée de manière à éviter l’initialisation d’une nouvelle.
Chapitre 3
Création en deux étapes
41
INFO La variable self est présente dans toutes les méthodes Objective-C. Il s’agit de l’un des deux arguments cachés passés au code d’implémentation de la méthode. La valeur initiale de self est toujours l’objet qui a reçu le message ayant conduit à l’exécution de la méthode. Le second argument caché est _cmd, qui identifie le message reçu. Les variables self et _cmd sont décrites dans le document http://developer.apple.com/mac/library/documentation/ Cocoa/Conceptual/ObjectiveC/ObjC.pdf.
Après l’affectation de la variable self, une instruction if permet d’effectuer l’initialisation des variables d’instance uniquement si self n’est pas nil. Ce test est important car, si self vaut nil, l’accès à la mémoire des variables d’instance peut être une erreur. Ce niveau de programmation défensive est généralement inutile car peu d’initialiseurs de classes retournent nil, mais, puisque nil est une valeur de retour valide, il est préférable de prendre de bonnes habitudes afin d’éviter ce problème occasionnel. Enfin, la méthode -initWithCenter:radius: retourne self. Il s’agit du fonctionnement le plus courant des initialiseurs. Chaque classe qui introduit un nouvel initialiseur désigné doit également redéfinir l’initialiseur désigné hérité pour qu’il invoque le nouveau. Puisque la classe MYCircle définit -initWithCenter:radius:, elle doit également implémenter -init de manière qu’il invoque -initWithCenter:radius: : // Redéfinir l’initialiseur désigné hérité. - (id)init { static const float MYDefaultRadius = 1.0f; // Invoquer l’initialiseur désigné avec les arguments par défaut. return [self initWithCenter:NSZeroPoint radius:MYDefaultRadius]; }
Si vous respectez les directives suivantes, l’invocation de n’importe quel initialiseur implémenté ou hérité par une classe conduira à une instance correctement initialisée : n
Vérifier que l’initialiseur désigné invoque l’initialiseur désigné de la super-classe.
n
Affecter à self l’objet retourné par l’initialiseur désigné de la super-classe.
n
Ne pas accéder aux variables d’instance si nil est retourné par l’initialiseur désigné de la super-classe.
n
Vérifier que l’initialiseur désigné de la super-classe est redéfini de manière à invoquer le nouvel initialiseur désigné.
n
Lors de la création d’une sous-classe, vérifier que chaque initialiseur qui n’est pas l’initialiseur désigné invoque l’initialiseur désigné.
42
Les design patterns de Cocoa
Ces directives simplifient énormément la création des sous-classes. Si elles ne sont pas suivies et si certains initialiseurs n’invoquent pas l’initialiseur désigné, la seule manière d’implémenter une sous-classe en garantissant la bonne initialisation de l’instance est de redéfinir chaque initialiseur hérité. INFO Si vous créez des sous-classes qui doivent être archivées, il est également nécessaire de redéfinir la méthode -initWithCoder: héritée pour qu’elle initialise correctement les objets lors de leur désarchivage (voir Chapitre 11).
Utiliser des zones dans les initialiseurs Si vous utilisez des zones mémoire dans votre code, il est important d’allouer la mémoire employée par les variables d’instance d’un objet à partir de la même zone que cet objet. Le stockage de références vers de la mémoire située en dehors de la zone d’un objet va à l’encontre de l’objectif des zones. La zone utilisée pour allouer un objet est précisée en lui envoyant un message -zone. Il est possible de récrire la classe MYCircle afin que chaque instance enregistre un libellé NSString alloué à partir de la zone de l’instance. @interface MYCircle : NSObject { NSPoint center; float radius; NSString *label; } // Initialiseur désigné. - (id)initWithCenter:(NSPoint)aPoint radius:(float)aRadius; @end @implementation MYCircle // Initialiseur désigné. - (id)initWithCenter:(NSPoint)aPoint radius:(float)aRadius { self = [super init]; if(nil != self) { center = aPoint; radius = aRadius; label = [[NSString allocWithZone:[self zone]] initWithString:@”default”]; } return self; }
Chapitre 3
Création en deux étapes
43
// Redéfinir l’initialiseur désigné hérité. - (id)init { // Invoquer l’initialiseur désigné avec les arguments par défaut return [self initWithCenter:NSZeroPoint radius:1.0f]; } @end
Les objets ne sont pas les seuls éléments alloués dans des zones. Les fonctions NSZoneMalloc(), NSZoneCalloc() et NSZoneFree() peuvent être utilisées pour allouer et libérer des blocs de mémoire dans des zones indiquées. En Objective-C 2.0, introduit par Mac OS X 10.5, le ramasse-miettes libère automatiquement la mémoire allouée avec void *__strong NSAllocateCollectable(NSUInteger size, NSUInteger options). Pour une question de rétrocompatibilité, l’invocation de NSAllocateCollectable() avec l’option NSCollectorDisabledOption conduit au même comportement que l’invocation de NSZoneMalloc(). Des objets peuvent également être copiés et désarchivés en utilisant des zones indiquées. Les méthodes -(id)copyWithZone:(NSZone *)aZone et -(id)mutableCopyWithZone::(NSZone *)aZone sont détaillées au Chapitre 12. La classe NSUnarchiver fournit la méthode -(void)setObjectZone:(NSZone *)aZone, qui permet de préciser la zone dans laquelle les objets désarchivés sont alloués. L’archivage et le désarchivage sont expliqués au Chapitre 11. Lorsque des objets sont alloués, ils doivent être désalloués à un moment ou à un autre. Le système de comptage des références mémoire de Cocoa permet de garantir que c’est bien le cas. Il est détaillé au Chapitre 10, qui explique comment la mémoire est utilisée par les objets. Pour de plus amples informations concernant le système de comptage des références de Cocoa, consultez la section Core Library > Cocoa > Objective-C Language > Memory Management Programming Guide for Cocoa dans la documentation de Xcode. Avec Mac OS X 10.5 et les versions ultérieures, vous pouvez utiliser le ramasse-miettes automatique à la place du système de comptage des références. Toutefois, puisque le ramasse-miettes est facultatif dans Cocoa, il est indispensable d’implémenter correctement la gestion des références dans les nouvelles classes que vous créez, excepté si vous imposez aux utilisateurs de ces classes l’activation du ramasse-miettes. Que les zones soient utilisées ou non, la désallocation d’un objet déclenche l’invocation de sa méthode -(void)dealloc. Vous ne devez pas appeler cette méthode vous-même. Elle est invoquée automatiquement au moment approprié. L’implémentation de la méthode -dealloc de la classe MYCircle s’assure de la bonne prise en charge de la variable d’instance label allouée dans l’initialiseur désigné :
44
Les design patterns de Cocoa
- (void)dealloc { [label release]; [super dealloc]; }
Si le ramasse-miettes est employé, la méthode -(void)finalize est invoquée à la place de -dealloc. La classe MYCircle ne définit pas cette méthode car, si le ramasse-miettes est activé, elle collecte automatiquement la mémoire allouée à la chaîne label. MYCircle ne requiert aucune action particulière lorsque sa mémoire est récupérée. Toutefois, il serait indispensable d’implémenter la méthode -finalize si la classe MYCircle avait alloué de la mémoire non récupérable en invoquant NSAllocateCollectable() avec l’option NSCollectorDisabledOption ou si elle devait effectuer d’autres opérations de terminaison, comme fermer des fichiers ouverts. Créer des instances temporaires Plusieurs classes Cocoa fournissent des méthodes qui combinent les étapes d’allocation et d’initialisation pour retourner des instances temporaires. Ces méthodes de commodité comprennent le nom de la classe dans leur nom. Par exemple, la classe NSString fournit la méthode de commodité +(id)stringWithString:(NSString *)aString, qui est comparable à l’initialiseur -(id)initWithString:(NSString *)aString employé par la classe MYCircle. Lorsque le ramasse-miettes n’est pas activé, la principale différence entre l’utilisation de [[NSString alloc] initWithString:@”some string”] et de [NSString stringWithString:@”some string”] réside dans le fait que +stringWithString: retourne une instance qui sera désallouée automatiquement, à moins que vous ne lui envoyiez un message -retain pour empêcher la désallocation. Lorsque le ramasse-miettes est utilisé, il n’y a aucune différence significative entre les deux techniques de création d’une nouvelle instance. Les méthodes comme +stringWithString: sont généralement implémentées de la manière suivante : + (id)stringWithString:(NSString *)aString { return [[[self alloc] initWithString:aString] autorelease]; }
Le Chapitre 10 présente less conséquences des messages -retain et -autorelease. Les méthodes de commodité qui permettent d’obtenir des instances sont presque toujours accompagnées d’initialiseurs aux noms comparables. Outre le fait de réduire la quantité de code que les programmeurs doivent écrire pour créer et initialiser des instances, les méthodes de commodité autorisent également certaines optimisations et sont combinées à d’autres patterns. Plus particulièrement, elles sont employées dans la mise
Chapitre 3
Création en deux étapes
45
en œuvre des patterns Singleton (voir Chapitre 13) et Regroupement de classes (voir Chapitre 25). Elles retournent parfois des objets Poids mouche (voir Chapitre 22). Elles ont pour inconvénient de retirer une certaine flexibilité à la manière d’allouer les instances car la technique est figée dans la méthode.
3.3
Exemples dans Cocoa
Le pattern Création en deux étapes est largement utilisé par Cocoa et vous devez le respecter si vous créez une sous-classe d’une classe Cocoa. Vous devez connaître l’initialiseur désigné pour que la sous-classe fonctionne correctement. Le Tableau 3.1 recense quelques classes Cocoa dont les programmeurs créent souvent des classes dérivées et identifie les initialiseurs désignés. Tableau 3.1 : Classes Cocoa importantes et leurs initialiseurs désignés
Classe
Initialiseur désigné
NSObject
-init
NSView
-initWithFrame:
NSCell
-initImageCell: et -initTextCell:
NSControl
-initWithFrame:
NSDocument
-init
NSWindowController
-initWithWindow:
La classe NSCell possède deux initialiseurs désignés, qui doivent être tous deux redéfinis dans les sous-classes. Toutefois, la classe dérivée est libre d’invoquer l’initialiseur désigné qu’elle souhaite. Prenons l’exemple d’une classe MYLabeledBarCell qui dérive de NSCell. Chaque instance de MYLabeledBarCell affiche un libellé et dessine une petite barre qui représente une valeur entre 0,0 et 1,0. Ces cellules peuvent être utilisées pour indiquer le niveau de charge d’une batterie ou la vitesse d’accélération de la souris dans un panneau de préférences. La barre est une indication rapide de la valeur, tandis que le libellé identifie la valeur. La valeur de la barre est fixée à l’aide de la méthode -(void)setBarValue:(float)aValue de la classe MYLabeledBarCell, le libellé, avec la méthode -(void)setLabel:(NSString *)aLabel ou la méthode -(void)setStringValue: (NSString *)aString héritée de la classe NSCell. La Figure 3.1 présente plusieurs instances MYLabeledBarCell.
46
Les design patterns de Cocoa
Figure 3.1 La fenêtre montre une matrice d’instances de MYLabeledBarCell dans une vue défilante.
L’implémentation suivante de MYLabeledBarCell invoque la méthode -(id)initTextCell:(NSString *)aString héritée des implémentations de -(id)initImageCell: (NSImage *)anImage et de -initTextCell:. #import @interface MYLabeledBarCell : NSCell { float barValue; // Valeurs dans l’intervalle 0.0 à 1.0. } // Initialiseurs désignés redéfinis. - (id)initImageCell:(NSImage *)anImage; - (id)initTextCell:(NSString *)aString; // Configuration redéfinie. - (BOOL)isOpaque; // Accesseurs. - (void)setLabel:(NSString *)aLabel; - (NSString *)label; - (void)setBarValue:(float)aValue; - (float)barValue; // Nouvelle méthode de dessin. - (void)drawBarInRect:(NSRect)aRect; @end ------------
Chapitre 3
Création en deux étapes
47
#import “MYLabeledBarCell.h” @implementation MYLabeledBarCell // Les instances de cette classe contiennent un libellé et une valeur réelle, // barValue. Le libellé est affiché comme une chaîne avec attributs. Une barre // verte est dessinée dans la partie inférieure de la cellule selon la valeur // de barValue, interprétée comme une fraction de la longueur totale. Si // barValue est >= 1.0, la barre est tracée sur toute la longueur. Si barValue // est > 5) % 6) + 1; // Nombre aléatoire entre 1 et 6. } - (BOOL)isBoxcar // Retourner YES si la dernière valeur obtenue est un six. { return (value == 6); } @end
Prenons un autre exemple, dans lequel une catégorie ajoute des méthodes de commodité à la classe Cocoa NSMutableArray : @interface NSMutableArray (MYAdditions) - (void)addObjectIfAbsent:(id)anObject; - (void)addObjectIfNotNil:(id)anObject; @end @implementation NSMutableArray (MYAdditions) // Ajouter des méthodes utiles aux listes modifiables. - (void)addObjectIfAbsent:(id)anObject // Ajouter anObject au récepteur uniquement si anObject n’est pas nil // et n’est pas déjà présent dans le récepteur. { if((nil != anObject) && ![self containsObject:anObject]) { [self addObject:anObject]; } } - (void)addObjectIfNotNil:(id)anObject // Ajouter anObject au récepteur uniquement si anObject n’est pas nil. { if(nil != anObject) { [self addObject:anObject]; } } @end
La méthode -(void)addObjectIfAbsent:(id)anObject nous permet de considérer n’importe quelle instance de NSMutableArray comme un ensemble ordonné. Chaque
Chapitre 6
Catégorie
75
objet est présent dans la liste au plus une fois et l’ordre dans lequel les objets sont ajoutés est conservé. NSMutableArray lance une exception lors de l’insertion d’un objet nil. La méthode -(void)addObjectIfNotNil:(id)anObject nous permet d’être un peu plus tranquilles dans notre code en évitant l’écriture de multiples contrôles de nil ou gestionnaires d’exceptions lors des ajouts d’objets à une liste. INFO
NSMutableArray est une interface publique à un regroupement de classes (voir Chapitre 25). Autrement dit, la création de sous-classes de NSMutableArray pose un problème et plusieurs sous-classes cachées de NSMutableArray peuvent exister. En utilisant une catégorie pour ajouter des méthodes à NSMutableArray, la création d’une sous-classe n’est plus nécessaire et les méthodes ajoutées sont automatiquement héritées par toutes les sous-classes cachées de NSMutableArray.
La catégorie (MYAdditions) appliquée à NSMutableArray illustre l’une des raisons pour lesquelles les catégories sont parfois préférées à l’héritage. Il est difficile, mais non impossible, de créer une sous-classe de NSMutableArray et de lui ajouter des méthodes. Toutefois, il n’existe aucune bonne solution pour obliger le code compilé à retourner des instances de la sous-classe à la place des instances de NSMutableArray. Par exemple, la classe Cocoa NSArray répond à la méthode -(id)mutableCopy en retournant une instance de NSMutableArray. Ce comportement est compilé dans les frameworks Cocoa. Pour obliger -mutableCopy à retourner une instance de notre sousclasse de NSMutableArray, nous devons remplacer la version de -mutableCopy implémentée par le framework. Il faut également remplacer toutes les méthodes du framework qui retournent un NSMutableArray pour qu’elles retournent à la place notre sous-classe. En ajoutant des méthodes via une catégorie, tous ces problèmes sont évités, car les méthodes ajoutées sont disponibles à toutes les instances de la classe étendue et ses sous-classes. Protocoles informels En Objective-C, un protocole formel est une construction du langage qui est utilisée pour déclarer les méthodes qu’un objet doit implémenter de manière à pouvoir servir dans certaines situations. Un objet est conforme à un protocole s’il fournit toutes les méthodes déclarées dans celui-ci. La conformité à un protocole formel est vérifiée par le compilateur Objective-C et peut également être contrôlée à l’exécution. Les protocoles formels sont détaillés dans la section Core Library > Cocoa > Objective-C Language > The Objective-C 2.0 Programming Language > Protocols de la documentation d’Apple fournie avec Xcode, ainsi que sur le site http://developer.apple.com/.
76
Les design patterns de Cocoa
La documentation Cocoa d’Apple emploie le terme protocole informel pour indiquer que les méthodes décrites dans le protocole sont, en théorie, disponibles dans n’importe quel objet. Cette garantie n’est pas assurée par le compilateur, qui n’a aucune connaissance des protocoles informels. Les méthodes d’un protocole informel sont généralement mises en œuvre dans une catégorie de la classe NSObject. Puisque quasiment tous les objets Cocoa héritent directement ou indirectement de NSObject, les méthodes ajoutées à NSObject dans une catégorie sont automatiquement héritées par pratiquement tous les objets Cocoa. Toutefois, l’utilisation des protocoles informels par Cocoa présente une difficulté. Très souvent, l’interface de la catégorie qui ajoute des méthodes à NSObject existe, sans que Cocoa fournisse l’implémentation de ces méthodes. Autrement dit, Apple a créé une interface de catégorie, sans proposer d’implémentation de la catégorie, de manière à faire croire au compilateur que les méthodes ajoutées sont disponibles dans pratiquement toutes les classes, alors qu’elles ne sont disponibles dans aucune. Cette approche est employée de manière à déclarer de manière informelle des méthodes qui seront invoquées dans certaines circonstances si et seulement si vous les implémentez dans l’une de vos classes. Les méthodes d’un délégué (voir Chapitre 15) en sont de bons exemples. De nombreuses classes Cocoa vérifient à l’exécution si les méthodes sont réellement disponibles avant de les invoquer. ATTENTION Faites attention à éviter les conflits sur les noms de méthodes entre plusieurs catégories. Les méthodes implémentées dans une catégorie sont toujours prioritaires sur les méthodes de même nom implémentées dans la classe elle-même. En revanche, si plusieurs catégories implémentent la même méthode, rien ne permet de garantir l’utilisation d’une implémentation précise. Pour éviter les conflits de noms potentiels, certains développeurs ajoutent des préfixes aux noms des méthodes des catégories. Cependant, nombreux sont ceux à trouver cette approche laide et l’évitent donc. Les frameworks Cocoa utilisent généralement des noms très verbeux pour les méthodes des protocoles informels. Cela réduit les risques de conflits, tout en améliorant la lisibilité et l’autodocumentation du code Cocoa.
La catégorie anonyme Avec l’arrivée d’Objective-C 2.0 dans Mac OS X 10.5, Apple a ajouté au langage une nouvelle caractéristique nommée extensions de classe, ou "la catégorie anonyme". La catégorie anonyme fonctionne comme n’importe quelle autre catégorie, à quelques exceptions près. Tout d’abord, aucun nom de catégorie n’est donné entre parenthèses dans le cas d’une déclaration de catégorie anonyme. Le code suivant utilise une catégorie anonyme pour déclarer l’ajout de la méthode -(void)setValue:(int)aValue à la classe MYDie :
Chapitre 6
Catégorie
77
@interface MYDie () –(void)setValue:(int)aValue; @end
Ensuite, chaque classe ne peut avoir qu’une seule catégorie anonyme. Enfin, les méthodes déclarées dans une catégorie anonyme doivent être implémentées dans le bloc @implementation normal de la classe. Contrairement aux catégories nommées, le compilateur Objective-C 2.0 vérifie que toutes les méthodes déclarées dans une catégorie anonyme sont réellement implémentées et génère un avertissement lorsque ce n’est pas le cas. Cette fonctionnalité du langage est généralement employée, de manière informelle, pour organiser les méthodes privées d’un objet. Objective-C ne permet pas de déclarer des méthodes publiques ou privées. Par convention, les développeurs placent toutes les déclarations de méthodes privées dans un en-tête privé qui contient la définition de la catégorie anonyme. Ainsi, les méthodes privées n’apparaissent pas dans l’en-tête public, mais le compilateur n’en génère pas moins un avertissement si une méthode déclarée n’est pas implémentée, ce qui n’est pas le cas avec les catégories nommées. INFO Puisque Objective-C ne prend pas directement en charge les méthodes privées, il n’existe aucun moyen d’empêcher l’envoi de messages qui utilisent l’API privée, quels que soient la manière et l’endroit où sont déclarées les méthodes privées. Le code client peut simplement définir, sans l’implémenter, une nouvelle catégorie nommée qui contient les définitions des méthodes, et le compilateur autorisera les messages qui invoquent ces méthodes privées sans générer d’erreurs ou d’avertissements au moment de la compilation.
Organiser le code Les catégories servent à décomposer l’implémentation des grandes classes. Les méthodes d’une classe peuvent souvent être séparées en groupes logiques de méthodes connexes. Plus le code d’une classe s’allonge, plus il est utile de découper l’implémentation en plusieurs fichiers, chacun contenant un ensemble de méthodes connexes regroupées dans une même catégorie. Par exemple, tous les accesseurs peuvent constituer une catégorie, tandis que les méthodes de prise en charge d’AppleScript peuvent en représenter une autre. Comme l’explique la section suivante, les frameworks Cocoa emploient cette technique pour organiser les implémentations de nombreuses classes Cocoa, notamment NSObject. Lorsque plusieurs développeurs travaillent sur un projet commun, chacun peut être chargé de la maintenance d’une catégorie. La gestion des versions est ainsi beaucoup plus simple, car les conflits sont moins nombreux. Le découpage de l’implémentation
78
Les design patterns de Cocoa
d’une classe réduit le temps de construction, car seuls les fichiers contenant des méthodes modifiées doivent être recompilés, non l’intégralité du code de la classe. Par ailleurs, en décomposant les grandes classes, les nouveaux membres de l’équipe ont moins de difficultés à appréhender le code. Choisir entre catégories et héritage Lorsque vient le moment de choisir entre l’héritage et les catégories, il n’y a pas toujours une séparation claire entre bonne et mauvaise décision. Finalement, il faut se fonder sur son expérience et ses préférences. Il peut être également intéressant d’examiner comment les autres développeurs Cocoa structurent leur code. Toutefois, certains facteurs peuvent peser dans un sens ou dans l’autre. Si de nouvelles variables d’instance doivent être ajoutées à une classe, l’héritage est sans doute le bon choix. Les catégories peuvent exploiter la mémoire associative (voir Chapitre 19) pour simuler l’ajout de nouvelles variables d’instance, mais cette approche a un coût au niveau des performances. La création de sous-classes dérivées de classes complexes, notamment celles qui se fondent sur le pattern Regroupement de classes (voir Chapitre 25), est généralement déconseillée. Les catégories sont une alternative à l’héritage. Si les méthodes implémentées apportent une fonctionnalité qui peut bénéficier à toutes les sous-classes d’une classe existante, alors, l’ajout d’une catégorie à la classe de base constitue probablement le meilleur choix. Parfois, une approche hybride fonctionne également. Il est possible de créer une sousclasse et une catégorie. La catégorie met en œuvre les méthodes qui représentent des extensions plus générales de la super-classe existante. La sous-classe étend ensuite la super-classe en ajoutant des méthodes qui s’appliquent uniquement à elle-même. En règle générale, l’héritage doit rester limité aux cas où le besoin de spécialisation est manifeste.
6.3
Exemples dans Cocoa
Cocoa utilise énormément les catégories et nous en avons déjà donné plusieurs exemples. Dans Cocoa, les catégories peuvent appartenir à trois groupes : n
les catégories qui servent uniquement à organiser des méthodes ;
n
les catégories qui définissent des protocoles informels ;
n
les catégories qui répartissent les implémentations sur plusieurs frameworks.
La suite de ce chapitre décrit les principales catégories Cocoa de chaque groupe. Certaines classes possèdent des catégories qui appartiennent à plusieurs groupes.
Chapitre 6
Catégorie
79
Utiliser les catégories pour l’organisation Pratiquement toutes les classes Cocoa sont organisées en plusieurs catégories. Depuis Mac OS X 10.4, les fichiers d’en-tête publics de Cocoa définissent soixante-neuf interfaces de catégories pour la classe de base NSObject. La plupart de ces catégories déclarent des méthodes de protocoles informels, mais certaines caractéristiques importantes de la classe de base sont organisées dans les catégories recensées au Tableau 6.1. Tableau 6.1 : Principales catégories utilisées pour organiser les méthodes implémentées par la classe de base NSObject
Nom de catégorie
Description
NSClassDescription
Comprend les méthodes nécessaires à l’utilisation des objets avec les fonctionnalités de scripts intégrées à Cocoa.
NSKeyValueCoding
Fournit les méthodes permettant de fixer et d’obtenir les valeurs des variables d’instance de n’importe quel objet.
NSKeyValueCodingExtras
Ajoute d’autres fonctionnalités permettant de fixer et d’obtenir les valeurs des variables d’instance.
NSKeyValueCodingException
Déclare des méthodes invoquées en cas de problèmes lors de la modification ou de l’obtention des valeurs des variables d’instance.
NSDelayedPerforming
Fournit des méthodes permettant d’envoyer des messages à des objets quelconques après un certain délai.
NSMainThreadPerformAdditions Comprend les méthodes permettant d’échanger des
messages entre des objets présents dans des threads enfants et des objets du thread principal d’une application. NSComparisonMethods
Déclare des méthodes, notamment -isEqualTo:, utilisées pour comparer deux objets.
NSScripting
Apporte à tous les objets Cocoa une prise en charge de base des scripts.
NSScriptClassDescription
Spécifie les méthodes -className et -classCode utilisées pour la prise en charge des scripts. La méthode -className est également utile dans de nombreux contextes autres que les scripts.
NSScriptValueCoding
Déclare les méthodes qui intègrent NSKeyValueCoding aux méthodes de script.
NSScriptObjectSpecifiers
Ajoute d’autres méthodes pour que des objets Cocoa quelconques s’interfacent avec les systèmes de scripts.
NSScriptingComparisonMethods Comparable à NSComparisonMethods, mais déclare d’autres
méthodes de comparaison utilisables avec les scripts.
80
Les design patterns de Cocoa
Le Tableau 6.1 ne présente pas une liste exhaustive des catégories d’organisation de NSObject mais servira de guide sur la manière de découper le code d’une classe. Neuf des douze catégories recensées dans ce tableau sont en rapport avec les scripts. Lorsque Apple a ajouté la prise en charge des scripts dans une des premières versions de Cocoa, il a été décidé d’implémenter cette seule fonctionnalité dans plusieurs catégories. Utiliser les catégories pour des protocoles informels Les protocoles informels sont souvent définis comme des catégories de NSObject. Les méthodes spécifiées dans des interfaces de catégories peuvent ne pas être implémentées par les classes du framework. Les protocoles informels constituent une sorte de zone grise dans la conception. Les protocoles formels sont vérifiés par le compilateur et garantissent les possibilités d’un objet, contrairement aux protocoles informels, qui n’apportent aucune garantie, uniquement des indications. Lorsque vous consultez la documentation d’Apple, faites particulièrement attention à toute mention d’un protocole informel. Bien qu’elle ne soit pas toujours explicite, recherchez les informations qui indiquent si un objet implémente déjà les méthodes du protocole informel ou si elles seront invoquées uniquement si vous les implémentez dans vos propres classes. La catégorie NSNibAwaking de NSObject est un exemple de protocole informel que vous devez implémenter dans vos propres classes. L’interface de cette catégorie se trouve dans le fichier d’en-tête NSNibLoading.h, qui fait partie du framework Application Kit de Cocoa. La catégorie déclare une seule méthode, -(void)awakeFromNib, qui n’est pas implémentée. À l’exécution, un message -awakeFromNib est envoyé à chaque objet chargé à partir d’un fichier .nib d’Interface Builder, après que tous les objets de ce fichier ont été chargés. Le code du framework Cocoa qui lit les fichiers .nib vérifie chaque objet chargé pour savoir s’il implémente -awakeFromNib avant d’appeler cette méthode. Autrement dit, si vous mettez en œuvre -awakeFromNib dans votre classe, la méthode sera invoquée, mais son absence ne provoquera aucune erreur. Au début, ce degré de dynamisme peut être troublant, mais les programmeurs Cocoa s’y habituent rapidement. Le protocole informel NSAccessibility est également déclaré comme une catégorie qui étend NSObject. Bien que de nombreuses classes Cocoa d’interface utilisateur implémentent les méthodes de NSAccessibility, la classe NSObject n’en fournit aucune version. Si vous créez une nouvelle sous-classe de NSObject qui doit être compatible avec les fonctionnalités d’accessibilité de Mac OS X, vous devez implémenter les méthodes déclarées dans l’interface de catégorie NSAccessibility. Il existe beaucoup d’autres exemples de protocoles informels déclarés comme des interfaces de catégories qui ne sont pas implémentés dans les frameworks Cocoa. La seule manière de réaliser la mise en œuvre appropriée des méthodes d’un protocole informel
Chapitre 6
Catégorie
81
dans vos propres classes est de consulter attentivement la documentation d’Apple à propos des protocoles informels et, dans certains cas, de passer par des tests. En pratique, les programmeurs rencontrent rarement des difficultés dans l’implémentation d’un protocole informel ou en raison de l’absence d’implémentation. Si une méthode d’un protocole informel est implémentée dans une classe dont dérive votre classe, l’implémentation de la méthode fournie par votre classe aura certainement besoin d’invoquer la version de la super-classe. Dans la documentation Apple de la classe, recherchez si l’invocation d’une implémentation héritée est requise. La méthode -awakeFromNib du protocole informel NSNibAwaking en est un bon exemple et fait partie des plus complexes. Si vous savez que la classe mère implémente -awakeFromNib, vous devez l’invoquer directement dans l’implémentation de la sous-classe : - (void)awakeFromNib { [super awakeFromNib]; // Ajouter le code propre à cette classe. }
Toutefois, que faire si le code source de la super-classe n’est pas disponible et si sa documentation ne précise pas si elle implémente déjà -awakeFromNib ? Considérons le cas d’une sous-classe imaginaire de la classe Cocoa NSControl. Voici une manière d’implémenter -awakeFromNib très courante : - (void)awakeFromNib { if([NSControl instancesRespondToSelector:@selector(awakeFromNib)]) { // Appeler l’implémentation de la super-classe. [super awakeFromNib]; } // Ajouter le code propre à cette classe. }
Le code satisfait le besoin direct : invoquer l’implémentation de -awakeFromNib fournie par la super-classe uniquement si elle existe. Il est également à l’épreuve des évolutions. Supposons que NSControl ne mette pas en œuvre -awakeFromNib. Dans la prochaine version du framework, Apple peut changer NSControl afin qu’elle implémente -awakeFromNib. Dans ce cas, le code aura le comportement adéquat et appellera la nouvelle implémentation de la super-classe. N’oubliez pas que ce problème existe uniquement parce que -awakeFromNib est déclarée, sans être implémentée, dans un protocole informel. Une autre solution au problème consiste à proposer l’implémentation via la classe de base qu’Apple a oubliée :
82
Les design patterns de Cocoa
@implementation NSObject (MYAdditions) - (void)awakeFromNib { } @end
Dès lors que la classe de base fournit une implémentation par défaut, l’implémentation de -awakeFromNib réalisée par n’importe quelle classe peut sans crainte invoquer [super awakeFromNib]. ATTENTION Faites attention lorsque vous ajoutez vos propres implémentations par défaut pour des méthodes comme -awakeFromNib. Si, dans une version ultérieure du framework, Apple ajoute une implémentation de -awakeFromNib à NSObject, toute mise en œuvre effectuée dans votre catégorie remplacera la nouvelle fournie par Apple. Ainsi, votre catégorie risque de vous empêcher de bénéficier de l’implémentation d’Apple et peut introduire des bogues dans d’autres classes du framework qui supposent la disponibilité de la version d’Apple.
Lorsque vous serez à l’aise avec les protocoles informels par Cocoa, vous risquez fort de créer vos propres protocoles en tant que catégories de NSObject ou d’autres classes pour mettre en œuvre les fonctionnalités de vos applications. L’exemple de -awakeFromNib l’a montré, il est préférable de fournir une implémentation par défaut pour chaque méthode déclarée dans le protocole informel. INFO Objective-C 2.0, arrivé avec Mac OS X 10.5, propose de nouveaux mots clés du langage qui vous permettent d’utiliser des protocoles formels là où vous auriez précédemment employé des protocoles informels. Les méthodes des protocoles formels peuvent ainsi être déclarées @optional ou @required. Le compilateur n’a pas besoin de vérifier si les méthodes déclarées après le mot clé @optional sont implémentées dans les classes qui se conforment au protocole. En revanche, toutes les méthodes déclarées après le mot clé @required doivent être implémentées. Si vous n’utilisez pas ces mots clés, toutes les méthodes du protocole sont par défaut considérées @required.
Utiliser les catégories pour le fractionnement du framework Cocoa se sert des catégories pour l’implémentation et la maintenance du code là où elles prennent tout leur sens. Par exemple, la classe NSAttributedString est définie dans le framework Cocoa non graphique Foundation et offre de nombreuses méthodes pour la création et la gestion du texte. Avec ce framework et NSAttributedString, les applications peuvent réaliser des traitements sophistiqués sur le texte, mais aucune
Chapitre 6
Catégorie
83
méthode ne permet d’afficher les chaînes de caractères avec attributs. Le framework Application Kit de Cocoa comprend une catégorie qui étend la classe NSAttributedString avec des méthodes de tracé des chaînes de caractères et d’autres opérations graphiques. L’organisation du code d’implémentation de NSAttributedString est ainsi idéale. La classe peut être utilisée dans des applications non graphiques, sans ajouter un coût ou des dépendances avec une logique de tracé et des ressources inutiles, mais elle peut également être employée dans des applications graphiques fondées sur Application Kit, avec toutes les méthodes de dessin automatiquement disponibles. Les catégories NSNibAwaking de NSObject et NSNibLoading de NSBundle en sont d’autres exemples. La classe NSObject est définie dans le framework Foundation et représente la classe de base de quasiment tout objet Cocoa. NSBundle est également définie dans le framework Foundation et met en œuvre le chargement dynamique des objets et des ressources dans une application en cours d’exécution. NSNibAwaking et NSNibLoading sont implémentées dans le framework Application Kit et déclarent les méthodes au chargement des objets depuis un fichier .nib d’Interface Builder. En étendant dans le framework Application Kit les classes du framework Foundation, il devient possible d’utiliser un objet de base dans un fichier .nib même si le framework Foundation ne possède aucune information ni dépendance avec ces fichiers. Lorsqu’une partie d’une classe est implémentée dans un framework et les autres parties dans d’autres frameworks, la mise en œuvre des fonctionnalités peut se faire là où elle prend tout son sens et où elle est plus facile à maintenir. Toutefois, cette technique ne doit être employée qu’avec des groupes de méthodes réellement indépendants.
6.4
Conséquences
Grâce à la possibilité d’ajouter ou de remplacer des méthodes dans des classes existantes, Cocoa est réellement extensible. Dans de nombreux cas, il vaut mieux préférer les catégories aux sous-classes. Plus précisément, les catégories constituent une solution lorsqu’il n’est pas possible d’obliger un framework existant à utiliser votre sous-classe à la place de la classe avec laquelle il a été compilé. Le pattern Catégorie est également la méthode conseillée pour étendre les classes d’un regroupement. Toutefois, comme n’importe quel autre outil puissant, les catégories peuvent être mal employées. Conflit de méthodes Lorsque plusieurs catégories ajoutent ou remplacent la même méthode, l’implémentation qui a la priorité sur toutes les autres dépend de l’ordre dans lequel le code est chargé dans le programme. Malheureusement, il n’est pas toujours possible de prévoir cet ordre ni de le contrôler. Autrement dit, il est fortement déconseillé de remplacer ou d’ajouter plusieurs fois la même méthode. Le problème se complique lorsque vous ne
84
Les design patterns de Cocoa
pouvez pas savoir si d’autres catégories ont déjà ajouté ou remplacé une méthode. Par exemple, un plugin ou un framework chargé par une application peut contenir une catégorie qui implémente les mêmes méthodes que celles mises en œuvre dans votre propre catégorie. La seule manière d’en être informé sera peut-être de constater le dysfonctionnement de l’application. Ce conflit entre plusieurs catégories qui implémentent la même méthode est plus théorique que pratique, mais il ne doit pas être négligé. Si vous pensez qu’une méthode utile doit être ajoutée à une classe Cocoa, il est probable qu’une autre personne ait eu la même idée. La cohérence de nommage des méthodes dans Cocoa augmente la probabilité que deux catégories distinctes ajoutent indépendamment la même fonctionnalité en utilisant le même nom de méthode. Ce problème de nom peut généralement être évité en utilisant un préfixe unique avec vos méthodes. La méthode -addObjectIfAbsent: ajoutée précédemment à NSMutableArray peut être nommée -myAddObjectIfAbsent:, ou équivalent, de manière à réduire les risques de conflit avec une autre catégorie qui étend NSMutableArray. Un problème connexe survient avec le temps, lorsque les utilisateurs mettent à niveau leurs systèmes. Supposons qu’Apple ajoute une méthode -addObjectIfAbsent: à NSMutableArray dans la prochaine version du framework Cocoa. La version proposée par ce chapitre masquera la version fournie par le nouveau framework, car l’implémentation réalisée dans une catégorie a toujours la priorité sur celle de la classe. Si un autre code du framework dépend d’une caractéristique de la méthode -addObjectIfAbsent: d’Apple et si cette caractéristique n’est pas mise en œuvre par la version dans la catégorie, ce code peut présenter un dysfonctionnement très subtil et difficile à diagnostiquer. Par conséquent, même si une méthode ajoutée à une catégorie ne présente aucun problème aujourd’hui, elle peut en présenter dans le futur. L’utilisation d’un préfixe unique avec les méthodes implémentées dans une catégorie constitue la seule réponse à ces problèmes. Il est donc fortement recommandé d’utiliser des préfixes lors de l’ajout de méthodes à des classes définies par les frameworks Cocoa. Bien entendu, si l’objectif est de redéfinir une méthode, par exemple pour corriger un bogue dans un framework, il ne faut pas utiliser un préfixe car votre nouvelle implémentation doit remplacer celle existante. Ensuite, il est bon d’effectuer des tests avec chaque nouvelle version du framework pour déterminer si votre version doit être conservée et la retirer dès qu’elle devient obsolète. Remplacer des méthodes Les catégories permettent de remplacer facilement des implémentations de méthodes, mais il n’existe aucun mécanisme pratique pour invoquer les méthodes remplacées à partir des nouvelles versions. Vous devez faire très attention à remplacer l’intégralité du comportement de la méthode, y compris les effets secondaires non documentés. En
Chapitre 6
Catégorie
85
règle générale, il est préférable de résoudre un bogue dans l’implémentation d’une méthode que de remplacer la méthode dans une catégorie. Le remplacement des méthodes est très difficile, exige des tests intensifs et ne doit être retenu qu’en dernier ressort. Maintenance du logiciel Les catégories constituent un moyen efficace de répartir les méthodes d’une classe dans plusieurs fichiers sources. En procédant ainsi, différents programmeurs peuvent travailler en même temps sur différentes parties de la même classe. De plus, cela permet de limiter la compilation requise après la modification d’une méthode. Seul le fichier qui contient le code modifié doit être recompilé ; les autres fichiers qui implémentent d’autres parties de la même classe ne sont pas concernés par la compilation. La véritable raison de diviser l’implémentation d’une classe en plusieurs catégories est de mettre en œuvre différentes méthodes dans différents frameworks, à la manière dont les méthodes graphiques sont ajoutées à la classe NSAttributedString du framework Foundation par l’intermédiaire d’une catégorie dans le framework Application Kit. Cependant, la décision d’implémenter une classe en répartissant ses méthodes dans différents frameworks ou sous-systèmes d’une application est difficile à prendre. L’un des objectifs du pattern Catégorie est de permettre l’implémentation et la maintenance du code là où elles prennent tout leur sens, mais une mauvaise utilisation des catégories peut en réalité compliquer la maintenance. En conservant toutes les implémentations des méthodes d’une classe dans un même fichier, les changements apportés à la classe se limitent à ce fichier. Lorsque l’implémentation d’une classe est répartie sur plusieurs fichiers sources, dans différents frameworks et sous-systèmes, il est plus complexe d’isoler le code à modifier et de vérifier que les changements n’ont pas introduit de nouveaux bogues. Dans certains cas, le responsable de la maintenance d’une classe peut ne pas être au courant de toutes les catégories existantes dans d’autres frameworks ou même dans des plugins de l’application. Ce point peut être amélioré en utilisant une convention de nommage pour les fichiers sources qui définissent ou implémentent des catégories. Par exemple, si la catégorie nommée MYAdditions étend la classe NSArray, vous pouvez nommer les fichiers sources NSArray+MYAdditions.h et NSArray+MYAdditions.m afin d’identifier rapidement la classe affectée.
7 Type anonyme et Conteneur hétérogène Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Objective-C définit le type id, également appelé type anonyme. Il signale au compilateur qu’une variable pointe sur un objet, mais sans donner d’informations plus précises sur le type de l’objet, qui est donc anonyme. Les programmeurs Objective-C s’habituent rapidement à l’omniprésence du type anonyme et oublient que cette caractéristique est rare dans les autres langages. Le type anonyme d’Objective-C et les conteneurs hétérogènes de Cocoa méritent une reconnaissance particulière, tant comme patterns que comme fondations à d’autres patterns.
7.1
Motivation
Le pattern Type anonyme est utilisé pour envoyer des messages à des objets anonymes, y compris ceux qui ne sont pas disponibles au moment de la compilation du code émetteur. L’idée est également de réduire le couplage entre les classes en limitant les informations que chacune doit posséder sur les autres. Le pattern Conteneur hétérogène propose des classes conteneurs puissantes qui peuvent stocker un nombre quelconque d’objets, de n’importe quel type, dans n’importe quelle combinaison.
88
Les design patterns de Cocoa
7.2
Solution
Si le langage Objective-C est aussi dynamique et flexible, le mérite en revient au système d’envoi de messages. Chaque fonctionnalité ajoutée par Objective-C au langage C de base est conçue pour faciliter la définition des objets et l’envoi des messages. En fait, un objet Objective-C peut se définir par "quelque chose qui peut recevoir des messages". Les classes Objective-C sont elles-mêmes des objets qui reçoivent des messages. Le Chapitre 3 explique comment les instances de classes sont créées en envoyant des messages aux objets classes. Un message demande à un objet de réaliser une action. La syntaxe Objective-C des envois de messages se fonde sur les crochets ([ et ]) pour délimiter le début et la fin d’une expression d’envoi de message : [récepteur sélecteur]
Le récepteur précise l’objet destinataire du message. Le sélecteur identifie le message à envoyer. Les messages peuvent inclure des arguments et retourner des valeurs. La syntaxe et la sémantique des envois de messages sont détaillées dans la section Core Library > Cocoa > Objective-C Language > The Objective-C 2.0 Programming Language de la documentation Xcode, ainsi que sur le site http://developer.apple.com. L’envoi de messages est flexible et dynamique car le récepteur et le sélecteur sont variables. Le message réellement envoyé et le récepteur sont déterminés au moment de l’exécution du programme. Lors de la compilation, il n’est pas toujours possible de connaître le type de l’objet qui recevra le message. D’autres langages orientés objet, comme C++ et Java, exigent que le type de chaque objet soit connu du compilateur. Cette contrainte n’existe pas en Objective-C. INFO En Objective-C, l’aspect le plus important de l’envoi de messages est non pas le type de l’objet qui recevra le message mais le fait qu’il puisse le recevoir. Si un objet comprend le message, alors, peu importe son type. Cette vision du typage est parfois appelée "typage canard" (duck typing) en référence au dicton suivant : "Si je vois un animal qui marche et cancane comme un canard, alors, j'appelle cet animal un canard." Dans de nombreux langages, il est primordial de connaître le type de l’objet dont on dispose pour savoir quels messages il est possible de lui envoyer. La différence dans ces deux manières de penser est subtile, mais ses implications permettent de nombreux patterns décrits dans cet ouvrage et y conduisent. Grâce au typage canard, l’implémentation de nombreux patterns est beaucoup plus simple en Objective-C que dans d’autres langages.
Des chaînes de caractères arbitraires peuvent être converties en sélecteurs au cours de l’exécution de l’application. Le sélecteur peut même avoir été saisi par l’utilisateur,
Chapitre 7
Type anonyme et Conteneur hétérogène
89
mais sans oublier de procéder aux vérifications qui s’imposent. Il est également possible que le sélecteur soit donné par du code chargé dynamiquement, qui n’existait pas au moment de la compilation du code qui envoie le message. Le système de messages d’Objective-C est si dynamique que le récepteur final du message peut ne pas se trouver dans le même processus que celui de l’expression qui envoie le message. Le Chapitre 27 explique comment Cocoa facilite l’envoi de messages à des objets situés dans différents processus, sur le même ordinateur ou sur des ordinateurs différents, en utilisant la même syntaxe que pour les messages locaux. Dans de nombreux cas, le compilateur n’a pas la possibilité de déterminer le récepteur final d’un message. Le type anonyme Objective-C fournit le type anonyme, id, qui représente un pointeur sur n’importe quel objet. Le type id ne convoie aucune information, hormis le fait que l’objet référencé par une variable de type id peut recevoir des messages. Le type id s’emploie comme n’importe quel autre type C, y compris dans la déclaration des variables, comme une structure ou une union, en tant que type des arguments d’une fonction ou d’une méthode, et comme type des valeurs de retour. En C, un pointeur peut avoir la valeur NULL, qui vaut 0. En Objective-C, un pointeur sur un objet peut contenir la constante nil, qui vaut également 0. L’envoi de messages à nil ne génère pas toujours des erreurs, contrairement à Ruby ou Java. En Objective-C, de tels messages retournent immédiatement nil. ATTENTION Ne vous fiez pas à la valeur retournée par un message envoyé à nil, excepté si le message est supposé retourner un pointeur ou un type convertible en un pointeur. Par exemple, la valeur retournée par les messages qui retournent un float ou une structure est indéfinie lorsqu’ils sont envoyés à nil.
Le type id est indispensable pour bénéficier de la flexibilité maximale permise par le langage Objective-C, mais, en règle générale, il est préférable de donner au compilateur autant d’informations sur les objets que possible. Lorsque les détails d’un objet sont connus, utilisez un type plus précis que id de manière à les transmettre au compilateur. Plus le compilateur dispose d’informations, plus il peut vous être utile au travers des avertissements.
90
Les design patterns de Cocoa
Pour déclarer un pointeur sur une instance d’une classe précise, utilisez le nom de la classe comme type du pointeur. Par exemple, pour déclarer une référence à une instance de la classe Cocoa NSArray, employez la syntaxe suivante : NSArray
*anArray;
Lorsque le compilateur rencontre ensuite anArray déclaré comme destinataire d’un message, il peut générer des avertissements si anArray n’est pas en mesure de répondre à ce message. Les bonnes pratiques recommandent d’être aussi précis que possible lors de la définition des pointeurs sur des objets. Par exemple, si vous savez que vous manipulez un NSTableView, alors, utilisez ce type pour déclarer la variable. Si vous savez seulement qu’elle sera une sous-classe de NSView, mais pas précisément quelle sousclasse, alors utilisez NSView. Enfin, si une totale flexibilité est requise, alors, vous pouvez employer id. Objective-C autorise la déclaration préalable de noms de classes à l’aide de la directive @class. Les déclarations suivantes indiquent au compilateur que NSArray, NSDictionary et NSNumber sont des noms de classes qui n’ont pas encore été définies : @class NSArray; @class NSDictionary, NSNumber;
Les déclarations préalables de noms de classes sont employées dans les mêmes situations que les structures C standard, telles que la résolution des problèmes de dépendances circulaires. Plus particulièrement, elles sont utilisées dans les déclarations d’interfaces de manière que deux classes ou plus puissent se référencer sans générer des erreurs : @class MYAcademicStatus @interface MYStudentRecord : NSObject { MYAcademicStatus *currentStatus; } @end -----------@interface MYAcademicStatus : NSObject { BOOL isEnroled; MYStudentRecord *record; } @end
La déclaration initiale de MYAcademicStatus est nécessaire pour que la déclaration de MYStudentRecord soit possible avant que la classe MYAcademicStatus elle-même ne
Chapitre 7
Type anonyme et Conteneur hétérogène
91
soit déclarée. Une fois que la classe MYStudentRecord a été déclarée, elle peut intervenir dans la déclaration de MYAcademicStatus. Lors de la déclaration des variables d’instance, il est possible d’utiliser le type id à la place des noms de classes spécifiques. Nous pouvons déclarer les classes MYStudentRecord et MYAcademicStatus de la manière suivante, sans passer par la déclaration préalable d’un nom de classe. Toutefois, le compilateur dispose alors d’informations moins complètes pour l’aider à compiler le code. @interface MYStudentRecord : NSObject { id currentStatus; } @end -----------@interface MYAcademicStatus : NSObject { BOOL isEnroled; id record; } @end
Que le type anonyme ou un type plus précis soit utilisé, tout message peut être envoyé à n’importe quel objet. En donnant au compilateur plus d’informations que celles transmises par id, il est en mesure de générer des avertissements lorsqu’il ne peut pas vérifier si le destinataire sait répondre à un message. Il est possible que la méthode qui réponde à un message particulier existe, mais le compilateur ne le sait pas. Cela se produit lorsque les méthodes sont mises en œuvre par la classe mais sans être déclarées dans l’interface. Cela peut également se produire lorsque les méthodes sont ajoutées à une classe à l’aide d’une catégorie chargée dynamiquement au moment de l’exécution (voir Chapitre 6). Si un récepteur ne répond pas à un message envoyé lors de l’exécution et si aucun traitement particulier ne permet de rediriger le message vers un autre récepteur, une erreur d’exécution est générée. Ce comportement est souvent considéré comme un bogue aussi grave que le plantage de l’application. En pratique, les erreurs sont faciles à éviter car il est possible de déterminer à l’exécution si un destinataire particulier peut répondre à un message précis avant que celui-ci ne soit envoyé. La classe Cocoa NSObject offre la méthode -(BOOL)respondsToSelector:(SEL)aSelector, qui retourne YES si le récepteur répond au sélecteur indiqué, sinon NO. La méthode -respondsToSelector: est héritée par quasiment toutes les classes Cocoa.
92
Les design patterns de Cocoa
Vous trouverez de plus amples informations concernant la classe NSObject et la méthode -respondsToSelector: dans la section Core Library > Cocoa > Objective-C Language > NSObject Class Reference de la documentation Xcode. YES et NO sont deux constantes définies par le type Objective-C BOOL, dont la déclaration suivante se trouve dans le fichier /usr/include/objc/objc.h : typedef char BOOL;
YES et NO sont également définies dans /usr/include/objc/objc.h : #define YES (BOOL)1 #define NO (BOOL)0
INFO Le fichier d’en-tête /usr/include/objc/objc.h contient la définition du type id et celles d’autres types qui présentent un intérêt pour l’étude de l’implémentation du moteur d’exécution du langage.
Le fichier objc.h ne fait pas strictement partie de Cocoa. Il s’agit d’un élément du moteur d’exécution d’Objective-C fourni par le système d’exploitation open-source Darwin utilisé par Mac OS X. Ces fichiers système sont normalement masqués à votre vue par Finder, mais rien ne vous empêche d’utiliser l’option ALLER AU DOSSIER du menu ALLER de Finder pour examiner n’importe quel fichier du système de fichiers, y compris ceux du dossier /usr/include. L’application Terminal d’Apple peut également servir à parcourir le système de fichiers. ATTENTION Quasiment toutes les classes Cocoa dérivent de NSObject. Plusieurs supposent que les méthodes fournies par NSObject sont disponibles dans tout objet référencé par le type id. En particulier, les classes collections de Cocoa supposent que chaque objet détenu par les collections réponde au moins aux messages déclarés pour les instances de NSObject. Le compilateur ne garantit pas qu’un objet référencé par une variable de type id est compatible avec NSObject, mais cette supposition est généralement fiable avec Cocoa.
Affectation Toute variable de type id peut être affectée à un pointeur sur une instance d’une classe précise : id NSArray
untypedObject; *anArray;
// On suppose que untypedObject est initialisé ici. anArray = untypedObject;
// Cette affectation est valide.
Chapitre 7
Type anonyme et Conteneur hétérogène
93
De même, un pointeur sur une instance de n’importe quelle classe peut être affecté à une variable de type id : id NSArray
untypedObject; *anArray;
// On suppose que anArray est initialisé ici. untypedObject = anArray;
// Cette affectation est valide.
Si vous souhaitez vérifier à l’exécution qu’une affectation impliquant le type anonyme a un sens, utilisez la méthode -isKindOfClass: de NSObject : id NSArray
untypedObject; *anArray;
// On suppose que untypedObject est initialisé ici. // Vérifier qu’une affectation a un sens. if([untypedObject isKindOfClass:[NSArray class]) { anArray = untypedObject; // Cette affectation est valide. }
Outre -(BOOL)isKindOfClass:(Class)aClass, la classe NSObject fournit également -(BOOL)conformsToProtocol:(Protocol *)aProtocol. Un protocole Objective-C établit la liste des méthodes qu’un objet assure implémenter, quelle que soit la hiérarchie d’héritage de sa classe. En plus de la classe NSObject, Cocoa propose un protocole NSObject qui déclare les méthodes que pratiquement toute classe Cocoa est supposée offrir. Lorsque c’est possible, il est plus souple de tester si un objet anonyme se conforme à un protocole que de vérifier s’il dérive d’une classe particulière. Les protocoles sont décrits dans la section Core Library > Cocoa > Objective-C Language > The Objective-C 2.0 Programming Language > Protocols de la documentation Xcode, ainsi que sur le site http://developer.apple.com/. Les protocoles peuvent constituer un bon compromis entre le typage anonyme et le typage statique. Par exemple, supposons que la classe d’un objet n’ait pas d’importance, mais que nous voulions juste être certains qu’il se conforme à MYProtocol. Dans ce cas, nous pouvons définir une variable de la manière suivante : id myProtocolObject;
En utilisant id, myProtocolObject reste un type anonyme et peut pointer sur un objet de n’importe quelle classe. Toutefois, le compilateur sait quels messages sont reconnus par l’objet et peut générer un avertissement en cas de tentative d’envoi d’un message qui ne fait pas partie de la liste indiquée.
94
Les design patterns de Cocoa
INFO Puisque des protocoles peuvent dériver d’autres protocoles et puisque l’héritage multiple est pris en charge, MYProtocol pourrait être défini par dérivation du protocole NSObject. Dans ce cas, seul MYProtocol serait mentionné dans la déclaration de la variable.
Conteneur hétérogène Cocoa propose un petit nombre de classes collections pour répondre à la plupart des besoins d’une application. Chaque collection contient des variables de type id. Elles sont donc hétérogènes, car des références à des objets de n’importe quel type peuvent y être insérées, dans n’importe quelle combinaison. Les classes collections prennent automatiquement en charge la réservation de l’espace nécessaire à tous leurs objets. ATTENTION Les classes collections de Cocoa se fondent sur le type id pour enregistrer des références à des objets de n’importe quel type. Toutefois, à moins que le ramasse-miettes de Mac OS X 10.5 ne soit utilisé, elles exigent que tout objet référencé dans une collection respecte les conventions de gestion de la mémoire par comptage des références décrites au Chapitre 10.
Cocoa utilise le pattern Énumérateur (voir Chapitre 8) pour offrir un accès flexible aux objets placés dans des collections. Les énumérateurs évitent d’avoir à modifier les classes collections employées lorsque l’application évolue et diminuent les bouleversements dans la logique du programme. Les classes collections de Cocoa se fondent sur le pattern Regroupement de classes (voir Chapitre 25). S’il leur apporte des interfaces simples et de nombreuses optimisations internes, il complexifie la création de sous-classes des collections existantes. Heureusement, les classes collections de Cocoa bénéficient de plus de dix années de tests en utilisation. Elles répondent à la majorité des besoins des applications et il est rarement nécessaire d’en créer des sous-classes. Il existe deux versions des classes collections : altérables (ou mutables) et inaltérables (ou immuables). Le contenu enregistré dans des collections altérables peut être modifié au cours de l’exécution du programme. Les collections inaltérables sont créées à partir d’un contenu précis, qui ne peut pas être modifié par la suite. Les classes NSArray et NSMutableArray correspondent à des collections ordonnées de références à des objets. Les classes NSDictionary et NSMutableDictionary enregistrent des associations entre des clés et des valeurs pour une recherche rapide. Les classes NSSet et NSMutableSet sont des collections non ordonnées de références uniques à des objets ; chaque objet ne peut être référencé qu’une seule fois dans un même ensem-
Chapitre 7
Type anonyme et Conteneur hétérogène
95
ble. Enfin, NSCountedSet, une sous-classe de NSMutableSet, fournit une collection non ordonnée, mais accepte d’enregistrer plusieurs références au même objet dans une collection. Les classes collections sont toutes apportées par le framework Foundation. Elles sont largement employées dans Cocoa et peuvent être adaptées à pratiquement tous les besoins. Pour de plus amples informations concernant ces classes, consultez la section Core Library > Cocoa > Objective-C Language > Foundation Framework Reference dans la documentation Xcode, ainsi que le site http://developer.apple.com/.
7.3
Exemples dans Cocoa
De nombreux design patterns de Cocoa exploitent le type anonyme, plus particulièrement les puissantes fonctionnalités du framework Application Kit. Les outlets et les cibles (voir Chapitre 17) utilisent le type anonyme pour éviter le couplage entre les objets de l’interface utilisateur et les objets personnalisés, cela dans le but de les réutiliser avec la flexibilité maximale. C’est également le cas des patterns Notification (voir Chapitre 14) et Délégué (voir Chapitre 15).
7.4
Conséquences
Certains langages ont la possibilité de détecter certaines erreurs au moment de la compilation, alors qu’il faut attendre l’exécution du programme pour qu’Objective-C les détecte. Le compilateur Objective-C ne peut jamais déterminer à coup sûr si le récepteur d’un message sera en mesure d’y répondre. Le niveau de flexibilité et de dynamisme apporté par le type anonyme peut sembler dangereux, mais, en pratique, il est rare que des objets ne puissent pas répondre aux messages qui leur sont envoyés, et les contrôles effectués par le moteur d’exécution permettent de détecter et d’éviter les erreurs. La simplicité qu’il autorise fait partie des avantages du type anonyme d’Objective-C. Ce langage fournit une infrastructure orientée objet riche, avec des ajouts minimes au langage C de base, en partie grâce au type anonyme. Celui-ci élimine le besoin de fonctionnalités complexes, comme les templates de C++ et les paquetages génériques d’Ada. Lorsqu’il est combiné à d’autres patterns, le type générique permet de réduire énormément la quantité de code nécessaire à la résolution de problèmes classiques. Il encourage également un couplage faible dans le code. Enfin, grâce à la possibilité d’envoyer n’importe quel message à n’importe quel récepteur, et cela sans avertissement de la part du compilateur, le langage Objective-C peut se dispenser d’extension ad-hoc complexes et sujettes aux erreurs. Pour mettre en œuvre l’envoi de messages à des objets distants et pour prendre en charge le chargement dyna-
96
Les design patterns de Cocoa
mique de code, de nombreux langages ont recours à d’autres langages, comme l’IDL (Interface Definition Language) de Corba, décrit sur le site http://www.corba.org/. Certaines technologies, à l’instar des EJB (Enterprise JavaBeans) de Sun et de COM et DCOM de Microsoft, existent pour que des objets présents dans des bases de code compilées séparément puissent communiquer sans être liés statiquement. Le type anonyme et les messages d’Objective-C permettent aux patterns Mandataire et Renvoi (voir Chapitre 27) d’offrir une réponse élégante au problème des messages distants, sans passer par des extensions lourdes du langage.
8 Énumérateur Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Les énumérateurs apportent un mécanisme d’accès séquentiel à tous les objets d’une collection, sans exposer la structure de données interne de la collection. Ils permettent d’écrire du code flexible et efficace qui utilise des classes collections, sans lier les applications à des implémentations particulières. Par ailleurs, les énumérateurs présentent une interface uniforme aux classes collections qui peuvent être étendues pour satisfaire une grande diversité de besoins, notamment des algorithmes de parcours différents. Les énumérateurs se fondent sur le pattern Type anonyme et rendent les conteneurs hétérogènes plus puissants. Le langage Objective-C 2.0, disponible depuis Mac OS X 10.5, ajoute des caractéristiques qui améliorent les énumérateurs en réduisant le code commun nécessaire à l’utilisation de ce pattern. En dehors de Cocoa, ce pattern est également appelé Itérateur ou Curseur.
8.1
Motivation
Les énumérateurs uniformisent le parcours des structures de données des collections. Ils présentent une interface indépendante du sens du parcours et des algorithmes mis en œuvre. Ils découplent la classe collection du code qui permet de la parcourir. Il est également possible que plusieurs opérations de parcours soient menées simultanément, mais Cocoa interdit généralement la modification des collections altérables pendant leur énumération.
98
8.2
Les design patterns de Cocoa
Solution
Pour parcourir les éléments d’une collection, il est nécessaire d’obtenir un énumérateur et de construire une boucle qui extrait les objets un à un à partir de cet énumérateur, jusqu’à épuisement des objets. Le développeur qui implémente sa propre classe collection doit également créer sa propre sous-classe NSEnumerator. L’énumération rapide d’Objective-C 2.0 simplifie le code de la boucle d’extraction, mais complexifie la classe collection. Les sections suivantes détaillent l’utilisation des énumérateurs et de l’énumération rapide, montrent comment créer de nouveaux énumérateurs et explique comment implémenter l’énumération rapide. Utiliser des énumérateurs Dans Cocoa, le pattern Énumérateur est représenté par la classe abstraite NSEnumerator. Presque toutes les collections de Cocoa fournissent une ou plusieurs méthodes qui retournent une instance d’une sous-classe concrète de NSEnumerator. De nombreux autres objets Cocoa offrent également des méthodes qui retournent un NSEnumerator pour itérer sur des ensembles d’objets pertinents. La méthode -objectEnumerator est généralement invoquée pour obtenir l’énumérateur approprié. Par exemple, NSArray propose les méthodes -objectEnumerator et -reverseObjectEnumerator pour obtenir des énumérateurs. Avec -objectEnumerator, les éléments du tableau sont parcourus dans l’ordre, depuis le premier jusqu’au dernier, en commençant par celui d’indice zéro ; il s’agit du fonctionnement généralement souhaité. Pour parcourir les éléments dans le sens inverse, de la fin jusqu’au début du tableau, il faut utiliser à la place -reverseObjectEnumerator. La classe NSDictionary offre également la méthode -objectEnumerator pour parcourir la liste des objets contenus. La méthode -keyEnumerator permet de parcourir les clés du dictionnaire à la place des objets qu’il contient. Pour connaître les possibilités de parcours d’une collection, recherchez simplement les méthodes qui retournent un NSEnumerator. Quelle que soit la façon d’obtenir l’énumérateur et quels que soient les objets qu’il permet de parcourir, il est toujours employé de la même manière. L’encapsulation mise en œuvre par l’énumérateur masque les algorithmes de parcours et propose une interface uniforme simple. La classe NSEnumerator définit uniquement les méthodes -nextObject et -allObjects. En général, seule -nextObject est utilisée. Lors de son premier appel, le premier objet de l’énumération est retourné. Chaque appel suivant retourne un objet, jusqu’à épuisement de la liste. Lorsque tous les objets ont été parcourus, -nextObject retourne nil. Voici le code classique qui permet de parcourir une énumération :
Chapitre 8
Énumérateur
99
id instance; NSEnumerator *enumerator = [myCollection objectEnumerator]; while (instance = [enumerator nextObject]) { // Utiliser l’instance. }
La méthode -allObjects permet d’obtenir un NSArray qui contient tous les objets de la liste restant à énumérer. La méthode -nextObject retourne toujours nil après une invocation de -allObjects. INFO Le nom de la méthode -allObjects peut être déroutant, puisque le NSArray retourné contient uniquement les objets qui restent à parcourir. Pour que le tableau retourné contienne tous les objets de la collection, il faut invoquer -allObjects avant tout appel à -nextObject.
Chaque sous-classe de NSEnumerator retient la collection en cours d’énumération, jusqu’à la fin de celle-ci. Cela permet d’éviter que les données sous-jacentes ne soient désallouées alors que leur parcours n’est pas terminé. Par ailleurs, si une collection est altérable, sa modification pendant le parcours n’est pas une opération considérée comme sûre. En cas de modification de la collection, des comportements indésirables peuvent survenir, notamment des objets omis ou répétés pendant l’énumération, des exceptions lancées et même un plantage de l’application. Utiliser l’énumération rapide L’énumération rapide est arrivée avec Objective-C 2.0 pour simplifier et potentiellement accélérer les boucles de parcours. Avec l’énumération rapide, le code de la section précédente se réduit à la boucle suivante : id instance; for (instance in myCollection) { // Utiliser l’instance. }
Ou à celle-ci : for (id instance in myCollection) { // Utiliser l’instance. }
La quantité de code à écrire est moindre et les intentions du programmeur sont plus claires. Au final, les risques de bogues sont réduits. Si la classe collection implémente correctement l’énumération rapide, son utilisation peut être plus efficace et plus fiable.
100
Les design patterns de Cocoa
Puisque la modification des collections altérables pendant leur parcours n’est pas autorisée dans Cocoa, l’énumération rapide installe des barrières de sécurité automatiques qui permettent de détecter les changements et lancent des exceptions lorsqu’ils ont lieu. Toutes les sous-classes de NSEnumerator ne détectent pas correctement la modification des données pendant leur parcours, ce qui peut conduire à des comportements imprévisibles de l’application et même à sa terminaison. En général, l’ordre de parcours de l’énumération rapide est identique à celui réalisé par l’instance de NSEnumerator retournée par un appel à la méthode -objectEnumerator. Toutefois, la classe NSEnumerator prend également en charge l’énumération rapide et peut être employée dans l’instruction for. Par exemple, voici comment parcourir un NSArray en sens inverse avec l’énumération rapide : id instance; for (instance in [myArrayInstance reverseObjectEnumerator]) { // Utiliser l’instance. }
Créer des énumérateurs personnalisés Si vous créez votre propre classe collection ou si vous disposez d’un objet dont le contenu doit pouvoir être énuméré, vous devez créer une sous-classe de NSEnumerator qui correspond à votre classe et ajouter à cette dernière une méthode qui crée et initialise une instance du nouvel énumérateur. Par exemple, supposons que vous disposiez d’une classe mettant en œuvre une liste chaînée et que son interface soit la suivante : @interface MYLinkedList : NSObject { unsigned long listLength; MYLinkedListNode *firstNode; MYLinkedListNode *lastNode; MYLinkedListNode *markerNode; } - (void)appendObject:(id)newObject; @property @property @property @property
(readwrite, retain) MYLinkedListNode *firstNode; (readwrite, retain) MYLinkedListNode *lastNode; (readonly) MYLinkedListNode *markerNode; (readonly) unsigned long listLength;
@end
La classe utilitaire privée MYLinkedListNode définit simplement deux propriétés : object pour l’objet contenu et nextNode comme pointeur sur l’instance suivante de MYLinkedListNode dans la liste. Le code complet n’est pas montré ici, mais il se trouve dans l’archive des codes sources que vous pouvez télécharger. Cette classe prend uniquement en charge l’ajout de nouveaux nœuds à la fin de la liste.
Chapitre 8
Énumérateur
101
La seule caractéristique inhabituelle de cette classe se trouve dans markerNode, qui désigne la fin de la liste. En général, la fin d’une liste est signalée par nil, mais, en raison de l’implémentation de l’énumération rapide, présentée plus loin dans ce chapitre, un marqueur non nil doit toujours être conservé en fin de la liste. Voici un début d’implémentation simple de la classe : @implementation MYLinkedList @synthesize @synthesize @synthesize @synthesize
firstNode; lastNode; markerNode; listLength;
- init { self = [super init]; markerNode = [[MYLinkedListNode alloc] init]; markerNode.object = [NSNull null]; markerNode.nextNode = nil; self.firstNode = self.markerNode; self.lastNode = self.markerNode; listLength = 0; return self; } - (void)appendObject:(id)newObject { MYLinkedListNode *newNode = [[MYLinkedListNode alloc] init]; newNode.object = newObject; newNode.nextNode = markerNode; if (self.markerNode == self.firstNode) { // Ajout du premier objet. self.firstNode = newNode; self.lastNode = newNode; } else { self.lastNode.nextNode = newNode; self.lastNode = newNode; } listLength++; } - (void)dealloc { MYLinkedListNode *node = firstNode; MYLinkedListNode *next = firstNode.nextNode; while (node != markerNode) { [node release]; node = next; next = next.nextNode; } firstNode = nil;
102
Les design patterns de Cocoa
lastNode = nil; [markerNode release]; markerNode = nil; [super dealloc]; } @end
Il est temps à présent d’ajouter la sous-classe de NSEnumerator qui permet de parcourir cette collection. Nous devons créer une méthode d’initialisation qui sera utilisée par MYLinkedList pour configurer une nouvelle énumération. Pour cela, il suffit de redéfinir la méthode -nextObject de NSEnumerator. L’énumérateur doit conserver un lien avec la collection en cours d’énumération. Il doit également connaître sa position courante dans le parcours des objets de collection. Enfin, pour plus de sécurité, il doit détecter les modifications apportées à la liste d’origine, juste pour le cas où une modification serait apportée pendant l’énumération. Puisque notre liste accepte uniquement les opérations d’ajout qui augmentent sa taille, nous pouvons suivre l’évolution de la longueur de la liste pour détecter les modifications. Voici l’interface du nouvel énumérateur qui satisfait ces contraintes : @interface MYLinkedListEnumerator : NSEnumerator { MYLinkedList *list; MYLinkedListNode *currentNode; unsigned long originalListLength; } - (id)initForList:(MYLinkedList *)theList; @property (readwrite, retain, nonatomic) MYLinkedList *list; @property (readwrite, retain, nonatomic) MYLinkedListNode *currentNode; @property (readonly) unsigned long originalListLength; @end
Voici l’implémentation de base correspondante : @implementation MYLinkedListEnumerator @synthesize list; @synthesize currentNode; @synthesize originalListLength; - (id)initForList:(MYLinkedList *)theList { self = [super init]; self.list = theList; self.currentNode = theList.firstNode; originalListLength = theList.listLength; return self; }
Chapitre 8
Énumérateur
103
- (id)nextObject { id object = nil; // Retourner nil si la fin de la liste est atteinte. MYLinkedListNode *nextNode = self.currentNode.nextNode; // Lancer une exception en cas de modification. if (list.listLength != self.originalListLength) { NSException *exception = [NSException exceptionWithName: @"MYLinkedListMutationException" reason: @"MYLinkedList a été modifié pendant une énumération" userInfo:nil]; @throw exception; } // Si la fin de la liste n’est pas atteinte, passer à l’objet suivant // et le retourner. if (self.currentNode != self.list.markerNode) { object = self.currentNode.object; self.currentNode = nextNode; } return object; } @end
La méthode d’initialisation -initForList: configure simplement l’état interne de l’énumérateur. La méthode -nextObject redéfinie effectue tout le travail. Elle vérifie tout d’abord que la liste n’a pas été modifiée. Dans une version plus robuste qui accepte d’autres formes de manipulation de la liste, nous pourrions ajouter une propriété numberOfMutations qui est incrémentée à chaque modification de la liste. Cette propriété serait ensuite utilisée pour détecter toute modification, par exemple l’échange de deux objets dans la liste. Quelle que soit la façon de détecter les modifications, il est généralement préférable de lancer une exception dès qu’une modification est découverte pendant l’énumération. Enfin, tant que la fin de la liste n’est pas atteinte, l’objet suivant est retourné à l’émetteur du message et la variable currentNode est mise à jour de manière à pointer sur le nœud suivant de la liste. Notez que la méthode -allObjects n’est pas redéfinie. NSEnumerator en propose une version générique, ce qui nous évite d’avoir à la redéfinir. Toutefois, juste pour le plaisir, en voici une version naïve : - (NSArray *)allObjects { NSMutableArray *array = [NSMutableArray arrayWithCapacity:originalListLength]; id object;
104
Les design patterns de Cocoa
// Remplir le tableau avec les objets qui restent à parcourir. while ((object = [self nextObject])) { [array addObject:object]; } return array; }
Cette implémentation fonctionne parfaitement, mais elle est plutôt inefficace. S’il existe une meilleure manière d’implémenter -allObjects en fonction de la structure de données de la collection personnalisée, alors, il vaut la peine d’optimiser son implémentation. Pour finir, nous devons ajouter la méthode -objectEnumerator à la classe MYLinkedList. Elle doit simplement retourner une instance à libération automatique de l’énumérateur, configurée et prête à fonctionner : - (NSEnumerator *)objectEnumerator { MYLinkedListEnumerator *enumerator = [[MYLinkedListEnumerator alloc] initForList:self]; [enumerator autorelease]; return enumerator; }
Avec cette méthode, il est possible d’utiliser l’instruction normale d’énumération, while, pour parcourir le contenu de MYLinkedList de la même manière que n’importe quelle classe collection du framework Foundation. Rien n’empêche de créer plusieurs sous-classes de NSEnumerator pour une même collection. Ces différentes sous-classes peuvent implémenter des algorithmes de parcours différents. Par exemple, supposons qu’il existe deux algorithmes de parcours connus pour notre structure de données. Par ailleurs, supposons que l’un d’eux soit plus rapide sur les grands jeux de données, tandis que l’autre est mieux adapté aux petits ensembles. Dans ce cas, nous pouvons envisager une implémentation de la méthode -objectEnumerator qui retourne la sous-classe appropriée de NSEnumerator en fonction de l’analyse des données à parcourir. Lorsqu’elles sont employées de cette manière, les sous-classes de NSEnumerator peuvent prendre en charge certains aspects du pattern Stratégie bien connu (http://fr.wikipedia.org/wiki/Stratégie_(patron_de_conception)). Si le parcours d’un tableau peut se faire en sens inverse, il existe bien d’autres possibilités, uniquement limitées par la créativité du développeur. Par exemple, un énumérateur pourrait être associé à une structure de données arborescente de manière à mettre en œuvre un parcours en profondeur, tandis qu’un autre offrirait un parcours en largeur. Si vous disposez d’une classe d’agrégation, constituée de plusieurs instances de collections, utilisez un énumérateur personnalisé permettant de parcourir l’ensemble des collections tout en masquant la complexité de l’implémentation sous-jacente.
Chapitre 8
Énumérateur
105
Implémenter l’énumération rapide Pour utiliser l’énumération rapide avec MYLinkedList, il est nécessaire d’adopter le protocole NSFastEnumeration, en implémentant la seule méthode -countByEnumeratingWithState:objects:count:. Comme peut le laisser entendre son nom, la mise en œuvre de cette méthode risque d’être compliquée. En voici le prototype : -(NSUInteger)countByEnumeratingWithState: (NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len
La méthode est conçue de manière à permettre les itérations par lots. Chaque fois qu’elle est invoquée, un ou plusieurs objets sont retournés indirectement ; la valeur de retour de la méthode indique le nombre d’objets retournés dans chaque lot. La méthode peut être invoquée plusieurs fois, jusqu’à ce que la collection soit parcourue intégralement. Lorsque l’énumération est terminée, elle retourne zéro pour signaler la fin. Par conséquent, même si tous les objets de la collection sont envoyés dans un seul lot, la méthode doit être appelée au moins deux fois pour chaque énumération. Le premier paramètre, state, est le plus important. Il s’agit d’un pointeur sur une structure de type NSFastEnumerationState, qui sert deux objectifs. Premièrement, elle permet d’enregistrer des informations d’état afin de pouvoir en disposer ultérieurement lors de la préparation du lot suivant. Deuxièmement, elle est utilisée pour retourner les objets en cours d’énumération. Voici la définition de cette structure : typedef struct { unsigned long state; id *itemsPtr; unsigned long *mutationsPtr; unsigned long extra[5]; } NSFastEnumerationState;
Le membre state permet d’enregistrer les informations dont vous avez besoin pour continuer le parcours là où il a été arrêté. Pour parcourir une liste chaînée, comme celle de notre exemple précédent, nous pouvons simplement enregistrer dans state un pointeur sur le nœud courant. Si l’état à conserver est plus complexe qu’un pointeur sur un objet, vous devez créer un nouvel objet qui contient toutes les informations requises et enregistrer un pointeur sur cet objet. Le membre itemsPtr doit être un pointeur sur un tableau C de pointeurs sur des objets. Ces objets correspondent aux objets réels retournés dans le lot courant. La valeur de retour de la méthode indique à l’énumération rapide le nombre d’objets présents dans le tableau désigné par itemsPtr. Si vous fournissez votre propre tableau, le choix de sa taille est de votre responsabilité, ce qui vous permet de contrôler la taille des lots.
106
Les design patterns de Cocoa
Pour empêcher les modifications au cours de l’énumération, le membre mutationsPtr doit pointer sur une propriété de l’objet collection qui sert d’indicateur de modification. Dans l’exemple précédent, la classe NSEnumerator utilisait la longueur de la liste pour détecter les modifications ; cette solution fonctionne également ici. S’il est possible de modifier la classe collection sans changer la taille de la collection, il faut alors trouver un autre moyen de détection. Si les modifications ne sont pas détectées, l’énumération rapide fonctionnera, mais des effets secondaires non souhaités, comme des éléments sautés, risquent de se produire. Enfin, vous pouvez totalement ignorer le membre extra de la structure. Il permet de contenir des informations d’état supplémentaires, mais il est préférable de les placer dans un objet désigné par state, car cette solution est plus conforme à une approche orientée objet. Les deux autres paramètres de la méthode -countByEnumeratingWithState:objects: count: sont stackbuf et len. Si vous fournissez votre propre tableau C pour contenir les lots d’objets, ces deux paramètres peuvent être ignorés. stackbuf est un tableau C de pointeurs dans lequel les objets sont placés, tandis que len fixe le nombre maximal d’objets que ce tableau peut contenir. Si vous préférez utiliser ce tableau à la place du vôtre, alors, state->itemsPtr doit avoir la même valeur que stackbuf. Puisque nous connaissons à présent l’objectif de la méthode, nous pouvons en réaliser quelques implémentations. Par exemple, si une classe qui implémente l’énumération rapide souhaite simplement permettre le parcours d’un groupe d’objets déjà contenus dans une classe collection de Foundation, alors, la mise en œuvre la plus simple de l’énumération rapide consiste à la déléguer à la classe collection elle-même : - (NSUInteger)countByEnumeratingWithState: (NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len { return [myCollection countByEnumeratingWithState:state objects:stackbuf count:len]; }
Si la classe possède déjà un tableau C d’objets et qu’elle soit inaltérable, l’implémentation reste simple. Supposons que le tableau C se nomme myCArrayOfObjects et qu’une propriété nommée myObjectsCount, de type entier long non signé, contienne le nombre d’objets dans ce tableau. L’implémentation de la méthode est alors parmi les plus simples possible et retourne tous les objets dans un seul lot : - (NSUInteger)countByEnumeratingWithState: (NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len { if (state->state == 0) { // Première invocation. state->state = 1; // Pour indiquer que la méthode a été appelée.
Chapitre 8
Énumérateur
107
state->itemsPtr = myCArrayOfObjects; } else { // Invocations ultérieures. Un seul lot, donc retourner zéro. return 0; } return myObjectsCount; }
Dans l’exemple précédent de MYLinkedList, nous ne disposions pas d’un tableau C. Par conséquent, pour l’implémentation de cette méthode, il est préférable d’utiliser le tableau proposé par défaut : - (NSUInteger)countByEnumeratingWithState: (NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len { MYLinkedListNode *currentNode; if (nil == (MYLinkedListNode *)state->state) { // Première invocation. Commencer au début de la liste. currentNode = self.firstNode; } else { // Repartir là où nous nous étions arrêtés. currentNode = (MYLinkedListNode *)state->state; } // Remplir stackbuf avec des objets de notre liste, jusqu’à ce que // le tableau soit plein ou qu’il n’y ait plus d’objets. NSUInteger nodeCount = 0; while ((currentNode != self.markerNode) && (nodeCount < len)) { stackbuf[nodeCount] = currentNode.object; currentNode = currentNode.nextNode; nodeCount++; } state->state = (unsigned long)currentNode; state->itemsPtr = stackbuf; // Cette propriété change en cas de modification de la liste. state->mutationsPtr = &listLength; return nodeCount; }
Nous pouvons à présent utiliser l’énumération rapide sur des instances de MYLinkedList comme s’il s’agissait d’instances de n’importe quelle classe collection de Foundation. Puisque NSEnumerator prend déjà en charge l’énumération rapide, il est inutile de modifier MYLinkedListEnumerator. Bien entendu, l’implémentation par défaut est générique et n’est donc pas aussi efficace que notre implémentation personnalisée. Il est possible d’ajouter du code semblable à celui ajouté à MYLinkedList pour augmenter les performances de l’énumération rapide sur MYLinkedListEnumerator, mais cette amélioration est laissée en exercice. Le principal problème est de maintenir la synchronisation des informations d’état entre l’énumérateur lui-même et le code de l’énumération rapide.
108
Les design patterns de Cocoa
Pour vos tests, téléchargez l’archive des codes sources de cet ouvrage. L’exemple d’énumération crée une instance de MYLinkedList avec vingt nœuds et les parcourt en employant différentes méthodes. Il montre également comment la modification de la liste au milieu d’une énumération lance une exception. Enfin, en ajoutant une instruction de journalisation à la fin du code d’énumération rapide, il est possible d’observer les comportements du traitement par lots : 2009-10-26 17:57:04.436 Enumeration[13663:10b] un tampon de taille 16 ; 16 objets chargés. 2009-10-26 17:57:04.437 Enumeration[13663:10b] 2009-10-26 17:57:04.437 Enumeration[13663:10b] 2009-10-26 17:57:04.438 Enumeration[13663:10b] 2009-10-26 17:57:04.438 Enumeration[13663:10b] 2009-10-26 17:57:04.438 Enumeration[13663:10b] 2009-10-26 17:57:04.439 Enumeration[13663:10b] 2009-10-26 17:57:04.439 Enumeration[13663:10b] 2009-10-26 17:57:04.439 Enumeration[13663:10b] 2009-10-26 17:57:04.440 Enumeration[13663:10b] 2009-10-26 17:57:04.446 Enumeration[13663:10b] 2009-10-26 17:57:04.449 Enumeration[13663:10b] 2009-10-26 17:57:04.449 Enumeration[13663:10b] 2009-10-26 17:57:04.450 Enumeration[13663:10b] 2009-10-26 17:57:04.451 Enumeration[13663:10b] 2009-10-26 17:57:04.451 Enumeration[13663:10b] 2009-10-26 17:57:04.451 Enumeration[13663:10b] 2009-10-26 17:57:04.452 Enumeration[13663:10b] un tampon de taille 16; 4 objets chargés. 2009-10-26 17:57:04.452 Enumeration[13663:10b] 2009-10-26 17:57:04.452 Enumeration[13663:10b] 2009-10-26 17:57:04.453 Enumeration[13663:10b] 2009-10-26 17:57:04.453 Enumeration[13663:10b] 2009-10-26 17:57:04.453 Enumeration[13663:10b] un tampon de taille 16; 0 objets chargés.
Énumération rapide invoquée avec 1: Chaîne #1 2: Chaîne #2 3: Chaîne #3 4: Chaîne #4 5: Chaîne #5 6: Chaîne #6 7: Chaîne #7 8: Chaîne #8 9: Chaîne #9 10: Chaîne #10 11: Chaîne #11 12: Chaîne #12 13: Chaîne #13 14: Chaîne #14 15: Chaîne #15 16: Chaîne #16 Énumération rapide invoquée avec 17: Chaîne 18: Chaîne 19: Chaîne 20: Chaîne Énumération
#17 #18 #19 #20 rapide invoquée avec
Trois appels de méthodes seront évidemment plus rapides que vingt. L’énumération rapide est ainsi plus efficace qu’une boucle standard avec NSEnumerator. Énumération interne Les méthodes d’émunération précédentes sont des itérations externes, ou actives. Autrement dit, la boucle d’itération est totalement sous le contrôle du programmeur et externe aux classes collections. Cocoa prend en charge un autre type d’itération, appelé itération interne ou passive. Il s’agit d’une itération implicite dans laquelle le contrôle explicite de la boucle est absent et même l’existence de la boucle est implicite. Par exemple, prenons les deux méthodes suivantes de NSArray : - (void)makeObjectsPerformSelector:(SEL)aSelector - (void)makeObjectsPerformSelector:(SEL)aSelector withObject:(id)anObject
Chapitre 8
Énumérateur
109
Elles envoient le même message à chaque objet d’un tableau. Il existe un raccourci qui permet d’éviter la boucle d’énumération explicite. La boucle existe toujours, mais elle est cachée. La deuxième méthode est très certainement implémentée avec une énumération rapide semblable à la manière suivante : - (void)makeObjectsPerformSelector:(SEL)aSelector withObject:(id)anObject { id object; for (object in self) { [object performSelector:aSelector withObject:anObject]; } }
8.3
Exemples dans Cocoa
Il est pratiquement impossible de programmer en Cocoa sans rencontrer une énumération. La plupart des classes collections, comme NSArray, NSDictionary, NSSet, NSCountedSet, NSHashTable ou NSMapTable implémentent la méthode -objectEnumerator pour retourner un énumérateur. De nombreuses collections proposent d’autres méthodes qui retournent des énumérateurs. NSArray offre -reverseObjectEnumerator pour effectuer un parcours en sens inverse. NSDictionary et NSMapTable utilisent -keyEnumerator pour parcourir les clés à la place des objets contenus. En général, les classes qui permettent de créer et de retourner des énumérateurs prennent également en charge l’énumération rapide. Elle produit souvent un parcours identique à celui obtenu avec l’énumérateur retourné par la méthode -objectEnumerator. Les classes NSDictionary et NSMapTable font exception, car l’itération rapide se fait sur les clés. En cas de doute, consultez toujours la documentation de la classe fournie par Apple. NSPointerArray est une nouvelle classe introduite dans 10.5 qui accepte les valeurs NULL. Elle constitue un cas intéressant. NSEnumerator ne fonctionne pas avec NSPointerArray car le premier NULL retourné au cours du parcours du contenu termine l’énumération. Toutefois, la classe NSPointerArray prend en charge l’énumération rapide. En effet, la mise en œuvre de l’implémentation rapide retourne le nombre de références contenues dans un lot. Autrement dit, certains objets retournés par l’énumération peuvent avoir la valeur NULL, sans que cela arrête la boucle, puisque les conditions de terminaison de la boucle ne dépendent pas du retour d’une valeur NULL ou nil.
110
8.4
Les design patterns de Cocoa
Conséquences
Les énumérateurs apportent un mécanisme cohérent pour le parcours d’une collection d’objets, indépendamment du type de la collection et de l’algorithme de parcours. Ce découplage permet au développeur de changer de type de collection ou de choisir des algorithmes de parcours différents sans être obligé de modifier le code d’énumération. Dans Cocoa, l’énumération est mise en œuvre par la création de sous-classes de NSEnumerator et par l’adoption du protocole NSFastEnumeration. Dans d’autres frameworks, la fonction de NSEnumerator prend souvent le nom de pattern Itérateur, mais les énumérateurs peuvent également représenter un cas particulier du pattern Stratégie en autorisant le choix de l’algorithme de parcours. Puisque les énumérateurs conservent leurs propres informations d’état, il est possible d’effectuer plusieurs parcours d’une même collection simultanément. En général, les sous-classes de NSEnumerator sont fortement liées à des classes collections spécifiques et disposent d’un accès privilégié aux structures de données internes de la collection. Cocoa interdit la modification des collections altérables pendant leur énumération. Certains environnements tentent de fournir des itérateurs robustes qui permettent de modifier les collections sous-jacentes. En raison du coût et de la complexité de cette approche, ainsi que de sa relative inutilité en pratique, Cocoa choisit d’interdire les modifications au profit d’une sûreté accrue vis-à-vis des threads et d’un code plus performant. Les énumérateurs de Cocoa sont unidirectionnels et ne peuvent pas être réinitialisés. Lorsque la fin d’une boucle d’itération est atteinte, l’énumérateur n’est plus d’aucune utilité. Pour effectuer un nouveau parcours, un nouvel énumérateur doit être demandé. Il est possible de créer des sous-classes qui se comportent à la manière d’un curseur, avec la possibilité d’aller vers l’avant, vers l’arrière, au début, à la fin ou à une position quelconque dans une liste, mais l’interface standard définie par Cocoa ne prend pas en charge ces comportements.
9 Exécution de sélecteur et Exécution retardée Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Les sélecteurs identifient les messages envoyés aux objets Objective-C. Ils sont utilisés par les récepteurs des messages pour sélectionner les méthodes à exécuter. Les sélecteurs sont au cœur de la puissance, de la flexibilité et du dynamisme d’Objective-C, ainsi que de la mise en œuvre des autres design patterns de Cocoa. Plus précisément, les sélecteurs servent de base à l’implémentation des patterns Notification, Délégué, Cible et action, Invocation et Renvoi. Grâce aux sélecteurs, il est possible de demander aux objets Cocoa d’exécuter des méthodes immédiatement ou après un certain délai. L’exécution différée de méthodes peut se révéler très pratique et sert parfois à maintenir la réactivité de l’interface utilisateur pendant l’exécution de longues tâches, à mettre en œuvre un mécanisme d’animation ou à proposer d’autres fonctionnalités temporelles. Dans l’orienté objet, les sélecteurs sont le pendant des pointeurs de fonctions du langage C. Un pointeur de fonction est une variable qui contient l’adresse d’une fonction compilée. Dans le langage C et ses dérivés, comme Objective-C et C++, le compilateur et l’éditeur de liens convertissent les appels explicites aux fonctions effectués dans le code source en instructions assembleur de saut à des adresses mémoire prédéterminées. Cette conversion est parfois appelée liaison. En utilisant un pointeur de fonction, le programmeur peut retarder la liaison d’un appel de fonction jusqu’à l’exécution de l’application. Par exemple, la valeur d’un pointeur de fonction peut être déterminée à
112
Les design patterns de Cocoa
l’exécution en fonction de l’entrée de l’utilisateur. Cette technique est parfois appelée liaison tardive. À l’instar des pointeurs de fonctions qui permettent de différer la spécification des fonctions qui seront invoquées, les sélecteurs permettent de différer la spécification des messages qui seront envoyés à un objet. Pourquoi les sélecteurs sont-il plus orientés objet que les pointeurs de fonctions et pourquoi les utiliser à leur place ? En quelque sorte, il s’agit de questions pièges car, au final, Objective-C utilise des pointeurs de fonctions. La réponse tient dans l’implémentation de l’envoi de messages par Objective-C, telle que décrite à la section "Solution" de ce chapitre. En bref, les sélecteurs sont utilisés par les objets pour sélectionner la méthode à exécuter et, ensuite, l’accès à l’implémentation de la méthode choisie se fait au travers d’un pointeur de fonction. Le rôle de l’objet dans la sélection de la méthode est un élément essentiel de l’orienté objet.
9.1
Motivation
Le pattern Exécution de sélecteur est utilisé pour retarder jusqu’à l’exécution la spécification du message qui sera envoyé à un objet. Il réduit également le couplage entre les objets, car les informations concernant les messages nécessaires aux émetteurs sont moindres. Utilisé avec les patterns Type anonyme et Conteneur hétérogène (voir Chapitre 7), le pattern Exécution de sélecteur permet de découpler totalement l’émetteur d’un message et son récepteur. À la compilation, l’émetteur d’un message n’a pas besoin de connaître le message qui sera envoyé ni l’objet qui le recevra. Cette possibilité est exploitée par le pattern Cible et action. Le pattern Exécution retardée permet de planifier l’envoi des messages à un moment futur indiqué. L’idée est également de pouvoir envoyer des messages qui seront exécutés dans le thread principal, même s’ils sont émis depuis un autre thread.
9.2
Solution
Pour détailler le rôle des sélecteurs dans le mécanisme Objective-C d’envoi de messages, il est nécessaire de présenter quelques concepts : n
Les objets offrent des méthodes pour effectuer des opérations. Une méthode est mise en œuvre par du code qui fait partie de l’implémentation de l’objet. L’accent est mis sur la méthode qui exécute une opération plutôt que sur l’opération ellemême, car, dans la programmation orientée objet, différents objets peuvent proposer différentes méthodes pour réaliser la même opération.
Chapitre 9
Exécution de sélecteur et Exécution retardée
113
n
Un message est une demande d’exécution d’une opération faite à un objet. L’objet détermine la méthode qui sera invoquée pour effectuer l’opération. Le même message peut être envoyé à différents objets et produire différents résultats.
n
Un sélecteur identifie le message envoyé à un objet et l’objet qui reçoit le message l’utilise pour choisir la méthode à invoquer. Dans certains cas, l’objet peut mettre en œuvre une logique de sélection complexe ou renvoyer le message à un autre objet.
En ayant ces concepts à l’esprit, l’utilisation des sélecteurs est très simple. Objective-C fournit le type de données SEL pour déclarer des variables de type sélecteur : SEL
aSelecteur;
// Déclarer une variable qui contient un sélecteur.
Une variable sélecteur est initialisée à l’aide de la syntaxe @selector() d’Objective-C : SEL
aSelecteur = @selector(update);
La classe de base NSObject offre la méthode -(id)performSelector:(SEL)aSelector pour envoyer un message pendant l’exécution. Elle est utilisée lorsque le message envoyé ne contient aucun argument et retourne un objet. SEL
aSelecteur = @selector(update);
// Ces trois lignes sont équivalentes. id result1 = [someObject update]; id result2 = [someObject performSelector:@selector(update)]; id result3 = [someObject performSelector:aSelecteur];
Il n’est pas obligatoire de préciser le sélecteur employé avec -performSelector: au moment de la compilation. Par exemple, Cocoa fournit la fonction C SEL NSSelectorFromString(NSString *), qui convertit une chaîne de caractères en sélecteur. La chaîne peut provenir de n’importe quelle source, y compris être saisie par l’utilisateur. Un sélecteur peut être converti en une chaîne de caractères grâce à la fonction C NSStringFromSelector() de Cocoa. INFO Objective-C permet d’envoyer n’importe quel message à n’importe quel objet. Si le récepteur ne dispose d’aucune méthode permettant de répondre à un message, plusieurs solutions s’offrent à lui, notamment renvoyer le message à un autre objet, ignorer le message, générer une exception ou signaler une erreur.
NSObject propose la méthode -(BOOL)respondsToSelector:(SEL)aSelector pour vérifier, avant d’envoyer le message, qu’un objet est en mesure de répondre à un sélecteur. Le code suivant convertit une chaîne de caractères en un sélecteur, mais utilise celui-ci uniquement si l’objet concerné est en mesure d’y répondre :
114
Les design patterns de Cocoa
id MYSendMessageToObject(NSString *userEnteredString, someObject) { SEL aSelector = NSSelectorFromString(userEnteredString)]; id result = nil; if([someObject respondsToSelector:aSelector]) { result = [someObject performSelector:aSelector]; } return result; }
Pour envoyer un message qui prend en argument un seul objet, utilisez la méthode -(id)performSelector:(SEL)aSelector withObject:(id)anObject de NSObject. Lorsque le message prend deux objets en argument, vous pouvez invoquer la méthode -(id)performSelector:(SEL)aSelector withObject:(id)anObject withObject: (id)anotherObject de NSObject. En revanche, si plus de deux objets doivent être passés en arguments du message, si les arguments ne sont pas des objets ou si la valeur de retour n’est pas un objet, il faut employer la classe Cocoa NSInvocation et le pattern Invocation (voir Chapitre 20). Exécution retardée Pour envoyer un message après un certain délai, utilisez la méthode -(void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay de NSObject. Le message avec le sélecteur et l’argument indiqués est planifié et envoyé après une temporisation stipulée en secondes. L’exécution retardée est mise en œuvre par la classe Cocoa NSRunLoop. Cette classe accepte les actions de l’utilisateur et surveille les communications d’une application Cocoa avec le système d’exploitation sous-jacent. Les demandes d’envoi différé de messages sont placées dans une file d’attente de la boucle d’exécution associée au thread qui effectue la requête. Chaque fois que la boucle recueille les événements liés aux actions de l’utilisateur, elle consulte également les demandes en attente. Si un temps suffisant s’est écoulé depuis le placement de la demande dans la file d’attente, la boucle d’exécution envoie le message avec l’argument indiqué. Toutefois, la boucle d’exécution peut ne pas être active si l’application est occupée à une autre activité. Par conséquent, elle ne peut pas garantir l’envoi du message en un temps précis. Elle peut seulement assurer qu’il sera envoyé au plus tôt. Si le délai indiqué est de zéro seconde, le message est envoyé aussi tôt que possible dès l’activation suivante de la boucle d’exécution. Dans tous les cas, -performSelector:withObject:afterDelay: retourne avant que le message demandé ne soit envoyé.
Chapitre 9
Exécution de sélecteur et Exécution retardée
115
La méthode de classe +(void)cancelPreviousPerformRequestsWithTarget: (id)aTarget selector:(SEL)aSelector object:(id)anArgument de NSObject permet d’annuler une demande d’envoi différé d’un message. Elle annule tous les messages retardés qui correspondent à la cible, au sélecteur et à l’argument indiqués et mis en attente par la boucle d’exécution du thread qui annule la demande. La classe NSRunLoop peut fonctionner en différents modes. Ils déterminent les sources d’entrée lues par la boucle d’exécution. La méthode -performSelector:withObject: afterDelay: place les demandes dans le mode NSDefaultRunLoopMode. Par conséquent, si la boucle d’exécution ne se trouve pas dans ce mode, le message demandé ne sera pas envoyé. Pour préciser les modes de la boucle d’exécution utilisés pour mettre en file d’attente les demandes de messages retardés, utilisez la méthode -(void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes de NSObject. Implémentation Objective-C de l’envoi de messages L’envoi de messages fondé sur les sélecteurs est fondamental dans la conception de Cocoa. C’est pourquoi nous allons examiner l’implémentation Objective-C de ce mécanisme. Elle est relativement simple, extrêmement élégante et permet à Cocoa d’exister. Pour utiliser les patterns Exécution de sélecteur et Exécution retardée dans votre code, vous n’avez pas nécessairement besoin de ces informations. INFO Le moteur d’exécution d’Objective-C d’Apple est open-source et disponible dans le projet Darwin (http://www.opensource.apple.com/projects/darwin). Le compilateur GNU fournit également une version du moteur d’exécution d’Objective-C (http://gcc.gnu.org/). Apple et les responsables de la maintenance de GNU font leur possible pour que les deux moteurs soient compatibles et partagent du code source. Toutefois, les fonctionnalités qui apparaissent dans une version peuvent ne pas se retrouver immédiatement dans l’autre.
Le langage Objective-C utilise une petite bibliothèque rapide de fonctions et de structures de données appelée moteur d’exécution. De nombreux langages de programmation utilisent un moteur d’exécution. La machine virtuelle Java en est l’un des plus connus, mais C++ et C possèdent également leur moteur. Le moteur d’exécution d’Objective-C est principalement écrit en C et peut être utilisé à partir de programmes C ou C++, même s’ils ne sont pas compilés avec un compilateur Objective-C ou Objective-C++. Le moteur d’exécution d’Objective-C apporte les technologies employées dans la mise en œuvre des design patterns de Cocoa. Toutefois, certains patterns de cet ouvrage ne se limitent pas à une application particulière des caractéristiques de ce moteur :
116
Les design patterns de Cocoa
n
Le moteur d’exécution prend en charge le chargement dynamique des objets Objective-C, ce qui permet au pattern Bundle d’exister.
n
Le moteur d’exécution crée toutes les instances d’objets et sous-tend le pattern Création dynamique.
n
Le moteur d’exécution met directement en œuvre le pattern Catégorie de manière à pouvoir ajouter des méthodes à des classes existantes.
n
Le moteur d’exécution implémente l’envoi de messages, qui est essentiel aux patterns Exécution de sélecteur et Exécution retardée, ainsi qu’aux patterns Mandataire et Renvoi.
Le système de messages est mis en œuvre à l’aide des deux fonctions C suivantes ou de variantes, selon les types de retour et les conventions d’appel des fonctions propres à la plateforme : id objc_msgSend(id self, SEL op, ...); id objc_msgSendSuper(struct objc_super *super, SEL op, ...);
Les fonctions de gestion des messages sont au cœur d’Objective-C. Lorsque le compilateur Objective-C rencontre une expression impliquant un message, comme [recepteur unSelecteurDeMessage], il la remplace par du code qui appelle objc_msgSend( recepteur, @selector(unSelecteurDeMessage)) dans la version compilée. La fonction objc_msgSend() recherche une méthode du récepteur qui correspond au sélecteur indiqué. Pour de plus amples informations concernant cette recherche, consultez "Messaging" dans la section Core Library > Cocoa > Objective-C Language > Objective-C 2.0 Runtime Programming de la documentation Xcode. Si la recherche ne permet pas de trouver une méthode appropriée, le message peut être renvoyé à un autre objet, comme décrit par les patterns Mandataire et Renvoi (voir Chapitre 27). Dans le cas où une méthode adéquate est trouvée, un pointeur de fonction C correspondant est utilisé pour invoquer une fonction qui implémente la méthode. Les pointeurs de fonctions qui correspondent aux implémentations de méthodes sont enregistrés dans des variables de type IMP, dont voici la déclaration : typedef id (*IMP)(id self, SEL _cmd, ...);
Les deux premiers arguments de la fonction référencée par un IMP correspondent au récepteur et au sélecteur passés à objc_msgSend(id self, SEL op, ...). Dans l’implémentation de la méthode, le récepteur est la variable self de la méthode. Les arguments supplémentaires d’une méthode sont également passés à son implémentation. Le moteur d’exécution d’Objective-C fourni par Apple utilise un code assembleur spécifique à la plateforme pour que les arguments supplémentaires soient disponibles dans l’implémentation de la méthode. Le moteur d’exécution de GNU utilise du code C
Chapitre 9
Exécution de sélecteur et Exécution retardée
117
portable pour obtenir les mêmes résultats, mais avec des performances moindres sur certains processeurs. La fonction objc_msgSendSuper(struct objc_super *super, SEL op, ...) opère exactement de la même manière que objc_msgSend(id self, SEL op, ...), excepté qu’elle commence par rechercher une méthode dans la super-classe du récepteur et ne prend pas en compte les méthodes implémentées par le récepteur lui-même. Le compilateur Objective-C génère un appel à la fonction objc_msgSendSuper() lorsque l’expression impliquant un message contient le mot clé super, comme dans [super unSelecteurDeMessage]. INFO La recherche de la méthode à invoquer peut demander du temps. Dans la plupart des cas, le moteur d’exécution d’Objective-C d’Apple évite cette recherche en plaçant l’IMP de chaque sélecteur dans un cache fourni par la classe elle-même. Les fonctions de gestion des messages commencent par rechercher dans le cache un IMP qui correspond au sélecteur indiqué. En général, cet IMP se trouve dans le cache et aucune recherche n’a lieu.
Vous pouvez convertir tout code qui envoie des messages en appel de fonction via un IMP. La classe de base NSObject propose une méthode qui permet d’obtenir directement un IMP : - (IMP)methodForSelector:(SEL)aSelector; + (IMP)instanceMethodForSelector:(SEL)aSelector;
Toutes les pièces sont à présent en place et la méthode -(id)performSelector: (SEL)aSelector peut être mise en œuvre par le code suivant : - (id) performSelector:(SEL)aSelector { IMP methodImplementation = [self methodForSelector:aSelector]; return (*IMP)(self, aSelector); }
9.3
Exemples dans Cocoa
Les sélecteurs sont très largement utilisés dans Cocoa. La méthode -performSelector: et la prise en charge connexe fournies par la classe de base NSObject sont étendues de différentes manières. Deux classes collections de Cocoa, NSArray et NSSet, implémentent les méthodes suivantes pour envoyer un message à chaque objet de la collection : - (void)makeObjectsPerformSelector:(SEL)aSel - (void)makeObjectsPerformSelector:(SEL)aSel withObject:(id)anObject
118
Les design patterns de Cocoa
L’autre classe collection prédominante de Cocoa, NSDictionary, permet d’obtenir des tableaux de toutes les valeurs et de toutes les clés contenues par l’intermédiaire des méthodes -(NSArray *)allValues et -(NSArray *)allKeys. Vous pouvez utiliser ces tableaux pour envoyer indirectement un message à tous les objets d’un dictionnaire. L’envoi de messages aux objets d’une collection peut souvent remplacer le pattern Énumérateur (voir Chapitre 8). Il ne faut pas fonder du code sur l’ordre d’envoi des messages au contenu d’une collection par les méthodes -makeObjectsPerformSelector: et -makeObjectsPerformSelector:withObject:. Par ailleurs, les messages ne doivent pas modifier les collections elles-mêmes. Les énumérateurs constituent un meilleur choix lorsque l’ordre est important ou lorsque vous avez besoin des valeurs retournées par chaque message envoyé. Le pattern Notification (voir Chapitre 14) permet aux objets d’enregistrer des messages qui seront envoyés en réponse à des événements futurs. Le message à envoyer est variable et indiqué à l’aide d’un sélecteur. Dans l’implémentation de la classe NSNotificationCenter, la méthode -performSelector:withObject: est invoquée pour envoyer réellement les messages. Les messages envoyés à un objet délégué sont généralement prédéfinis, mais l’objet qui les envoie doit tout d’abord vérifier que le délégué peut répondre. Le pattern Délégué (voir Chapitre 15) illustre l’utilisation de la méthode -respondsToSelector: de la classe NSObject. Le pattern Invocation (voir Chapitre 20) de Cocoa est employé dans plusieurs cas, notamment pour la prise en charge du annuler/rétablir automatique et la distribution de messages via le pattern Mandataire (voir Chapitre 27). Le pattern Invocation et la classe NSInvocation proposent une implémentation de la liaison tardive des messages plus complète que la simple méthode -performSelector:. NSInvocation peut enregistrer des messages dont la valeur de retour n’est pas un objet ou dont les arguments sont complexes. NSInvocation enregistre le sélecteur du message à envoyer avec le récepteur du message et tous les arguments. Le pattern Outlet, cible et action (voir Chapitre 17) révèle toute la puissance et la flexibilité de la liaison tardive avec un sélecteur. Une action n’est qu’un sélecteur variable et la cible n’est qu’un objet anonyme (voir Chapitre 7). C’est grâce à la liaison tardive, c’est-à-dire la possibilité de retarder la spécification de l’action et de la cible jusqu’à l’exécution, que le pattern Cible et action se montre si utile. Enfin, l’une des dernières variantes Cocoa les plus intéressantes du pattern Exécution de sélecteur concerne la possibilité d’envoyer en toute sécurité des messages qui s’exécuteront dans le thread principal même s’ils proviennent d’un thread différent. Il ne s’agit pas d’une solution générale pour l’échange de messages entre threads, mais cette
Chapitre 9
Exécution de sélecteur et Exécution retardée
119
faculté peut se révéler étonnamment pratique. Le thread principal est celui dans lequel s’exécute la fonction C main(). Par exemple, la plupart des tracés AppKit doivent être réalisés dans le thread principal. Les autres threads envoient des messages au thread principal pour demander des tracés avec AppKit. À l’instar de l’implémentation Cocoa de l’exécution retardée, les messages envoyés depuis d’autres threads peuvent être placés dans la file d’attente de la boucle d’exécution du thread principal. Par ailleurs, le mode de la boucle d’exécution doit parfois être pris en compte. C’est possible avec les méthodes suivantes : - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array
Lors de l’envoi d’un message qui sera exécuté dans le thread principal, il est impossible d’accéder à la valeur de retour, si elle existe. L’argument waitUntilDone: permet de préciser si le thread émetteur doit poursuivre son exécution de manière asynchrone ou attendre que le message ait été exécuté dans le thread principal. Contrairement à l’exécution retardée, lorsqu’un message a été placé dans la file d’attente d’exécution du thread principal, il ne peut pas être annulé.
9.4
Conséquences
Grâce aux patterns Exécution de sélecteur et Exécution retardée, il est possible d’envoyer des messages variables, et cela à des instants futurs. Ils remplacent en partie le design pattern Commande (http://fr.wikipedia.org/wiki/Commande_(patron_de_ conception)). Le pattern Invocation (voir Chapitre 20) est une implémentation plus complète de ce pattern. D’un point de vue ingénierie logicielle, les patterns Exécution de sélecteur et Exécution retardée sont une application de la liaison tardive aux systèmes orientés objet. Le langage Objective-C et d’autres langages de programmation, comme Smalltalk, Ruby et Python, offrent une prise en charge de la liaison tardive, sous une forme ou sous une autre, au niveau du langage. Le CLR (Common Language Runtime) et le langage C# de Microsoft prennent également en charge la liaison tardive. Pour comprendre les conséquences de sa prise en charge au niveau du langage telle qu’elle est employée dans Cocoa et implémentée par Objective-C, il est utile de la comparer aux approches alternatives. Les développeurs de frameworks ont tenté d’intégrer les implémentations orientées objet de la liaison tardive à de nombreux langages de programmation. C’est notamment le cas de COM (Component Object Model) de Microsoft, puis de DCOM (Distributed Component Object Model), connu sous le nom Active-X. COM apporte la liaison tar-
120
Les design patterns de Cocoa
dive et DCOM l’utilise pour les communications entre objets au travers d’un réseau. CORBA (Common Object Request Broker Architecture) de l’OMG (Object Management Group) ajoute la liaison tardive et une variante de l’envoi de messages au travers d’un réseau à divers langages de programmation. Dans toutes ces approches d’intégration, le programmeur doit apprendre et utiliser un autre langage, comme l’IDL (Interface Definition Language) de CORBA, ou écrire un long code fastidieux pour utiliser la liaison tardive. À l’opposé, dans Cocoa, la liaison tardive est omniprésente et n’impose pas l’utilisation du pattern Commande ou d’un IDL. Tous les messages Objective-C emploient la liaison tardive et aucun code supplémentaire n’est donc nécessaire pour l’utiliser. Le framework .NET et les frameworks destinés à Smalltalk, Ruby et Python permettent également d’employer la liaison tardive sans ou avec peu de code spécial. Les frameworks construits au-dessus des langages qui prennent en charge la liaison tardive de manière intrinsèque sont capables de mettre en œuvre des communications distribuées entre objets sans imposer un effort particulier aux programmeurs. Par exemple, grâce aux patterns Mandataire et Renvoi de Cocoa, les programmeurs peuvent envoyer des messages à des objets anonymes au travers du réseau en procédant exactement de la même manière que pour des messages destinés à des objets locaux. Le framework .NET emploie une technique équivalente.
10 Accesseur Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Accesseur correspond à une technique qui permet de canaliser tous les accès aux propriétés d’un objet au travers de méthodes parfaitement définies et faciles à reconnaître appelées accesseurs. Les propriétés sont généralement enregistrées dans des variables d’instance, mais elles peuvent parfois l’être différemment ou calculées en fonction des besoins. Le pattern Accesseur améliore la flexibilité de l’implémentation tout en réduisant les risques d’erreurs. Voici ses avantages : n
Flexibilité d’implémentation. Les propriétés peuvent être enregistrées sous forme de variables d’instance ou en utilisant d’autres techniques, comme le pattern Mémoire associative, et l’implémentation peut être modifiée sans affecter une autre partie du code.
n
Travail de maintenance minimal. Les accès aux propriétés d’un objet passent par quelques méthodes, ce qui limite le nombre d’endroits où le code doit être retouché en cas de modification des propriétés.
n
Respect plus facile des conventions Cocoa de gestion de la mémoire par comptage des références. La gestion de la mémoire se faisant dans les méthodes d’accès, il est beaucoup plus simple d’adhérer aux conventions Cocoa et d’isoler le code de gestion de la mémoire dans quelques méthodes.
n
Prise en charge des technologies Cocoa KVC (Key Value Coding) et KVO (Key Value Observing). Ces technologies fonctionnent uniquement avec les classes qui offrent des méthodes d’accès correctement nommées. La mise en œuvre des bindings Cocoa (voir Chapitre 32) se fonde sur KVC et KVO.
122
n
Les design patterns de Cocoa
Autoriser un traitement particulier lorsque des outlets connectés dans Interface Builder sont restaurés au cours du chargement d’un fichier .nib.
Le rôle crucial des accesseurs dans le pattern Mémoire associative est décrit au Chapitre 19. Le pattern Accesseur facilite l’encapsulation des données et celle des opérations sur ces données, dans la meilleure tradition de la programmation orientée objet. Une bonne utilisation de ce pattern est particulièrement importante avec Cocoa lorsque le ramasse-miettes facultatif introduit dans Mac OS X 10.5 n’est pas activé. Sans le ramasse-miettes, les conventions Cocoa de gestion de la mémoire par comptage des références doivent être suivies scrupuleusement pour éviter les erreurs. De nombreux bogues sont liés à des problèmes d’allocation dynamique de la mémoire et le pattern Accesseur permet de réduire le nombre d’endroits du code où peuvent survenir ces problèmes d’allocation. Ce chapitre décrit plusieurs implémentations classiques des accesseurs et se focalise sur le lien entre les accesseurs et les conventions Cocoa de gestion de la mémoire par comptage des références. Cette gestion est présentée en détail car les objets Cocoa doivent la prendre en charge, tout au moins pour un futur immédiat, jusqu’à ce que l’usage du ramasse-miettes automatique soit répandu. Grâce aux accesseurs, le travail nécessaire à la prise en charge du comptage des références est réduit. INFO Le langage Objective-C 2.0 ajoute les directives @property et @synthesize pour formaliser la déclaration d’une propriété et générer automatiquement les méthodes d’accès correctement nommées. Ce chapitre explique comment écrire vos propres méthodes d’accès et le code généré par @synthesize. La syntaxe de @property est présentée brièvement à la section "Copie des propriétés Objective-C 2.0" du Chapitre 12.
10.1
Motivation
L’idée est de canaliser les accès aux propriétés d’un objet au travers de méthodes de manière à masquer les détails d’implémentation et de confiner à ses méthodes le code de gestion de la mémoire. Dans Cocoa, le système de comptage des références apporte une solution pragmatique relativement simple au difficile problème de gestion de la mémoire, mais il faut employer des patterns pour que ce système soit utilisé correctement, et les accesseurs permettent d’en assurer la simplicité. Grâce au pattern Accesseur, des technologies Cocoa de haut niveau, comme les bindings, le codage clé-valeur (KVC, Key Value Coding) et l’observation clé-valeur (KVO, Key Value Observing), peuvent être employées dans vos propres classes.
Chapitre 10
10.2
Accesseur
123
Solution
Les accesseurs sont des méthodes utilisées pour fixer et obtenir la valeur des propriétés d’un objet. L’accesseur le plus simple prend la forme d’une méthode qui retourne la valeur d’une variable d’instance non objet. Par exemple, considérons un objet qui mémorise un taux d’intérêt sous forme d’une variable d’instance réelle nommée interestRate. L’implémentation de la méthode suivante retourne cette valeur : - (float)interestRate { return interestRate; }
Même si la méthode est extrêmement simple, il est préférable d’offrir cet accesseur plutôt que de manipuler directement la variable. Si toutes les parties du code qui utilise le taux d’intérêt invoquent l’accesseur pour obtenir sa valeur, l’implémentation de la classe peut ensuite être modifiée sans crainte. Par exemple, l’enregistrement du taux d’intérêt dans une variable d’instance n’est sans doute pas nécessaire s’il est possible de calculer sa valeur. Lorsque les accesseurs sont utilisés, l’implémentation de la méthode -(float)interestRate peut être modifiée de manière à retourner une valeur calculée ou obtenue à partir d’un serveur, sans que cette modification ne remette en cause les autres parties du code qui l’utilisent. Les accesseurs qui retournent directement des valeurs sont nommés d’après la valeur retournée. Ainsi, la valeur de interestRate est retournée par la méthode -interestRate. Cocoa débute le nom d’un accesseur par le mot get pour indiquer que la valeur est retournée indirectement par référence. L’implémentation suivante retourne une référence sur le taux d’intérêt : - (void)getInterestRate:(float *)aFloatPtr { if(NULL != aFloatPtr) { *aFloatPtr = interestRate; } }
Puisque les raisons de retourner des valeurs par référence sont rares, les accesseurs nommés avec get sont peu fréquents dans Cocoa. Même les structures C complexes sont généralement retournées par valeur. Lorsque la taille d’une valeur est constante, la valeur doit être retournée directement. Certaines méthodes, comme -(void)getBytes: (void *)aBuffer de NSData, retournent des valeurs par référence car le nombre d’octets retournés ne peut pas être déterminé au moment de la compilation. Parmi les autres méthodes get, citons -(void)getObjects:(id *)aBuffer de NSArray, -(void) getCharacters:(unichar *)aBuffer de NSString et -(void)getValue:(void *) aBuffer de NSValue. Dans chaque cas, la valeur copiée dans la mémoire référencée a
124
Les design patterns de Cocoa
une taille variable. Il existe néanmoins une autre raison de retourner des valeurs par référence : lorsqu’une même méthode doit retourner plusieurs valeurs. La méthode -(void)getRed:(CGFloat *)red green:(CGFloat *)green blue:(CGFloat *)blue alpha:(CGFloat *)alpha de NSColor, qui retourne quatre valeurs réelles par référence, en est un bon exemple. Les accesseurs qui permettent de fixer les propriétés non objets sont également simples. La valeur du taux d’intérêt enregistré dans un objet peut être fixée en invoquant la méthode -(void)setInterestRate:(float)aRate, dont voici l’implémentation : - (void)setInterestRate:(float)aRate { interestRate = aRate; }
Les accesseurs set (également appelés mutateurs) sont, pour les mêmes raisons, tout aussi importants que les accesseurs qui retournent des valeurs. Lorsqu’une seule méthode permet de fixer la valeur d’une propriété d’un objet, le mécanisme d’enregistrement de cette propriété peut être modifié sans affecter les autres parties du code. Par ailleurs, en confinant les modifications d’une propriété aux accesseurs, le débogage est simplifié. Au cours de la phase de débogage, si la valeur d’une propriété n’est pas correcte ou douteuse, un point arrêt dans l’implémentation de l’accesseur permet de stopper l’exécution dès que la propriété est modifiée et d’identifier l’origine de la modification invalide. L’implémentation des mutateurs est souvent plus complexe que celle montrée précédemment. Ils constituent un endroit naturel où placer la logique applicative qui contraint des valeurs, signale à d’autres objets la modification d’une propriété ou recalcule des valeurs en fonction de la propriété modifiée. Pour les objets qui affichent des valeurs, l’actualisation de l’affichage peut être prévue après la modification d’une propriété dans un accesseur. Par exemple, la classe NSTextField du framework Application Kit implémente sa méthode -(void)setIntValue:(int)aValue de la manière suivante : - (void)setIntValue:(int)aValue { [[self cell] setIntValue:aValue]; [self setNeedsDisplay:YES];
// // // //
Fixer la valeur enregistrée par l’instance de NSCell associée. Demander l’actualisation de l’affichage.
}
Cocoa fournit systématiquement des accesseurs pour les propriétés non objets et il est bon que vous preniez cette habitude dans vos classes. Les accesseurs pour les propriétés objets sont encore plus importants, car ils représentent un endroit idéal où centraliser la gestion de la mémoire. Le système Cocoa de gestion de la mémoire par comptage des
Chapitre 10
Accesseur
125
références est simple, puissant et flexible, mais sa bonne compréhension est indispensable à une utilisation correcte des classes Cocoa et à l’implémentation des accesseurs dans vos propres classes. Gérer la mémoire par comptage des références La mémoire utilisée par les objets Cocoa est allouée dynamiquement en fonction des besoins. Dès qu’une zone de mémoire est allouée, l’application doit en conserver la trace et ne pas oublier de la désallouer (libérer) lorsqu’elle n’est plus nécessaire. Si la zone n’est pas désallouée, des fuites de mémoire surviennent et des problèmes de performance d’exécution se présentent. Une fuite de mémoire importante peut conduire au plantage de l’application. Depuis Mac OS X 10.5, Cocoa dispose d’une fonctionnalité intégrée de gestion de la mémoire appelée ramasse-miettes automatique. Grâce au ramasse-miettes, le moteur d’exécution du langage est capable de détecter les zones de mémoire allouées qui ne sont plus utilisées et de les libérer automatiquement. Le ramasse-miettes permet d’éviter de nombreuses erreurs d’allocation de la mémoire, mais il a un coût. Historiquement, il avait un impact négatif sur les performances et, dans certains cas, limitait le type de logiciel qu’il était possible de développer. Par exemple, le ramasse-miettes est difficile à employer avec les messages distribués. Cocoa dispose d’un ramasse-miettes multithread à hautes performances, mais il est facultatif. Il n’est pas disponible pour l’iPhone version 3.0. Si vous utilisez du code qui ne fonctionne pas avec le ramassemiettes, peut-être parce qu’il est antérieur à son arrivée, vous devez prendre en charge l’ancien système de gestion de la mémoire par comptage des références. La classe de base NSObject offre un ensemble de méthodes qui permettent d’incrémenter et de décrémenter un compteur, que l’on retrouve dans chaque objet. Ce compteur conserve le nombre d’objets qui font référence à un objet. Le Chapitre 19 décrit une implémentation partielle du système de comptage des références. Lors de son allocation, un objet Cocoa possède un compteur de références implicitement égal à un. Si ce compteur atteint zéro, l’objet est désalloué immédiatement. Si un objet doit enregistrer une référence sur un autre objet, le compteur de références de l’objet référencé est augmenté en invoquant la méthode -retain de NSObject. Lorsqu’un objet n’a plus besoin d’une référence sur un autre objet, le compteur de références de l’objet référencé est diminué en invoquant la méthode -release. Chaque objet est initialisé avec un compteur de références égal à un. Par conséquent, il ne sera pas désalloué tant qu’il n’aura pas été libéré autant de fois qu’il a été gardé, plus une libération supplémentaire qui correspond à l’allocation initiale. Si les objets respectent les conventions d’appel de -retain et de -release, aucun objet ne sera désalloué s’il est toujours utilisé et tous les objets seront désalloués dès qu’ils ne seront plus utilisés.
126
Les design patterns de Cocoa
La gestion de la mémoire par comptage des références est moins pratique que le ramasse-miettes automatique car les programmeurs ne doivent pas oublier d’appeler -retain et -release au lieu de se reposer sur le moteur d’exécution du langage pour cette gestion. Par ailleurs, ce système est en réalité un peu plus complexe que nous l’avons décrit, mais il est flexible, rapide, pratique et compatible avec les objets distribués. Pour de plus amples informations concernant la gestion de la mémoire par comptage des références dans Cocoa, consultez la section Core Library > Cocoa > Objective-C Language > Memory Management Programming Guide > Object Ownership and Disposal dans la documentation Xcode. Des conseils et des explications plus détaillées sont également disponibles sur les pages http://www.stepwise.com/Articles/Technical/ MemoryManagement.html, http://www.stepwise.com/Articles/Technical/2001-0311.01.html et http://www.stepwise.com/Articles/Technical/HoldMe.html. Accesseurs qui gèrent les compteurs de références Les exemples d’accesseurs suivants respectent les conventions Cocoa de gestion de la mémoire lorsque les propriétés objets sont fixées et retournées. La solution la plus simple pour retourner une valeur objet depuis un accesseur consiste à la retourner directement. Supposons qu’un objet mémorise une propriété title sous forme d’une variable d’instance de type NSString nommée _myTitle. Cet intitulé est retourné par la méthode -(NSString *)title suivante : - (NSString *)title { return _myTitle; }
INFO Dans cet exemple, une propriété nommée title est enregistrée dans une variable d’instance nommée _myTitle. Le nom de l’accesseur doit correspondre au nom de la propriété telle qu’elle est présentée aux utilisateurs d’une classe. Toutefois, l’implémentation de la méthode peut fournir la valeur de la propriété en adoptant n’importe quelle logique appropriée. Les noms des propriétés et les noms des variables d’instance n’ont pas à être identiques.
L’implémentation de -title est adaptée à la plupart des cas, mais une approche plus sophistiquée pourra être requise si l’application qui utilise l’intitulé est multithread. Dans ce type d’application, il est possible qu’un thread d’exécution modifie _myTitle ou le libère après qu’il a été retourné par la méthode -title, mais avant que le thread qui a appelé -title ait eu l’opportunité de retenir l’objet retourné. Dans ce cas, le compteur de références de _myTitle peut atteindre zéro et l’objet peut être relâché, en laissant le code qui a invoqué -title avec un pointeur sur un objet désalloué (invalide).
Chapitre 10
Accesseur
127
Pour prendre en charge les applications multithreads, une solution consiste à retenir et à relâcher automatiquement l’objet retourné : - (NSString *)title { id result; // Verrouillage. result = [[_myTitle retain] autorelease]; // Déverrouillage. return result; }
Chaque appel à -autorelease planifie un appel à -release, qui se produira après un certain délai. La méthode -autorelease peut toujours être invoquée à la place de -release, mais elle est moins efficace, car une logique et des structures de données supplémentaires sont nécessaires pour la mise en œuvre de la temporisation. L’association de -retain et de -autorelease sur l’objet retourné garantissent que son compteur de références n’atteindra pas zéro avant que le code appelant ait eu l’opportunité de le retenir. Les commentaires sur le verrouillage et le déverrouillage indiquent là où des verrous sont requis pour obtenir une méthode sûre vis-à-vis des threads. Vous devez employer des verrous et connaître les scénarios d’interblocage possibles. Les verrous et les interblocages sont décrits dans le document Threading Programming Guide, disponible dans la rubrique Guides sur le site http://developer.apple.com/mac/library/, ainsi que dans la section Core Library > Cocoa > Performance de la documentation Xcode. INFO En général, un verrou utilisé dans un accesseur get doit également être employé dans l’accesseur set correspondant. Par ailleurs, l’objet du verrou doit être créé avant tout appel aux accesseurs. La programmation multithread est un sujet complexe qui sort du cadre de ce livre.
L’implémentation la plus fréquente d’un accesseur set pour une propriété objet suit le modèle illustré dans la méthode -(void)setTitle:(NSString *)aTitle suivante : - (void)setTitle:(NSString *)aTitle { [aTitle retain]; [_myTitle release]; _myTitle = aTitle; }
128
Les design patterns de Cocoa
L’argument aTitle sera enregistré comme nouvelle valeur de la variable d’instance _myTitle. Le nouvel objet à mémoriser doit être retenu afin qu’il ne soit pas désalloué alors qu’il est toujours utilisé. L’ancien objet enregistré dans la variable d’instance est relâché car il n’est plus utilisé. Si aucun autre objet n’a retenu l’ancienne valeur, elle est désallouée immédiatement suite à sa libération. Enfin, la nouvelle valeur est affectée à la variable instance _myTitle. INFO Les objets Objective-C sont toujours enregistrés et passés par référence. La variable d’instance _myTitle est un pointeur sur un objet. L’argument aTitle est un pointeur sur un autre objet. Lors de l’exécution de l’instruction _myTitle = aTitle;, seul un pointeur est copié. La variable d’instance _myTitle pointe sur le même objet que aTitle.
L’ordre dans lequel la nouvelle valeur d’un objet est retenue et l’ancienne valeur est relâchée est important. Le mutateur peut être invoqué avec un argument nil, un argument objet qui fait référence à un objet autre que celui déjà enregistré ou un argument qui fait référence à l’objet déjà mémorisé. Dans tous ces cas, la propriété existante peut valoir nil. Voici le comportement du mutateur dans chacun de ces cas : n
Si l’argument du mutateur est nil, le message de retenue est envoyé à nil. Il est tout à fait possible d’envoyer un message à nil tant qu’aucune valeur de retour n’est attendue. L’objet actuellement mémorisé est libéré. Enfin, nil est enregistré comme nouvelle valeur de la propriété objet.
n
Si l’argument fait référence à un objet qui n’est pas celui déjà enregistré, le nouvel objet est retenu afin qu’il ne soit pas désalloué. L’ancien objet est libéré et sera désalloué si aucun autre objet ne le retient. Enfin, le pointeur sur le nouvel objet devient la valeur de la propriété modifiée.
n
Si l’argument fait référence à l’objet déjà enregistré, le compteur de références de cet objet est au moins égal à un car l’objet est utilisé. Le mutateur doit commencer par retenir l’objet afin que son compteur de références ne soit pas inférieur à deux. La référence d’objet déjà enregistrée est libérée, mais, puisqu’elle désigne l’objet qui vient d’être retenu, le compteur de références de l’objet reste supérieur à un. Enfin, une affectation de pointeur, non risquée, fixe la propriété à la valeur qu’elle avait déjà.
Lorsque l’objet passé en argument à un mutateur est identique à celui déjà enregistré, le cas est critique. Si l’ordre de retenue et de libération est modifié, il est possible que la valeur de l’objet soit libérée et immédiatement désallouée avant que l’affectation ait lieu. La propriété de l’objet pointera alors sur un objet désalloué.
Chapitre 10
Accesseur
129
Le modèle de mutateur employé pour la méthode -setTitle: doit être revu pour prendre en charge les problèmes liés au multithread : - (void)setTitle:(NSString *)aTitle { id oldValue; [aTitle retain]; // Verrouillage. oldValue = _myTitle; _myTitle = aTitle; // Déverrouillage. [oldValue release]; }
Le multithread soulève de nombreux problèmes subtils et, si les implémentations des accesseurs présentées ou d’autres variantes doivent être utilisées, elles ne sont pas suffisantes pour garantir un comportement correct dans tous les cas. En général, il ne vaut pas la peine d’essayer de mettre en œuvre des accesseurs sûrs vis-à-vis des threads. Lorsque le même objet doit être utilisé par plusieurs threads, il est souvent préférable d’imposer un verrouillage explicite dans le code qui invoque les accesseurs ou d’utiliser l’une des techniques de communication entre threads décrites dans le document Threading Programming Guide. Confiner la gestion de la mémoire aux accesseurs Si le pattern Accesseur est appliqué de manière cohérente, la gestion de la mémoire allouée aux objets reste principalement confinée aux accesseurs. Lors de l’initialisation des instances, utilisez un accesseur set pour fixer la valeur de chaque propriété de type objet. Par exemple, l’implémentation suivante de la méthode -(id)initWithStringValue: (NSString *)aValue utilise un mutateur pour enregistrer la chaîne de caractères au lieu de l’affecter directement à la variable d’instance : - (id)initWithStringValue:(NSString *)aValue { self = [super init]; [self setStringValue:aValue]; // Fixer la valeur initiale de la propriété. return self; }
Le principe d’initialisation des instances est décrit au Chapitre 3. La méthode -dealloc permet indirectement de libérer des objets référencés en utilisant les mutateurs et en évitant le code de gestion de la mémoire :
130
Les design patterns de Cocoa
- (void)dealloc { [self setStringValue:nil];
// Toute valeur précédente est libérée.
[super dealloc]; }
INFO Certains programmeurs évitaient d’utiliser les accesseurs dans les méthodes d’initialisation et dans -dealloc car une sous-classe peut redéfinir des accesseurs hérités pour obtenir un comportement particulier. L’utilisation des accesseurs redéfinis dans la méthode d’initialisation de la super-classe peut conduire à des effets secondaires avant que l’instance de la sousclasse soit totalement initialisée. De même, les accesseurs invoqués depuis la méthode -dealloc de la super-classe peuvent provoquer des effets secondaires dans les instances partiellement désallouées. Toutefois, il n’existe aucune alternative à l’utilisation des accesseurs lorsque vous employez des variables d’instance synthétiques avec le moteur d’exécution d’Objective-C 2.0 ou utilisez des propriétés qui ne sont pas implémentées comme des variables d’instance. Dans de tels cas, les accesseurs constituent la seule manière d’initialiser les propriétés ou de les fixer à nil.
Altérabilité Les accesseurs décrits jusqu’à présent fixent ou retournent directement les valeurs des variables d’instance. Lorsque les valeurs des variables d’instance sont des objets, l’altérabilité, ou mutabilité, des objets doit être prise en compte. L’altérabilité fait référence à la capacité d’un objet à changer d’état au cours de sa vie. Si un objet est inaltérable, il est créé dans un certain état ou avec des propriétés qui n’évolueront pas jusqu’à sa désallocation. L’état ou les propriétés des objets altérables peuvent changer sans limitation. De nombreuses classes Cocoa du framework Foundation existent en versions inaltérable et altérable, comme NSString et NSMutableString, NSArray et NSMutableArray, ainsi que d’autres. Si un pointeur sur une variable d’instance altérable est retourné par un accesseur, l’encapsulation de la classe qui possède la variable d’instance peut être contournée. L’état ou les propriétés de l’objet retourné peuvent être modifiés sans que les autres objets qui contiennent des références sur l’objet modifié en soient informés. Cela ne peut pas se produire lorsque des objets inaltérables sont retournés. Jusqu’à présent, les exemples d’accesseurs pour la variable d’instance _myTitle ont supposé que l’objet mémorisé par _myTitle était immuable. Si _myTitle est enregistrée comme une chaîne de caractères altérable, elle peut être retournée sans problème par un accesseur get qui prétend retourner un objet inaltérable. La définition de classe suivante présente les diverses méthodes accesseurs possibles lorsque des objets mutables sont impliqués :
Chapitre 10
Accesseur
131
@interface MYTitleStorage { NSMutableString *_myTitle; } @end @implementation MYTitleStorage - (id)init { self = [super init]; [self setTitle:@”Default Title”]; return self; } - (NSString *)title { return _myTitle; // Ce retour est sûr car nous indiquons retourner // un type immuable et les autres programmes doivent // respecter cette condition. } - (NSMutableString *)mutableTitle { // Retourner une copie de la variable d’instance. Ainsi, les modifications // apportées à la copie n’affecteront pas la variable instance. return [[_myTitle mutableCopy] autorelease]; } - (void)setTitle:(NSString *)aTitle { NSMutableString *newValue = [aTitle mutableCopy]; [_myTitle release]; _myTitle = newValue; } - (void)dealloc { [self setTitle:nil]; [super dealloc]; } @end
Les accesseurs qui manipulent des propriétés objets altérables emploient souvent la méthode -(id)mutableCopy, déclarée par le protocole NSMutableCopying, pour copier l’objet passé en argument ou retourné, de manière qu’aucune référence à la variable d’instance mutable ne sorte de sa classe. Lorsque le message -mutableCopy est reçu par un objet, une nouvelle copie du récepteur est retournée. Cette copie possède un comp-
132
Les design patterns de Cocoa
teur de références égal à un et doit être relâchée ou relâchée automatiquement. Le pattern Copie est expliqué au Chapitre 12 et la méthode -mutableCopy est documentée dans le framework Foundation et à la section Core Library > Cocoa > Objective-C Language > Foundation Famework Reference > NSMutableCopying de la documentation Xcode. NSKeyValueCoding Le protocole informel NSKeyValueCoding est défini par le framework Foundation et détaillé à la section Core Library > Cocoa > Objective-C Language > Foundation Framework Reference > NSKeyValueCoding de la documentation Xcode. Les protocoles informels sont présentés au Chapitre 6. En résumé, un protocole informel est un groupe de méthodes que l’on peut supposer exister, même sur un objet anonyme. NSKeyValueCoding définit un mécanisme qui permet d’accéder aux propriétés d’un objet par leur nom. Les principales méthodes sont -setValue:(id)aValue forKey: (NSString *)aKey et -(id)valueForKey:(NSString *)aKey, qui, respectivement, fixent et renvoient les valeurs des propriétés nommées. Dans tous les cas, l’argument aKey: est une référence à une instance de NSString, qui précise le nom de la propriété à laquelle accéder. L’argument aValue: de la méthode -setValue:forKey: est une référence à un objet, et -valueForKey: retourne une référence à un objet. Les propriétés non objets sont fixées et retournées en les enveloppant dans des instances de la classe Cocoa NSValue.
En un sens, le protocole NSKeyValueCoding constitue une alternative à l’utilisation des accesseurs. Toutefois, les accesseurs sont si intéressants que même les méthodes du protocole NSKeyValueCoding les utilisent lorsque c’est possible. Voici, dans l’ordre, les techniques employées par les méthodes de NSKeyValueCoding pour fixer ou retourner les propriétés d’un objet : 1. Vérifier l’existence des méthodes d’accès nommées - ou -get et les utiliser pour retourner une valeur. Vérifier l’existence d’une méthode nommée -set: et l’utiliser pour fixer la valeur. Pour les méthodes -get et -set:, la première lettre de la chaîne Clé est mise en majuscules de manière à respecter les conventions de nommage des méthodes Cocoa. 2. Si une méthode accesseur correspondant directement au nom de la clé n’est pas disponible, vérifier l’existence des méthodes nommées -_, -_get et -_set:. 3. Si aucune méthode accesseur n’est trouvée, tenter d’accéder directement à la variable d’instance. Cette variable peut être nommée ou _.
Chapitre 10
Accesseur
133
4. Enfin, s’il est impossible d’accéder à la propriété au travers des accesseurs ou directement par une variable d’instance, invoquer l’une des méthodes -handleQueryWithUnboundKey: ou -handleTakeValue:forUnboundKey: de NSKeyValueCoding selon le cas. Les implémentations par défaut de ces méthodes lèvent une exception, mais vous pouvez les redéfinir pour changer ce comportement. Le protocole NSKeyValueCoding déclare d’autres méthodes que nous ne décrirons pas. Le point important concernant NSKeyValueCoding quant aux accesseurs est que même ses méthodes utilisent les accesseurs. Si vous implémentez les méthodes d’accès adéquates, vous pouvez être certain qu’elles seront invoquées pour accéder aux propriétés de votre objet. INFO Depuis Mac OS X 10.2, le code Cocoa de chargement d’un fichier .nib n’utilise pas les méthodes de NSKeyValueCoding, mais emploie néanmoins des stratégies semblables pour fixer les propriétés d’un objet. Si vous fournissez des accesseurs, le code de chargement d’un fichier .nib les utilisera. Dans le cas contraire, les propriétés de votre objet seront fixées directement si cela est possible. Les frameworks Cocoa ne lancent aucune exception si les propriétés ne sont pas disponibles ou si elles ne peuvent pas être fixées lors du chargement d’un fichier .nib.
Outlets d’Interface Builder Les outlets sont des variables d’instance qui, en utilisant l’application Interface Builder d’Apple, peuvent être connectées de manière à pointer sur d’autres objets (voir Chapitre 17). Interface Builder enregistre tous les objets interconnectés dans des fichiers .xib en utilisant le pattern Archivage et désarchivage (voir Chapitre 11). Lors du chargement (désarchivage) des objets dans une application Cocoa en cours d’exécution, les outlets sont reconnectés. Si vous fournissez des accesseurs correctement nommés pour les outlets de votre objet, ces méthodes seront invoquées pour fixer la valeur des outlets. Vos accesseurs sont libres d’effectuer tout traitement supplémentaire requis. Il est important que, si vous implémentez des mutateurs, ces méthodes fixent réellement la valeur des outlets concernés, car, dans le cas contraire, les connexions établies dans Interface Builder ne seront pas rétablies lors du chargement du fichier .nib. Si vous ne fournissez pas des mutateurs correctement nommés pour chaque outlet, le code Cocoa de chargement des fichiers .nib fixera les outlets directement à l’aide des fonctions du moteur d’exécution d’Objective-C. Propriétés Objective-C 2.0 Le langage Objective-C 2.0, arrivé avec Mac OS X 10.5, apporte une nouvelle syntaxe pour la déclaration des propriétés objets et réduit la saisie nécessaire à l’utilisation du pattern Accesseur dans vos propres classes. Lorsqu’il rencontre la directive @synthe-
134
Les design patterns de Cocoa
size, le compilateur Objective-C 2.0 génère automatiquement les méthodes d’accès en fonction de la déclaration des propriétés. La nouvelle syntaxe des propriétés est autodescriptive. Elle clarifie le comportement des accesseurs dans la déclaration de la classe.
Pour de plus amples informations concernant la syntaxe de déclaration des propriétés, consultez la section Core Library > Cocoa > Objective-C Language > The Objective-C 2.0 Programming Language > Declared Properties de la documentation Xcode. Pour maîtriser la nouvelle syntaxe, il faut bien comprendre qu’elle est facultative et n’offre aucune nouvelle possibilité significative. Elle permet principalement de réduire la quantité de code à saisir pour implémenter de nouvelles classes. Toutefois, le code généré par le compilateur en réponse à la directive @synthesize ne souffre pas des erreurs humaines et fonctionne automatiquement avec la gestion de la mémoire, le codage clé-valeur, l’observation clé-valeur et Interface Builder, que le ramasse-miettes soit utilisé ou non.
10.3
Exemples dans Cocoa
Les classes Cocoa fournissent des accesseurs pour toutes les propriétés qui peuvent être examinées ou fixées en dehors de la classe dans laquelle elles sont utilisées. En général, il n’existe aucune raison d’accéder directement aux variables d’instance d’une classe Cocoa, excepté peut-être lors de l’écriture d’une sous-classe, et même cette pratique est rare. Par exemple, lors du développement d’une classe dérivée de la classe Cocoa NSView, il est possible d’accéder directement à sa variable d’instance _subviews. Néanmoins, il est presque toujours préférable d’accéder à cette propriété par l’intermédiaire de la méthode -(NSArray *)subviews de NSView. En utilisant l’accesseur, le code de la sous-classe est plus flexible et permet à l’implémentation de la super-classe d’évoluer sans systématiquement remettre en cause celle des sous-classes. Le même principe s’applique à quasiment tous les cas de dérivation d’une classe Cocoa. Une autre propriété de NSView va nous servir à illustrer cette possibilité de conserver la flexibilité dans l’implémentation des classes grâce à l’utilisation des accesseurs à la place des références directes. Chaque instance de NSView enregistre une référence à sa vue supérieure, dans la variable _superview, et une référence à la fenêtre qui contient la vue, dans la variable _window. Puisque chaque instance de NSView se trouve toujours dans la même fenêtre que sa vue supérieure, l’implémentation de NSView pourrait être modifiée de manière qu’une seule variable d’instance contienne une référence à une vue supérieure ou une référence à une fenêtre. Les vues qui possèdent une vue supérieure enregistrent cette référence. Les vues qui ne possèdent pas de vue supérieure enregistrent une référence à la fenêtre qui contient la vue. Ensuite, l’implémentation de la méthode -(id)window de NSView retourne la fenêtre lorsque la vue supérieure n’existe
Chapitre 10
Accesseur
135
pas ou le résultat de [[self superview] window] lorsqu’elle existe. Cet exemple est un peu tiré par les cheveux, car il n’y a aucune raison d’optimiser la mémorisation des instances de NSView en réduisant le nombre de variables d’instance, mais il montre le niveau de souplesse obtenu avec les accesseurs. Cette flexibilité est encore plus importante dans les classes dont vous pouvez modifier la conception ou l’implémentation plusieurs fois au cours de la vie du logiciel. Les accesseurs Cocoa qui retournent par valeur des propriétés non objets sont tellement nombreux qu’il n’y a aucun intérêt à les recenser. Simplement, il ne faut pas oublier que ces propriétés sont presque toujours retournées par valeur, même lorsqu’elles sont d’un type complexe. Par exemple, les méthodes -(NSRect)frame et -(NSRect)bounds de NSView retournent des structures NSRect par valeur. Les accesseurs -(void)setFrame:(NSRect)aRect et -setBounds:(NSRect)aRect prennent en argument une structure NSRect passée par valeur. Dans la même veine, la méthode d’accès -(NSRange) rangeValue de NSValue retourne une structure NSRange par valeur. Les classes Cocoa fournissent rarement des accesseurs qui retournent des propriétés non objets par référence et leur nom commence toujours par get. Ce type d’accesseur est employé uniquement lorsque la taille de la propriété retournée est variable ou lorsqu’une méthode doit retourner plusieurs valeurs. Le Tableau 10.1 recense toutes les méthodes accesseurs Cocoa qui retournent les valeurs non objets par référence, ainsi que leur classe. Tableau 10.1 : Accesseurs qui retournent des valeurs non objets par référence
Classe
Accesseur
NSArray
- (void)getObjects:(id *)objects - (void)getObjects:(id *)objects range:(NSRange)range
NSData
- (void)getBytes:(void *)buffer - (void)getBytes:(void *)buffer length:(unsigned)length - (void)getBytes:(void *)buffer range:(NSRange)range
NSFormatter
- (BOOL)getObjectValue:(id *)obj forString:(NSString *)string errorDescription:(NSString **)error
NSInvocation
- (void)getReturnValue:(void *)retLoc - (void)getArgument:(void *)argumentLocation atIndex:(int)index
NSMethodSignature
- (const char *)getArgumentTypeAtIndex:(unsigned)index
NSPathUtilities
- (BOOL)getFileSystemRepresentation:(char *)cname maxLength:(unsigned)max
136
Les design patterns de Cocoa
Tableau 10.1 : Accesseurs qui retournent des valeurs non objets par référence (suite)
Classe
Accesseur
NSRunLoop
- (CFRunLoopRef)getCFRunLoop
NSString
- (void)getCharacters:(unichar *)buffer - (void)getCharacters:(unichar *)buffer range:(NSRange)aRange - (void)getLineStart:(unsigned *)startPtr end:(unsigned *) lineEndPtr contentsEnd:(unsigned *)contentsEndPtr forRange:(NSRange)range - (void)getCString:(char *)bytes - (void)getCString:(char *)bytes maxLength:(unsigned)maxLength - (void)getCString:(char *)bytes maxLength:(unsigned)maxLength range:(NSRange)aRange remainingRange:(NSRangePointer)leftoverRange
NSBezierPath
- (void)getLineDash:(float *)pattern count:(int *)count phase:(float *)phase
NSValue
- (void)getValue:(void *)value
NSBitmapImageRep
- (void)getBitmapDataPlanes:(unsigned char **)data - (void)getCompression:(NSTIFFCompression *)compression factor:(float *)factor + (void)getTIFFCompressionTypes:(const NSTIFFCompression **) list count:(int *)numTypes
NSButtonCell
- (void)getPeriodicDelay:(float *)delay interval:(float *) interval
NSButton
- (void)getPeriodicDelay:(float *)delay interval:(float *) interval
NSCell
- (void)getPeriodicDelay:(float *)delay interval:(float *) interval
NSColor
- (void)getRed:(float *)red green:(float *)green blue:(float *) blue alpha:(float *)alpha - (void)getHue:(float *)hue saturation:(float *)saturation brightness:(float *)brightness alpha:(float *)alpha - (void)getWhite:(float *)white alpha:(float *)alpha - (void)getCyan:(float *)cyan magenta:(float *)magenta yellow:(float *)yellow black:(float *)black alpha:(float *) alpha
Chapitre 10
Accesseur
137
Tableau 10.1 : Accesseurs qui retournent des valeurs non objets par référence (suite)
Classe
Accesseur
NSLayoutManager
- (unsigned)getGlyphs:(NSGlyph *)glyphArray range:(NSRange)glyphRange - (void)getFirstUnlaidCharacterIndex:(unsigned *)charIndex glyphIndex:(unsigned *)glyphIndex - (unsigned)getGlyphsInRange:(NSRange)glyphsRange glyphs:(NSGlyph *)glyphBuffer characterIndexes:(unsigned *) charIndexBuffer glyphInscriptions:(NSGlyphInscription *) inscribeBuffer elasticBits:(BOOL *)elasticBuffer bidiLevels:(unsigned char *)bidiLevelBuffer - (unsigned)getGlyphsInRange:(NSRange)glyphsRange glyphs:(NSGlyph *)glyphBuffer characterIndexes:(unsigned *) charIndexBuffer glyphInscriptions:(NSGlyphInscription *) inscribeBuffer elasticBits:(BOOL *)elasticBuffer
NSMatrix
- (void)getNumberOfRows:(int *)rowCount columns:(int *) colCount - (BOOL)getRow:(int *)row column:(int *)col ofCell:(NSCell *) aCell - (BOOL)getRow:(int *)row column:(int *)col forPoint:(NSPoint)aPoint
NSOpenGL
- (void)getValues:(long*)vals forAttribute:(NSOpenGLPixelFormatAttribute)attrib forVirtualScreen:(int)screen - (void)getValues:(long *)vals forParameter:(NSOpenGLContextParameter)param
NSWorkspace
- (BOOL)getInfoForFile:(NSString *)fullPath application:(NSString **)appName type:(NSString **)type - (BOOL)getFileSystemInfoForPath:(NSString *)fullPath isRemovable:(BOOL *)removableFlag isWritable:(BOOL *) writableFlag isUnmountable:(BOOL *)unmountableFlag description:(NSString **)description type:(NSString **) fileSystemType
10.4
Conséquences
L’utilisation des accesseurs constitue presque toujours la meilleure solution pour obtenir et fixer les propriétés d’un objet depuis du code qui n’implémente pas l’objet. En général, les accesseurs sont employés exclusivement pour accéder à des variables d’instance, même depuis le code d’implémentation de la classe. Les accesseurs permettent
138
Les design patterns de Cocoa
de diminuer le nombre d’endroits dans le code où la gestion explicite de la mémoire est requise et apportent de nombreux autres avantages. Toutefois, les accesseurs ajoutent au moins le coût d’un appel de méthode supplémentaire, voire plusieurs. Il est plus efficace d’accéder directement aux variables d’instance. Vous devez toujours employer les accesseurs, jusqu’à ce qu’un profilage ou d’autres techniques montrent que des performances plus élevées sont nécessaires. Dans ce cas, vous pouvez remplacer les accesseurs par un accès direct aux variables.
11 Archivage et désarchivage Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
L’archivage permet de conserver des objets, ainsi que toutes les relations et les dépendances qui existent entre les objets archivés. Le désarchivage recrée les objets et les relations qui ont été précédemment archivés. Certains langages de programmation orientés objet, comme Ruby, Python, Java et C#, emploient le terme "sérialisation" pour décrire le pattern Archivage et désarchivage. Dans Cocoa, la mise en œuvre des communications interprocessus se fonde sur l’archivage et le désarchivage de manière à copier les objets entre les processus. Les objets archivés par un processus sont désarchivés dans un autre à l’aide des classes NSPortCoder et NSDistributedNotificationCenter. L’application Interface Builder archive les objets qui sont configurés et connectés à l’aide de cet outil. Les applications Cocoa désarchivent ensuite les objets enregistrés dans des fichiers Interface Builder de manière à restaurer les objets et les connexions configurés. Un processus semblable est mis en place lorsque Interface Builder passe en mode simulation. Les objets à tester sont archivés dans un tampon mémoire, pour être ensuite désarchivés de manière à obtenir une copie opérationnelle des objets prêts pour les tests. Les objets archivés sont généralement stockés sous forme de données binaires. La lecture et l’écriture des données binaires en mémoire ou sur le disque est rapide, tout comme leur transfert au travers du réseau. Toutefois, il est parfois plus utile d’enregistrer les objets sous forme d’un fichier texte lisible par un être humain. Pour cela, Cocoa prend en charge l’archivage et le désarchivage à partir de fichiers XML, avec les quelques limitations décrites dans ce chapitre.
140
11.1
Les design patterns de Cocoa
Motivation
Le pattern Archivage et désarchivage est employé chaque fois qu’un groupe d’objets interdépendants doit être copié ou enregistré. Certaines applications Cocoa adoptent cette solution pour enregistrer leurs données dans des fichiers. Puisque la plupart des objets Cocoa sont directement compatibles avec le pattern Archivage et désarchivage, l’enregistrement des données de l’application consiste simplement à archiver ses objets. Pour recharger les données, il suffit de désarchiver les objets. L’état précédent de l’application est alors restauré. Le pattern Archivage et désarchivage est parfois employé pour mettre en œuvre la copie profonde (voir Chapitre 12).
11.2
Solution
L’archivage et le désarchivage se reposent sur les objets pour coder leur état interne dans une archive et le décoder ensuite à partir d’une archive. Puisqu’ils assurent leur propre codage et décodage, les objets conservent l’encapsulation et le masquage des données. Les objets qui font référence à d’autres objets donnent généralement à ces derniers l’opportunité de procéder à leur codage. Par conséquent, le codage d’un seul objet peut conduire à l’ajout de nombreux objets dans une archive. Par exemple, si un objet en cours de codage possède des variables d’instance objets, elles seront, en général, également codées. Si un objet ne code pas ses variables d’instance, son désarchivage risque de produire un objet incomplet. Toutefois, si une variable d’instance possède une valeur par défaut ou si elle peut être calculée à partir des autres variables d’instance, son codage n’est sans doute pas obligatoire. Chaque objet doit déterminer lui-même les variables d’instance à coder. Le point clé de l’implémentation du pattern Archivage et désarchivage réside dans le traitement des objets interdépendants. Quelle que soit la complexité des relations entre des objets, chaque objet présent dans une archive ne doit être codé qu’une seule fois dans cette archive. Cela réduit l’espace de stockage nécessaire à l’archive, mais, surtout, cela simplifie la restauration des relations lors du désarchivage. Si plusieurs objets archivés font référence au même objet, les copies désarchivées doivent également faire référence à une seule copie de cet objet. De même, dans un groupe d’objets archivés, deux objets ou plus peuvent se faire mutuellement référence. De telles références circulaires sont en partie résolues automatiquement par la règle qui impose une seule représentation de chaque objet dans une même archive. L’implémentation Cocoa de l’archivage et du désarchivage prend en charge les questions de taille des types de données et d’ordre des octets inhérentes à l’échange des données entre plateformes. Ainsi, les archives créées sur un ordinateur 32 bits peuvent être désarchivées sur une machine 64 bits, et vice versa. Même si les objets sont archivés sur
Chapitre 11
Archivage et désarchivage
141
un ordinateur à base de processeur PowerPC qui utilise un certain ordre des octets pour les valeurs multi-octets, il est possible de les désarchiver sur une machine Intel qui utilise un ordre des octets inverse. Les conversions automatiques d’ordre des octets sont également indispensables aux communications interprocessus entre des ordinateurs dissemblables sur un réseau. La gestion intégrée des versions d’objets permet de désarchiver des objets qui ont été archivés sur des versions différentes d’une application ou du système d’exploitation. Cocoa prend en charge la substitution d’objets au cours du désarchivage. Par exemple, Interface Builder archive parfois des objets substituables qui ont enregistré des connexions avec d’autres objets dans l’archive. Lorsque ces objets sont désarchivés dans une application en cours d’exécution, un objet propre à l’application remplace l’objet substituable et les connexions effectuées dans Interface Builder sont rétablies avec l’objet de l’application. La substitution d’objet est combinée à la gestion des versions pour mettre à jour automatiquement les objets lors de leur désarchivage. Voyons ce qui se produit lorsqu’une classe est obsolète ou remplacée entre les versions 1.0 et 2.0 d’une application. Lorsque l’application 2.0 désarchive une instance de cette ancienne classe, elle peut la remplacer par une instance de la nouvelle classe et effectuer, au cours de cette substitution, toutes les conversions de données nécessaires. Codage conditionnel Le codage conditionnel limite le nombre d’objets archivés lorsque de nombreux objets interdépendants existent, sans que toutes les relations aient à être conservées. Les objets peuvent être codés de manière conditionnelle ou inconditionnelle. Lorsqu’un objet est codé sans condition, il est toujours ajouté à l’archive, s’il ne s’y trouve pas déjà. Lors du désarchivage, tous les objets qui font référence à l’objet codé sans condition voient leur référence restaurée. Le codage conditionnel d’un objet implique que les références à l’objet ne sont restaurées que s’il est placé dans l’archive. Pour rejoindre l’archive, un objet doit avoir été codé inconditionnellement au moins une fois. Si un objet codé conditionnellement est placé dans une archive, alors, les références à cet objet sont restaurées de manière normale lors du désarchivage. En revanche, si un objet n’est pas ajouté à l’archive, tous les objets qui lui faisaient référence voient ces références fixées à nil lors du désarchivage. La Figure 11.1 illustre ce qui se passe lorsqu’un objet est codé de manière exclusivement conditionnelle : l’objet C qui a été codé conditionnellement n’existe plus après le désarchivage des objets. Si un objet est codé inconditionnellement par au moins un objet lui faisant référence, alors, suite à son décodage, toutes les références sont restaurées, qu’elles soient chacune conditionnelles ou inconditionnelles. La Figure 11.2 illustre ce qui arrive aux références d’objets codés conditionnellement lorsque l’objet référencé est également codé inconditionnellement par un autre objet.
142
Les design patterns de Cocoa
Relations entre les objets lors du codage
Relations entre les objets après le décodage
A
A
l ne
n
itio
d on
Inconditionnel
C
C Co
nd
itio
nn
el
B
B
Figure 11.1 Les objets codés de manière exclusivement conditionnelle n’existent plus après le désarchivage.
Relations entre les objets lors du codage
Relations entre les objets après le décodage
A
l
ne
A
n itio
nd
C
Inconditionnel
Co
Inc
on
dit
ion
ne
C
l
B
B
Figure 11.2 Les références conditionnelles sont conservées dès qu’un seul objet code inconditionnellement l’objet référencé.
Chapitre 11
Archivage et désarchivage
143
Le codage conditionnel est utilisé lorsque seul un sous-ensemble des objets d’un groupe doit être archivé. Par exemple, chaque objet NSView possède une seule référence à son "parent" (vue supérieure) et des références à toutes ses vues "enfants" (vues inférieures). Les relations entre les instances de NSView sont détaillées au Chapitre 16. D’un point de vue conceptuel, chaque vue "détient" ses vues inférieures et les considère comme une partie intrinsèque d’elle-même. Par conséquent, lors du décodage d’une instance de NSView, toutes ses vues inférieures sont inconditionnellement décodées. À l’opposé, une vue possède peu d’informations sur sa vue supérieure et fonctionne correctement quelle que soit la vue qui la détient. C’est pourquoi NSView code de manière conditionnelle sa vue supérieure. Les concepts de possession d’un objet sont détaillés dans la section Core Library > Cocoa > Objective-C Language > Memory Management Programming Guide for Cocoa de la documentation Xcode, ainsi que sur le site http:// developer.apple.com. Puisque NSView code de manière conditionnelle sa vue supérieure, vous pouvez archiver une partie de la hiérarchie des vues sans être obligé d’en archiver l’intégralité. Si vous décidez d’archiver une vue qui se trouve au milieu d’une hiérarchie, elle codera l’ensemble de ses vues inférieures, mais pas nécessairement sa vue supérieure.
11.3
Exemples dans Cocoa
La classe NSKeyedArchiver représente la meilleure manière de créer une archive d’objets interdépendants : [NSKeyedArchiver archivedDataWithRootObject:someObject];
La méthode +(NSData *)archivedDataWithRootObject:(id)rootObject envoie un message à l’objet racine indiqué pour lui demander de se coder inconditionnellement. Cet objet demande ensuite aux objets auxquels il fait référence de se coder de manière conditionnelle ou inconditionnelle. La ligne de code qui demande l’archivage de l’objet racine suffit à archiver l’intégralité d’une hiérarchie d’objets. L’objet racine peut être n’importe quel objet qui se conforme au protocole NSCoding, y compris une instance de NSArray ou de NSDictionary. L’instance de NSData retournée par +archivedDataWithRootObject: peut être enregistrée dans un fichier, sous forme d’un attribut Core Data ou comme valeur par défaut dans les préférences de l’utilisateur. Le désarchivage d’un objet à l’aide de la méthode +(id)unarchiveObjectWithData: (NSData *)data de NSKeyedUnarchiver se fait de la manière suivante : [NSKeyedUnarchiver unarchiveObjectWithData:someData];
NSKeyedUnarchiver demande à l’objet racine précédemment codé de se décoder luimême. Au cours de la procédure de décodage, l’objet racine demande aux objets référencés de se décoder. Une fois la procédure terminée, l’intégralité de la hiérarchie des objets codés est décodée et les relations entre les objets sont rétablies.
144
Les design patterns de Cocoa
Pour étudier l’utilisation pratique du pattern Archivage et désarchivage, considérons une application Cocoa qui enregistre une couleur dans les valeurs par défaut de l’utilisateur. Ces valeurs constituent un mécanisme standard pour enregistrer des préférences système, utilisateur et applicatives ou des valeurs par défaut. L’accès aux valeurs par défaut de l’utilisateur se fait au travers de la classe NSUserDefaults, qui se fonde sur le pattern Mémoire associative (voir Chapitre 19) pour enregistrer des couples clé-valeur. Le code suivant enregistre une chaîne de caractères dans les valeurs par défaut de l’utilisateur : [[NSUserDefaults standardUserDefaults] setObject:@"http://www.stepwise.com" forKey:@"homePage"];
Toutefois, NSUserDefaults est capable d’enregistrer uniquement les objets qui sont des instances des classes suivantes : NSData, NSString, NSNumber, NSDate, NSArray et NSDictionary. Cocoa encapsule les couleurs dans la classe NSColor, qui ne fait pas partie de la liste des classes directement reconnues par NSUserDefaults. Si vous voulez enregistrer une couleur dans les valeurs par défaut de l’utilisateur, il faut associer le pattern Catégorie (voir Chapitre 6) au pattern Archivage et désarchivage de manière à étendre NSUserDefaults. Les méthodes de catégorie suivantes profitent du fait que les objets NSColor sont déjà conformes au protocole NSCoding pour enregistrer n’importe quel objet NSColor dans les valeurs par défaut de l’utilisateur et le recharger : @implementation NSUserDefaults (ColorHandling) - (void)setColor:(NSColor *)theColor forKey:(NSString *)key { NSData *data = [NSKeyedArchiver archivedDataWithRootObject:theColor]; [self setObject:data forKey:key]; } - (NSColor *)colorForKey:(NSString *)key { NSData *data = [self dataForKey:key]; return [NSKeyedUnarchiver unarchiveObjectWithData:data]; } @end
En utilisant une technique équivalente, n’importe quel objet conforme au protocole NSCoding peut être archivé et enregistré dans les valeurs par défaut de l’utilisateur sous forme d’un NSData. Implémenter le protocole NSCoding Le protocole NSCoding définit uniquement deux méthodes, -encodeWithCoder: et -initWithCoder:. Les objets se placent eux-mêmes dans une archive en implémentant la méthode -(void)encodeWithCoder:(NSCoder *)coder. Ils procèdent à leur décodage en implémentant la méthode -(id)initWithCoder:(NSCoder *)coder. La classe
Chapitre 11
Archivage et désarchivage
145
de base Cocoa NSObject n’est pas conforme au protocole NSCoding, mais la plupart des autres classes Cocoa le sont. Si votre classe dérive d’une classe Cocoa qui se conforme déjà au protocole NSCoding, vous devez redéfinir les méthodes de NSCoding pour qu’elles invoquent les implémentations héritées, puis codent ou décodent les informations propres à la sous-classe. INFO Les méthodes -encodeWithCoder: et -initWithCoder: sont de parfaits exemples du pattern Patron de méthode décrit au Chapitre 4. Ces méthodes sont redéfinies dans chaque sous-classe créée et sont invoquées automatiquement par Cocoa au moment approprié. Vous ne devez pas les appeler directement, excepté pour invoquer leur implémentation dans la super-classe à partir de leur version redéfinie.
Pour ajouter la prise en charge du codage et du décodage à une classe qui n’hérite pas de la conformité avec NSCoding, il faut lui faire adopter ce protocole en implémentant les méthodes -encodeWithCoder: et -initWithCoder:. Le code suivant est extrait de la classe WordInformation de l’exemple WordPuzzle, disponible dans l’archive des codes sources de cet ouvrage. @interface WordInformation : NSObject { NSString *word; NSString *clue; NSMutableDictionary *puzzleSpecificAttributes; } @end // Clés de codage. static NSString *CodingKeyWord = @"word"; static NSString *CodingKeyClue = @"clue"; static NSString *CodingKeyPuzzleSpecificAttributes = @"puzzleSpecificAttributes"; - (id)initWithCoder:(NSCoder *)coder { if (nil != (self = [super init])) { [self setWord:[coder decodeObjectForKey:CodingKeyWord]]; [self setClue:[coder decodeObjectForKey:CodingKeyClue]]; [self setPuzzleSpecificAttributes:[coder decodeObjectForKey: CodingKeyPuzzleSpecificAttributes]]; } return self; }
146
Les design patterns de Cocoa
- (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:[self word] forKey:CodingKeyWord]; [coder encodeObject:[self clue] forKey:CodingKeyClue]; [coder encodeObject:[self puzzleSpecificAttributes] forKey: CodingKeyPuzzleSpecificAttributes]; }
WordInformation dérive directement de NSObject. La plupart des méthodes de cette classe ont été omises pour que l’exemple reste court, mais l’implémentation du protocole NSCoding est présentée. La méthode -encodeWithCoder: code inconditionnellement toutes les variables d’instance du récepteur en utilisant le pattern Accesseur (voir Chapitre 10). La méthode -initWithCoder: décode les objets codés et fixe la valeur des variables d’instance à l’aide des accesseurs. En utilisant les accesseurs, la gestion de la mémoire pour les objets décodés est plus simple, que le ramasse-miettes soit activé ou non.
À l’instar des autres initialiseurs (voir Chapitre 3), la méthode -initWithCoder: affecte à self la valeur retournée par l’initialiseur désigné de la super-classe. Cette affectation de self est indispensable car l’initialiseur hérité peut retourner un objet différent de celui qui a reçu le message. En tant que sous-classe directe de NSObject, WordInformation n’appelle pas directement les méthodes héritées de NSCoding car NSObject n’est pas conforme à ce protocole. Si votre classe hérite de la conformité au protocole NSCoding, vous devez non seulement implémenter les méthodes de NSCoding, mais également invoquer les versions héritées, comme le montre le code suivant. L’affectation de self reste obligatoire, mais au lieu d’appeler l’initialiseur désigné de la classe vous devez invoquer la version héritée de -initWithCoder:. Le code suivant est extrait de la classe WordMatchPuzzleView de l’application WordPuzzle : // Clés de codage. static NSString *CodingKeyDataSource = @"dataSource"; static NSString *CodingKeyPrototypeWordView = @"prototypeWordView"; static NSString *CodingKeyPrototypeClueView = @"prototypeClueView"; static NSString *CodingKeyWordConnectionPoints = @"wordConnectionPoints"; static NSString *CodingKeyClueConnectionPoints = @"clueConnectionPoints"; static NSString *CodingKeyConnectionLines = @"connectionLines"; // Méthodes de codage. - (id)initWithCoder:(NSCoder *)coder { if (nil != (self = [super initWithCoder:coder])) { [self setDataSource:[coder decodeObjectForKey: CodingKeyDataSource]];
Chapitre 11
Archivage et désarchivage
147
[self setPrototypeWordView:[coder decodeObjectForKey: CodingKeyPrototypeWordView]]; [self setPrototypeClueView:[coder decodeObjectForKey: CodingKeyPrototypeClueView]]; [self setWordConnectionPoints:[coder decodeObjectForKey: CodingKeyWordConnectionPoints]]; [self setClueConnectionPoints:[coder decodeObjectForKey: CodingKeyClueConnectionPoints]]; [self setConnectionLines:[coder decodeObjectForKey: CodingKeyConnectionLines]]; } return self; } - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; [coder encodeConditionalObject:[self dataSource] forKey: CodingKeyDataSource]; [coder encodeObject:[self prototypeWordView] forKey: CodingKeyPrototypeWordView]; [coder encodeObject:[self prototypeClueView] forKey: CodingKeyPrototypeClueView]; [coder encodeObject:[self wordConnectionPoints] forKey: CodingKeyWordConnectionPoints]; [coder encodeObject:[self clueConnectionPoints] forKey: CodingKeyClueConnectionPoints]; [coder encodeObject:[self connectionLines] forKey: CodingKeyConnectionLines]; }
WordMatchPuzzleView code conditionnellement sa variable d’instance dataSource. Par conséquent, si un WordMatchPuzzleView est codé par lui-même, la variable dataSource n’est pas placée dans l’archive. Dans l’application WordPuzzle, la source de données sert à initialiser automatiquement une grille vierge avec des mots et des définitions. Si vous archivez une instance de WordMatchPuzzleView déjà initialisée, il est inutile de conserver la source de données qui a servi à l’initialisation. En revanche, si vous archivez un groupe d’objets plus vaste, y compris la source de données, et une ou plusieurs instances de WordMatchPuzzleView, la variable dataSource arrivera dans l’archive et sera disponible pour générer de nouvelles grilles après le désarchivage du groupe d’objets.
Coder et décoder des types non objets NSKeyedArchiver et NSKeyedUnarchiver fournissent les méthodes qui permettent de coder et de décoder les valeurs non objets, y compris les réels et les entiers 32 ou 64 bits. Le type de données BOOL et certaines structures C très utilisées, comme NSPoint, NSSize et NSRect, sont également pris en charge.
148
Les design patterns de Cocoa
Le code suivant provient de la classe WordConnectionPoint de l’application WordPuzzle. Dans cet exemple, certaines valeurs non objets sont codées et décodées : // Clés de codage. static NSString *CodingKeyFrame = @"frame"; static NSString *CodingKeyColor = @"color"; static NSString *CodingKeyIsFilled = @"isFilled"; static NSString *CodingKeyLineWidth = @"lineWidth"; static NSString *CodingKeyAssociatedWordInformation = @"associatedWordInformation"; // Méthodes de codage. - (id)initWithCoder:(NSCoder *)coder { if(nil != (self = [super init])) { [self setFrame:[coder decodeRectForKey:CodingKeyFrame]]; [self setColor:[coder decodeObjectForKey:CodingKeyColor]]; [self setIsFilled:[coder decodeBoolForKey:CodingKeyIsFilled]]; [self setLineWidth:[coder decodeFloatForKey: CodingKeyLineWidth]]; [self setAssociatedWordInformation:[coder decodeObjectForKey: CodingKeyAssociatedWordInformation]]; } return self; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeRect:[self frame] forKey:CodingKeyFrame]; [coder encodeObject:[self color] forKey:CodingKeyColor]; [coder encodeBool:[self isFilled] forKey:CodingKeyIsFilled]; [coder encodeFloat:[self lineWidth] forKey:CodingKeyLineWidth]; [coder encodeConditionalObject:[self associatedWordInformation] forKey:CodingKeyAssociatedWordInformation]; }
Cocoa ne prend pas en charge les autres types de données, comme les structures C quelconques, les unions, les champs de bits, les pointeurs non objets ou les tableaux de valeurs autres que des octets. Apple propose de nombreux conseils concernant le codage des types de données non reconnus dans la section Core Library > Cocoa > Data Management > Archives and Serializations Programming Guide for Cocoa > Encoding and Decoding C Data Types de la documentation Xcode. Nous pouvons résumer ces conseils aux deux solutions suivantes : encapsuler les types de données non reconnus dans des objets conformes au protocole NSCoding et coder ces objets, ou décomposer les types de données en éléments reconnus, comme les entiers et les réels, et coder ces éléments individuels. Si vous enveloppez des valeurs multi-octets dans des objets NSData, vous devez tenir compte des problèmes d’ordre des octets (endian), car les données peuvent être décodées sur une plateforme différente. Par ailleurs, dans tous les cas, excepté les plus sim-
Chapitre 11
Archivage et désarchivage
149
ples, l’utilisation d’objets NSData pour envelopper des structures C est irréalisable. La représentation mémoire des structures C n’est définie par aucun standard et les compilateurs les enregistrent donc différemment. En particulier, ils sont libres d’ajouter des octets de remplissage entre les membres d’une structure ou les champs de bits. Par conséquent, la définition d’une structure peut donner une taille différente selon le compilateur. Dans la même veine, les membres codés d’une structure qui sont des pointeurs seront inutilisables lorsqu’ils seront décodés dans une application qui s’exécute dans un espace d’adressage différent. INFO Par défaut, les objets décodés sont alloués dans la zone mémoire par défaut. Si vous devez décoder des objets en utilisant une zone mémoire différente, indiquez-la avec la méthode -(void)setObjectZone:(NSZone *)zone de NSKeyedUnarchiver, avant le décodage de tout objet. Vous pouvez déterminer la zone utilisée par NSKeyedArchiver en invoquant la méthode -(NSZone *)objectZone. Dans pratiquement tous les cas, les objets décodés doivent être alloués dans la zone de l’objet qui les décode. La méthode -(NSZone *)zone de NSObject permet de connaître la zone de l’objet qui demande le décodage.
Substitution d’objet Lors de son codage, un objet peut se remplacer par une classe ou une instance de substitution. Au cours du codage, NSKeyedArchiver appelle la méthode -(Class)classForKeyedArchiver de l’objet. Vous pouvez la redéfinir de manière à retourner une classe différente de celle de l’objet codé. Ensuite, NSKeyedArchiver invoque la méthode -(id)replacementObjectForKeyedArchiver:(NSKeyedArchiver *)archiver de l’objet en cours de codage. En la redéfinissant, vous pouvez remplacer l’instance codée par une autre instance. Enfin, NSKeyedArchiver appelle +(NSArray *)classFallbacksForKeyedArchiver: et code le tableau retourné, s’il existe, avec l’objet codé. Si la classe réelle d’un objet n’existe pas au moment où celui-ci est décodé, NSKeyedUnarchiver utilise la première classe du tableau qui existe pour décoder l’objet. En redéfinissant +classFallbacksForKeyedArchiver:, vous pouvez fournir des informations de compatibilité afin que les objets puissent être décodés dans chaque version d’une application qui utilise des classes différentes. NSKeyedUnarchiver permet de déléguer à un autre objet le contrôle de la substitution lors du décodage. Les délégués sont détaillés au Chapitre 15. Lorsque NSKeyedUnarchiver est incapable de décoder un objet, il tente d’envoyer le message -(Class)unarchiver:(NSKeyedUnarchiver *)unarchiver cannotDecodeObjectOfClassName: (NSString *)name originalClasses:(NSArray *)classNames au délégué. Celui-ci peut implémenter cette méthode pour retourner la classe qui permet de poursuivre le décodage.
150
Les design patterns de Cocoa
Enfin, après le décodage d’un objet, le message -awakeAfterUsingCoder: lui est envoyé. Vous pouvez redéfinir cette méthode de manière à calculer les valeurs des variables d’instance qui n’ont pas pu être décodées ou pour retourner un objet différent de celui qui vient d’être décodé. Décodage à partir d’un fichier .nib Le décodage des objets codés dans un fichier .nib d’Interface Builder peut présenter un problème. Au cours de cette procédure, l’objet peut avoir besoin d’envoyer des messages à un objet référencé mais qui n’est pas encore décodé. Dans ce cas, comment un objet fait-il pour savoir au cours de son décodage s’il peut accéder aux objets avec lesquels il a des relations ? La réponse tient dans la méthode -awakeFromNib. Lorsque des objets sont décodés à partir d’un fichier .nib, Application Kit envoie automatiquement le message -awakeFromNib à chaque objet décodé qui est en mesure d’y répondre. Ce message est envoyé uniquement après que tous les objets de l’archive ont été décodés et initialisés. Par conséquent, lorsqu’un objet reçoit ce message, il est certain que tous ses outlets sont fixés. Le message -awakeFromNib est également envoyé aux objets lorsque Interface Builder entre en mode simulation, car il code alors les objets dans une archive .nib en mémoire et les décode immédiatement. Lorsque des fichiers .nib sont désarchivés par une application Cocoa, elle désigne un objet hors du fichier qui sera "propriétaire" des objets décodés. Cet objet externe est représenté par l’icône FILE’S OWNER dans Interface Builder. Toutes les connexions ou références faites à FILE’S OWNER depuis le fichier .nib sont reconstituées via une substitution d’objet de manière à référencer le propriétaire fourni par l’application. L’objet désigné comme le propriétaire reçoit également un message -awakeFromNib chaque fois qu’un fichier .nib est désarchivé avec cet objet en tant que propriétaire. Vous pouvez utiliser l’implémentation de -awakeFromNib dans l’objet propriétaire pour terminer l’initialisation nécessaire après le chargement d’un fichier .nib. N’oubliez pas que la méthode -awakeFromNib du propriétaire sera invoquée chaque fois que ce propriétaire est utilisé pour le chargement d’un fichier .nib.
11.4
Conséquences
L’archivage et le désarchivage constituent un mécanisme standard pour conserver ou copier des objets interdépendants. Ce pattern sous-tend l’implémentation des messages distribués dans Cocoa. Les objets qui sont passés entre les applications par envoi de messages sont parfois archivés et désarchivés de manière à créer des copies dans l’application réceptrice. Il est également possible d’utiliser les mandataires (voir Chapitre 27) à la place de la copie.
Chapitre 11
Archivage et désarchivage
151
La plupart des classes Cocoa sont conformes au protocole NSCoding et peuvent donc être utilisées avec l’archivage et le désarchivage. Lors de la création de classes personnalisées, un travail supplémentaire est nécessaire pour implémenter les méthodes de NSCoding. Le programmeur doit également étudier les questions de compatibilité entre les applications et les archives, afin que les anciennes versions de l’application puissent utiliser des archives plus récentes, et vice versa. Dans certains langages et frameworks, vous trouverez une prise en charge de ce pattern plus automatique que dans le langage Objective-C et Cocoa. Toutefois, l’approche retenue par Cocoa permet un meilleur contrôle de l’archivage et du désarchivage par le programme, avec une implémentation extensible et flexible. Elle offre de nombreux points de raccordement, comme -awakeAfterUsingCoder: pour la substitution d’objet, et, si nécessaire, vous pouvez employer vos propres formats d’archivage en écrivant des sous-classes de NSCoder. Il existe deux autres techniques pour la persistance des objets en Cocoa. Vous pouvez utiliser des listes de propriétés (plists) si tous les objets à enregistrer sont convertibles en de telles listes. Pour de plus amples informations concernant les listes de propriétés, consultez la section Core Library > Cocoa > Data Management > Property List Programming Guide de la documentation Xcode. Les valeurs par défaut des utilisateurs mentionnées dans ce chapitre sont enregistrées sous forme de listes de propriétés. Le framework Core Data de Cocoa enregistre les objets et leurs relations en utilisant une technique issue des bases de données relationnelles. Ce framework et ses patterns sont détaillés au Chapitre 30. Core Data peut servir à enregistrer les données d’une application dans des archives, mais, à l’instar des listes de propriétés, il ne reconnaît qu’un ensemble réduit de types de données. Toutefois, puisque Core Data prend en charge l’enregistrement des objets NSData, il est possible de mélanger les approches et d’enregistrer des objets archivés à l’aide de Core Data.
12 Copie Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Copie sert à créer de nouvelles instances d’objets en copiant des instances existantes. La copie d’un objet n’est pas toujours aussi évidente qu’on pourrait le penser. Par exemple, si l’objet copié contient d’autres objets, la copie doit-elle garder les objets de l’original ou doit-elle contenir des copies de ces objets ? La copie est une alternative à la création en deux étapes (voir Chapitre 3) et permet au pattern Prototype (voir Chapitre 21) d’exister. Elle joue également un rôle dans les accesseurs (voir Chapitre 10). À un niveau plus élevé, les opérations de l’interface utilisateur, comme le copier-coller et le glisser-déposer, sont fréquemment implémentées par copie des objets de l’application.
12.1
Motivation
Le pattern Copie est utilisé pour la création d’un nouvel objet qui est une copie d’un autre objet. La copie capture l’état d’un objet à un moment donné. Le langage C utilise le passage par valeur pour transmettre aux fonctions leurs arguments. Autrement dit, ces arguments sont implicitement copiés afin que les modifications qui leur sont apportées dans la fonction affectent uniquement les copies, non les originaux. Si vous êtes un programmeur C ou Objective-C expérimenté, vous pouvez aller directement à la prochaine section de ce chapitre. La suite de cette section explique brièvement le passage par valeur et pourquoi il peut être nécessaire de copier explicitement les objets ObjectiveC lorsqu’ils sont passés en arguments à des fonctions ou à des méthodes.
154
Les design patterns de Cocoa
Le programme en ligne de commande suivant illustre le passage par valeur avec les types C : #import static void SimplePassByValue(float floatArgument) { floatArgument = floatArgument * 3.0f; NSLog(@"Dans SimplePassByValue, floatArgument = %f", floatArgument); } int main (int argc, const char * argv[]) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; float floatArgument = 1.0f; NSLog(@"Avant SimplePassByValue, floatArgument = %f", floatArgument); SimplePassByValue(floatArgument); NSLog(@"Après SimplePassByValue, floatArgument = %f", floatArgument); [pool release]; return 0; }
Il produit la sortie suivante, qui montre que, malgré la modification de floatArgument dans la fonction SimplePassByValue(), les changements affectent uniquement la copie implicite de floatArgument qui existe dans SimplePassByValue(), non la variable déclarée dans main(). Avant SimplePassByValue, floatArgument = 1.000000 Dans SimplePassByValue, floatArgument = 3.000000 Après SimplePassByValue, floatArgument = 1.000000
En C, et par conséquent en Objective-C, les arguments sont toujours passés par valeur. Même les structures C, comme NSRect de Cocoa, sont implicitement copiées et passées par valeur. Toutefois, il est possible de passer un pointeur sur une valeur, puis de modifier indirectement cette valeur au travers du pointeur. Le pointeur lui-même est copié implicitement, mais la valeur ciblée par le pointeur n’est pas copiée. L’exemple suivant et la sortie produite montrent le résultat du passage de pointeurs sur des valeurs en arguments. La valeur de la variable floatArgument déclarée dans main() est modifiée par l’appel à PassPointer(). #import static void PassPointer(float *floatPointer) { *floatPointer = *floatPointer * 3.0f; NSLog(@"Dans PassPointer, *floatPointer = %f", *floatPointer); }
Chapitre 12
Copie
155
int main (int argc, const char *argv[]) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; float floatArgument = 1.0f; NSLog(@"Avant PassPointer, floatArgument = %f", floatArgument); PassPointer(&floatArgument); NSLog(@"Après PassPointer, floatArgument = %f", floatArgument); [pool release]; return 0; } -----------Avant PassPointer, floatArgument = 1.000000 Dans PassPointer, *floatPointer = 3.000000 Après PassPointer, floatArgument = 3.000000
Les objets Objective-C sont toujours passés comme des pointeurs. Le compilateur générera une erreur si vous tentez de passer un objet à la place d’un pointeur sur cet objet. Dans l’exemple suivant, l’objet NSMutableString nommé aString, instancié dans la fonction main(), est donc modifié par la fonction PassObjectPointer() : #import static void PassObjectPointer(NSMutableString *aString) { [aString setString:@"Modifié"]; NSLog(@"Dans PassObjectPointer, aString = %@", aString); } int main (int argc, const char *argv[]) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSMutableString *aString = [NSMutableString stringWithString: @"Original"]; NSLog(@"Avant PassObjectPointer, aString = %@", aString); PassObjectPointer(aString); NSLog(@"Après PassObjectPointer, aString = %@", aString); [pool release]; return 0; } -----------Avant PassObjectPointer, aString = Original Dans PassObjectPointer, aString = Changé Après PassObjectPointer, aString = Changé
Les programmeurs Objective-C se familiarisent rapidement avec les effets du passage des objets par pointeurs et supposent qu’une fonction C ou une méthode Objective-C qui accepte en argument un pointeur sur un objet peut modifier cet objet via le pointeur.
156
Les design patterns de Cocoa
Il s’agit du comportement standard et généralement celui que l’on souhaite. Si vous devez passer un pointeur sur l’un de vos objets, tout en étant certain que l’objet ne sera pas modifié, une solution consiste à copier cet objet et à passer un pointeur sur la copie à la place d’un pointeur sur l’original. Quelles que soient les modifications effectuées sur la copie, l’original est préservé. Si vous écrivez une fonction ou une méthode qui mémorise un pointeur sur un objet, vous devrez sans doute créer une copie et conserver un pointeur sur la copie. De cette manière, les changements apportés ultérieurement à l’objet d’origine n’affecteront pas la copie mémorisée.
12.2
Solution
La copie d’un objet n’a pas toujours un sens, notamment lorsque l’objet encapsule une ressource unique ou rare. Par exemple, la classe Cocoa NSHost contient des informations concernant les noms réseau et les adresses Internet uniques d’un ordinateur. La copie d’une instance de NSHost n’a pas beaucoup de sens car elle dupliquerait alors les noms réseau et les adresses Internet, qui sont supposés uniques. La classe NSApplication encapsule une connexion au serveur de fenêtres Quartz de Mac OS X afin que les applications Cocoa puissent dessiner des fenêtres à l’écran. Puisque chaque application ne peut avoir qu’une seule connexion au serveur de fenêtres, la classe NSApplication utilise le pattern Singleton (voir Chapitre 13). La copie d’une instance de NSApplication n’a pas de sens car une seule instance est autorisée dans chaque application. Lorsque la copie est prise en charge, il faut choisir entre une opération en surface et une opération en profondeur. La copie superficielle produit une copie de l’objet lui-même, sans les objets que celui-ci contient. Autrement dit, dans le cas d’une copie superficielle d’un objet, le résultat est un nouvel objet qui contient des pointeurs sur les objets présents dans l’original. En revanche, la copie profonde implique également les objets contenus. Par conséquent, le résultat d’une telle copie est un objet qui contient des pointeurs sur des copies des objets présents dans l’original. En général, les copies profondes vont aussi loin que possible. Les objets contenus dans les objets contenus dans les objets sont également copiés, jusqu’à la profondeur maximale, de manière à copier tous les objets de la hiérarchie globale. Les classes Cocoa compatibles avec la copie mettent toutes en œuvre le pattern Copie dans la version superficielle. Ainsi, la copie d’une instance de NSArray produit une nouvelle instance qui contient des pointeurs sur les objets de l’original. Vous pouvez mettre en œuvre ce pattern dans vos propres classes pour obtenir des copies superficielles ou profondes, selon les besoins de vos applications.
Chapitre 12
Copie
157
Pour obtenir des copies profondes des objets Cocoa, une solution pratique consiste à utiliser le pattern Archivage et désarchivage (voir Chapitre 11). Si l’objet à copier et tous les objets qu’il contient se conforment au protocole NSCoding, le code suivant produit une copie profonde. NSCoding est le protocole Objective-C qui sous-tend l’archivage et le désarchivage. id // // // {
MYDeepCopyObject(id anObject) Cette fonction accepte n’importe quel objet conforme au protocole NSCoding et en retourne une copie profonde. L’objet retourné doit être libéré explicitement, excepté si le ramasse-miettes automatique est activé. return [[NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:anObject]] retain];
}
La mutabilité des objets obtenus par copie doit également être prise en compte. La mutabilité fait référence à la possibilité d’altérer ou de modifier une instance après sa création. Si un objet ne peut pas être modifié après avoir été créé, il est dit inaltérable ou immuable. Par exemple, après qu’une instance de la classe Cocoa NSString a été créée, la chaîne de caractères encapsulée par cette instance ne peut pas être modifiée. La classe NSString n’offre aucune méthode pour modifier directement la chaîne contenue. Toutefois, Cocoa propose une sous-classe de NSString, nommée NSMutableString, qui fournit des méthodes de manipulation du contenu de la chaîne. D’autres classes existent également dans leurs variantes altérables et inaltérables. NSArray et sa sous-classe NSMutableArray, NSDictionary et sa sous-classe NSMutableDictionary, ainsi que NSSet et sa sous-classe NSMutableSet en sont autant d’exemples. Certaines classes Cocoa très utilisées existent uniquement en version inaltérable. C’est notamment le cas de NSColor, NSNumber, NSDate et NSNotification. De nombreuses classes Cocoa, mais pas toutes, utilisent le concept de mutabilité, et les programmeurs qui débutent avec Cocoa se demandent souvent pourquoi les objets immuables existent. La réponse se trouve dans le problème du passage par valeur décrit à la section "Motivation" de ce chapitre. La copie d’un objet évite que les modifications apportées à l’original n’affectent la copie, et vice versa. Cependant, lorsqu’un objet est inaltérable, il ne peut pas être modifié et il est donc inutile d’en effectuer une copie. L’enregistrement d’un pointeur sur un objet immuable, sans en faire une copie, est parfaitement sûr. Les objets immuables peuvent être utilisés comme s’ils étaient des types de données C ordinaires passés par valeur. Lorsqu’un objet Cocoa existe en version altérable et inaltérable, sa copie produit toujours un nouvel objet immuable. Par exemple, si vous envoyez le message -copy à une instance de NSMutableSet, l’objet retourné est une instance de la classe inaltérable NSSet.
158
Les design patterns de Cocoa
Dans certains cas, il existe une méthode -(id)mutableCopy qui retourne une copie mutable. Par exemple, l’envoi du message -mutableCopy à une instance de NSMutableSet ou de NSSet retourne une instance de NSMutableSet. La section suivante détaille la copie altérable et le protocole NSMutableCopying.
12.3
Exemples dans Cocoa
Cocoa définit également le protocole Objective-C NSCopying, auquel se conforment les objets qui peuvent être copiés. NSCopying déclare une seule méthode, -(id)copyWithZone:(NSZone *)zone. Le type NSZone et les zones de mémoire sont présentés au Chapitre 3. La classe de base NSObject déclare la méthode -(id)copy, dont l’implémentation vérifie si le récepteur du message -copy est conforme au protocole NSCopying. Dans l’affirmative, la méthode -copy appelle [self copyWithZone:[self zone]]. En revanche, elle lance une exception Objective-C si le récepteur n’est pas conforme au protocole NSCopying. Par conséquent, si vous créez une classe dérivée de NSObject et si les instances de cette sous-classe peuvent être copiées, elle doit se conformer au protocole NSCopying ou redéfinir la méthode -copy héritée. Implémenter NSCopying La classe WordInformation suivante dérive directement de NSObject. Elle est inaltérable et implémente le protocole NSCopying afin d’effectuer une copie superficielle. Vous la trouverez dans l’application WordPuzzle, disponible dans l’archive des codes sources de cet ouvrage. WordInformation est utilisée au Chapitre 11 et nous l’étendons dans ce chapitre pour implémenter NSCopying : @interface WordInformation : NSObject { NSString *word; NSString *clue; NSMutableDictionary *puzzleSpecificAttributes; } @end @implementation WordInformation // Plusieurs méthodes sont omises pour simplifier l’exemple. // NSCopying - (id)copyWithZone:(NSZone *)aZone { return [self retain]; } @end
Chapitre 12
Copie
159
Dans le cas d’une classe inaltérable, l’implémentation de -copyWithZone: n’a pas besoin d’effectuer une copie. Il lui suffit de retourner un pointeur gardé sur l’objet, car il n’y a aucun risque que l’objet retourné soit modifié. De nombreux accesseurs Cocoa se fondent sur la méthode -copy, et la possibilité d’éviter la copie dans ces accesseurs grâce aux objets immuables constitue une réelle optimisation. La classe WordMutableInformation est une version altérable de WordInformation. Voici sa mise en œuvre de la copie superficielle pour un objet mutable : @interface WordMutableInformation : WordInformation { } @end @implementation WordMutableInformation // Plusieurs méthodes sont omises pour simplifier l’exemple. // NSCopying - (id)copyWithZone:(NSZone *)aZone { // L’initialiseur est invoqué pour la nouvelle instance // de la classe immuable. id result = [[[self class] allocWithZone:aZone] initWithWord:[self word] clue:[self clue]]; [[result puzzleSpecificAttributes] addEntriesFromDictionary: [self puzzleSpecificAttributes]]; return result; } @end
La déclaration @interface de WordMutableInformation n’inclut pas sa conformité au protocole , car elle hérite de la classe WordInformation. L’implémentation de la méthode -copyWithZone: par WordMutableInformation alloue une nouvelle instance de la classe immuable WordInformation à partir de la zone mémoire indiquée. La nouvelle instance est ensuite initialisée et ses propriétés sont fixées de manière à correspondre à l’original. Implémenter la copie profonde Le code suivant étend la classe WordInformation en utilisant les patterns Catégorie et Archivage et désarchivage de manière à mettre en œuvre la copie profonde. Puisqu’il n’existe aucun protocole formel NSDeepCopying, le nom de la méthode -(id)deepCopy est arbitraire :
160
Les design patterns de Cocoa
@interface WordInformation (WordDeepCopyingSupport) - (id)deepCopy; @end @implementation WordInformation (WordDeepCopyingSupport) - (id)deepCopy { return [[NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:self]] retain]; } @end
Cette implémentation de -deepCopy ne précise pas de zone mémoire, car les zones sont rarement utilisées dans les applications Cocoa modernes. La méthode -copyWithZone: stipule une zone uniquement pour assurer une rétrocompatibilité avec le code ancien. La méthode -(id)deepCopy ajoutée à WordInformation est automatiquement héritée par WordMutableInformation. L’implémentation de la copie profonde est la même, que l’objet soit mutable ou non. Il existe toutefois une subtilité : la copie superficielle d’un objet altérable retourne un objet inaltérable, tandis que l’implémentation de -deepCopy pour les objets altérables retourne un objet altérable. La catégorie suivante ajoute la prise en charge de la copie profonde pour tous les objets qui se conforment au protocole NSCoding : @interface NSObject (DeepCopyingSupport) - (id)deepCopy; @end @implementation NSObject (DeepCopyingSupport) - (id)deepCopy { return [[NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:self]] retain]; } @end
La méthode -archivedDataWithRootObject: de NSKeyedArchiver lancera une exception Objective-C si l’objet à archiver, self, n’est pas conforme à NSCoding.
Chapitre 12
Copie
161
Implémenter NSMutableCopying Le protocole NSMutableCopying de Cocoa déclare une seule méthode, -(id)mutableCopyWithZone:(NSZone *)aZone. Tout comme l’implémentation de la méthode -copy par NSObject appelle -copyWithZone:, sa mise en œuvre par défaut de -(id)mutableCopy invoque -mutableCopyWithZone:. Si le message -mutableCopy est envoyé à un objet qui n’est pas conforme à NSMutableCopying ou qui n’implémente pas la méthode -mutableCopy, la mise en œuvre de -mutableCopy par NSObject lance une exception. L’exemple suivant étend la classe WordInformation en lui ajoutant une implémentation de NSMutableCopying : @interface WordInformation : NSObject { NSString *word; NSString *clue; NSMutableDictionary *puzzleSpecificAttributes; } @end @implementation WordInformation // Plusieurs méthodes sont omises pour simplifier l’exemple. // NSCopying - (id)copyWithZone:(NSZone *)aZone { return [self retain]; } // NSMutableCopying - (id)mutableCopyWithZone:(NSZone *)aZone { // L’initialiseur est invoqué pour la nouvelle instance // de la classe immuable. id result = [[[self class] allocWithZone:aZone] initWithWord:[self word] clue:[self clue]]; [[result puzzleSpecificAttributes] addEntriesFromDictionary: [self puzzleSpecificAttributes]]; return result; } @end
Dans la classe WordMutableInformation, il est inutile de redéfinir l’implémentation de -mutableCopyWithZone: fournie par WordInformation, car celle-ci effectue toutes les opérations requises, que le récepteur de -mutableCopyWithZone: soit mutable ou non.
162
Les design patterns de Cocoa
Copie obligatoire Lorsque vous utilisez vos propres classes avec celles de Cocoa, elles doivent, dans certains cas, implémenter NSCopying. Par exemple, la classe Cocoa NSDictionary copie les objets qui servent de clés. Par conséquent, tout objet employé comme une clé dans une instance de NSDictionary doit se conformer au protocole NSCopying. Le pattern Prototype (voir Chapitre 21) se fonde sur la copie. Les objets prototypes sont copiés en fonction des besoins. Par exemple, les instances de la classe NSCell sont copiées pour remplir les lignes et les colonnes d’une instance de NSMatrix. NSCell et NSMatrix sont décrites au Chapitre 21 et dans la section Core Library > Cocoa > User Experience de la documentation Xcode. Copie des propriétés Objective-C 2.0 La syntaxe @property d’Objective-C 2.0 introduite par Mac OS X 10.5 interagit avec le pattern Copie. Cette syntaxe permet de générer automatiquement les méthodes d’accès aux propriétés d’un objet. Les propriétés sont généralement des variables d’instance, mais elles peuvent également être implémentées à l’aide du pattern Mémoire associative (voir Chapitre 19) ou par d’autres techniques. Voici comment déclarer publiquement la classe immuable WordInformation en utilisant la syntaxe d’Objective-C 2.0 : @interface WordInformation : NSObject { NSString *word; NSString *clue; NSMutableDictionary *puzzleSpecificAttributes; } @property (readonly, retain) NSString *word; @property (readonly, retain) NSString *clue; @property (readonly, copy) NSMutableDictionary *puzzleSpecificAttributes; @end
La déclaration @property (readonly, retain) NSString *word; indique aux utilisateurs de la classe et au compilateur Objective-C 2.0 comment sont implémentés les accesseurs pour la propriété word. Les propriétés word et clue sont déclarées en lecture seule (readonly) dans l’interface de la classe afin que les utilisateurs sachent que la méthode accesseur -(NSString *)word existe, sans pouvoir supposer que la méthode -(void)setWord:(NSString *)aString existera. Cela ne concerne pas les utilisateurs de la classe, mais la propriété word est déclarée retenue (retain) car ce point est très important dans l’implémentation de WordInformation. Les propriétés qui sont initialement déclarées en lecture seule sont souvent redéclarées en lecture/écriture (readwrite) dans une extension de la classe (voir Chapitre 6), par exemple de la manière suivante : @interface WordInformation () @property (readwrite, retain) NSString *word;
Chapitre 12
Copie
163
@property (readwrite, retain) NSString *clue; @property (readwrite, copy) NSMutableDictionary *puzzleSpecificAttributes; @end
Ces nouvelles déclarations indiquent au compilateur que les accesseurs -setWord:, -setClue: et -setPuzzleSpecificAttributes: existent, mais uniquement pour être utilisés par l’implémentation de la classe. Autrement dit, l’interface de la classe WordInformation indique à ses utilisateurs qu’elle est extérieurement inaltérable, même si son implémentation interne peut modifier ses propriétés. Le compilateur accepte que les déclarations readonly soient ensuite remplacées par readwrite, mais aucun autre attribut de la propriété ne peut être modifié dans une redéclaration. L’attribut retain des propriétés word et clue indique que les méthodes -setWord: et -setClue: correspondantes retiennent leur argument au lieu de le copier. L’extension de la classe WordInformation change la déclaration de la propriété puzzleSpecificAttributes en @property (readwrite, copy) NSMutableDictionary *puzzleSpecificAttributes;. L’attribut copy précise que l’argument de la méthode -setPuzzleSpecificAttributes: sera copié par l’implémentation au lieu d’être simplement retenu. Par conséquent, l’objet passé à cette méthode doit être conforme au protocole NSCopying. Le compilateur Objective-C 2.0 utilise l’attribut assign des propriétés lorsque retain ou copy n’est pas précisé. Les déclarations @property (readonly, assign) et @property (readonly) produisent un résultat identique. Toutefois, le compilateur génère un avertissement lorsqu’une déclaration utilise par défaut l’attribut assign avec une propriété objet conforme au protocole NSCopying. Pour éviter cet avertissement, vous devez préciser explicitement l’un des attributs assign, retain ou copy. La syntaxe Objective-C 2.0 suivante déclare la classe WordMutableInformation : @interface WordMutableInformation : WordInformation { } @property (readwrite, retain) NSString *word; @property (readwrite, retain) NSString *clue; @property (readwrite, copy) NSMutableDictionary *puzzleSpecificAttributes; @end
Puisque les propriétés sont déclarées readwrite dans l’interface, les utilisateurs de la classe WordMutableInformation savent que ses instances sont altérables. Le compilateur s’assure que les accesseurs permettant de fixer et de retourner les propriétés existent. Les méthodes -(void)setWord:(NSString *)aString et -(void)setClue: (NSString *)aString gardent leurs propriétés respectives. La méthode -(void)setPuzzleSpecificAttributes: (NSMutableDictionary *)aDictionary copie le dictionnaire indiqué en utilisant un code semblable au suivant :
164
Les design patterns de Cocoa
–(void)setPuzzleSpecificAttributes:(NSMutableDictionary *)aDictionary { if(puzzleSpecificAttributes != aDictionary) { [puzzleSpecificAttributes release]; puzzleSpecificAttributes = [aDictionary copy]; } }
Éviter NSCopyObject() Cocoa fournit une fonction C nommée NSCopyObject(), mais vous ne devez pas l’utiliser. Elle réalise une copie bit à bit d’un objet. Si l’objet copié possède des variables d’instance qui pointent sur d’autres objets, les pointeurs sont copiés, mais les compteurs de références de ces objets ne sont pas incrémentés comme l’exigent les conventions Cocoa de gestion de la mémoire par comptage des références. Même si vous utilisez ObjectiveC 2.0 et le ramasse-miettes, NSCopyObject() ne respecte pas les déclarations @property et ne tient pas compte des modificateurs facultatifs __strong et __weak utilisés avec le ramasse-miettes. Pour de plus amples informations concernant ces modificateurs, consultez la section Core Library > Cocoa > Objective-C Language > Garbage Collection Programming Guide > Garbage Collection API dans la documentation Xcode.
12.4
Conséquences
La copie des objets est presque aussi fondamentale que l’allocation et l’initialisation de nouvelles instances. Cocoa se fonde sur des conventions de niveau framework pour allouer et initialiser les instances (voir Chapitre 3). Le pattern Copie se fonde également sur de simples conventions établies par les frameworks Cocoa. Il permet d’utiliser la sémantique du passage par valeur avec les objets et on le retrouve dans la mise en œuvre des patterns Accesseur et Prototype. Néanmoins, l’implémentation correcte des conventions de copie requiert une certaine prudence de la part des concepteurs de classes. La gestion Cocoa de la mémoire par comptage des références doit être prise en compte dans l’implémentation de la copie. En raison de l’interdépendance entre les propriétés Objective-C 2.0 et le protocole NSCopying, la ligne de démarcation entre les fonctionnalités du framework et la prise en charge du langage au niveau compilateur est floue. Avant Objective-C 2.0, il était possible d’utiliser toutes les fonctionnalités du langage Objective-C sans n’utiliser aucun des frameworks Cocoa. La mise en œuvre d’Objective-C 2.0 par Apple dépend du protocole NSCopying et du pattern Copie tel qu’implémenté par Cocoa. Le code source du compilateur Objective-C 2.0 est disponible dans la Gnu Compiler Collection (http:// gcc.gnu.org/). Les futures versions pourraient implémenter la syntaxe des propriétés Objective-C 2.0 sans dépendre des protocoles spécifiques à Cocoa.
Partie III Patterns qui favorisent le découplage Les patterns décrits dans cette partie apportent des fonctionnalités puissantes et vous permettent de les contrôler et de les étendre sans introduire un couplage inutile entre les objets. Le couplage minimal est un principe directeur essentiel de la conception de Cocoa et l’élément dont la contribution à la productivité du programmeur est la plus importante. Ces patterns font partie des plus répandus et des plus réutilisés dans Cocoa. Voici les chapitres de cette partie du livre : 13. Singleton ; 14. Notification ; 15. Délégué ; 16. Hiérarchie ; 17. Outlet, cible et action ; 18. Chaîne de répondeurs ; 19. Mémoire associative ; 20. Invocation ; 21. Prototype ; 22. Poids mouche ; 23. Décorateur.
13 Singleton Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Singleton est utilisé lorsqu’il ne doit exister qu’une seule instance d’une classe et que les autres objets de l’application doivent pouvoir y accéder facilement. En général, la documentation Cocoa emploie l’expression instance partagée là où le pattern Singleton est utilisé. La classe Cocoa NSApplication est un exemple de singleton. Elle gère la connexion entre l’application et le serveur de fenêtres pour les tracés et la réception des événements. Cocoa exploite parfois la création dynamique et les bundles pour remplacer l’instance par défaut de NSApplication par une instance de votre propre classe.
13.1
Motivation
L’idée est d’établir des conventions pour la création et l’accès à une instance dans les cas où une seule instance d’une classe doit être créée. Les singletons représentent souvent des dispositifs physiques, des ressources virtuelles ou d’autres propriétés système qui sont uniques et qui ne peuvent pas ou ne doivent pas être dupliquées. Il est utile d’avoir une relation de cardinalité unaire entre les instances et les dispositifs ou les concepts qu’elles représentent.
168
13.2
Les design patterns de Cocoa
Solution
Une classe qui utilise le pattern Singleton répond à trois objectifs : n
encapsuler une ressource partagée ;
n
proposer une manière standard de créer une instance partagée ;
n
proposer une manière standard d’accéder à l’instance partagée.
Chaque objectif présente ses propres défis et peut être implémenté de différentes façons en Objective-C. Les sections suivantes décrivent les techniques employées pour mettre en œuvre le pattern Singleton dans Cocoa, ainsi que les compromis réalisés. Encapsuler une ressource partagée Lorsque le pattern Singleton est utilisé avec Objective-C, la question qui revient souvent est : "Pourquoi créer une instance au lieu d’utiliser simplement un objet classe ?" En effet, Objective-C propose de véritables objets classes qui peuvent recevoir des messages et être utilisés comme n’importe quel autre objet, d’autant que le moteur d’exécution d’Objective-C s’assure qu’une seule occurrence d’un objet classe est présente en mémoire dans chaque application. La réponse est subtile et est en rapport avec la flexibilité et la facilité de maintenance. Une classe peut parfaitement servir à encapsuler une ressource partagée. Prenons le cas d’un jeu écrit en Cocoa, dans lequel une classe gère l’enregistrement des scores du joueur sur un ordinateur. Elle pourrait proposer l’interface suivante : @interface MYGameHighScoreManager : NSObject { } + (void)registerScore:(NSNumber *)score playerName:(NSString *)name; + (NSEnumerator *)scoreEnumerator;
La classe MYGameHighScoreManager pourrait offrir une interface plus complexe mais, tant que ses méthodes sont des méthodes de classe, comme indiqué par le signe +, elle peut être utilisée sans créer au préalable une instance. Par exemple, lorsqu’une partie est terminée, le score du joueur peut être enregistré en envoyant le message +registerScore:playerName: directement à la classe MYGameHighScoreManager : [MYGameHighScoreManager registerScore:[NSNumber numberWithInt:[self score]] playerName:[self playerName]];
Les problèmes liés à l’utilisation directe de la classe surviennent lorsqu’une sous-classe de MYGameHighScoreManager doit être créée. Supposons que la classe MYGameNetworkHighScoreManager dérivée de MYGameHighScoreManager soit créée. Il faut alors modi-
Chapitre 13
Singleton
169
fier l’ensemble du code qui utilise la classe MYGameHighScoreManager pour remplacer toutes ses occurrences par MYGameNetworkHighScoreManager. Autrement dit, en figeant le nom de la classe partout où elle est utilisée dans le code, il devient plus difficile d’employer ultérieurement une autre classe. Plusieurs techniques permettent d’éviter de figer un nom de classe dans le code. La plus simple consiste à utiliser une variable globale qui contient un pointeur sur la classe employée. Par exemple, le code d’enregistrement des scores peut être modifié de la manière suivante : extern Class GameHighScoreClass; [GameHighScoreClass registerScore:[NSNumber numberWithInt:[self score]] playerName:[self playerName]];
Il faut évidemment commencer par initialiser la variable globale GameHighScoreClass. La ligne suivante peut apparaître dans la procédure d’initialisation de l’application, par exemple dans la méthode -applicationWillFinishLaunching: du délégué de NSApplication : // Configurer le contrôleur par défaut des scores. GameHighScoreClass = [MYGameHighScoreManager class];
Ou bien l’initialisation peut utiliser une sous-classe spécialisée : // Configurer le contrôleur par défaut des scores. GameHighScoreClass = [MYGameNetworkHighScoreManager class];
Grâce à la flexibilité apportée par la variable globale, il n’est plus nécessaire de modifier plusieurs lignes de code uniquement pour changer la classe qui gère les scores. Toutefois, l’utilisation d’une variable globale amène tous les problèmes de maintenance liés aux variables non encapsulées. La programmation orientée objet a cet avantage important d’éviter les recours aux variables globales. L’encapsulation d’une variable globale dans une classe représente une solution idéale. Les classes encapsulent des informations concernant les instances et offrent une interface pour les créer. Pour atteindre les objectifs de flexibilité et de facilité de maintenance, l’approche la plus propre consiste à utiliser une instance de MYGameHighScoreManager pour gérer les scores et à l’utiliser également pour encapsuler l’accès à l’instance. La suite de ce chapitre décrit les techniques qui permettent d’encapsuler la création d’une seule instance partagée et de donner accès à cette instance. La liste de scores, telle qu’encapsulée par la classe MYGameHighScoreManager, est une ressource partagée simple, mais le pattern peut s’appliquer dans n’importe quel cas de partage d’une ressource.
170
Les design patterns de Cocoa
Créer et accéder à une instance partagée Pour disposer d’une instance partagée, il faut modifier l’interface de MYGameHighScoreManager de manière que la gestion passe par des méthodes d’instance. Une méthode de classe doit également être ajoutée pour accéder à l’instance partagée. Son nom est généralement +sharedInstance. Voici la nouvelle interface : @interface MYGameHighScoreManager : NSObject { } + (id)sharedInstance; - (void)registerScore:(NSNumber *)score playerName:(NSString *)name; - (NSEnumerator *)scoreEnumerator;
Une implémentation de base de la méthode +sharedInstance pourrait prendre la forme suivante : + (MYGameHighScoreManager *)sharedInstance { static MYGameHighScoreManager *myInstance = nil; if (!myInstance) { myInstance = [[[self class] alloc] init]; // Ajouter les autres initialisations requises. } return myInstance; }
Dans cette mise en œuvre, une variable statique locale détient un pointeur sur l’instance partagée. Ainsi, tous les accès à cette valeur doivent passer par la méthode +sharedInstance. Elle est initialisée à nil par le compilateur. Lors de la première invocation de la méthode, le code crée et initialise l’objet partagé, qui est retourné à l’appelant. L’appel à +alloc mérite un commentaire. Au lieu d’envoyer ce message directement à MYGameHighScoreManager, nous l’envoyons à [self class]. Normalement, le résultat obtenu est identique. Nous utilisons cette approche car nous voulons profiter du polymorphisme d’Objective-C. En recherchant l’objet classe à l’exécution, l’instance partagée peut être une instance d’une sous-classe particulière. Pour que l’instance partagée soit un objet MYGameNetworkHighScoreManager, un code semblable au suivant peut être exécuté au cours de l’initialisation de l’application, probablement dans la méthode -applicationDidFinishLaunching: du délégué de NSApplication : extern Class MYGameNetworkHighScoreManager; [MYGameNetworkHighScoreManager sharedInstance];
Dans cet exemple, la valeur de retour n’est pas mémorisée car l’objet réel n’est pas utile. Le message est envoyé uniquement pour déclencher la création de l’instance partagée et pour être certain qu’elle est de la classe souhaitée. Si vous voulez garantir l’uti-
Chapitre 13
Singleton
171
lisation de votre sous-classe pour l’instance partagée, il est possible de mettre en place une technique équivalente avec la plupart des autres singletons de Cocoa. L’instance partagée de NSApplication fait évidemment exception. Elle est créée automatiquement par Cocoa avant que votre code ne soit exécuté. La solution à ce problème passe par le pattern Création dynamique. Pour choisir une sous-classe particulière de NSApplication, sélectionnez la cible de l’application dans Xcode, ouvrez le panneau INFO et sélectionnez PROPERTIES. Au centre du panneau, vous trouvez un champ intitulé PRINCIPAL CLASS, dans lequel le nom de votre sous-classe de NSApplication peut être indiqué (voir Figure 13.1). Figure 13.1 Indiquer la classe NSApplication dans Xcode.
Par cette modification dans Xcode, le nom saisi est affecté à la clé "Principal class" dans le fichier Info.plist. Lorsque Cocoa démarre l’application, il examine cette valeur et utilise la création dynamique pour instancier la classe indiquée et en faire l’objet application partagé. Nous pouvons employer une approche comparable pour éviter
172
Les design patterns de Cocoa
d’appeler +sharedInstance au moment de l’initialisation de l’application. Voici le code de la méthode +sharedInstance modifié de sorte qu’elle recherche le nom de la classe dans la clé MYGameHighScoreManagerClass du fichier Info.plist de l’application. + (MYGameHighScoreManager *)sharedInstance { static MYGameHighScoreManager *myInstance = nil; if (!myInstance) { NSBundle *mainBundle = [NSBundle mainBundle]; NSDictionary *info = [mainBundle infoDictionary]; NSString *className = [info objectForKey: @"MYGameHighScoreManagerClass"]; Class *myClass = NSClassFromString(className); if (!myClass) { myClass = self; // Dans une méthode de classe, self est une classe. } myInstance = [[myClass alloc] init]; // Ajouter les autres initialisations requises. } return myInstance; }
À présent, pour choisir la sous-classe à utiliser, il suffit de donner une valeur à la clé MYGameHighScoreManagerClass dans le fichier Info.plist de l’application. Si la clé est absente ou si sa recherche échoue, cette version du code se replie sur la valeur retournée par l’appel à [self class]. Contrôler l’instanciation Pour que l’implémentation du pattern Singleton soit complète, il faut empêcher la création d’autres instances de la classe. La mise en œuvre de la méthode +sharedInstance contrôle la création et l’accès à une seule instance, mais elle n’empêche pas un autre code d’appeler +alloc pour fabriquer d’autres instances. Des modifications sont donc requises. Chaque méthode qui déclenche l’allocation d’une nouvelle instance doit être redéfinie de manière à empêcher l’instanciation. Il s’agit des méthodes +new, +alloc, +allocWithZone:, -copyWithZone: et -mutableCopyWithZone:. Il faut également revoir la méthode +sharedInstance pour qu’elle puisse allouer une instance sans invoquer la méthode +alloc à présent redéfinie. Voici une solution : + (id)hiddenAlloc { return [super alloc]; } + (id)alloc { NSLog(@"%@ : utiliser +sharedInstance à la place de +alloc", [[self class] name]); return nil; }
Chapitre 13
Singleton
173
+ (id)new { return [self alloc]; } + (id)allocWithZone:(NSZone *)zone { return [self alloc]; } - (id)copyWithZone:(NSZone *)zone { // -copy héritée de NSObject invoque -copyWithZone:. NSLog(@"MYGameHighScoreManager : l’appel à -copy peut être un bogue."); [self retain]; return self; } - (id)mutableCopyWithZone:(NSZone *)zone { // -mutableCopy héritée de NSObject invoque -mutableCopyWithZone:. return [self copyWithZone:zone]; } + (MYGameHighScoreManager *)sharedInstance { static MYGameHighScoreManager *myInstance = nil; if (!myInstance) { NSBundle *mainBundle = [NSBundle mainBundle]; NSDictionary *info = [mainBundle infoDictionary]; NSString *className = [info objectForKey: @"MYGameHighScoreManagerClass"]; Class *myClass = NSClassFromString(className); if (!myClass) { myClass = self; } myInstance = [[myClass hiddenAlloc] init]; // Ajouter les autres initialisations requises. } return myInstance; }
La méthode +hiddenAlloc est considérée privée et n’est pas déclarée dans l’en-tête de la classe. Elle pourrait être omise si aucune sous-classe du singleton n’était jamais créée, car un appel à [super alloc] conviendrait. Toutefois, puisque nous voulons que la valeur de myClass soit obtenue dynamiquement à l’exécution, il est probable que la méthode +alloc redéfinie soit appelée par l’envoi du message +alloc. L’invocation de +hiddenAlloc évite ce problème. Par ailleurs, si des sous-classes risquent d’être créées, leur code devra sans doute appeler ou redéfinir l’implémentation de +alloc de la superclasse. En créant +hiddenAlloc, nous proposons un point de raccordement que les développeurs de sous-classes peuvent utiliser si nécessaire. Les nouvelles versions des méthodes de création de l’objet journalisent à présent une erreur et retournent nil. Elles pourraient également retourner le résultat d’un appel à
174
Les design patterns de Cocoa
+sharedInstance si les appels successifs à -init étaient inoffensifs. Toutefois, la bonne approche est souvent de retourner nil. Certains développeurs préfèrent lancer une exception au lieu de retourner nil, alors que d’autres considèrent cette approche un peu extrême.
Les nouvelles méthodes de copie augmentent simplement le compteur de références de l’objet de manière à conserver la sémantique du système Cocoa de comptage des références. En revanche, elles ne retournent pas une nouvelle instance. Cela permet de garder la nature unique de la classe. Puisque, techniquement, la copie d’un singleton est une erreur qui peut être le symptôme d’un bogue du code appelant, nous journalisons un message lors d’une telle tentative, même si ce n’est pas obligatoire. Selon le contexte et les préférences du programmeur, il est possible de lancer une exception ou de ne pas journaliser le message. Désallocation Les singletons posent également un problème quant à la destruction de l’instance partagée. Puisque l’instance est créée dans +sharedInstance et qu’elle n’est jamais libérée, le compteur de références doit toujours être supérieur ou égal à un, tout au moins tant qu’aucun code défaillant n’appelle -release plus de fois que nécessaire. Par conséquent, l’instance ne devrait, en théorie, jamais être désallouée. Toutefois, si l’instance partagée représente un dispositif physique, il peut être nécessaire de lui donner l’opportunité de s’arrêter proprement lorsque l’application est fermée. Pour cela, la meilleure solution consiste à faire en sorte que l’objet partagé accepte la notification NSApplicationWillTerminateNotification et qu’il se ferme suite à sa réception. Habituellement, une instance partagée doit être créée une seule fois au cours de la vie de l’application et doit être fermée uniquement lorsque celle-ci se termine. Cependant, dans certaines utilisations de ce pattern, il peut être souhaitable que l’instance partagée puisse être désallouée, pour ensuite créer une nouvelle instance en cas de besoin. Dans ce cas, les choses se compliquent. Tout d’abord, la variable statique myInstance doit sortir de la méthode +sharedInstance afin que d’autres méthodes de classe puissent y accéder. Ensuite, une méthode comme +attemptDealloc doit être créée et invoquée dès que le code tente de désallouer l’instance partagée : + (void)attemptDealloc { if ([myInstance retainCount] != 1) return; [myInstance release]; myInstance = nil; }
Si un autre objet garde toujours l’instance partagée, elle ne doit pas être désallouée. Si elle est uniquement retenue par l’objet classe, alors, nous pouvons lui envoyer le mes-
Chapitre 13
Singleton
175
sage de libération. Bien entendu, cela ne fonctionne que si -dealloc a été redéfinie pour empêcher la désallocation. Puisqu’il est relativement courant de supposer qu’une instance partagée durera aussi longtemps que l’application, il est également assez fréquent que les objets ne la retiennent pas même s’ils possèdent des références sur cette instance. Cette pratique est pourtant dangereuse. Il est plus sûr de ne jamais conserver un pointeur sur l’instance partagée et d’invoquer +sharedInstance chaque fois qu’une référence est requise. Déterminer si le singleton a été créé Il est parfois utile de savoir si l’instance partagée a déjà été créée. Certaines classes Cocoa permettent d’obtenir cette information, d’autres non. Par exemple, NSSpellChecker offre la méthode +sharedSpellCheckerExists, qui retourne YES ou NO. En revanche, NSApplication ne propose pas une telle méthode, puisque l’on peut raisonnablement supposer que la réponse sera toujours YES. Pour ajouter ce type de méthode à notre implémentation du singleton, la variable statique myInstance doit sortir de la méthode +sharedInstance afin que d’autres méthodes de classes puissent y accéder. Voici une implémentation possible de +sharedInstanceExists : + (BOOL)sharedInstanceExists { return (nil != myInstance); }
Sûreté vis-à-vis des threads Si un singleton risque d’être utilisé dans une application multithread, il est important de faire en sorte qu’il soit sûr vis-à-vis des threads. Nous n’avons pas fait cet effort dans l’exemple précédent uniquement pour qu’il reste simple et clair. Néanmoins, il peut parfaitement fonctionner, car le tableau des scores a de fortes chances d’être manipulé uniquement depuis le thread principal. En revanche, si un singleton est utilisé par plusieurs threads, il faut assurer l’atomicité des accès aux propriétés et employer des blocs @synchronized() ou des instances de NSLock aux endroits pertinents. Travailler avec Interface Builder Si un singleton doit être utilisé dans Interface Builder, le code précédent requiert quelques modifications. Pour établir des connexions ou des bindings avec le singleton, il doit être instancié dans Interface Builder, mais cette instanciation se fait au travers des méthodes +alloc et -init et suppose un comportement classique. Puisque +alloc retourne nil, le fonctionnement est différent. La solution la plus simple pour contour-
176
Les design patterns de Cocoa
ner ce problème consiste à ne plus considérer un appel à +alloc comme une erreur et à lui faire invoquer +sharedInstance : + (id)alloc { return [self sharedInstance]; }
Toutefois, un nouveau problème survient. La méthode -init peut à présent être invoquée plusieurs fois et doit donc être réentrante. Pour la plupart des singletons, la meilleure solution réside dans une méthode -init qui retourne immédiatement si elle a déjà été invoquée une fois : - (id)init { if (![[self class] sharedInstanceExists]) { // Code d’initialisation normal. } return self; }
13.3
Exemples dans Cocoa
Plusieurs classes de Cocoa sont des singletons. Nous l’avons mentionné au début de ce chapitre, c’est le cas de NSApplication, qui encapsule la connexion entre l’application Cocoa et le serveur de fenêtres. Elle reçoit des événements et les distribue aux objets appropriés par l’intermédiaire du pattern Premier répondeur. Elle envoie également des commandes de dessin. Enfin, elle représente l’application elle-même, prenant en charge les événements de niveau applicatif, comme le masquage et la fermeture. La variable globale NSApp est un pointeur sur l’instance de NSApplication partagée. NSWorkspace, qui encapsule les communications de l’application avec le Finder et le système de fichiers sous-jacent, est un autre exemple. Puisqu’il n’existe qu’un seul Finder et que des connexions multiples avec le Finder n’ont pas de sens, cette classe est mise en œuvre comme un singleton.
C’est également le cas de la classe NSFontManager. Elle représente l’ensemble des polices de caractères installées sur le système et prend en charge l’accès aux objets de ces polices afin qu’ils puissent être partagés. Cela permet de garder l’efficacité de l’application, car NSFontManager s’assure qu’une seule instance de NSFont sera créée pour chaque police installée sur le système. Puisque le système ne possède qu’une seule collection de polices de caractères, le gestionnaire est également un singleton. NSDocumentController, NSHelpManager, NSNull, NSProcessInfo, NSScriptExecutionContext et NSUserDefaults sont d’autres exemples. Certains panneaux standard, comme NSColorPanel et NSFontPanel, sont également partagés. Un coup d’œil à la
Chapitre 13
Singleton
177
documentation de la classe Cocoa permet généralement de savoir si elle est un singleton. Il suffit de rechercher une méthode de classe dont le nom contient le mot "shared". En Cocoa, la méthode qui permet d’obtenir une instance partagée a un nom composé du mot "shared" et du nom de la classe, sans le préfixe "NS". Par exemple, la méthode +sharedWorkspace permet d’obtenir l’instance partagée de NSWorkspace, et +sharedApplication retourne l’instance partagée de NSApplication. Vous pouvez également rencontrer la méthode plus générique +sharedInstance utilisée avec certains objets. Les instances partagées peuvent parfois être obtenues en invoquant la méthode de classe +new. Cet usage de la méthode +new est hérité des anciennes versions des frameworks et est obsolète. La méthode +new jouait un rôle essentiel dans les premiers frameworks développés en Objective-C, mais ne doit plus être utilisée avec Cocoa. Avec l’évolution de Cocoa, la nature partagée de certaines classes a changé. Par exemple, avant l’avènement des "sheets", l’ensemble de l’application ne proposait qu’un exemplaire des panneaux d’impression et de mise en page. L’affichage de l’un d’eux bloquait les autres fenêtres, jusqu’à ce qu’il soit fermé. Les classes NSPrintPanel et NSPageLayout étaient donc des singletons. À présent, puisqu’ils peuvent être affichés en plusieurs exemplaires simultanément, en tant que "sheets" sur les fenêtres de l’application, il ne s’agit plus d’objets partagés. Bien que ces cas soient rares, ils peuvent affecter le code qui se fonde sur la nature partagée de ces objets. Par exemple, certains développeurs donnent au panneau d’impression des valeurs par défaut dès le début du code et supposent ensuite que ces paramètres vont apparaître automatiquement chaque fois que le panneau est affiché. Puisque les "sheets" ont permis la création de plusieurs panneaux d’impression, ce comportement n’est plus vrai et le code a dû être modifié.
13.4
Conséquences
L’un des patterns les plus simples de Cocoa est appelé "objet partagé" dans la documentation. Il est plus généralement connu sous le nom Singleton. Un objet partagé est utilisé lorsqu’une classe doit être instanciée une et une seule fois. L’exemple d’objet partagé le plus évident est peut-être l’objet application central. Toute application Cocoa possède une seule instance de NSApplication. En effet, il est relativement normal que l’objet qui représente une application en cours d’exécution n’apparaisse qu’une seule fois dans cette application. Plusieurs autres classes Cocoa sont également des objets partagés. Cela inclut les objets qui représentent certains panneaux de l’interface utilisateur, comme les panneaux de couleur et de police de caractères, ainsi que des objets de plus bas niveau, comme le
178
Les design patterns de Cocoa
gestionnaire de polices, certains objets de script et ceux qui représentent les ressources du système, comme le Finder. Les classes Cocoa qui offrent une méthode dont le nom contient le mot "shared" utilisent une variante de ce pattern. Le code qui permet à de nouvelles classes de se comporter comme des singletons n’est pas difficile à écrire. Nous l’avons vu dans ce chapitre, il est important de tenir compte de toutes les méthodes de classe en lien avec la création, la copie et la destruction des objets. La documentation doit préciser aux utilisateurs de la classe la méthode qui permet d’obtenir l’instance partagée. Pour mettre en œuvre ce pattern, l’objet classe fournit une méthode qui est accessible globalement et qui permet d’obtenir l’instance partagée. Par ailleurs, la méthode +alloc est désactivée afin d’empêcher la création d’instances supplémentaires. L’unique instance partagée est créée lors de la première demande et elle est retournée par chaque demande suivante. Lors de la conception des objets, il est important de ne pas abuser de ce pattern. Une classe ne doit être un singleton que si elle représente quelque chose qui n’existe réellement qu’en un exemplaire unique. Il arrive que certaines choses répondent initialement à ce critère, mais que ce ne soit plus le cas lorsque l’application évolue. Par exemple, si le jeu qui a servi d’exemple dans ce chapitre évolue vers une version multijoueur, chaque joueur doit alors disposer de son propre tableau des scores. Dans ce cas, le singleton ne peut plus être appliqué à la mise en œuvre de ce tableau. Le pattern Gestionnaire (voir Chapitre 28) constitue souvent une meilleure alternative. Pour illustrer cela, l’exemple du tableau des scores sera étendu au Chapitre 28 de manière à prendre en charge plusieurs joueurs.
14 Notification Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Notification permet de mettre en place des communications entre des objets sans impliquer un couplage fort entre ces derniers. Un objet peut diffuser des informations à d’autres objets sans tout connaître de ceux-ci. La classe Cocoa NSNotification encapsule les informations à diffuser. Les objets qui souhaitent recevoir des informations s’inscrivent eux-mêmes auprès d’une instance de la classe NSNotificationCenter. Les objets inscrits sont des observateurs et le pattern Notification est parfois appelé pattern Observateur dans d’autres frameworks. Les observateurs inscrits précisent les formes de communication souhaitées. Lorsqu’une notification est postée dans un centre de notification, celui-ci la distribue aux observateurs appropriés. Une seule notification peut être diffusée à un nombre quelconque d’observateurs. L’objet qui envoie un message à un centre de notification n’a pas besoin de connaître les observateurs existants ni le nombre d’observateurs qui recevront la notification. De même, les observateurs n’ont pas forcément besoin de connaître l’origine des notifications. La Figure 14.1 illustre les relations entre l’émetteur d’une notification, un centre de notification et les observateurs. La classe NSNotificationCenter gère les observateurs inscrits sous forme d’objets anonymes en utilisant le pattern Conteneur hétérogène (voir Chapitre 7). La notification et la délégation (voir Chapitre 15) sont des patterns connexes. La notification est comparable à l’observation clé-valeur (voir Chapitre 32).
180
Les design patterns de Cocoa
Une notification est remise en utilisant le sélecteur indiqué de l’observateur inscrit. Observateur Observer Observer
NSNotificationCenter
*
-postNotification:
NSNotification
name
-addObserver:selector: name:object::
object Un objet
*
userInfo
* L’instance de NSNotification est créée et postée à un NSNotificationCenter. Figure 14.1 Les relations entre les centres de notification et les observateurs.
14.1
Motivation
Le pattern Notification est utilisé pour établir une communication anonyme entre des objets au moment de l’exécution. Dans le pattern MVC, les notifications franchissent les frontières des sous-systèmes, sans créer des liens entre ces sous-systèmes. Les objets du modèle génèrent souvent des notifications, qui finissent par atteindre des objets du contrôleur, qui réagissent en actualisant les objets de la vue. Dans l’autre sens, les objets du modèle et du contrôleur observent les notifications qui peuvent provenir des soussystèmes Vue et Contrôleur. Par exemple, lorsqu’une application Cocoa est en passe d’être fermée, l’objet NSApplication poste la notification NSApplicationWillTerminateNotification dans le centre de notification par défaut de l’application. Les objets du modèle qui doivent mettre en place des opérations de nettoyage avant que l’application ne se termine s’inscrivent en tant qu’observateurs de manière à recevoir cette notification. Le pattern Notification permet de diffuser des messages. Les notifications peuvent être postées par un nombre quelconque d’objets et reçues également par un nombre quelconque d’objets. Le pattern Notification autorise des relations de cardinalités un-à-plusieurs et plusieurs-à-plusieurs entre les objets. Le pattern Notification est utilisé avec la classe Cocoa NSDistributedNotificationCenter pour mettre en place une communication asynchrone simple entre processus. Le pattern Notification est utilisé lorsque des objets anonymes doivent observer de manière passive des événements importants et réagir à leur arrivée. À l’opposé, le pattern Délégué est utilisé lorsque des objets anonymes doivent influer de manière active sur les événements lorsqu’ils se produisent.
Chapitre 14
14.2
Notification
181
Solution
Le pattern Notification n’est pas propre à Cocoa et il est possible d’en implémenter une version simple à l’aide des classes Foundation et des patterns Objet anonyme, Conteneur hétérogène et Exécution de sélecteur. Le code de cette section combine plusieurs design patterns pour mettre en œuvre la notification, mais il ne reflète pas nécessairement les implémentations retenues par les classes Cocoa. Une application réelle doit se fonder sur les classes NSNotification et NSNotificationCenter. Si vous vous intéressez uniquement à l’observation et à l’envoi de notifications, sans vous préoccuper de leur mise en œuvre avec d’autres patterns Cocoa, passez directement à la section "Exemples dans Cocoa". MYNotification Tout d’abord, créez une classe MYNotification qui jouera un rôle équivalant à la classe Cocoa NSNotification. Les instances de MYNotification encapsulent les informations qui concernent les notifications : @class MYNotification : NSObject { NSString *name; id object; NSDictionary *infoDictionary; }
// Identification de la notification. // Un objet anonyme // Informations arbitraires associées.
- (id)initWithName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)someUserInfo; @property (readonly, copy) NSString *name; @property (readonly, assign) id object; @property (readonly, copy) NSDictionary *infoDictionary; @end
Voici une implémentation simple de la classe MYNotification : @interface MYNotification () // Redéclarer les propriétés afin que leur valeur puisse être fixée // par des méthodes implémentées dans cette classe. @property (readwrite, copy) NSString *name; @property (readwrite, assign) id object; @property (readwrite, copy) NSDictionary *infoDictionary; @end @implementation MYNotification @synthesize name; @synthesize object; @synthesize infoDictionary;
182
Les design patterns de Cocoa
- (id)initWithName:(NSString *)aName object:(id)anObject userInfo:(NSDictionary *)someUserInfo { [self setName:aName]; [self setObject:anObject]; [self setInfoDictionary:someUserInfo]; return self; } - (void)dealloc { [self setName:nil]; [self setObject:nil]; [self setInfoDictionary:nil]; [super dealloc]; } @end
MYNotificationCenter Les instances de la classe MYNotificationCenter enregistrent les informations concernant les observateurs dans un conteneur hétérogène nommé observersDictionary. MYNotificationCenter équivaut à la classe Cocoa NSNotificationCenter. @class MYNotificationCenter : NSObject { NSMutableDictionary *observersDictionary; } + (id)defaultCenter; - (void)addObserver:(id)notificationObserver selector:(SEL)notificationSelector name:(NSString *)notificationName object:(id)objectOfInterest; - (void)removeObserver:(id)notificationObserver; - (void)postNotification:(MYNotification *)aNotification; - (void)postNotificationName:(NSString *)aName object:(id)objectOfInterest userInfo:(NSDictionary *)someUserInfo; @end
La méthode -addObserver:selector:name:object: de MYNotificationCenter permet d’inscrire un observateur. Le premier argument correspond à l’observateur. Le deuxième est un sélecteur (voir Chapitre 9) qui identifie le message Objective-C à envoyer à l’observateur lorsqu’une notification appropriée est postée. Le sélecteur doit préciser une méthode qui prend un argument de type pointeur sur une instance de notification. Les troisième et quatrième arguments, name: et object:, identifient les notifi-
Chapitre 14
Notification
183
cations qui intéressent l’observateur. Seules les notifications dont les noms correspondent à celui indiqué sont passées à l’observateur inscrit. Si un observateur souhaite recevoir plusieurs sortes de notifications, il peut s’inscrire plusieurs fois auprès du centre de notification, en précisant à chaque fois un nom de notification différent. De manière comparable, l’argument object: identifie l’objet qui intéresse l’observateur. Seules les notifications contenant l’objet indiqué sont transmises à l’observateur. La classe MYNotificationCenter fait preuve d’une certaine souplesse avec cet argument. Lorsque nil est passé, toutes les notifications qui correspondent au nom précisé sont livrées, quel que soit l’objet qu’elles contiennent. À maints égards, MYNotificationCenter reprend les possibilités de NSNotificationCenter. Toutefois, avec la classe Cocoa NSNotificationCenter, si l’argument name: de -addObserver:selector:name:object est nil, l’observateur est inscrit de manière à recevoir toutes les notifications associées à l’argument object: indiqué. La classe MYNotificationCenter n’accepte pas les inscriptions avec un nom de notification nil. Les objets qui se sont inscrits en vue de recevoir certaines notifications peuvent souhaiter annuler leur inscription. Selon la convention établie par Cocoa, les observateurs se désinscrivent eux-mêmes en invoquant la méthode -removeObserver: du centre de notification. Un appel à cette méthode désinscrit toutes les inscriptions antérieures de l’observateur auprès de ce centre. En général, l’appel à -removeObserver: se fait dans l’implémentation de la méthode -dealloc de l’observateur. Un objet désalloué ne doit pas rester inscrit en tant qu’observateur. INFO Lorsque le ramasse-miettes de Cocoa, proposé par Objective-C 2.0 avec Mac OS X 10.5, est activé, NSNotificationCenter désinscrit automatiquement les observateurs qui ne sont plus utilisés par ailleurs dans l’application.
MYNotificationCenter et NSNotificationCenter ne retiennent pas les observateurs ni les objets qui les intéressent. Si elles retenaient les observateurs inscrits, des cycles de retenue risqueraient d’apparaître et empêcheraient la désallocation des observateurs. La méthode -dealloc des observateurs ne serait jamais invoquée, car ils seraient retenus par le centre de notification.
La classe privée suivante stocke les informations concernant les observateurs inscrits, mais ne retient pas l’observateur ou l’objet qui l’intéresse : @interface _MYNotificationObserverRecord : NSObject { id object; // Objet anonyme d’intérêt. id observer; // Observateur anonyme. SEL selector; // Sélecteur à invoquer. }
184
Les design patterns de Cocoa
@property (readwrite, assign) id object; @property (readwrite, assign) id observer; @property (readwrite, assign) SEL selector; @end
L’implémentation de _MYNotificationObserverRecord inclut uniquement les méthodes d’accès aux propriétés de la classe. @implementation _MYNotificationObserverRecord @synthesize object; @synthesize observer; @synthesize selector; @end
MYNotificationCenter mémorise indirectement les instances de _MYNotificationObserverRecord : la variable observersDictionary de MYNotificationCenter est un dictionnaire altérable, dont les clés sont des noms de notifications, et les valeurs, des listes contenant deux instances de MYNotificationObserverRecord. @interface MYNotificationCenter () @property (readwrite, retain) NSMutableDictionary *observersDictionary; @end @implementation MYNotificationCenter @synthesize observersDictionary; + (id)defaultCenter { // L’instance "par défaut" partagée créée comme nécessaire. static id sharedNotificationCenter = nil; if(nil == sharedNotificationCenter) { sharedNotificationCenter = [[MYNotificationCenter alloc] init]; } return sharedNotificationCenter; } // Initialiseur désigné. - (id)init { if(nil != (self = [super init])) { [self setObserversDictionary:[NSMutableDictionary dictionary]]; }
Chapitre 14
Notification
return self; } - (void)dealloc { [self setObserversDictionary:nil]; [super dealloc]; } - (void)addObserver:(id)notificationObserver selector:(SEL)notificationSelector name:(NSString *)notificationName object:(id)objectOfInterest { // Cette classe a besoin d’un notificationName non nil. NSNotification // ne souffre pas de cette contrainte. NSParameterAssert(notificationName); _MYNotificationObserverRecord *newRecord = [[[_MYNotificationObserverRecord alloc] init] autorelease]; [newRecord setObject:objectOfInterest]; [newRecord setObserver:notificationObserver]; [newRecord setSelector:notificationSelector]; // Ce tableau contient les observateurs inscrits pour // chaque nom de notification. NSArray *observers = [observersDictionary objectForKey:notificationName]; if(nil != observers) { [observers addObject:newRecord]; } else { // Puisqu’il s’agit du premier observateur pour notificationName, // nous créons la liste pour l’enregistrer, ainsi que tous les // futurs observateurs du même nom de notification. [observersDictionary setObject:[NSMutableArray arrayWithObject:newRecord] forKey:notificationName]; } } - (void)removeObserver:(id)notificationObserver { if(nil != notificationObserver) { for(NSMutableArray *observers in [self observersDictionary]) { NSInteger i; for(i = [observers count] - 1; i >= 0; i—) { currentObserverRecord = [observers objectAtIndex:i];
185
186
Les design patterns de Cocoa
if(notificationObserver == [currentObserverRecord observer]) { [observers removeObjectAtIndex:i]; } } } } } - (void)postNotification:(MYNotification *)aNotification { NSParameterAssert(aNotification); NSAssert(nil != [aNotification name], @"Nom de notification nil"); NSArray *observers = [observersDictionary objectForKey: [aNotification name]]; for(id currentObserverRecord in observers) { id object = [currentObserverRecord object]; if(nil == object || object == [aNotification object]) { // L’observateur est intéressé par les notifications avec // n’importe quel objet ou au moins celui-ci. [[currentObserverRecord observer] performSelector: [currentObserverRecord selector] withObject:aNotification]; } } } - (void)postNotificationName:(NSString *)aName object:(id)objectOfInterest userInfo:(NSDictionary *)someUserInfo; { // Cette méthode crée une instance de MYNotification appropriée et // la poste ensuite. MYNotification *newNotification = [[[MYNotification alloc] initWithName:aName object:objectOfInterest userInfo:someUserInfo] autorelease]; [self postNotification:newNotification]; } @end
L’application d’exemple disponible dans l’archive des codes sources de cet ouvrage propose quelques cas de test pour les classes MYNotification et MYNotificationCenter. Mémoire associative L’implémentation de MYNotificationCenter se fonde sur le pattern Mémoire associative (voir Chapitre 19). La mémoire associative, incarnée par la classe NSDictionary de
Chapitre 14
Notification
187
Cocoa, permet d’enregistrer des objets quelconques associés à d’autres objets appelés clés. L’idée est de pouvoir rechercher rapidement un objet en fonction de sa clé. Dans l’exemple de MYNotificationCenter, les noms des notifications servent de clés pour rechercher les listes qui contiennent des instances de _MYNotificationObserverRecord. La mémoire associative a également une autre utilisation subtile : la classe MYNotification et la classe Cocoa NSNotification permettent de passer un dictionnaire contenant des informations utilisateur (userInfo) avec chaque notification. Ce dictionnaire contient des couples clé-valeur dont la signification est propre à l’application. Certaines notifications postées par les classes Cocoa exploitent cette possibilité de passer des informations supplémentaires. Par exemple, la classe NSTextView poste la notification NSTextViewDidChangeSelectionNotification dès que la sélection de l’utilisateur change. Cette notification précise la plage de la sélection précédente au travers de la clé NSOldSelectedCharacterRange dans le dictionnaire userInfo. Le code suivant montre comment un objet s’inscrit lui-même de manière à recevoir la notification NSTextViewDidChangeSelectionNotification. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textViewSelectionDidChange:) name:NSTextViewDidChangeSelectionNotification object:nil];
Voici une implémentation de -(void)textViewSelectionDidChange:(NSNotification *)aNotification qui utilise le dictionnaire des informations utilisateur : –(void)textViewSelectionDidChange:(NSNotification *)aNotification { NSValue *oldSelectionRangeValue = [[aNotification userInfo] objectForKey:@"NSOldSelectedCharacterRange"]; NSRange
oldSelectionRange = [oldSelectionRangeValue rangeValue];
// Utiliser oldSelectionRange. }
14.3
Exemples dans Cocoa
Le pattern Notification est très utilisé dans Cocoa et la documentation Apple de chaque classe qui poste des notifications réserve une section à ce sujet. Les classes Cocoa postent leurs notifications dans le centre de notification par défaut, que l’on obtient via la méthode +defaultCenter de la classe NSNotificationCenter. Vous pouvez utiliser le centre de notification par défaut. Les programmeurs créent rarement des centres de notifications spécifiques à des applications.
188
Les design patterns de Cocoa
Noms de notifications globaux Les noms des notifications sont des instances de NSString. La documentation d’une classe Cocoa indique les notifications qu’elle poste en précisant des noms globaux, comme NSApplicationDidFinishLaunchingNotification, NSApplicationDidUpdateNotification et NSTableViewColumnDidResizeNotification. Apple recommande de copier et de coller ces noms directement dans votre code. Si vous examinez les fichiers d’en-tête de Cocoa, vous trouverez des déclarations semblables à la suivante : extern NSString *NSTableViewColumnDidResizeNotification;
Si vous pouvez jeter un œil au code source d’Apple, vous verrez que la variable globale NSTableViewColumnDidResizeNotification est initialisée de la manière suivante : NSString *NSTableViewColumnDidResizeNotification = @"NSTableViewColumnDidResizeNotification";
Autrement dit, d’un point de vue pratique, les deux exemples de code suivants produisent le même résultat : // S’inscrire en utilisant le symbole global. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tableViewColumnDidResize:) name:NSTableViewColumnDidResizeNotification object:nil]; // S’inscrire en utilisant la constante NSString locale. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(tableViewColumnDidResize:) name:@"NSTableViewColumnDidResizeNotification" object:nil];
Les chaînes globales déclarées dans les fichiers d’en-tête de Cocoa peuvent être utilisées de manière interchangeable avec celles définies par des variables locales dans votre code. Utilisez la variable globale fournie par le framework pour éviter de figer des chaînes constantes dans votre code. Notifications Will et Did Les noms des notifications Cocoa suivent un schéma de nommage cohérent qui vous permet de déterminer celles que vous souhaitez observer. Les noms qui comprennent le mot "Will" sont utilisés avec les notifications qui signalent aux observateurs qu’un événement va se produire. Les noms qui incluent le mot "Did" correspondent à des notifications qui signalent aux observateurs qu’un événement s’est produit. Dans certains cas, les deux types de notifications existent pour le même événement. Par exemple, la classe NSApplication poste NSApplicationWillHideNotification et NSApplicationDidHideNotification.
Chapitre 14
Notification
189
Notifications synchrones et asynchrones Poster une notification dans un NSNotificationCenter est une opération synchrone. Autrement dit, lorsque vous postez une notification avec -postNotification: ou toute autre méthode apparentée de NSNotificationCenter, la notification est remise à tous les observateurs inscrits concernés avant que le contrôle ne revienne à votre code. Par ailleurs, vous devez tenir compte de ce comportement synchrone dans vos traitements des notifications. Si vous réalisez des opérations longues dans le code de traitement des notifications, vous retardez d’autant sa réception par les autres objets, tout comme le retour du contrôle au code qui l’a postée. Une astuce fréquente consiste à lancer les traitements complexes des notifications en utilisant une exécution retardée. Dans le code de traitement de la notification, planifiez l’envoi futur d’un message et quittez immédiatement la méthode : - (void)tableViewSelectionDidChange:(NSNotification *)aNotification { // Planifier l’envoi d’un message futur et revenir de la méthode. [self performSelector:@selector(doComplexProcessing:) withObject:[aNotification object] afterDelay:0.0f]; } - (void)doComplexProcessing:(id)anObject { // Effectuer un traitement complexe fondé sur anObject. }
Lorsque le comportement asynchrone requis ne se borne pas à l’envoi d’un message différé, utilisez la classe NSNotificationQueue. Ses instances mettent en œuvre une file d’attente asynchrone FIFO (First In First Out). Lorsque vous invoquez la méthode -(void)enqueueNotification:(NSNotification *)notification postingStyle: (NSPostingStyle)postingStyle coalesceMask:(NSUInteger)coalesceMask forModes:(NSArray *)modes de NSNotificationQueue, la notification indiquée est placée à la fin de la file d’attente et le contrôle revient immédiatement à l’appelant. L’instance de NSNotificationQueue poste la notification dans un NSNotificationCenter conformément au comportement défini par les valeurs de postingStyle, de coalesceMask et de modes. À partir de là, la notification est traitée de manière synchrone par le NSNotificationCenter. La Figure 14.2 illustre les relations entre l’objet qui place une notification en file d’attente, la file d’attente des notifications, le centre de notification et les observateurs inscrits. Dans une application Cocoa, chaque thread dispose d’une file d’attente par défaut pour les notifications, à laquelle on accède via la méthode +(NSNotificationQueue *) defaultQueue de la classe NSNotificationQueue. Cette file d’attente poste les notifications dans le centre de notification par défaut du thread. Tout comme vous pouvez créer un centre de notification propre à une application, vous pouvez créer une file d’attente des notifications réservée à l’application. Par exemple, le code suivant alloue deux nouvelles files d’attente, qui posteront les notifications au même objet existant myApplicationSpecificNotificationCenter :
190
Les design patterns de Cocoa
Une notification est remise en utilisant le sélecteur indiqué de l’observateur inscrit. Observateur Observer Observer
NSNotificationCenter
-postNotification:
Poste la notification plus tard
-addObserver:selector:name:object:::
*
NSNotification
Revient immédiatement
name Un objet object userInfo
*
NSNotificationQueue
-enqueueNotification:postingStyle: coalesceMask:forModes:
* L’instance de NSNotification est créée et postée à un NSNotificationQueue. Figure 14.2 Les relations entre les files d’attente des notifications et les centres de notification. NSNotificationQueue *applicationSpecificNotificationQueue1 = [[NSNotificationQueue alloc] initWithNotificationCenter:myApplicationSpecificNotificationCenter]; NSNotificationQueue *applicationSpecificNotificationQueue2 = [[NSNotificationQueue alloc] initWithNotificationCenter:myApplicationSpecificNotificationCenter];
L’argument postingStyle: de la méthode -enqueueNotification:postingStyle: coalesceMask:forModes: de NSNotificationQueue reconnaît trois styles : NSPostASAP, NSPostWhenIdle et NSPostNow. Le style NSPostASAP demande à la file d’attente de poster la notification au début de la prochaine itération de la boucle d’exécution et, en cela, équivaut à l’exemple de -performSelector:withObject:afterDelay: présenté précédemment. Pour de plus amples informations concernant la boucle d’exécution de Cocoa, consultez la section Core Library > Cocoa > Events & Other Input > NSRunLoop Class Reference de la documentation Xcode. Le style NSPostWhenIdle indique à la file d’attente de poster la notification dès que la boucle d’exécution de Cocoa est inactive, c’est-à-dire lorsque aucun événement utilisateur n’est en attente ou qu’aucune autre source d’entrée ne possède des données prêtes à être traitées. Enfin, le style NSPostNow stipule à la file d’attente de poster la notification immédiatement et de manière synchrone. La seule différence entre placer en file d’attente une notification avec le style NSPostNow et invoquer directement la méthode -postNotification: de NotificationCenter réside dans la fusion des notifications redondantes de la file effectuée par NSNotificationQueue. Cette fusion signifie simplement que les modifications semblables placées dans la file d’attente sont combinées en une seule et postées une seule fois.
Chapitre 14
Notification
191
L’argument coalesceMask: permet de préciser la manière dont la file d’attente doit gérer les notifications semblables. Les options disponibles sont NSNotificationNoCoalescing, NSNotificationCoalescingOnName et NSNotificationCoalescingOnSender. Utilisez l’opérateur OU binaire du langage C pour préciser à la fois NSNotificationCoalescingOnName et NSNotificationCoalescingOnSender. Dans ce cas, seules les notifications de même nom et de même objet sont fusionnées. L’argument forModes: permet de passer une liste qui précise les modes de la boucle d’exécution dans lesquels la file d’attente est autorisée à poster les notifications. Si cet argument vaut nil, la file d’attente poste les notifications uniquement lorsque la boucle d’exécution est dans le mode NSDefaultRunLoopMode. La documentation de la classe NSRunLoop décrit tous les modes disponibles. Notifications distribuées Un mécanisme de Cocoa permet de poster des notifications qui seront transmises à toutes les applications qui s’exécutent sur le même ordinateur. Ces notifications distribuées présentent quelques restrictions et sont relativement inefficaces comparées aux autres techniques de communication interapplications, mais elles sont d’un usage très simple. Chaque application Cocoa dispose d’une instance par défaut de la classe NSDistributedNotificationCenter, à laquelle on accède via la méthode +defaultCenter de cette classe. Puisque NSDistributedNotificationCenter dérive de NSNotificationCenter, les notifications sont postées dans le NSDistributedNotificationCenter comme dans un NSNotificationCenter normal, à l’aide de la méthode -postNotification:. Les notifications sont postées dans les centres de notification distribués en mode asynchrone et ne sont donc pas reçues immédiatement. La classe NSDistributedNotificationCenter offre la méthode -(void)postNotificationName:(NSString *) notificationName object:(NSString *)notificationSender userInfo:(NSDictionary *)userInfo deliverImmediately:(BOOL)deliverImmediately pour que vous puissiez préciser comment gérer la situation dans laquelle certains observateurs sont suspendus par le système d’exploitation et ne s’exécutent donc pas. L’argument object: des notifications postées dans un NSNotificationCenter doit obligatoirement être une instance de NSString. Avec les notifications distribuées, les arguments object: proviennent d’applications différentes de celles des observateurs. Par conséquent, le filtrage des notifications distribuées se fait d’après la valeur de l’argument object: interprété comme une chaîne de caractères, non une adresse. L’argument userInfo: des notifications distribuées est codé à l’aide du pattern Archivage et désarchivage (voir Chapitre 11). Tous les objets du dictionnaire userInfo: doivent donc implémenter l’archivage et le désarchivage.
192
Les design patterns de Cocoa
Pour observer des notifications distribuées, les objets s’inscrivent à l’aide des méthodes -(void)addObserver:(id)anObserver selector:(SEL)aSelector name:(NSString *) notificationName object:(NSString *)anObject ou -(void)addObserver:(id) anObserver selector:(SEL)aSelector name:(NSString *)notificationName object: (NSString *)anObject suspensionBehavior:(NSNotificationSuspensionBehavior) suspensionBehavior de NSDistributedNotificationCenter. La méthode -setSuspended: de NSDistributedNotificationCenter permet de suspendre l’envoi des notifications distribuées aux observateurs. Lorsqu’elle est invoquée avec YES en argument, le centre de notification distribuée de l’application stoppe temporairement sa réception des notifications. L’objet NSApplication des applications fondées sur Application Kit invoque automatiquement -setSuspended:YES lorsque l’application est inactive et -setSuspended:NO lorsqu’elle devient active. Les applications Cocoa qui n’utilisent pas le framework Application Kit doivent gérer explicitement l’interruption des notifications distribuées en invoquant -setSuspended: au moment opportun. Il existe quatre façons de gérer les notifications qui sont postées mais non reçues par une application suspendue, selon la valeur de l’argument suspensionBehavior: de la méthode -addObserver:selector:name:object:suspensionBehavior:. Les comportements reconnus sont précisés par le type NSNotificationSuspensionBehavior : NSNotificationSuspensionBehaviorDrop, NSNotificationSuspensionBehaviorCoalesce, NSNotificationSuspensionBehaviorHold et NSNotificationSuspensionBehaviorDeliverImmediately. Dans le cas de NSNotificationSuspensionBehaviorDrop, les notifications distribuées qui seraient normalement reçues ne le sont pas et ne restent pas dans la file d’attente en vue d’une livraison ultérieure. Avec NSNotificationSuspensionBehaviorCoalesce, la livraison d’au moins une notification dont le nom et l’objet correspondent est planifiée pour le moment où l’observateur ne sera plus suspendu. Si NSNotificationSuspensionBehaviorHold est utilisé, les notifications sont placées en file d’attente et seront toutes remises lorsque l’observateur ne sera plus suspendu. Le nombre de notifications qui peuvent rejoindre la file d’attente n’est pas fixé, mais il est soumis aux contraintes du système d’exploitation. Par conséquent, lorsque vous utilisez NSNotificationSuspensionBehaviorHold, vous devez faire attention à ne pas gaspiller les ressources système. Enfin, pour NSNotificationSuspensionBehaviorDeliverImmediately, les notifications sont envoyées immédiatement aux observateurs, que les notifications distribuées soient suspendues ou non. Ces livraisons immédiates ne doivent être utilisées que pour les notifications critiques qui ne doivent pas être retardées ou ignorées.
Chapitre 14
Notification
193
Les observateurs sont retirés du centre de notification distribuée à l’aide de la méthode -removeObserver:name:object: héritée par NSDistributedNotificationCenter de sa super-classe NSNotificationCenter. Lors de la désallocation d’un objet inscrit pour l’observation des notifications distribuées, l’observateur doit se retirer lui-même de tous les centres de notification de manière à éviter que des notifications soient envoyées à des objets désalloués.
14.4
Conséquences
Le plus gros inconvénient du pattern Notification réside dans le fait que les concepteurs de classes doivent anticiper tous les besoins de notifications. Les développeurs d’Apple ont pu deviner que les programmeurs auraient besoin d’effectuer un traitement particulier lorsqu’une fenêtre est en passe d’être fermée et fournissent donc la notification NSWindowWillCloseNotification. Mais un compromis doit être trouvé. En effet, poster une notification occupe le processeur, même si aucun observateur ne s’est inscrit pour cette notification. De plus, il n’est pas envisageable de poster une notification pour chaque changement d’état d’une application. Les concepteurs doivent donc trouver un équilibre entre trop et pas assez de notifications. Le pattern Délégué (voir Chapitre 15) est étroitement lié au pattern Notification. En réalité, les classes Cocoa utilisent un délégué là où les notifications pourraient être employées. En règle générale, les notifications doivent être utilisées lorsque plusieurs objets peuvent potentiellement observer la notification. Le délégué est utile lorsqu’un seul objet doit avoir l’opportunité d’influer ou de réagir à des changements.
15 Délégué Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Un délégué est un objet auquel on donne l’opportunité de réagir aux changements intervenus dans un autre objet ou d’influer sur le comportement d’un autre objet. L’idée de base est que deux objets se coordonnent pour résoudre un problème. L’un des objets est très général et conçu pour être réutilisé dans une grande variété de situations. Il enregistre une référence sur un autre objet, son délégué, et lui envoie des messages aux moments opportuns. Les messages peuvent simplement informer le délégué qu’un événement s’est produit, lui donner l’occasion de réaliser un traitement supplémentaire ou lui demander des informations critiques qui décideront de la suite. Le délégué est généralement un objet personnalisé unique dans le sous-système Contrôleur de l’application. Le délégué représente l’un des patterns les plus simples et les plus flexibles de Cocoa. Son existence est rendue possible par le pattern Type anonyme (voir Chapitre 7). Les délégués mettent en exergue les bénéfices des objets anonymes dans la conception des classes réutilisables.
15.1
Motivation
Les délégués simplifient la personnalisation du comportement d’un objet, tout en minimisant le couplage entre les objets. La classe Cocoa NSWindow assujettit le comportement d’une fenêtre à un délégué. NSWindow est une classe très générale qui encapsule tous les aspects des fenêtres d’une interface graphique. Les fenêtres peuvent être redimensionnées et déplacées. Elles embarquent des contrôles qui permettent de les fermer,
196
Les design patterns de Cocoa
de les réduire ou de les agrandir. La majorité des applications Cocoa graphiques se fondent sur des fenêtres, même si leur gestion doit souvent être personnalisée. Par exemple, une application peut souhaiter contraindre la taille d’une fenêtre ou donner aux utilisateurs l’opportunité d’enregistrer les modifications apportées au contenu d’une fenêtre avant qu’elle ne soit fermée. Pour personnaliser la classe standard NSWindow, une solution consiste à en créer une sous-classe et à y implémenter les nouveaux comportements. Toutefois, l’héritage conduit à un couplage étroit entre la sous-classe et sa super-classe. À trop utiliser l’héritage, on obtient de nombreuses classes spécifiques à l’application et peu réutilisables. Les relations entre la classe dérivée et sa classe mère sont établies statiquement au moment de la compilation. Bien souvent, une certaine flexibilité est souhaitée à l’exécution. Par exemple, les contraintes de redimensionnement d’une fenêtre peuvent varier en fonction des actions de l’utilisateur au cours de l’exécution. Plus important encore, la logique de personnalisation du comportement d’une fenêtre peut dépendre des détails d’implémentation de l’application. Avec le pattern MVC, omniprésent dans Cocoa, les fenêtres font évidemment partie du sous-système Vue, mais il est préférable d’encapsuler les détails de l’application dans le modèle ou le contrôleur. La création d’une sousclasse de NSWindow dans le but d’ajouter une logique applicative débouche sur une contamination entre des sous-systèmes distincts. Grâce aux délégués, il est généralement inutile de créer une sous-classe de NSWindow. Cette classe dispose d’un délégué et lui envoie des messages juste avant que la fenêtre ne soit redimensionnée, fermée ou autrement modifiée. Lorsque le délégué reçoit les messages envoyés par la fenêtre, il peut mettre en œuvre les traitements requis spécifiques à l’application, comme vérifier si la fenêtre contient des modifications non enregistrées avant qu’elle ne soit fermée et, dans l’affirmative, donner à l’utilisateur l’occasion de les enregistrer ou d’annuler l’opération. Le délégué peut faire partie du contrôleur et présenter un couplage faible avec les autres sous-systèmes. Il doit simplement mettre en œuvre les méthodes qui correspondent aux messages envoyés par la fenêtre. L’utilisation des délégués simplifie le développement des applications. Dans de nombreux cas, ce pattern est à la fois plus simple à implémenter et plus flexible que d’autres solutions. Le délégué a le choix d’implémenter certaines, toutes ou aucune des méthodes qui correspondent aux messages. Un même objet peut servir de délégué à plusieurs objets. Par exemple, toutes les fenêtres d’une application peuvent partager le même délégué. Inversement, chaque fenêtre peut avoir un délégué différent ou en changer au cours de l’exécution. Pour aller plus loin dans les motivations qui sous-tendent le pattern Délégué, examinons le côté client d’une application client-serveur hypothétique. Le client établit une
Chapitre 15
Délégué
197
seule connexion réseau à un serveur et affiche des fenêtres de données qui contiennent des informations fournies par ce serveur. Le client peut également afficher d’autres types de données, comme des panneaux de polices de caractères, des correcteurs orthographiques, des préférences des utilisateurs, etc. Lorsque l’utilisateur ferme la dernière fenêtre de données, on doit lui proposer de fermer la connexion réseau au serveur ou de la laisser ouverte pour une utilisation ultérieure. Étudions à présent les informations nécessaires à la mise en œuvre de cette fonctionnalité. Pour savoir si la fenêtre en cours de fermeture est la dernière fenêtre de données ouverte, nous avons besoin d’informations sur toutes les autres fenêtres de données ouvertes (couplage). La fermeture de la connexion réseau demande des informations détaillées sur les classes réseau (couplage). L’implémentation du comportement dans une classe dérivée de NSWindow introduit un couplage entre cette sous-classe, qui fait partie du sous-système Vue, et les classes réseau, qui ne font évidemment pas partie de ce sous-système. Par ailleurs, en donnant à chaque fenêtre de données ouverte des informations concernant les autres fenêtres de données ouvertes, nous pouvons facilement arriver à un code spaghetti difficile à maintenir, comme le suggère la Figure 15.1, où les flèches indiquent un couplage entre les objets. Figure 15.1 Couplage entre les objets obtenu par la création d’une sousclasse de NSWindow pour implémenter le comportement de l’application.
Fenêtre de données
Fenêtre de données
Fenêtre de données
Fenêtre de données
Fenêtre de données
Réseau
À l’opposé, lorsque le pattern Délégué est utilisé, chaque fenêtre possède uniquement des informations sur son délégué. La Figure 15.2 montre le couplage entre les fenêtres et l’objet qui sert de délégué à toutes ces fenêtres. Les lignes en pointillé indiquent un couplage faible. Chaque fenêtre sait uniquement que son délégué est en mesure de recevoir certains des messages qu’elle peut envoyer. Le délégué gère simplement un décompte des fenêtres de données ouvertes, sans informations particulières sur les fenêtres elles-mêmes.
198
Les design patterns de Cocoa
Figure 15.2
Fenêtre de données
Couplage entre les objets obtenu par l’utilisation du pattern Délégué. Fenêtre de données
Fenêtre de données
Délégué Fenêtre de données
Fenêtre de données
Réseau
Puisque le couplage entre les fenêtres de données et le délégué est faible, il est fort probable que des modifications puissent être apportées aux fenêtres ou au délégué sans que les autres objets en soient affectés. Cette flexibilité est la bienvenue. Voyons par exemple ce qui se passe lorsque l’application cliente est étendue pour que différentes fenêtres de données affichent des informations provenant de serveurs différents. La Figure 15.3 illustre une mise en œuvre possible qui bénéficie du pattern Délégué. Fenêtre de données
Fenêtre de données
Fenêtre de données
Délégué
Délégué
Fenêtre de données
Fenêtre de données
Réseau
Réseau
Figure 15.3 Couplage entre les objets obtenu par l’utilisation de plusieurs délégués.
Chapitre 15
Délégué
199
Le pattern Délégué étant utilisé, aucun code ne doit être modifié pour activer plusieurs connexions réseau. Le délégué de chaque fenêtre est affecté au moment de l’exécution. Lorsque la dernière fenêtre qui affiche des données provenant d’un serveur précis est fermée, le délégué peut offrir à l’utilisateur la possibilité de fermer la connexion à ce serveur sans que cela influe sur les connexions maintenues par les autres délégués. Les délégués permettent d’éviter l’un des besoins les plus fréquents d’utilisation de l’héritage multiple. Si l’objectif est de fermer une connexion réseau lorsque la dernière fenêtre qui affiche du contenu obtenu via cette connexion est fermée, il peut être tentant de créer une nouvelle classe qui hérite à la fois de la gestion d’une connexion réseau et de la fermeture d’une fenêtre. Cependant, l’héritage multiple n’est pas reconnu dans le langage Objective-C et le pattern Délégué propose une solution plus flexible et plus souple que la fusion de deux hiérarchies d’héritage distinctes. Le pattern Délégué est très général et peut être employé dans de nombreuses situations. Nous avons mis l’accent sur le délégué de NSWindow, mais uniquement pour présenter l’un des exemples les plus évidents. De nombreuses classes Cocoa utilisent des délégués comme alternative à l’héritage. Par exemple, la classe NSApplication se sert d’un délégué pour mettre en place des traitements particuliers, comme quand le démarrage d’une application Cocoa vient d’être terminé ou lorsqu’elle va être fermée. Le fait de disposer d’une alternative à l’héritage se révèle encore plus important lorsque les classes se complexifient. Certaines classes Cocoa, comme NSBrowser, sont suffisamment complexes pour que la création d’une sous-classe sans introduire d’erreurs constitue un véritable défi. À l’opposé, une classe conçue comme délégué d’une instance de NSBrowser peut dériver directement de NSObject et être implémentée très simplement, en se focalisant uniquement sur les particularités de l’application en cours de développement. INFO La documentation Apple des classes Cocoa qui utilisent un délégué inclut une section intitulée "Delegate Methods". Elle décrit les messages que les instances de la classe peuvent envoyer à un délégué, ainsi que les circonstances de leur envoi. Les objets qui jouent le rôle de délégué sont libres d’implémenter tout sous-ensemble des messages de délégué documentés.
15.2
Solution
Un délégué est un objet référencé à l’aide du type anonyme d’Objective-C, id. La référence au délégué est généralement une variable d’instance nommée delegate et les méthodes qui utilisent le pattern Accesseur (voir Chapitre 10) permettent de fixer ou de retourner le délégué courant. La classe MYBarView suivante illustre l’implémentation classique du code de prise en charge d’un délégué :
200
Les design patterns de Cocoa
@interface MYBarView : NSView { IBOutlet id delegate; //! Le délégué, s’il existe. NSColor *barColor; //! La couleur de la barre. float barValue; //! La valeur (entre 0.0 et 1.0) à représenter. } //! Accesseurs. - (id)delegate; - (void)setDelegate:(id)anObject; - (float)barValue; - (void)setBarValue:(float)aValue; - (NSColor *)barColor; - (void)setBarColor:(NSColor *)aColor; //! Actions. - (IBAction)takeBarValueFrom:(id)sender; @end
En Cocoa, la classe NSView représente une zone rectangulaire dans une fenêtre et permet d’effectuer des tracés dans cette zone. MYBarView est une sous-classe de NSView qui remplit une partie de la zone avec une barre colorée. La Figure 15.4 montre une application Cocoa simple qui affiche plusieurs instances de la classe MYBarView. Figure 15.4 Une application simple qui utilise plusieurs instances de MYBarView avec des délégués.
La classe MYBarView, implémentée dans la suite de cette section, permet à un délégué de contrôler le fonctionnement d’une instance. L’objet délégué peut aisément réaliser un
Chapitre 15
Délégué
201
contrôle, propre à l’application, sur l’intervalle des valeurs affichables par chaque instance de MYBarView. Il peut également implémenter des traitements spécifiques déclenchés par le changement d’une valeur, comme modifier la couleur de la barre en fonction de sa valeur. L’utilisation d’un délégué permet de personnaliser le comportement sans créer une sous-classe de MYBarView. Il est également possible de personnaliser chaque instance en utilisant des objets délégués différents pour chacune d’elles ou en fondant la logique du délégué sur l’instance particulière de MYBarView qui a invoqué la méthode. Implémenter la prise en charge du délégué Le premier élément de la prise en charge d’un délégué réside dans la variable d’instance delegate définie comme un IBOutlet de type id. Le type IBOutlet utilisé dans la déclaration d’une variable d’instance indique simplement à Interface Builder que cette variable accepte des connexions à d’autres objets (voir Chapitre 17). En déclarant la variable delegate de type id, le compilateur Objective-C sait que n’importe quel objet peut être utilisé comme délégué (voir Chapitre 7). Les méthodes -delegate et -setDelegate:(id)anObject sont des accesseurs (voir Chapitre 10) qui permettent aux programmeurs de fixer et d’obtenir les objets délégués pendant l’exécution. INFO Au cours du chargement d’un fichier .nib, la méthode -setDelegate: est invoquée automatiquement de manière à reconstruire les connexions à l’outlet délégué qui avaient été établies dans Interface Builder. La détermination automatique des noms des accesseurs est expliquée au Chapitre 10.
L’étape suivante consiste à définir les messages qui peuvent être envoyés au délégué. Le protocole formel Objective-C 2.0 suivant définit les messages avec le mot clé @optional pour que les délégués qui se conforment au protocole ne soient pas obligés d’implémenter toutes les méthodes : //! Un protocole formel définit les messages envoyés depuis MYBarView // à son délégué. @protocol MYBarViewDelegate @optional - (float)barView:(id)barView shouldChangeValue:(float)newValue; - (void)barViewWillChangeValue:(NSNotification *)aNotification; - (void)barViewDidChangeValue:(NSNotification *)aNotification; @end
Toutes les versions d’Objective-C employées avec Cocoa prennent en charge les protocoles informels. La catégorie suivante de NSObject est un protocole informel qui déclare les messages du délégué de MYBarView :
202
Les design patterns de Cocoa
//! Un protocole informel définit les messages envoyés depuis MYBarView // à son délégué. @interface NSObject (MYBarViewDelegateSupport) - (float)barView:(id)barView shouldChangeValue:(float)newValue; - (void)barViewWillChangeValue:(NSNotification *)aNotification; - (void)barViewDidChangeValue:(NSNotification *)aNotification; @end
Les catégories et les protocoles informels sont des caractéristiques d’Objective-C très employées par Cocoa. Certains messages au délégué Cocoa prennent en argument un NSNotification. Les notifications sont décrites par le pattern Notification au Chapitre 14. Ce pattern est étroitement lié au pattern Délégué. En réalité, à certains messages de délégué fournis par la classe MYBarView correspondent des notifications. Cette pratique, fournir à la fois des messages de délégué et des notifications, est courante dans Cocoa et est illustrée par la classe MYBarView. Lorsque des notifications sont utilisées, il est important de leur attribuer des noms uniques. Les notifications postées par MYBarView correspondent aux messages de délégué connexes : //! Noms des notifications. extern NSString *MYBarViewDidChangeValueNotification; extern NSString *MYBarViewWillChangeValueNotification;
Nommer les messages de délégué Il existe une convention pour nommer les messages envoyés aux délégués. Chaque nom de message commence par identifier le type de l’objet qui envoie le message. Ainsi, les noms des messages de délégué émis par la classe MYBarView commencent par barView. Ensuite, ils comprennent l’un des trois verbes suivants : "should", "will" ou "did". Les messages qui utilisent should sont supposés retourner une valeur et prennent généralement en argument l’identification de l’objet qui envoie le message. Ils sont envoyés au délégué avant un changement dans l’objet émetteur. Le délégué a l’opportunité d’intervenir sur le changement. Par exemple, la classe Cocoa NSText envoie le message -textShouldBeginEditing: à son délégué et attend en retour une valeur booléenne. Si le délégué retourne NO, l’édition ne commence pas. Ainsi, le délégué influe sur le comportement de l’objet qui envoie les messages. Les messages qui utilisent will ne sont pas supposés retourner une valeur. Ils sont envoyés avant un changement et sont strictement informatifs. Suite à la réception d’un tel message, le délégué peut implémenter une méthode pour synchroniser l’état de l’application ou effectuer un traitement supplémentaire. Enfin, les messages did sont envoyés après un changement. Ils sont également strictement informatifs et donnent au délégué l’opportunité d’effectuer un traitement après le changement.
Chapitre 15
Délégué
203
La déclaration de l’interface de MYBarView contient toutes les informations nécessaires à la configuration d’une instance de MYBarView dans Interface Builder et à sa connexion à un délégué. Avant d’examiner l’écriture du délégué lui-même, voyons comment mettre en œuvre l’utilisation du délégué dans la classe MYBarView. L’intégralité du code source de cette classe est disponible dans l’archive des exemples de cet ouvrage. Une application illustre toutes les caractéristiques de la classe, y compris le tracé et l’initialisation. Le code suivant se focalise sur la prise en charge du délégué, indépendamment des autres fonctionnalités de MYBarView. Les accesseurs de MYBarView restent classiques et sont présentés pour mettre en avant ce qu’ils ne contiennent pas. En général, les accesseurs ne doivent pas envoyer des messages au délégué, car celui-ci les invoque. Le faire risque de conduire à une récursion infinie. //! Accesseurs. - (float)barValue //! Retourner la valeur du récepteur. { return barValue; } - (void)setBarValue:(float)aValue //! Fixer la valeur du récepteur. { barValue = aValue; [self setNeedsDisplay:YES]; } - (NSColor *)barColor //! Retourner la couleur du récepteur. { return barColor; } - (void)setBarColor:(NSColor *)aColor //! Fixer la couleur du récepteur. { [aColor retain]; [barColor release]; barColor = aColor; [self setNeedsDisplay:YES]; }
Les méthodes -(void)setBarValue:(float)aValue et -(void)setBarColor:(NSColor *)aColor se fondent sur les implémentations classiques des accesseurs pour, respectivement, une propriété non objet et une propriété objet. Dans Cocoa, la classe NSColor encapsule des couleurs et le code de -setBarColor: est dès lors plus classique. Il existe une distinction cruciale entre un accesseur standard comme -setBarColor: et celui qui fixe l’objet délégué :
204
Les design patterns de Cocoa
- (id)delegate //! Retourner le délégué du récepteur. { return delegate; } - (void)setDelegate:(id)anObject //! Fixer le délégué du récepteur. { delegate = anObject; // Note : non retenu ! }
L’objet couleur fixé par -setBarColor: est retenu, contrairement à l’objet délégué fixé par -(void)setDelegate:(id)anObject. La nuance est subtile, mais importante pour éviter les cycles de retenue. Un cycle de retenue (retain cycle) se produit lorsque deux objets ou plus conservent chacun une référence sur l’autre. On arrive alors à une situation où aucun des objets ne peut être désalloué car chacun est toujours utilisé par l’autre. Les conventions Cocoa de gestion de la mémoire sont brièvement expliquées lors de la description du pattern Accesseur (voir Chapitre 10) et les problèmes liés aux cycles de retenue sont décrits dans la section Core Library > Cocoa > Objective-C Language > The Objective-C 2.0 Programming Language > Memory Management de la documentation Xcode. La philosophie générale est la suivante : puisque les objets qui acceptent un délégué peuvent parfaitement fonctionner sans délégué, le délégué n’est pas une propriété critique qui doit être retenue. Les objets ne "possèdent" pas leurs délégués et ne doivent donc pas les retenir. Les objets implémentés dans les frameworks Cocoa ne retiennent pas leurs délégués. Si un objet possède toujours un pointeur sur son délégué après la désallocation de celui-ci, des erreurs, comme le plantage de l’application, peuvent résulter de l’envoi de messages au délégué désalloué. Par conséquent, les objets qui servent de délégués doivent implémenter -dealloc de manière à envoyer des messages -setDelegate:nil de façon qu’aucun autre objet ne possède des pointeurs sur l’objet qui va être désalloué. Le code des accesseurs de MYBarView est donné ici pour souligner les différences entre l’implémentation des délégués et celle des autres propriétés. Avec Objective-C 2.0, nous pourrions utiliser des déclarations @property dans l’interface de MYBarView : @property (assign, nonatomic, readwrite) IBOutlet id delegate; @property (retain, nonatomic, readwrite) NSColor *barColor; @property (nonatomic, readwrite) float barValue;
L’implémentation de la méthode -(IBAction)takeBarValueFrom:(id)sender de la classe MYBarView appelle des méthodes privées de MYBarView pour envoyer des messages de délégué à un objet délégué :
Chapitre 15
Délégué
205
//! Actions. - (IBAction)takeBarValueFrom:(id)sender /*! Fixer la valeur du récepteur à la variable floatValue de l’émetteur. Le délégué du récepteur a l’opportunité de modifier la nouvelle valeur avant qu’elle ne soit affectée et le délégué est informé de la modification avant et après l’affectation de la valeur. */ { float newValue = [sender floatValue]; newValue = [self _myBarShouldChangeValue:newValue]; [self _myBarWillChangeValue]; [self setBarValue:newValue]; [self _myBarDidChangeValue]; }
La méthode -takeBarValueFrom: est typique des actions décrites au Chapitre 17. Elle peut être invoquée par n’importe quel objet Cocoa qui utilise le pattern Outlet, cible et action et peut être connectée dans Interface Builder. L’implémentation de la méthode appelle trois méthodes privées pour envoyer indirectement des messages aux délégués. La première méthode privée se nomme -(float)_myBarShouldChangeValue:(float) newValue. - (float)_myBarShouldChangeValue:(float)newValue /*! Donner au délégué l’opportunité de changer la nouvelle valeur. */ { if([[self delegate] respondsToSelector: @selector(barView:shouldChangeValue:)]) { newValue = [[self delegate] barView:self shouldChangeValue:newValue]; } return newValue; }
INFO En Objective-C, n’importe quel message peut être envoyé à n’importe quel récepteur. Les méthodes sont privées uniquement par le fait qu’elles ne sont pas mentionnées dans l’interface d’une classe au travers d’un fichier d’en-tête public. La convention de nommage de ces méthodes privées, c’est-à-dire en débutant leur nom par un souligné, réduit les risques qu’un programmeur les redéfinisse ou les invoque par mégarde.
Si le délégué sait répondre à un message -barView:shouldChangeValue:, il lui est envoyé et la valeur retournée par le délégué est utilisée comme nouvelle valeur pour MYBarView. Si le délégué ne prend pas en charge -barView:shouldChangeValue:, la nouvelle valeur est utilisée telle quelle. Un délégué qui implémente -barView:shouldChangeValue: peut refuser la modification en retournant la valeur existante de l’objet appelant. De même, il peut limiter ou retourner un multiple de la valeur.
206
Les design patterns de Cocoa
Le message de délégué suivant est envoyé par la méthode -(void)_myBarWillChangeValue : - (void)_myBarWillChangeValue /*! Signaler au délégué et au centre de notification par défaut que la valeur va changer. */ { NSNotification *notification; notification = [NSNotification notificationWithName: MYBarViewWillChangeValueNotification object:self]; if([[self delegate] respondsToSelector: @selector(barViewWillChangeValue:)]) { [[self delegate] barViewWillChangeValue:notification]; } [[NSNotificationCenter defaultCenter] postNotification:notification]; }
Une instance temporaire de NSNotification est créée et initialisée avec un nom et avec l’instance de MYBarView qui poste la notification. Juste avant que la notification ne soit envoyée au centre de notification par défaut, on vérifie si le délégué sait répondre au message -barViewWillChangeValue:. Dans l’affirmative, le message est envoyé avec l’objet de notification en argument. Le délégué qui reçoit le message peut utiliser la propriété object de la notification pour déterminer la barre qui a envoyé le message. La classe NSNotification et sa propriété object sont décrites dans la documentation d’Apple. La méthode privée -(void)_myBarDidChangeValue est comparable à la méthode -_myBarWillChangeValue: et est invoquée après que la valeur de la barre a changé. - (void)_myBarDidChangeValue /*! Signaler au délégué et au centre de notification par défaut que la valeur a changé. */ { NSNotification *notification; notification = [NSNotification notificationWithName: MYBarViewDidChangeValueNotification object:self]; if([[self delegate] respondsToSelector: @selector(barViewDidChangeValue)]) { [[self delegate] barViewDidChangeValue:notification]; } [[NSNotificationCenter defaultCenter] postNotification:notification]; }
Chapitre 15
Délégué
207
Implémenter un délégué Pour mettre en œuvre un objet qui joue le rôle de délégué, il suffit d’implémenter les méthodes qui correspondent aux messages de délégué devant être reçus. Dans l’exemple suivant, la classe MYValueLimitColorChanger propose une implémentation pour uniquement deux des trois messages de délégué qu’un objet MYBarView peut envoyer. Elles contraignent une barre afin qu’elle ne représente pas une valeur inférieure à 0,25 et qu’elle modifie sa couleur en fonction de cette valeur. Il s’agit de comportements arbitraires, mais ils représentent parfaitement le rôle d’un délégué dans la personnalisation d’une application. @implementation MYValueLimitColorChanger //! Messages de délégué. - (float)barView:(id)barView shouldChangeValue:(float)newValue { float result = newValue; if(0.25f > result) { result = 0.25f; } return result; } - (void)barViewDidChangeValue:(NSNotification *)aNotification { if(0.75f < [[aNotification object] barValue]) { [[aNotification object] setBarColor:[NSColor blackColor]]; } else { [[aNotification object] setBarColor:[NSColor grayColor]]; } } @end
Une instance de MYValueLimitColorChanger peut être créée dans Interface Builder et connectée en tant que délégué d’autant d’instances de MYBarView que souhaité. Il est également possible de créer une instance de MYValueLimitColorChanger dans le code et d’en faire le délégué d’une ou de plusieurs barres en invoquant la méthode -setDelegate: de MYBarView. La classe MYValueLimitColorChanger est aussi simple que possible. Les seules méthodes qu’elle ajoute à sa super-classe sont celles destinées au traitement des messages de délégué. La véritable puissance de la délégation apparaît plus nettement dans les situa-
208
Les design patterns de Cocoa
tions plus complexes. Le délégué peut interagir avec la vue de la barre conformément à une logique applicative complexe qui dépend de nombreux autres objets. La classe MYValuePropagator implémente uniquement une méthode déléguée de MYBarView. Elle fixe la valeur d’une autre barre à celle de la barre qui envoie le message. @class MYBarView; @interface MYValuePropagator : NSObject { IBOutlet MYBarView *barViewToControl; }
//! l’objet à contrôler
//! Déclarer un accesseur avec la directive @property d’Objective-C 2.0. @property (readwrite, retain, nonatomic) IBOutlet MYBarView *barViewToControl; @end -----------@implementation MYValuePropagator //! Laisser le compilateur Objective-C 2.0 générer le code de l’accesseur. @synthesize barViewToControl; - (void)barViewDidChangeValue:(NSNotification *)aNotification { if([aNotification object] != [self barViewToControl]) { [[self barViewToControl] setBarValue: [[aNotification object] barValue]]; } } @end
Sources de données Les sources de données (data sources) sont comparables aux délégués, mais leur rôle diffère. Les délégués réagissent aux modifications ou contrôlent d’autres objets. Une source de données fournit des données à un autre objet lorsqu’il en a besoin. Les délégués sont toujours facultatifs ; l’objet qui utilise un délégué se replie sur un comportement par défaut lorsque aucun délégué ne lui a été affecté. Un objet qui utilise une source de données ne sera probablement pas opérationnel sans une source de données valide. Par exemple, la classe Cocoa NSTableView retrouve des données à partir d’une source de données en fonction de ses besoins. L’utilisation d’une source de données apporte plusieurs avantages. Tout d’abord, elle préserve la séparation entre les sous-systèmes du pattern MVC. Le tracé de la table et les fonctionnalités d’édition de NSTableView appar-
Chapitre 15
Délégué
209
tiennent clairement au sous-système Vue. Le calcul, l’obtention et la mémorisation des données à afficher font évidemment partie du sous-système Modèle. Les calculs et la mémorisation des données ne changent pas même si le mécanisme d’affichage évolue. Les mêmes données du modèle pourraient être affichées dans un graphique en secteurs, enregistrées dans un fichier ou imprimées. L’objet qui sert de source de données à une instance de NSTableView se trouve habituellement dans le sous-système Contrôleur. La source de données répond aux demandes de NSTableView en obtenant les données à partir du modèle. NSTableView est indépendante des détails d’obtention des données depuis le modèle. De même, les classes du modèle ne sont pas liées aux objets de la vue qui affichent les données. L’emploi d’une source de données conduit également à une solution efficace vis-à-vis du traitement des données et de l’utilisation de la mémoire. Par exemple, même si la table contient des millions de lignes, seules quelques dizaines sont affichées simultanément à l’écran. NSTableView demande donc à sa source de données uniquement les données qui peuvent être affichées dans les lignes visibles. Si le modèle doit calculer les données ou si elles doivent être récupérées à partir d’une base de données au travers du réseau, il est inutile de calculer ou de lire un million de lignes de données à la fois. À l’instar d’un délégué, l’objet source de données n’est pas retenu par l’objet qui l’utilise. Une seule source de données peut fournir des données à plusieurs objets. Dans la documentation des classes qui exigent une source de données, Apple précise les messages qui seront envoyés à la source de données. Comme pour les délégués, il existe en général un protocole informel qui déclare les méthodes qu’une source de données doit implémenter.
15.3
Exemples dans Cocoa
Les classes Cocoa suivantes utilisent un délégué : NSApplication, NSBrowser, NSControl, NSDrawer, NSFontManager, NSFontPanel, NSMatrix, NSOutlineView, NSSplitView, NSTableView, NSTabView, NSText, NSTextField, NSTextView et NSWindow. Les classes NSOutlineView et NSTableView utilisent également une source de données. Plusieurs classes Objective-C du framework WebKit d’Apple utilisent des sources de données et des délégués. Quasiment chaque application Cocoa graphique non triviale comprend un objet qui joue le rôle de délégué pour l’instance partagée de NSApplication de l’application. La classe NSApplication déclare un peu plus d’une vingtaine de méthodes de délégué. Elles vont de -(void)application:(NSApplication *)sender openFiles:(NSArray *) filenames, que vous pouvez implémenter pour contrôler l’ouverture des fichiers dans votre application, à -(NSApplicationTerminateReply)applicationShouldTerminate: (NSApplication *)sender, qui permet de contrôler la terminaison de l’application.
210
Les design patterns de Cocoa
Avant d’envisager l’écriture d’une sous-classe d’une classe Cocoa, vérifiez si vous ne pouvez pas atteindre votre objectif au travers d’une méthode de délégué.
15.4
Conséquences
Grâce au pattern Délégué, la nécessité de créer des sous-classes des classes Cocoa pour mettre en œuvre un comportement spécifique à l’application est énormément réduite. Une grande partie des classes Cocoa les plus complexes comme NSApplication, NSBrowser, NSTableView, NSText et NSWindow font rarement, voire jamais, l’objet d’une sous-classe car le pattern Délégué constitue une meilleure solution. Le pattern Délégué diminue le couplage entre les objets. L’héritage crée le couplage le plus étroit possible entre la classe dérivée et sa classe mère. Le pattern Délégué crée une relation beaucoup moins forte grâce aux objets anonymes. Le couplage entre un objet et son délégué est si faible que l’objet peut fonctionner sans aucun délégué et un délégué est libre d’implémenter uniquement un sous-ensemble des méthodes de délégué. Le pattern Délégué apporte une flexibilité à l’exécution. Chaque instance d’une classe qui utilise les délégués peut posséder son propre délégué. Chaque délégué d’un objet peut être indiqué dans Interface Builder ou à l’exécution et peut être changé en fonction des besoins au cours de l’exécution. Dans une application MVC, l’utilisation d’une source de données simplifie la séparation entre la vue et le modèle. Comme dans le cas des délégués, l’utilisation du type id d’Objective-C diminue le couplage entre la source de données et l’objet qui a besoin des données. La réutilisation des objets dans les sous-systèmes Vue et Modèle est ainsi plus facile, tout en gardant la possibilité de mettre en œuvre un comportement spécifique à l’application. Néanmoins, le besoin d’un délégué ou d’une source de données doit être anticipé par le concepteur d’une classe. Si la prise en charge des délégués n’est pas prévue ou si les messages de délégué ne sont pas envoyés au moment opportun, l’implémentation d’un comportement spécifique à l’application devra sans doute passer par l’héritage. La prise en charge d’un délégué ou d’une source de données dans vos classes exige plusieurs lignes de code et le respect de certaines conventions d’utilisation d’Objective-C et de Cocoa. Par exemple, il est crucial que l’objet qui utilise un délégué ou une source de données ne retienne pas le délégué ou la source de données, même si cela semble contraire aux conventions standard de Cocoa. En effet, vous évitez ainsi les cycles de retenue, mais cela signifie également que vous devez faire attention à l’ordre de désallocation des objets. Si une source de données est désallouée avant l’objet qui l’utilise, des erreurs d’exécution peuvent se produire.
16 Hiérarchie Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Dans Cocoa, les hiérarchies sont souvent employées pour organiser les objets. Une hiérarchie d’objets de vue organise et contrôle les systèmes de tracé et de coordonnées dans Cocoa. Le pattern Chaîne de répondeurs (voir Chapitre 18) exploite la hiérarchie de vues. Les hiérarchies se rencontrent également dans de nombreux modèles de données et arbres syntaxiques, en raison de leur nature intrinsèquement hiérarchique. Les hiérarchies définissent des relations entre des objets de manière à lever toute ambiguïté sur les objets responsables de la mémorisation des autres objets. Par ailleurs, elles constituent une alternative à la création de sous-classes. Dans Cocoa, elles sont utilisées lorsque des objets présentent une relation "a un", tandis que les sous-classes sont employées dans le cas d’une véritable relation "est un". Dans Cocoa, les hiérarchies sont étroitement associées au bien connu pattern Composite.
16.1
Motivation
L’idée est d’exprimer une relation "a un" entre des objets. Un groupe d’objets coopérant est considéré comme un seul objet. La personnalisation doit être possible sans passer par des sous-classes, en permettant aux objets d’un groupe d’être reconfigurés ou remplacés par d’autres. La complexité d’une classe parente doit pouvoir être réduite en partageant la responsabilité des données et des comportements entre les objets enfants. Un groupe d’objets doit être traité de la même manière qu’un seul objet.
212
16.2
Les design patterns de Cocoa
Solution
Les développeurs sont fréquemment confrontés à des données dont le stockage, la manipulation et le parcours seront plus efficaces si elles sont mémorisées dans une structure arborescente. Souvent, ces avantages s’appliquent également aux objets d’une application s’ils sont organisés en structure arborescente. Par exemple, les applications de dessin vectoriel, de mise en page ou de création de diagrammes permettent souvent de regrouper des objets afin que l’utilisateur puisse déplacer une figure complexe comme s’il s’agissait d’un seul objet graphique. En général, ces applications proposent également une bibliothèque ou une palette d’objets prédéfinis. En interne, une classe de base, comme MYGraphic, sert souvent de parent à tous les éléments graphiques manipulés par l’utilisateur. Ces éléments graphiques sont des sousclasses comme MYSquare, MYCircle, etc. Pour mettre en œuvre les groupes d’objets, une classe nommée MYGroup peut être créée, par dérivation de MYGraphic. Avec un tel modèle de données, un groupe d’objets peut être traité comme n’importe quel autre objet graphique normal. Il peut être redimensionné, conduisant alors au redimensionnement de tous les objets graphiques enfants, qui conservent leur taille relative. Le code qui envoie le message de redimensionnement à l’objet graphique n’a pas besoin de savoir s’il concerne un objet ou plusieurs. Pour obtenir le rectangle englobant d’un objet graphique, le groupe cumule les rectangles de tous ses enfants et retourne un seul rectangle qui les englobe tous. Le code qui demande le rectangle englobant obtient la réponse souhaitée, que la requête concerne un seul objet graphique ou un groupe, sans qu’il ait à faire la différence. Les applications graphiques ne sont pas les seules à bénéficier de ces structures. Par exemple, prenons un sous-système qui analyse et exécute des scripts. Outre les instructions individuelles, il est possible d’écrire des blocs d’instructions. En Objective-C, un bloc correspond au code qui se trouve entre des accolades. Les boucles et les instructions if/then contrôlent l’exécution des blocs. Puisqu’un bloc n’est qu’un ensemble d’instructions regroupées, l’analyse syntaxique d’un script fait émerger une structure hiérarchique. Les éléments d’un script analysé peuvent être représentés par des objets MYStatement. Les blocs sont représentés par des objets de la classe MYBlock, qui est une sous-classe de MYStatement. En considérant un bloc comme une sorte d’instruction, le code de manipulation d’un script devient beaucoup plus simple. Au lieu de vérifier s’il s’agit d’une instruction ou d’un bloc d’instructions, le code d’exécution du script peut simplement invoquer une méthode -execute définie par la classe MYStatement. Ainsi, les instructions individuelles et les blocs d’instructions sont traités de la même manière.
Chapitre 16
Hiérarchie
213
À un moment donné, il faut néanmoins prendre une décision sur le code à exécuter, car le code d’exécution d’une instruction individuelle est évidemment différent de celui employé pour exécuter un bloc. Le code d’exécution d’un bloc a besoin d’une boucle pour exécuter chacune des instructions, certaines d’entre elles pouvant être d’autres blocs. Cependant, au lieu d’obliger le code appelant à faire la différence entre des instructions et des blocs et à invoquer le code d’exécution correspondant, il est possible d’exploiter le moteur d’exécution d’Objective-C. Selon la classe de l’objet courant, le code approprié sera invoqué automatiquement en respectant la hiérarchie des objets analysés. Implémenter une hiérarchie Revenons à l’exemple d’une application qui manipule des objets graphiques et des groupes d’objets graphiques. Le code de prise en charge d’une structure hiérarchique est simple, à tel point que nous pouvons montrer les parties des classes MYGraphic et MYGroup qui concernent la hiérarchie sans prendre une application spécifique en référence. Supposons que la classe MyGraphic présente l’interface simplifiée suivante : @interface MYGraphic : NSObject { NSRect bounds; } - (NSRect)bounds; - (void)draw; @end
Dans une application réelle, cette classe offrirait également des méthodes pour définir les limites et des accesseurs pour d’autres attributs de l’objet graphique, comme la couleur, l’épaisseur du tracé, l’angle de rotation, etc. Puisqu’il s’agit d’une classe de base abstraite, l’implémentation des méthodes mentionnées est très simple : @implementation MYGraphic - (id)init { self = [super init]; if (!self) return nil; bounds = NSMakeRect(0.0, 0.0, 0.0, 0.0); return self; } - (NSRect)bounds { return bounds; }
214
Les design patterns de Cocoa
- (void)draw { // Redéfinie par les sous-classes de manière à effectuer le tracé réel. } @end
Pour mettre en œuvre un objet MYGroup, des méthodes de manipulation des objets enfants sont nécessaires. Un tableau est utilisé pour mémoriser les enfants. Voici une proposition d’interface : @interface MYGroup : MYGraphic { NSMutableArray *children; } - (void)addChild:(MYGraphic *)aChild; - (NSArray *)children; @end
Outre l’écriture des nouvelles méthodes de prise en charge des objets enfants, nous devons également redéfinir les méthodes -bounds et -draw de manière à parcourir tous les enfants. Dans le cas de -bounds, le rectangle englobant résultant est enregistré dans la variable d’instance bounds héritée. Une autre solution serait d’actualiser cette variable dès qu’un enfant est ajouté ou retiré, ce qui nous éviterait de redéfinir la méthode -bounds. Voici une implémentation possible : @implementation MYGroup - (id)init { self = [super init]; if (!self) return nil; children = [[NSMutableArray alloc] init]; return self; } - (void)dealloc { [children release]; [super dealloc]; } - (void)addChild:(MYGraphic *)aChild { [children addObject: aChild]; } - (NSArray *)children { return [[children copy] autorelease]; }
Chapitre 16
Hiérarchie
215
- (NSRect)bounds { if ([children count] == 0) { bounds = NSZeroRect; return bounds; } else { bounds = [[children objectAtIndex:0] bounds]; for (MYGraphic *child in children) { bounds = NSUnionRect(bounds, [child bounds]); } } return bounds; } - (void)draw { for (MYGraphic *child in children) { [child draw]; } } @end
Lorsqu’on manipule une sous-classe de MYGraphic, le code appelant peut obtenir le rectangle englobant en invoquant la méthode -bounds sans être obligé de savoir si des objets graphiques enfants existent. De même, pour le tracé, il est inutile de faire la distinction entre un objet graphique individuel et un groupe. L’objet MYGroup passe simplement le message -draw à chacun des enfants. Il est parfois utile que les objets enfants possèdent un pointeur sur leur objet parent. L’ajout de ces pointeurs permet de faciliter la mise en œuvre des comportements dépendant du contexte. Les pointeurs sur les objets parents doivent être actualisés dès qu’un objet est ajouté en tant qu’enfant d’un autre ou retiré. Pour illustrer l’utilité de ces pointeurs, voyons comment les objets NSView, décrits plus loin dans ce chapitre, font également partie d’une chaîne de répondeurs (voir Chapitre 18). La chaîne de répondeurs n’existerait pas si les vues ne possédaient pas des pointeurs sur leur objet parent, également appelé vue supérieure. Pour la mise en œuvre d’une hiérarchie, nous pouvons organiser le code comme dans l’exemple précédent, mais également selon deux autres approches. La première consiste à placer des messages prototypes de gestion des enfants, comme -addChild:, dans l’interface de la classe MYGraphic, avec une implémentation par défaut qui lance une exception. Cette solution permet de traiter uniformément tous les objets de la hiérarchie, mais au risque de lancer des exceptions à l’exécution. Si l’ajout de cercles à un carré n’a pas beaucoup de sens, leur ajout à un groupe d’objets peut en avoir.
216
Les design patterns de Cocoa
La deuxième manière d’organiser et d’implémenter le code serait de déplacer les messages de gestion des enfants dans la classe MYGraphic et d’y mettre également la variable d’instance children et le code de gestion des enfants. Dans ce cas, aucune exception d’exécution ne serait lancée lorsqu’un enfant est ajouté à un autre objet graphique. Cette solution présente néanmoins un inconvénient. En effet, tout au moins pour ce modèle de données, autoriser chaque sorte d’objet graphique à contenir des enfants n’a pas de sens. Pourtant, Cocoa utilise cette dernière approche avec sa hiérarchie NSView. Toute vue Cocoa est en mesure de contenir des vues secondaires. La hiérarchie des vues de Cocoa Une application Cocoa graphique possède des fenêtres, et chaque fenêtre contient une hiérarchie d’objets NSView. Même si NSView est une classe abstraite, elle comprend tout le code et les variables qui permettent d’ajouter et de manipuler des objets enfants, appelés vues inférieures. En réalité, malgré sa nature abstraite, il existe des instances réelles de NSView au sommet des hiérarchies de vues dans la plupart des fenêtres Cocoa. La Figure 16.1 présente une hiérarchie de vues type. L’interface utilisateur se trouve à gauche et les vues de la fenêtre sont recensées à droite. L’objet NSView qui se trouve au sommet de la hiérarchie correspond à la vue de contenu de la fenêtre. Les quatre vues qui se trouvent en dessous sont ses vues inférieures, et ainsi de suite.
NSView
NSButton
NSSlider
NSScrollView
NSClipView
NSTextField
NSScroller
NSTextView
Figure 16.1 Un exemple de hiérarchie de vues.
La Figure 16.1 montre bien que la plupart des éléments Cocoa d’interface utilisateur plus complexes, comme les éditeurs de texte, sont en réalité composés de plusieurs sous-classes de NSView qui coopèrent. NSTableView est un autre exemple de ces objets
Chapitre 16
Hiérarchie
217
complexes. L’objet NSTableView ou NSTextView réel se trouve en bas de la hiérarchie constituée des instances de NSScrollView, de NSClipView et de NSScroller. La véritable puissance de cette conception se révèle lorsque des vues standard doivent être personnalisées. Par exemple, supposons qu’un développeur souhaite ajouter un bouton pour sélectionner l’échelle d’une vue contrôlée par une instance de NSScrollView. Le bouton peut être ajouté en tant que vue inférieure d’un NSScrollView et la méthode -tile peut être redéfinie dans une sous-classe. L’implémentation de -tile doit simplement fixer les positions des vues inférieures afin qu’elles soient agencées correctement, l’une à côté de l’autre. Vous trouverez un exemple d’une telle implémentation dans le dossier /Developer/Examples/AppKit/Sketch. Néanmoins, la création d’une sous-classe est rarement obligatoire. L’agencement d’une interface utilisateur complexe se fait habituellement dans Interface Builder avec des classes Cocoa standard. De manière générale, Cocoa favorise la construction des interfaces utilisateurs par composition, non par création de sous-classes. Pour les développeurs, cela permet de réduire le nombre de classes à maintenir et simplifie souvent le code à écrire. Systèmes de coordonnées dans la hiérarchie de vues Chaque objet NSView dispose de son propre système de coordonnées. Un objet vue prend le coin inférieur gauche de la vue pour origine des tracés. Par conséquent, chaque vue gère en réalité deux rectangles. bounds correspond au rectangle qui entoure la zone de tracé de la vue, tout comme frame. Toutefois, bounds se fonde sur le système de coordonnées de la vue, tandis que frame est donné dans le système de coordonnées de la vue supérieure. Il est ainsi possible de convertir des points entre les différents systèmes de coordonnées des vues. Puisque les rectangles bounds et frame peuvent avoir des tailles et des origines différentes, le passage d’un système de coordonnées à l’autre ne se fait pas nécessairement par simple conversion. Il est possible qu’une mise à l’échelle soit également impliquée. En raison de la complexité inhérente à la conversion des points d’un système de coordonnées à l’autre, la classe Cocoa NSView propose plusieurs méthodes d’aide. Pour effectuer les conversions, NSView définit les méthodes -convertPoint:fromView: et -convertPoint:toView:, ainsi que -convertSize:... et -convertRect:.... Dans la version fromView de ces méthodes, un point, une taille ou un rectangle sont convertis depuis le système de coordonnées de la vue dans celui du récepteur. Les méthodes toView opèrent dans le sens inverse, depuis le système de coordonnées du récepteur dans celui de la vue en argument. Si l’argument view est nil, la conversion se fait vers ou depuis le système de coordonnées de la fenêtre. La seule contrainte est que les deux vues, le récepteur et l’argument, se trouvent dans la même fenêtre. Autrement dit, elles doivent toutes deux faire partie de la même hiérarchie.
218
Les design patterns de Cocoa
L’existence de plusieurs systèmes de coordonnées peut amener une certaine confusion. On peut donc raisonnablement se demander pourquoi Cocoa se donne la peine d’offrir cette fonctionnalité. L’une des principales raisons vient du fait que des groupes de vues peuvent être repositionnés et redimensionnés l’un par rapport à l’autre. Des hiérarchies complètes de vues peuvent être déplacées dans une fenêtre ou d’une fenêtre à une autre, en modifiant uniquement le cadre de la vue au sommet de la hiérarchie. Cela simplifie énormément la manipulation de plusieurs vues. Grâce aux systèmes de coordonnées séparés de chaque vue, le code de tracé dans une vue est également plus facile à écrire, car il n’a pas à tenir compte des conversions et des mises à l’échelle. Parcourir la hiérarchie de vues Pour comprendre la mise en œuvre d’un tout, l’une des meilleures solutions consiste à examiner chaque élément individuel dans son contexte. Dans le but de comprendre l’assemblage des différents objets d’interface Cocoa, le parcours d’une hiérarchie de vues se révèle souvent plus utile que la simple lecture de leur documentation. Tel est l’objectif de l’application ViewFinder, que vous trouverez dans l’archive des codes sources de cet ouvrage. L’interface principale est un objet NSBrowser qui affiche la liste de toutes les vues présentes dans la fenêtre "principale" courante de l’application. Il suffit de cliquer sur une fenêtre pour en faire la fenêtre principale, puis de sélectionner n’importe quelle vue listée par le navigateur pour l’entourer d’un rectangle. La Figure 16.2 présente l’application en cours d’exécution.
Figure 16.2 L’interface de l’application ViewFinder.
Chapitre 16
Hiérarchie
219
Pour mettre en exergue les vues sélectionnées, nous utilisons une fenêtre superposée. L’application ViewFinder se fonde sur une sous-classe personnalisée de NSView qui trace son rectangle englobant. Cette vue est ensuite placée dans une fenêtre transparente, superposée à la fenêtre courante à l’écran. Puisque la vue n’a besoin d’aucune variable d’instance, l’en-tête de la classe indique uniquement qu’elle dérive de NSView : #import @interface MYHighlightingView : NSView { } @end
Le code trace un rectangle rouge semi-transparent aux limites de la vue. Puisque la vue n’est pas opaque, les clics de souris passent au travers et atteignent la fenêtre qui se trouve en dessous. Le fonctionnement de la fenêtre ou de la vue du dessous n’est donc pas perturbé par sa mise en exergue. #import "MYHighlightingView.h" @implementation MYHighlightingView - (void)drawRect:(NSRect)rect { NSRect myBounds = [self bounds]; [[[NSColor redColor] colorWithAlphaComponent:0.5] set]; NSFrameRectWithWidthUsingOperation(myBounds, 2.0, NSCompositeSourceOver); } - (BOOL)isOpaque { return NO; } @end
La classe contrôleur de l’application et le délégué de NSBrowser dans l’interface utilisateur graphique sont représentés par la classe MYViewFinderController. L’en-tête définit quelques outlets qui doivent être connectés aux éléments de l’interface utilisateur dans le panneau du navigateur. Il définit également la méthode -browserSelectionChanged:, qui doit être l’action envoyée par l’objet NSBrowser. La liste browserPath sert à conserver une trace de la sélection de l’utilisateur dans le NSBrowser. #import @interface { IBOutlet IBOutlet IBOutlet IBOutlet
MYViewFinderController : NSObject NSBrowser NSTextField NSTextField NSTextField
*browser; *viewClassField; *windowPositionField; *sizeField;
220
Les design patterns de Cocoa
NSWindow NSMutableArray NSWindow
*viewedWindow; *browserPath; *highlightWindow;
} - (void)mainWindowChanged:(id)sender; - (IBAction)browserSelectionChanged:(id)sender; @end
L’implémentation débute par la méthode -init. Elle alloue la liste browserPath et crée la fenêtre qui servira à surligner les objets NSView sélectionnés. Puisque la fenêtre n’est pas opaque, avec un arrière-plan incolore, elle ignore tous les événements. De cette manière, la vue mise en exergue reste utilisable. La méthode -dealloc contient le code de nettoyage habituel. #import "MYViewFinderController.h" #import "MYHighlightingView.h" @implementation MYViewFinderController - (id)init { if (nil != (self = [super init])) { browserPath = [[NSMutableArray alloc] init]; highlightWindow = [[NSWindow alloc] initWithContentRect:NSMakeRect(-10.0, 10.0, 5.0, 5.0) styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; [highlightWindow setBackgroundColor: [NSColor clearColor]]; [highlightWindow setAlphaValue:1.0]; [highlightWindow setOpaque:NO]; [highlightWindow setLevel:(NSNormalWindowLevel + 1)]; MYHighlightingView *highlightView = [[[MYHighlightingView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 5.0, 5.0)] autorelease]; [highlightView setAutoresizingMask: (NSViewWidthSizable | NSViewHeightSizable)]; [highlightWindow setContentView:highlightView]; } return self; } - (void)dealloc { [highlightWindow orderOut:self]; [highlightWindow release]; [browserPath release]; [super dealloc]; }
Le navigateur doit être réinitialisé dès que la fenêtre principale de l’application change et lors du démarrage de l’application. Pour cela, nous faisons de cet objet contrôleur le
Chapitre 16
Hiérarchie
221
délégué de l’application et faisons en sorte que certaines notifications de NSApplication envoient un message -mainWindowChanged: au contrôleur. Lorsque la fenêtre principale change, le navigateur est réinitialisé et l’interface utilisateur est effacée. - (void)applicationDidUpdate:(NSNotification *)aNotification { [self mainWindowChanged:self]; } - (void)applicationDidUnhide:(NSNotification *)aNotification { [self mainWindowChanged:self]; } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [self performSelector:@selector(mainWindowChanged:) withObject:self afterDelay:0.0]; } - (void)mainWindowChanged:(id)sender { NSWindow *mainWindow = [NSApp mainWindow]; if (mainWindow && (viewedWindow != mainWindow)) { viewedWindow = mainWindow; [browserPath removeAllObjects]; [browserPath addObject:[viewedWindow contentView]]; [browser loadColumnZero]; [viewClassField setStringValue:@""]; [windowPositionField setStringValue:@""]; [sizeField setStringValue:@""]; [highlightWindow orderOut:self]; [browser selectRow:0 inColumn:0]; } }
Pour la mise en œuvre des méthodes déléguées de NSBrowser, il sera utile d’avoir une méthode qui permet de localiser un NSView dans la hiérarchie à partir des identifiants de colonne et de ligne du navigateur. Lors de la sélection des objets dans le navigateur, ils sont ajoutés à la liste browserPath du contrôleur. Par conséquent, la colonne demandée peut servir d’indice dans cette liste pour retrouver la vue supérieure. Ensuite, la ligne demandée peut servir d’indice dans la liste des vues inférieures pour atteindre l’objet approprié. Le code est complété par quelques vérifications des limites : -(NSView *)representedViewAtRow:(NSInteger)row column:(NSInteger)column { NSView *representedView = nil; if (column == 0) { if (row != 0) return nil; // Ne doit jamais se produire. representedView = [browserPath objectAtIndex:0]; }
222
Les design patterns de Cocoa
else { NSView *parent = [browserPath objectAtIndex:(column - 1)]; NSArray *children = [parent subviews]; int numChildren = [children count]; if ((row >= 0) && (row < numChildren)) { representedView = [children objectAtIndex:row]; } } return representedView; }
La première méthode déléguée du navigateur, -browser:numberOfRowsInColumn:, lui sert à déterminer le nombre de lignes dans une colonne donnée. Elle commence par s’assurer que la liste browserPath est à jour. Si la demande concerne la colonne zéro, il n’existe alors qu’un seul objet, la vue de contenu de la fenêtre. Sinon nous avons besoin du nombre de vues secondaires de l’objet sélectionné dans la colonne précédente. L’objet sélectionné est fourni par le tableau browserPath. - (NSInteger)browser:(NSBrowser *)sender numberOfRowsInColumn:(NSInteger)column { int ret = 0; int columnCount = [browserPath count]; if (column >= columnCount) { [self browser:sender selectRow: [sender selectedRowInColumn:(column - 1)] inColumn:(column - 1)]; columnCount = [browserPath count]; if (column > columnCount) { return 0; } } if (column == 0) { if (columnCount > 0) { ret = 1; } else { ret = 0; } } else { ret = [[[browserPath objectAtIndex:(column - 1)] subviews] count]; } return ret; }
Chapitre 16
Hiérarchie
223
Lorsque le navigateur est prêt à afficher une de ses cellules, on demande au délégué de la remplir. La méthode -representedViewAtRow:column: précédente permet d’obtenir la vue représentée par cette cellule. L’intitulé de la cellule est fixé au nom de classe de l’objet. Si l’objet ne possède pas de vue inférieure, il s’agit d’un nœud feuille. - (void)browser:(NSBrowser *)sender willDisplayCell:(id)cell atRow:(NSInteger)row column:(NSInteger)column { NSView *representedView = [self representedViewAtRow:row column:column]; if (representedView) { [cell setTitle:[representedView className]]; [cell setLeaf:(([[representedView subviews] count] > 0) ? NO : YES)]; } [cell setLoaded:YES]; }
La méthode suivante permet de maintenir le tableau browserPath à jour lorsque la sélection dans le navigateur change. Si une sélection est effectuée dans une colonne du navigateur à gauche de la sélection précédente, des objets sont alors retirés du tableau browserPath. Le nouvel objet sélectionné est ensuite ajouté à la fin du tableau. - (BOOL)browser:(NSBrowser *)sender selectRow:(NSInteger)row inColumn:(NSInteger)column { if ((row < 0) || (column < 0)) return NO; if (column == 0) { while ([browserPath count] > 1) { [browserPath removeLastObject]; } } else { NSView *representedView = [self representedViewAtRow:row column:column]; while ([browserPath count] > column) { [browserPath removeLastObject]; } if (!representedView) { // Ne doit jamais se produire. return NO; } [browserPath addObject:representedView]; } return YES; }
224
Les design patterns de Cocoa
La dernière méthode de la mise en œuvre de l’application est -browerSelectionChanged:. Elle est invoquée lorsque l’utilisateur clique sur une cellule dans le navigateur. Elle commence par vérifier que le tableau browserPath est à jour. Elle prend ensuite la vue sélectionnée, c’est-à-dire le dernier élément du tableau, et affiche des informations concernant sa classe, sa position dans la fenêtre et sa taille. Enfin, la fenêtre de mise en exergue est redimensionnée afin que son cadre soit identique à celui de la vue. Le rectangle rouge autour de la vue sélectionnée est alors tracé. - (IBAction)browserSelectionChanged:(id)sender { NSView *selectedView = nil; NSRect selectedFrame; NSRect windowFrame; NSRect showFrame; int lastColumn = [sender selectedColumn]; int row = [sender selectedRowInColumn:lastColumn]; if (row < 0) { lastColumn—; row = [sender selectedRowInColumn:lastColumn]; } [self browser:sender selectRow:row inColumn:lastColumn]; selectedView = [browserPath lastObject]; [viewClassField setStringValue:[selectedView className]]; selectedFrame = [selectedView convertRect:[selectedView bounds] toView:nil]; [windowPositionField setStringValue: [NSString stringWithFormat:@"(%f, %f)", selectedFrame.origin.x, selectedFrame.origin.y]]; [sizeField setStringValue: [NSString stringWithFormat:@"%f x %f", selectedFrame.size.width, selectedFrame.size.height]]; windowFrame = [viewedWindow frame]; showFrame = NSMakeRect( windowFrame.origin.x + selectedFrame.origin.x, windowFrame.origin.y + selectedFrame.origin.y, selectedFrame.size.width, selectedFrame.size.height); [highlightWindow setFrame:showFrame display:YES]; [highlightWindow orderFront:self]; }
16.3
Exemples dans Cocoa
Nous l’avons déjà indiqué dans ce chapitre, NSView, qui permet de construire des interfaces utilisateurs complexes, est la hiérarchie la plus utilisée dans Cocoa. Dans une application Cocoa graphique, les tracés et les interactions de l’utilisateur passent par des vues.
Chapitre 16
Hiérarchie
225
Une autre hiérarchie, par nature non graphique, est utilisée dans la manipulation des documents XML. Après avoir parsé un document XML, la classe NSXML produit un objet NSXMLDocument. Cet objet contient l’intégralité de l’arbre qui correspond au document XML, sous forme d’une hiérarchie d’objets. La classe NSXMLNode est la classe de base des objets qui composent l’arbre, comme NSXMLDocument et NSXMLElement. Les méthodes de NSXMLNode, qui permettent d’ajouter et de retirer des objets enfants, sont héritées par tous les éléments du document. Les développeurs se servent souvent des hiérarchies pour créer leurs modèles de données. Les classes collections du framework Foundation de Cocoa facilitent cette opération. En général, la classe NSMutableArray est utilisée pour stocker des objets enfants, mais il est possible d’opter pour NSMutableSet lorsque l’ordre des enfants n’est pas important.
16.4
Conséquences
Puisque Cocoa utilise une hiérarchie de vues pour représenter les interfaces utilisateurs, les besoins en sous-classes s’en trouvent réduits. Il est conseillé aux développeurs d’ajouter des vues en tant que vues secondaires de manière à construire des interfaces utilisateurs complexes à partir de blocs plus simples. Puisque chaque vue possède son propre système de coordonnées, il est possible de créer des groupes complexes de vues et de les déplacer facilement d’une fenêtre à une autre ou de les faire apparaître dans une zone d’une même fenêtre. Des bénéfices comparables peuvent être obtenus avec des modèles d’objets personnalisés en organisant ces objets en hiérarchie, si cela se révèle approprié. Les hiérarchies doivent être préférées à la création de sous-classes lorsque les relations entre deux objets sont de type "a un", non "est un". La flexibilité d’Objective-C et des classes collections du framework Foundation de Cocoa facilite cette approche.
17 Outlet, cible et action Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Pour construire une interface graphique, nous avons besoin d’un système qui permette de configurer les objets de l’interface, comme les boutons, les curseurs, les champs de saisie et les articles de menu, et de les connecter aux opérations de l’application. Par exemple, une application peut fournir un article de menu pour centrer le texte sélectionné et un autre pour envoyer une demande de billet à un serveur de réservation. Trouver le meilleur système pour connecter les objets de l’interface utilisateur aux opérations de l’application est un problème récurrent. Une solution consiste à créer des sous-classes des objets de l’interface utilisateur, comme les articles de menu, pour une application spécifique. Par exemple, vous pouvez créer les classes CenterTextMenuItem et SendTicketRequestMenuItem. Cependant, cette approche présente plusieurs inconvénients. Le premier réside dans le nombre de classes et la quantité de code à écrire. Il faut créer une nouvelle sous-classe distincte pour chaque opération définie par l’application, et ce pour chaque application. Le second est lié au fait que, lorsque le pattern MVC est utilisé, certaines opérations, comme l’envoi d’une réservation, font clairement partie du modèle ou du contrôleur, alors que les articles de menu font partie de la vue. La création d’une sous-classe d’un article de menu dédiée à l’envoi d’une demande de réservation crée un couplage entre l’article de menu et des objets qui n’ont aucun rapport avec l’affichage. La création de nombreuses sous-classes encourage la redondance dans le code. Que va-t-il se passer si vous offrez également une interface de script pour envoyer
228
Les design patterns de Cocoa
une demande de réservation ou fournissez un bouton et un article de menu ? Un code semblable sera finalement utilisé pour la réservation d’un billet dans chaque objet qui soumet les demandes. Certains frameworks orientés objet proposent une alternative à l’utilisation des multiples sous-classes en affectant un identifiant unique à chaque objet de l’interface utilisateur. Pour configurer un objet précis, il faut parcourir tous les objets de l’interface utilisateur jusqu’à trouver celui qui possède l’identifiant unique recherché. Lorsque l’utilisateur clique sur un bouton ou sélectionne un article de menu, l’application utilise une table ou une instruction switch pour déterminer l’action qui correspond à l’identifiant de l’objet activé. Toutefois, ce besoin de coordonner la signification des identifiants entre les objets de l’interface utilisateur et la logique applicative crée un couplage. Des problèmes de maintenance des identifiants uniques peuvent se poser. Par exemple, lors de la conception d’une interface, vous pouvez copier et coller des objets depuis une interface existante vers la nouvelle. Si les identifiants des objets sont copiés avec les objets, comment faire pour que les identifiants copiés soient uniques dans chaque application ? Des questions peuvent également se poser sur la manière dont les identifiants uniques sont associés aux opérations de l’application. Dans l’idéal, vous ne devez pas avoir à écrire manuellement une très longue instruction switch ou à remplir manuellement une table de correspondance. Le mécanisme doit permettre aux applications de configurer les objets de l’interface utilisateur et à ces objets d’invoquer des opérations de l’application, sans pour cela créer des sous-classes sinon inutiles, sans écrire du nouveau code dans le sous-système Vue, sans créer un couplage entre la vue et les autres sous-systèmes de l’application et sans établir manuellement des correspondances entre des identifiants uniques et des opérations. Par ailleurs, la solution doit permettre des comportements contextuels. Par exemple, lorsque l’utilisateur sélectionne l’article de menu pour centrer du texte, le texte actuellement sélectionné entre en ligne de compte. Le résultat de l’activation d’un article de menu doit changer en fonction des interactions de l’utilisateur. Si aucun texte n’est sélectionné, l’article de menu devrait probablement être désactivé de manière à montrer que son utilisation n’aura aucun effet. Cocoa apporte la solution requise au travers du pattern Outlet, cible et action, qui simplifie la mise en œuvre des interfaces utilisateurs et contribue à la flexibilité et à la productivité apportées par des outils comme Interface Builder.
17.1
Motivation
L’idée est d’employer le design pattern Outlet, cible et action de Cocoa de manière à parvenir aux objectifs suivants :
Chapitre 17
Outlet, cible et action
229
n
Prendre en charge la configuration des objets de l’interface utilisateur dans le code.
n
Préciser les actions de l’application qui doivent être effectuées suite aux interactions de l’utilisateur avec les objets de l’interface.
n
Éviter le couplage entre les objets réutilisables de l’interface utilisateur et le comportement propre à l’application.
n
Éviter le code redondant lorsque plusieurs objets de l’interface utilisateur invoquent les mêmes actions d’une application.
n
Permettre des comportements contextuels fondés sur les interactions de l’utilisateur avec les objets de l’interface.
17.2
Solution
Un outlet est une variable d’instance qui contient une référence (pointeur) sur un autre objet, comme un objet d’interface utilisateur. Les outlets peuvent être fixés dans Interface Builder ou par programmation. Dans Interface Builder, des lignes de connexion sont tracées depuis les objets qui possèdent des outlets vers les objets référencés. Dans le code, les outlets sont fixés via les accesseurs (voir Chapitre 10) ou à l’aide du codage clé-valeur (voir Chapitre 19). Les cibles sont des outlets particuliers. Dans Interface Builder, vous tirez des lignes de connexion pour préciser l’objet référencé par un outlet (voir Figure 17.1). Le principe est le même pour tous les outlets, mais la Figure 17.1 montre la connexion d’une cible. Lorsque vous connectez une cible, Interface Builder vous permet de sélectionner un message d’action qui sera envoyé à la cible (voir Figure 17.2). Les messages d’action peuvent également être précisés dans le code à l’aide d’un accesseur. Interface Builder se fonde sur certains critères pour reconnaître les outlets, comme l’illustre la déclaration de la classe MYController suivante. Une variable d’instance dont le type est id et dont le nom ne commence pas par un caractère souligné est automatiquement considérée comme un outlet. Par ailleurs, toute variable d’instance de type pointeur sur un objet et dont la déclaration comprend la macro IBOutlet est traitée comme un outlet. @interface MYController : NSObject { id sampleOutlet; // Pour IB, cette variable est un outlet. IBOutlet NSMatrix *sampleMatrix; // Pour IB, cette variable est un outlet. id _myPrivateIVar; // Pour IB, cette variable n’est pas un // outlet en raison du ’_’ initial. NSView *sampleView; // Pour IB, cette variable n’est pas un // outlet car le type n’est pas id et la // macro IBOutlet n’est pas utilisée.
230
Les design patterns de Cocoa
IBOutlet NSView
*_myOtherView;
// Pour IB, cette variable est un outlet // malgré le ’_’ initial car la macro // IBOutlet est utilisée.
} @end
Figure 17.1 Fixer les outlets dans Interface Builder en traçant des lignes de connexion.
La macro IBOutlet est définie dans le fichier NSNibDeclarations.h, qui fait partie du framework Cocoa Application Kit, et le préprocesseur C la remplace par un seul caractère espace. IBOutlet ne modifie pas la signification du code compilé, mais indique simplement à Interface Builder l’existence d’un outlet dont le type est plus précis que id. Lorsqu’un type spécifique est indiqué, Interface Builder en tient compte et l’utilise pour limiter la liste des objets qui peuvent être connectés à l’outlet. Interface Builder mémorise les informations concernant les connexions entre les outlets et les autres objets à l’aide de la classe NSNibOutletConnector. Lorsque des objets sont chargés depuis un fichier .nib dans l’application en cours d’exécution, les instances de
Chapitre 17
Outlet, cible et action
231
Figure 17.2 Sélectionner dans Interface Builder l’action qui sera envoyée à une cible.
NSNibOutletConnector chargées reçoivent automatiquement un message -establishConnection pour qu’elles fixent les valeurs des variables d’instance qui sont des outlets. NSNibOutletConnector implémente -establishConnection à l’aide du pattern Accesseur si une méthode accesseur appropriée existe. À l’exécution, NSNibOutletConnector recherche une méthode nommée -set:, où correspond au nom d’une variable d’instance dont la première lettre est en majuscule. Par exemple, pour la classe MYController suivante, les instances de NSNibOutletConnector qui représentent les connexions Interface Builder à sampleOutlet, sampleMatrix et _myOtherView invoqueront automatiquement -setSampleOutlet:, -setSampleMatrix: et -set_myOtherView: pour rétablir ces connexions lors du chargement des objets depuis un fichier .nib.
232
Les design patterns de Cocoa
@interface MYController : NSObject { id sampleOutlet; // Pour IB, cette variable est un outlet. IBOutlet NSMatrix *sampleMatrix; // Pour IB, cette variable est un outlet. id _myPrivateIVar; // Pour IB, cette variable n’est pas un // outlet en raison du ’_’ initial. NSView *sampleView; // Pour IB, cette variable n’est pas un // outlet car le type n’est pas id et la // macro IBOutlet n’est pas utilisée. IBOutlet NSView *_myOtherView; // Pour IB, cette variable est un outlet // malgré le ’_’ initial car la macro // IBOutlet est utilisée. } @end @implementation MYController - (void)setSampleOutlet:(id)anObject; - (void)setSampleMatrix:(NSMatrix *)aMatrix; - (void)set_myOtherView:(NSView *)aView; @end
La méthode -set_myOtherView: ne respecte pas les conventions de nommage des accesseurs. Normalement, la première lettre du deuxième mot est mise en majuscule, comme dans -set_MyOtherView:. Toutefois, dans toutes les versions de Mac OS X jusqu’à la version 10.5, NSNibOutletConnector ne suit pas cette convention. Les futures versions pourraient utiliser le codage clé-valeur de Cocoa pour implémenter NSNibOutletConnector et ce comportement pourrait évoluer pour devenir plus standard. Si aucun accesseur n’est disponible, NSNibOutletConnector utilise les informations enregistrées dans le moteur d’exécution d’Objective-C pour rechercher l’adresse mémoire de chaque outlet et affecter ensuite leur valeur. Outlets Les outlets ne sont pas différents des autres variables d’instance et peuvent être utilisés directement dans le code de l’application. Après que tous les objets ont été chargés à partir d’un fichier .nib et initialisés, un message -awakeFromNib est envoyé à chacun d’eux. Au moment où un objet reçoit ce message, tous ses outlets ont été fixés aux valeurs données dans Interface Builder. Le message -awakeFromNib est également envoyé à l’objet qui est désigné comme propriétaire du nib au moment du chargement du fichier .nib. Ces fichiers sont normalement chargés à l’aide de la méthode +loadNibNamed:owner: de la classe NSBundle (voir la section Core Library > Cocoa > Resource Management > NSBundle Class Reference de la documentation Xcode). Si le même objet est utilisé comme propriétaire dans plu-
Chapitre 17
Outlet, cible et action
233
sieurs invocations de +loadNibNamed:owner:, le message -awakeFromNib lui est envoyé autant de fois lors de chaque chargement d’un fichier .nib. Une certaine prudence est de mise pour l’implémentation des accesseurs des objets qui peuvent être chargés à partir d’un fichier .nib. Ces méthodes sont invoquées dès que possible au cours du chargement des fichiers .nib, mais les objets sont chargés dans un ordre indéfini. Les accesseurs sont donc invoqués dans n’importe quel ordre et les dépendances avec des variables d’instance autres que celle en cours de définition doivent être évitées. Au moment où -awakeFromNib est appelée, tous les objets ont été replacés dans l’état qui a été défini dans Interface Builder. L’implémentation de -awakeFromNib doit se charger des dernières initialisations qui se fondent sur plusieurs outlets ou l’état d’autres objets chargés depuis le fichier .nib. Le Chapitre 11, dédié au pattern Archivage et désarchivage, décrit de manière plus complète la procédure de création et de chargement des fichiers .nib. Cibles Les classes Cocoa NSControl, NSActionCell et NSMenuItem fournissent un outlet nommé target et une variable d’instance correspondante nommée action. Interface Builder gère de manière spéciale les connexions à l’outlet target et vous permet de préciser le message d’action qui sera envoyé à l’objet référencé par target (les actions sont présentées plus loin dans ce chapitre). La cible et l’action sont au centre de la flexibilité et de la puissance de Cocoa. Toute classe qui déclare les variables d’instance target et action peut être utilisée avec ce pattern. Interface Builder enregistre les connexions cible/action sous forme d’instances de la classe NSNibControlConnector. Au cours du chargement d’un fichier .nib, les connexions cible/action sont rétablies de la même manière que les connexions des outlets. Le message -establishConnection est envoyé automatiquement et les instances de NSNibControlConnector y répondent en replaçant les variables d’instance target et action dans l’état défini avec Interface Builder. Puisque la cible peut pointer sur n’importe quel objet et que l’action est une variable, la flexibilité obtenue est prodigieuse. Un objet d’interface utilisateur comme NSButton, qui est une sous-classe de NSControl, peut servir à envoyer n’importe quelle action à n’importe quelle cible, sans avoir besoin d’écrire une sous-classe ou du code personnalisé. Il peut être configuré intégralement dans Interface Builder. NSControl, NSActionCell et NSMenuItem implémentent les accesseurs -target et -setTarget: pour accéder à la cible depuis le code.
234
Les design patterns de Cocoa
Actions Toute méthode qui retourne void et qui a un objet en argument peut être utilisée comme une action. NSControl et NSActionCell définissent les accesseurs -action et -setAction: pour accéder à l’action depuis le code. Les actions sont enregistrées sous forme de sélecteurs. Un sélecteur identifie de manière unique un message Objective-C (voir Chapitre 9). Il existe plusieurs façons d’obtenir des sélecteurs. La plus simple consiste à utiliser la directive @selector() du compilateur Objective-C. L’exemple suivant envoie un message -setAction: avec le sélecteur du message -copy: en argument : [someControl setAction:@selector(copy:)];
Un nom de message est converti en sélecteur à l’aide de la fonction Cocoa NSSelectorFromString(), et un sélecteur est converti en chaîne de caractères par la fonction NSStringFromSelector(). L’exemple suivant fixe l’action d’un objet, la récupère ensuite et la convertit en chaîne de caractères : [someControl setAction:NSSelectorFromString(@"copy")]; NSLog(NSStringFromSelector([someControl action]);
Pour de plus amples informations concernant la directive @selector(), consultez la section Core Library > Cocoa > Objective-C Language > The Objective-C 2.0 Programming Language > Appendix A; Language Summary de la documentation Xcode. Les fonctions NSSelectorFromString() et NSStringFromSelector() sont documentées dans la section Core Library > Cocoa > Data Management > Foundation Functions Reference. Chaque classe dérivée de NSControl ou de NSActionCell envoie son message d’action à sa cible à divers moments, selon les circonstances. La classe NSButton, une sousclasse de NSControl, envoie normalement son message d’action après que le bouton de la souris a été appuyé et relâché alors que le pointeur se trouvait au-dessus du bouton, mais elle peut être configurée avec d’autres comportements. La classe NSSlider, une sous-classe de NSControl, peut être configurée de manière à envoyer son message d’action à chaque déplacement du curseur ou uniquement lorsque l’utilisateur relâche le bouton de la souris. Les curseurs peuvent également être configurés pour n’envoyer que les valeurs qui correspondent aux graduations de leur échelle. NSButton et NSSlider font partie des contrôles les plus simples. Les contrôles plus complexes, comme NSMatrix et NSTableView, proposent des comportements plus sophistiqués. Quelle que soit la raison de l’envoi d’un message d’action, cette opération est toujours effectuée avec la méthode -sendAction:to:from: de la classe NSApplication. Cette classe est un exemple du pattern Singleton, ce qui signifie qu’il n’en existe qu’une seule
Chapitre 17
Outlet, cible et action
235
instance dans chaque application. Cette instance peut être obtenue en envoyant le message +sharedApplication à la classe NSApplication : [NSApplication sharedApplication];
Il existe également une variable globale, NSApp, qui pointe sur l’unique instance de NSApplication. Les classes NSControl et NSActionCell envoient les messages d’action aux cibles en utilisant un code semblable au suivant : [[NSApplication sharedApplication] sendAction:[self action] to:[self target] from:self];
Le premier argument de -sendAction:to:from: correspond au sélecteur mémorisé dans la variable d’instance action. Il identifie le message à envoyer. Le deuxième argument est l’objet cible référencé par la variable d’instance target. Le dernier argument est l’objet à passer en argument à la méthode identifiée par le sélecteur d’action. L’argument from: est généralement l’émetteur du message. Le récepteur peut s’en servir pour obtenir de plus amples informations sur le contrôle qui envoie le message. Par exemple, lorsqu’un curseur est déplacé, il envoie son message d’action à sa cible en se passant en argument. Lorsque la cible reçoit le message, elle utilise son argument sender pour obtenir la valeur représentée par le curseur. Le code suivant met en œuvre une méthode -volumeSliderDidChange: hypothétique qui sert d’action à une instance de NSSlider : - (void)volumeSliderDidChange:(id)sender { // Vérifier que l’émetteur anonyme répond à -floatValue. if([sender respondsToSelector:@selector(floatValue)]) { // Fixer le volume à la valeur réelle de l’émetteur. [self setVolume:[sender floatValue]]; } }
Actions et chaînes de répondeurs L’objet partagé NSApplication joue un rôle crucial dans l’implémentation du mécanisme cible/action. Si l’argument to: de la méthode -sendAction:to:from: de la classe NSApplication est un objet valide, le message d’action est envoyé directement à la cible. En revanche, si cet argument est nil, le récepteur final du message d’action est déterminé par l’état courant de l’application et l’objet d’interface utilisateur à l’origine de l’événement. Lorsque la cible d’un message d’action est nil, la méthode -sendAction:to:from: utilise une version étendue de la chaîne de répondeurs pour rechercher un objet capable de répondre à l’action et la lui envoie. En conséquence, si la cible d’un objet Cocoa est
236
Les design patterns de Cocoa
fixée à nil, l’action de l’objet est sensible au contexte. Lorsque l’utilisateur interagit avec une application, la chaîne de répondeurs reflète constamment le contexte actuel. Chaque fois que la chaîne de répondeurs change, la liste des récepteurs potentiels du message d’action change également. Par exemple, un article de menu peut être configuré pour envoyer le message -copy:. Si la cible de cet élément est nil, l’objet qui reçoit le message -copy: dépend du premier répondeur dans la fenêtre active. Le premier répondeur est l’objet qui possède le focus. La fenêtre active est la fenêtre de premier plan qui reçoit les événements du clavier. Si le premier répondeur est un champ de saisie, alors, ce champ reçoit le message d’action -copy: envoyé par l’article de menu. Si un autre objet possède le focus, alors, il reçoit le message d’action -copy:. Lorsque les connexions cible/action sont réalisées avec l’objet FIRST RESPONDER dans Interface Builder (voir Figure 17.1), la cible est fixée à nil. Cet objet représente simplement tout objet qui possède le focus à un moment donné au cours de l’exécution de l’application. La Figure 17.3 montre le panneau IDENTITY de l’inspecteur d’Interface Builder, qui permet de définir de nouveaux messages d’action envoyés à la chaîne de répondeurs. Figure 17.3 L’inspecteur d’Interface Builder permet de définir de nouveaux messages d’action qui peuvent être envoyés au premier répondeur.
Le Chapitre 18 détaille le pattern Cocoa Chaîne de répondeurs, notamment la procédure de recherche du récepteur d’un message d’action envoyé à nil. Le premier objet de la chaîne qui peut répondre à un message d’action reçoit ce message. Si aucun objet de la
Chapitre 17
Outlet, cible et action
237
chaîne de répondeurs n’est en mesure de répondre à l’action, le comportement par défaut de Cocoa est d’émettre un bip. Toutefois, il est rare que des messages d’action ne puissent pas être traités par un objet de la chaîne de répondeurs, car la validation automatique des menus et des contrôles de Cocoa utilise la même chaîne d’objets pour déterminer si chaque objet qui envoie l’action doit être activé. Normalement, s’il n’existe aucun répondeur pour le message d’action d’un objet, ce dernier est automatiquement désactivé et ne peut envoyer son message d’action. Pour de plus amples informations concernant la validation automatique, consultez les sections Core Library > Cocoa > User Experience > Application Menu and Pop-up List Programming Topics for Cocoa > Enabling Menu Items et Core Library > Cocoa > User Experience > Toolbar Programming Topics for Cocoa > Validating Toolbar Items de la documentation Xcode. Interface Builder et Xcode communiquent de manière à découvrir automatiquement les actions proposées par chaque classe. Toute méthode déclarée selon le schéma suivant est une méthode d’action utilisable par Interface Builder : - (IBAction)someAction:(id)sender;
IBAction est une macro du préprocesseur qu’il convertit en void. Les méthodes d’action doivent retourner void et accepter un seul argument de type objet. En déclarant vos méthodes avec la macro IBAction, Interface Builder les trouvera plus facilement.
L’argument objet d’une méthode d’action n’est pas forcément de type id. Tout pointeur sur un type objet peut être utilisé. Par exemple, la déclaration suivante correspond à une méthode d’action : - (IBAction)volumeSliderDidChange:(NSSlider *)sender
17.3
Exemples dans Cocoa
La grande majorité des didacticiels d’introduction à Cocoa décrit les outlets, les cibles et les actions. Apple propose également son didacticiel en ligne à l’adresse http://developer.apple.com/documentation/Cocoa/Conceptual/ObjCTutorial. Pour utiliser les outlets, les cibles et les actions dans vos applications, il est essentiel de comprendre la chaîne de répondeurs. La méthode -sendAction:to:from: de la classe NSApplication se charge de la distribution des messages d’action envoyés à une cible et gère le cas où aucune cible n’est précisée et où le contexte permet de trouver un récepteur. Le design pattern Outlet, cible et action est employé dans toutes les applications Cocoa qui offrent une interface utilisateur selon le pattern MVC. Les cibles désignent habituellement des objets présents dans le sous-système Vue ou Contrôleur. Les objets du
238
Les design patterns de Cocoa
modèle ne doivent pas posséder d’outlets puiqu’ils sont censés pointer sur des objets de la vue ou du contrôleur et le modèle ne doit pas présenter des dépendances avec ces objets. De plus, les objets du modèle ne doivent pas définir des méthodes d’action, car les actions sont envoyées par les objets du sous-système Vue et les objets du modèle ne doivent pas interagir directement avec la vue. La Figure 17.4 illustre les connexions et la séquence des opérations impliquant les outlets, les cibles et les actions que l’on rencontre généralement dans une application MVC. Modèle
Contrôleur
Vue NSButton IBOutlet id target SEL action = -play:
MYSongPlayer
MYPlayerController -sendAction:to:
NSArray *songs
IBOutlet id songPlayer
float volumeDb
IBOutlet id playButton
-setEnabled:
11
NSButton
IBOutlet id pauseButton -playCurrentSong
3
2 IBOutlet id target
-pauseCurrentSong
9
-(IBAction)play:
1
-setVolumeDb:
7
-(IBAction)pause:
8
SEL action = -pause:
-selectNextSong
-(IBAction)takeVolumeFrom: 5
-sendAction:to:
-selectPreviousSong
-(IBAction)next:
-setEnabled:
4
10
-(IBAction)previous:
NSSlider IBOutlet id target SEL action = -takeVolumeFrom: float value -sendAction:to: -(float)floatValue
6
Figure 17.4 Séquence type des opérations de réponse aux interactions de l’utilisateur.
La Figure 17.4 représente une partie de la conception d’une application simple de lecture de fichiers audio. Les possibilités de mémoriser des chansons, de les jouer, de connaître le morceau en cours de lecture et de modifier le volume font partie du modèle. Le modèle doit être opérationnel quelle que soit la manière dont l’utilisateur interagit avec ses données. La Figure 17.4 montre une application qui propose un bouton "Lecture", un bouton "Pause" et un curseur de réglage du volume, mais le modèle doit fonctionner quelle que soit l’interface. Par exemple, l’utilisateur peut disposer d’un script qui sélec-
Chapitre 17
Outlet, cible et action
239
tionne des chansons dans une liste de lecture et demande au lecteur de les jouer l’une après l’autre. Un article de menu peut suspendre la lecture ou un bouton "Muet" peut couper le son. Les deux boutons et le curseur de la vue possèdent chacun leurs cibles respectives, qui pointent sur la même instance de MYPlayerController dans le contrôleur. Lors d’un appui sur un bouton ou du déplacement du curseur, la méthode -sendAction:to: de l’objet correspondant est invoquée, avec pour conséquence d’invoquer la méthode -sendAction:to:from: de NSApplication en lui passant le bouton ou le curseur concerné dans l’argument from:, qui se nomme généralement sender. Puisque les cibles sont fixées explicitement pour chacun des boutons et pour le curseur, NSApplication enverra chaque message d’action directement à l’instance de MYPlayerController. Cette classe possède des outlets connectés à chacun des deux boutons et à l’instance de MYSongPlayer dans le modèle. Puisque la classe MYPlayerController connaît l’objet MYSongPlayer, elle est donc couplée au modèle, mais celui-ci ne possède aucune information sur le contrôleur. L’instance de MYPlayerController possède également des outlets connectés aux objets de la vue. Le contrôleur est couplé à la vue, mais il dispose de très peu d’informations sur les objets de la vue avec lesquels il communique. La vue ne présente aucune dépendance avec le contrôleur. Puisque les cibles des boutons et du curseur sont définies avec le type id, elles peuvent être connectées à tout objet qui répond aux messages d’action affectés. Voici la séquence des opérations illustrée à la Figure 17.4. À l’étape 1, l’utilisateur appuie sur le bouton "Lecture". Cela déclenche l’invocation de la méthode -play: de MYPlayerController avec le bouton cliqué passé dans l’argument sender. À l’étape 2, MYPlayerController réagit en invoquant [[self songPlayer] playCurrentSong];, ce qui conduit MYSongPlayer à démarrer la lecture de la chanson choisie. MYPlayerController ignore simplement l’argument sender dans l’implémentation de -play:. À l’étape 3, MYPlayerController invoque [[self playButton] setEnabled:NO]; car le morceau est déjà en cours de lecture et il est préférable de ne pas appuyer de nouveau sur le bouton "Lecture". À l’étape 4, MYPlayerController invoque [[self pauseButton] setEnabled:YES]; car on peut à présent suspendre la lecture de la chanson. INFO Dans la conception de MYPlayerController, rien ne suppose que les outlets playButton et pauseButton seront connectés à des boutons. Ils peuvent être connectés à n’importe quel objet qui répond au message -setEnabled: et tous les descendants des classes Cocoa NSControl et NSActionCell répondent à -setEnabled: et à -floatValue. MYPlayerController n’est que faiblement couplé aux objets de la vue, car vous pouvez remplacer les boutons par des articles de menu ou d’autres objets de l’interface utilisateur sans modification ou conséquence sur le fonctionnement de la classe MYPlayerController.
240
Les design patterns de Cocoa
À l’étape 5, l’utilisateur ajuste le curseur "Volume" et le message d’action -takeVolumeFrom: est envoyé à l’instance de MYPlayerController. Le curseur est passé dans l’argument sender du message d’action. Voici l’implémentation de la méthode -takeVolumeFrom: dans MYPlayerController : - (IBAction)takeVolumeFrom:(id)sender { if([sender respondsToSelector:@selector(floatValue)]) { float newVolume = [sender floatValue]; // étape 6 [[self songPlayer] setVolumeDb:newVolume]; // étape 7 } }
À l’étape 6, MYPlayerController demande à l’émetteur de fournir sa valeur réelle. Dans cette conception, l’émetteur est un curseur, mais il pourrait s’agir d’un champ de saisie ou de n’importe quel autre objet capable de répondre au message -floatValue. À l’étape 7, la méthode -setVolumeDb: de MYSongPlayer est invoquée, ce qui déclenche la validation de l’argument passé et l’ajustement du volume. À l’étape 8, l’utilisateur appuie sur le bouton "Pause", ce qui débouche sur l’envoi du message d’action -pause: avec le bouton concerné en argument. À l’étape 9, l’instance de MYPlayerController invoque la méthode -pauseCurrentSong de MYSongPlayer. À l’étape 10, MYPlayerController appelle [[self playButton] setEnabled:YES]; car, puisque la lecture est à présent suspendue, un appui sur le bouton "Lecture" a un sens. À l’étape 11, MYPlayerController appelle [[self pauseButton] setEnabled:NO]; puisque l’utilisateur ne doit pas pouvoir suspendre une lecture déjà en pause. La conception de l’interface utilisateur illustrée à la Figure 17.4 ne nécessite aucun code dans le sous-système Vue. Après avoir créé l’interface de la classe MYPlayerController dans Xcode, l’intégralité de l’interface utilisateur peut être construite et connectée dans Interface Builder, sans aucune sous-classe des objets de vue et aucun code généré. Même si une autre interface est créée pour des scripts ou avec des articles de menu et des champs de saisie à la place des boutons et du curseur, le contrôleur ou le modèle n’ont aucunement besoin d’être revus. Si la classe MYSongPlayer vient à être modifiée, il est fort probable que vous puissiez toujours utiliser la classe MYPlayerController telle quelle. Dans le pire des cas, vous devrez mettre à jour MYPlayerController pour qu’elle communique avec un nouveau modèle, mais en aucun cas un changement du modèle ne vous obligera à modifier la vue.
Chapitre 17
17.4
Outlet, cible et action
241
Conséquences
En Objective-C, la prise en charge des sélecteurs au niveau du langage et la possibilité d’envoyer n’importe quel message à n’importe quel objet fournissent une solution flexible au problème d’intégration des objets de l’interface utilisateur avec le code applicatif. Par exemple, grâce aux possibilités dynamiques d’envoi de messages, les systèmes de gestion manuelle des messages, que l’on trouve dans les autres frameworks, ne sont plus nécessaires. De nombreux frameworks postent des événements identifiés par des entiers uniques dès que des objets de l’interface utilisateur changent d’état. Les objets qui reçoivent les événements se chargent ensuite de les décoder et d’interpréter les informations transmises avec les événements. Ce décodage des événements conduit à du code redondant, par exemple sous forme d’instructions switch ou de tables de correspondance, en de nombreux endroits et augmente le travail de maintenance. Le système de messages intégré à Objective-C relègue aux oubliettes tout traitement manuel des événements. Le pattern Signaux et slots développé par Trolltec pour son framework multiplateforme Qt C++ reprend une partie des principes du pattern Outlet, cible et action. Les programmeurs doivent créer des sous-classes des classes d’interface utilisateur du framework pour ajouter des signaux et des slots propres à l’application. Trolltec fournit un outil, nommé Meta Object Compiler, pour le prétraitement du code C++ de l’application de manière à générer le code d’implémentation des signaux et des slots. Après que le code spécialisé a été généré et compilé, les signaux sont comparables aux actions de Cocoa et les slots équivalent aux cibles. Les outils de développement de Cocoa et d’Apple évitent ce prétraitement et la génération du code en utilisant le dynamisme d’Objective-C, notamment les patterns Exécution de sélecteur, Type anonyme et Chaîne de répondeurs. Grâce aux outils comme Interface Builder, qui permettent d’établir graphiquement des outlets et des actions, le code nécessaire à la mise en œuvre des interfaces utilisateurs est réduit et leur intégration avec le code applicatif est simplifiée. Dans de nombreux cas, l’interface utilisateur d’une application Cocoa n’a besoin d’aucun code personnalisé. Dans les autres frameworks, même si des outils sont utilisés pour produire des interfaces utilisateurs, ils génèrent habituellement du code qui gère les interactions entre les objets de l’interface et les autres objets. Le code généré doit être maintenu tout au long de la durée de vie du projet et peut facilement devenir une source de bogues. Interface Builder ne génère aucun code. Il crée des instances des classes existantes, fixe l’état de ces instances et les archive dans des fichiers .nib qui seront ultérieurement désarchivés dans les applications en cours d’exécution (voir Chapitre 11). Aucun code source n’est généré pour l’interface utilisateur. En général, une évolution de l’interface utilisateur ne demande aucune recompilation.
242
Les design patterns de Cocoa
Il est plutôt rare de créer des sous-classes des classes Cocoa d’interface utilisateur. Le pattern Outlet, cible et action apporte toute la flexibilité requise par la plupart des applications. Grâce au nombre réduit de sous-classes, le couplage est moindre, tout comme le code à maintenir. Les patterns Notification (voir Chapitre 14) et Délégué (voir Chapitre 15) apportent la flexibilité supplémentaire qui réduit le besoin de sous-classes des classes de la vue. Les bindings Cocoa fournissent un mécanisme automatique de synchronisation des variables entre des objets. Lorsque deux variables ou plus sont liées, la modification de la valeur de l’une conduit à la modification automatique de toutes les variables liées. Les bindings Cocoa sont détaillés au Chapitre 32 et constituent parfois une alternative aux outlets, cibles et actions.
18 Chaîne de répondeurs Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
La chaîne de répondeurs est un élément central et crucial de toute application Cocoa graphique. Elle dirige les événements utilisateurs vers les objets appropriés et simplifie l’implémentation des fonctionnalités contextuelles. Le pattern Chaîne de répondeurs est également appelé Chaîne de responsabilités. Ce chapitre décrit les problèmes comportementaux et logiques que permet de résoudre la chaîne de répondeurs. Les fonctionnalités de l’application, aussi variées que la gestion des actions de l’utilisateur, la validation automatique des articles de menu, l’annulerrétablir, le copier-coller, le choix de la police, l’affichage des erreurs et toute forme de saisie contextuelle, sont simplifiées par ce pattern. Il est étroitement lié aux hiérarchies (voir Chapitre 16), qui, par nature, sont principalement structurelles. La chaîne de répondeurs exploite cette structure pour mettre en œuvre des comportements contextuels.
18.1
Motivation
L’idée est de diriger l’action de l’utilisateur vers l’élément d’interface utilisateur approprié. Les messages d’action doivent pouvoir être retransmis dynamiquement vers les éléments d’interface actuellement actifs ou sélectionnés. Les éléments d’interface doivent pouvoir mettre à jour automatiquement leur état en réponse aux actions de l’utilisateur ou à la modification de l’état de l’application. La mise en œuvre des fonctionnalités contextuelles de l’application doit être simplifiée.
244
18.2
Les design patterns de Cocoa
Solution
Dans la plupart des applications à interface graphique, il existe une notion d’utilisateur focalisé sur un élément d’interface particulier dans une fenêtre précise. Le problème classique est de diriger les messages et l’événement vers l’objet ayant ce focus. Lorsque celui-ci change, la cible des messages doit suivre dynamiquement le focus. Cocoa résout ce problème en utilisant le pattern Chaîne de responsabilités de manière à mettre en œuvre la chaîne de répondeurs. Le pattern Chaîne de responsabilités découple l’émetteur d’un message de son récepteur. Un message ou un événement est transmis le long d’une liste chaînée d’objets, jusqu’à ce que l’un d’eux traite le message. Ainsi, de multiples objets ont l’opportunité de traiter ou d’ignorer le message. Grâce au dynamisme d’Objective-C, l’implémentation de ce pattern en Cocoa est extrêmement puissante et, dans la plupart des cas, se révèle également plus simple que son équivalent dans d’autres frameworks applicatifs. Terminologie En Cocoa, tous les objets qui répondent aux actions de l’utilisateur sont des classes dérivées de la classe abstraite NSResponder. Le premier rôle d’un répondeur est de répondre aux actions de l’utilisateur, qui proviennent généralement du clavier ou de la souris. Puisque NSApplication, NSWindow et NSView dérivent de NSResponder, une majorité des classes d’AppKit rencontrées et utilisées par les développeurs Cocoa sont des répondeurs. Les classes NSWindowController, NSViewController et NSDrawer sont également des sous-classes de NSResponder. Lors des interactions de l’utilisateur avec l’application, Cocoa suit automatiquement le focus fixé par l’utilisateur. La fenêtre qui reçoit les événements du clavier est la fenêtre "active" (appelée "key" en anglais par référence au clavier, keyboard). Le document qui possède le focus est appelé fenêtre "principale". En général, puisque l’utilisateur manipule directement le document, la fenêtre active et la fenêtre principale sont les mêmes. Toutefois, il arrive que le focus de l’utilisateur soit sur deux fenêtres. Par exemple, dans une application multidocument avec des panneaux utilitaires, le focus peut se trouver sur le document alors que l’utilisateur est en train de saisir des informations dans un panneau utilitaire. Ces informations sont supposées modifier, d’une manière ou d’une autre, le document ayant le focus. Dans ce cas, le panneau utilitaire est la fenêtre active, tandis que le document est la fenêtre principale. L’objet application gère les fenêtres active et principale. Les références aux objets NSWindow correspondants peuvent être obtenues à partir de l’instance de NSApplication en envoyant, respectivement, les messages -keyWindow et -mainWindow. Dans une fenêtre donnée, le focus de l’utilisateur sera généralement associé à une vue précise. Par exemple, s’il clique dans un champ de saisie, le focus passera dans ce champ afin que l’utilisateur puisse effectuer sa saisie. La vue qui possède le focus est
Chapitre 18
Chaîne de répondeurs
245
appelée premier répondeur et représente le point de départ de la chaîne de répondeurs. Pour obtenir une référence à l’objet qui représente le premier répondeur d’une fenêtre, il suffit d’envoyer le message -firstResponder à cette fenêtre. La chaîne de répondeurs Les répondeurs sont chaînés les uns aux autres afin qu’un événement puisse être traité par plusieurs d’entre eux. Si le premier répondeur n’est pas capable de traiter l’entrée de l’utilisateur ou choisit de ne pas la traiter, il la passe au répondeur suivant dans la chaîne. Le message -nextResponder peut être envoyé à n’importe quel répondeur de manière à connaître le suivant dans la chaîne. Le message -setNextResponder: permet de modifier une chaîne de répondeurs. Le Chapitre 16 décrit la hiérarchie des vues utilisée par Cocoa. Cette hiérarchie définit la plus grande partie de la chaîne de répondeurs. En général, le répondeur suivant d’une vue est sa vue supérieure. Parfois, des objets peuvent être insérés dans la chaîne entre une vue et sa vue supérieure, mais ce cas est moins fréquent. La vue de contenu d’une fenêtre désigne sa fenêtre comme son répondeur suivant. La plupart des fenêtres ont un répondeur suivant égal à nil, ce qui termine la chaîne. Une fenêtre gérée par un NSWindowController pointe sur le contrôleur de fenêtre et ce contrôleur pointe généralement sur nil. Toutes les chaînes de répondeurs finissent par mener à nil de manière à éviter les boucles infinies. Figure 18.1 Un exemple de chaîne de répondeurs.
NSTextView
NSClipView
NSScrollView
NSView
NSWindow
La Figure 18.1 illustre une chaîne de répondeurs. La fenêtre qui contient la chaîne se trouve à gauche. Si l’utilisateur a cliqué dans l’instance de NSTextView pour saisir du texte, elle devient le premier répondeur. La chaîne de répondeurs créée dans ce cas se trouve à droite. Elle commence par le NSTextView, puis se poursuit par ses NSClipView et NSScrollView. La vue de contenu de la fenêtre, un NSView, vient ensuite. L’instance de NSWindow termine la chaîne.
246
Les design patterns de Cocoa
Dans ce modèle, chaque fenêtre possède sa propre chaîne de répondeurs. Lorsque l’utilisateur déplace le focus d’une vue à une autre dans une fenêtre, la chaîne de répondeurs de celle-ci est mise à jour. Avant que le focus ne soit déplacé, un message -acceptsFirstResponder est tout d’abord envoyé à un objet pour savoir s’il est prêt à devenir le prochain premier répondeur. En général, seules les vues qui gèrent les événements du clavier répondent YES. Dans ce cas, un message -resignFirstResponder est envoyé au premier répondeur courant pour lui demander de renoncer à son statut de premier répondeur. Parfois, par exemple pour les champs qui valident le texte saisi, la réponse peut être NO tant que les données saisies sont invalides. Enfin, l’objet qui devient le nouveau premier répondeur reçoit le message -becomeFirstResponder. Les événements du clavier et de la souris sont convertis en objets NSEvent par l’instance de NSApplication et transmis ensuite à l’objet répondeur approprié en suivant une chaîne de répondeurs. Pour les événements du clavier et les événements de déplacement de la souris, le premier répondeur de la fenêtre active représente le début de la chaîne. Pour les clics de souris, la vue, sous le pointeur de la souris, la plus loin dans la chaîne représente le début. Si un répondeur ne souhaite pas prendre en charge l’événement, il le passe au répondeur suivant dans la chaîne, et ainsi de suite jusqu’à ce qu’un objet traite l’événement ou que la fin de la chaîne soit atteinte. Cocoa gère les événements de cette manière pour que leur distribution soit guidée par la structure hiérarchique des objets de l’interface utilisateur. Les sources d’entrée ne sont jamais couplées directement aux objets qui reçoivent l’entrée. À la place, une liste ordonnée d’objets basée sur le focus de l’utilisateur a l’opportunité de répondre aux événements. L’entrée de l’utilisateur est automatiquement dirigée par le contexte courant de l’application. La chaîne de répondeurs étendue Lors de la conception d’un framework applicatif, la distribution des messages envoyés par les articles de menu représente un problème complexe. Prenons pour exemples certains des messages d’action classiques envoyés par les articles de menu. Un message comme -copy: sera probablement traité par le premier répondeur de la fenêtre active. Le délégué de la fenêtre principale, généralement un NSDocument, prendra sans doute en charge le message -save:. Le message de terminaison d’une application, -terminate:, devra être traité par l’instance partagée de NSApplication. Enfin, le message de création d’un nouveau document, -new:, aura tout lieu d’être pris en charge par le NSDocumentController partagé. Vous le constatez, le routage de tous ces messages à l’objet approprié n’est pas trivial. Certains frameworks envoient les événements générés par les menus à la fenêtre active, lui laissant le soin de déterminer comment ils doivent poursuivre leur route. Cela impli-
Chapitre 18
Chaîne de répondeurs
247
que généralement la création de sous-classes de l’objet fenêtre simplement pour prendre en charge les événements générés par les articles de menu. Cocoa opte pour une solution qui évite la création de sous-classes, tout en étant plus précise que le simple envoi d’un événement à une fenêtre. Cocoa envoie les messages cible/action à une chaîne de répondeurs, en commençant par le premier répondeur de la fenêtre active. Le message cible/action est envoyé au premier objet qui peut répondre. Toutefois, la chaîne de répondeurs de la fenêtre active ne suffit pas à couvrir toutes les possibilités. Dans l’exemple précédent, celui avec une fenêtre de document et un panneau utilitaire, certains articles de menu, comme "Enregistrer", doivent agir sur le document lui-même, non sur le panneau utilitaire. Par conséquent, les messages doivent être envoyés à la chaîne de répondeurs de la fenêtre active et à celle de la fenêtre principale si ces deux fenêtres sont différentes. Par ailleurs, certains messages doivent aller à l’objet application et il peut également être pratique que les délégués de la fenêtre et de l’application aient l’opportunité de répondre. Dans une application basée sur les documents, le NSDocumentController implémente également certaines actions, comme "Nouveau" et "Tout enregistrer". Si nous réunissons tous ces points, voici la chaîne de répondeurs étendue utilisée par le mécanisme cible/action de Cocoa : 1. Commencer par le premier répondeur de la fenêtre active. 2. Suivre la chaîne de répondeurs en remontant la hiérarchie des vues. 3. Essayer l’objet fenêtre. 4. Essayer le délégué de la fenêtre, qui est souvent une instance de NSDocument. 5. Le suivant est une instance de NSWindowController, si elle existe. 6. Répéter les étapes 1 à 5 avec le premier répondeur de la fenêtre principale. 7. Essayer l’objet NSApplication et son délégué. 8. Essayer le NSDocumentController, s’il existe. Malheureusement, même si certains objets NSView prennent en charge les délégués, ceux-ci ne sont pas inclus dans cette chaîne de répondeurs étendue. Seuls les délégués de la fenêtre et de l’application y participent. La Figure 18.2 illustre une chaîne de répondeurs étendue pour une fenêtre et un panneau utilitaire. La fenêtre du haut est la fenêtre principale et fait partie d’un NSDocument. Le panneau du bas est la fenêtre active. Dans les deux fenêtres, le NSTextView est le premier répondeur. La chaîne de répondeurs étendue résultante est présentée à droite. Des libellés sont ajoutés à droite de certains objets pour préciser leur rôle, comme celui
248
Les design patterns de Cocoa
de délégué. Si les fenêtres active et principale sont les mêmes, cette chaîne est plus courte car une seule hiérarchie de vues doit être parcourue. Dans une application qui n’utilise pas l’architecture de document de Cocoa, les objets du document et du contrôleur de documents disparaissent également.
NSTextView
Premier répondeur de la fenêtre active
NSTextView
NSClipView
NSClipView
NSScrollView
NSScrollView
NSView
NSView
NSPanel
Premier répondeur de la fenêtre principale
NSWindow
Fenêtre principale
MyDocument
Délégué de la fenêtre principale
Fenêtre active
NSWindow Controller
NSApplication
MyAppDelegate
Délégué de l’application
NSDocument Controller
Figure 18.2 Un exemple de chaîne de répondeurs étendue.
Lorsqu’un article de menu est connecté à l’objet FIRST RESPONDER dans Interface Builder, cela signifie que le message d’action passera dans la chaîne de répondeurs étendue jusqu’à ce qu’un objet capable d’y répondre soit trouvé. Comme pour les événements du clavier et de la souris, cela découple les cibles des émetteurs et rend automatiquement contextuelles les actions des menus.
Chapitre 18
Chaîne de répondeurs
249
Lorsqu’un développeur connecte un contrôle à FIRST RESPONDER dans Interface Builder, la cible de l’action est en réalité fixée à nil. Pour configurer manuellement un contrôle de manière que l’action passe dans la chaîne de répondeurs étendue, l’action doit être fixée normalement et la cible à nil. Par exemple, pour qu’un NSControl envoie le message -terminate: (celui envoyé par l’article de menu "Quitter"), vous devez écrire le code suivant : [myControl setAction:@selector(terminate)]; [myControl setTarget:nil];
Pour envoyer un message dans la chaîne de répondeurs étendue, NSApplication fournit la méthode -sendAction:to:from:. Sa méthode -targetForAction:to:from: permet de savoir si un objet de la chaîne répondra à un message, sans réellement envoyer ce message. Par exemple : [NSApp sendAction:@selector(terminate) to:nil from:self]; id myTarget = [NSApp targetForAction:@selector(terminate) to:nil from:self];
Parcourir la chaîne de répondeurs étendue Pour bien comprendre la chaîne de répondeurs, il est parfois utile de voir les objets présents dans la chaîne à un moment donné et de constater comment le contexte de l’application modifie la liste de ces objets. En ajoutant un simple objet dans une application, vous pouvez facilement afficher sur la console le contenu de la chaîne de répondeurs. Son interface est simple. La méthode d’action -trace: apporte la fonctionnalité la plus importante : #import @interface MyResponderChainTracer : NSObject { int count; } - (void)traceChain:(id)currentResponder; - (IBAction)trace:(id)sender; @end
L’implémentation se contente de suivre la chaîne de répondeurs des fenêtres active et principale, en journalisant une ligne pour chaque objet trouvé : #import "MyResponderChainTracer.h" @implementation MyResponderChainTracer - (void)traceChain:(id)currentResponder { while (currentResponder) {
250
Les design patterns de Cocoa
NSLog(@"Répondeur %d : %@", count, currentResponder); count++; if ([currentResponder isKindOfClass:[NSWindow class]] || [currentResponder isKindOfClass:[NSApplication class]]) { id delegate = [currentResponder delegate]; if (delegate) { NSLog(@"Répondeur %d (délégué) : %@", count, [currentResponder delegate]); count++; } } if ([currentResponder respondsToSelector:@selector(nextResponder)]) { currentResponder = [currentResponder nextResponder]; } else { currentResponder = nil; } } } - (IBAction)trace:(id)sender { NSWindow *keyWindow = [NSApp keyWindow]; NSWindow *mainWindow = [NSApp mainWindow]; count = 1; NSLog(@"***** Début de la trace *****"); [self traceChain:[keyWindow firstResponder]]; if (keyWindow != mainWindow) { [self traceChain:[mainWindow firstResponder]]; } [self traceChain:NSApp]; // Retirer cette ligne si l’application n’est pas basée sur les documents : [self traceChain:[NSDocumentController sharedDocumentController]]; NSLog(@"***** Fin de la trace *****"); } @end
Pour utiliser cet objet, instanciez-le dans le fichier Main.nib de l’application et créez un article de menu qui envoie l’action -trace: à l’instance de MyResponderChainTracer. Dans l’application qui s’exécute, sélectionnez simplement l’option du menu pour obtenir la chaîne de répondeurs à l’instant donné. Insérer des objets dans la chaîne de répondeurs Il est possible de manipuler manuellement la chaîne de répondeurs de manière à y insérer d’autres objets. La seule contrainte est que les objets insérés soient des sous-classes de NSResponder, comme la classe NSViewController. Parfois, l’insertion d’un contrô-
Chapitre 18
Chaîne de répondeurs
251
leur de vue dans la chaîne de répondeurs, entre la vue contrôlée et sa vue supérieure, a un sens. C’est notamment le cas si le contrôleur de vue est une sous-classe personnalisée qui implémente une méthode de gestion d’un événement ou une méthode cible/ action. Lorsqu’une vue est ajoutée à une autre en tant que vue inférieure, le répondeur suivant est fixé automatiquement. Par conséquent, la modification de la chaîne doit se faire après la création de la hiérarchie de vues. Par exemple, pour insérer un contrôleur de vue, voici le code à utiliser : // myView a déjà été ajouté à sa vue supérieure. NSResponder *theNextResponder = [myView nextResponder]; [myView setNextResponder:myViewController]; [myViewController setNextResponder:theNextResponder];
Cette façon de faire peut être utile si le contrôleur de vue redéfinit -keyDown: de manière à accepter des raccourcis clavier, comme la touche Suppr. Lorsque le contrôleur de vue est inséré dans la chaîne après la vue, il répond uniquement si la vue ellemême est active. Cela a un sens lorsqu’une fenêtre donnée possède plusieurs couples vue-contrôleur. Si la vue est la seule de ce type dans la fenêtre ou si les actions du contrôleur doivent toujours être actives, alors, il est possible de l’insérer après le contrôleur de la fenêtre. En plaçant le contrôleur en différents endroits de la chaîne, il est possible de modifier les comportements et de décider lorsqu’il répondra ou non aux entrées de l’utilisateur. Exploiter la chaîne de répondeurs Si vous souhaitez implémenter des fonctionnalités contextuelles, la meilleure solution consiste à profiter des chaînes de répondeurs existantes. Par exemple, aux débuts de NeXTSTEP, le prédécesseur de Cocoa, les articles de menu devaient être activés et désactivés manuellement. La validation automatique des articles de menu a fini par être ajoutée. Pour que les développeurs puissent adopter facilement cette nouvelle fonctionnalité, la chaîne de répondeurs a servi de base à son implémentation. Dans Cocoa, la validation d’un menu se fonde sur une utilisation simple de la chaîne de répondeurs. Si aucun objet de la chaîne de répondeurs ne peut répondre à l’action envoyée par l’article de menu, celui-ci doit être désactivé. Avec la validation la plus simple, si l’utilisateur place le focus sur un élément d’interface qui répond au message d’action de l’article de menu, celui-ci peut alors être activé. Toutefois, prenons le cas d’un objet de texte. Les actions couper et copier n’ont pas de sens si aucun texte n’est sélectionné. Par conséquent, il est raisonnable d’ajouter une méthode de validation facultative, -validateMenuItem:, qui retourne YES ou NO pour décider de l’activation de l’article de menu. En réalité, l’article de menu interroge l’objet d’interface cible pour savoir si l’action a un sens étant donné l’état courant de l’objet.
252
Les design patterns de Cocoa
En implémentant ainsi la validation des menus, une grande quantité de code de prise en charge de l’activation et de la désactivation des articles de menu peut être supprimée, ce qui simplifie énormément les applications Cocoa. La mise en œuvre de certains comportements contextuels peut suivre une approche comparable. Par exemple, prenons la création d’une sous-classe de bouton qui peut se valider automatiquement à la manière des objets NSMenuItem. Puisque l’objet n’a aucunement besoin de déclarer des variables ou des méthodes d’instance, l’en-tête correspondant est simple : #import @interface MyValidatingButton : NSButton { } @end
L’implémentation réalise deux opérations. Tout d’abord, le bouton est configuré de manière à observer la notification NSApplicationDidUpdateNotification pour qu’il puisse se valider périodiquement. Cela conduit à des validations plus fréquentes que nécessaire, mais cette solution reste simple et nous permet de nous concentrer sur la validation elle-même. Pour valider le bouton, il est nécessaire de déterminer sa cible et d’invoquer ensuite une méthode de validation sur celle-ci. Si le résultat de la validation exige un changement de l’état d’activation du bouton, il est réalisé. Voici l’implémentation complète du bouton à validation : #import "MyValidatingButton.h" @implementation MyValidatingButton - (void)awakeFromNib { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidUpdate) name:NSApplicationDidUpdateNotification object:nil]; } - (void)applicationDidUpdate:(id)userInfo { BOOL validated = NO; id myTarget = [NSApp targetForAction:[self action] to:[self target] from:self]; if (myTarget) validated = YES; if ([myTarget respondsToSelector:@selector(validateMenuItem)]) { NSMenuItem *myItem = [[NSMenuItem alloc] initWithTitle:[self title] action:[self action] keyEquivalent:@""];
Chapitre 18
Chaîne de répondeurs
253
validated = [myTarget validateMenuItem:myItem]; [myItem release]; } if ([self isEnabled] != validated) { [self setEnabled:validated]; } } @end
S’il n’existe aucune cible valide, le bouton doit être désactivé. Par conséquent, nous commençons par initialiser validated à NO. La cible est déterminée en utilisant la méthode -targetForAction:to:from: de NSApplication. Si une cible est trouvée, nous validons provisoirement le bouton et fixons validated à YES. Ensuite, si la cible répond à la méthode de validation de l’article de menu -validateMenuItem:, nous créons un article de menu factice qui reprend l’intitulé et l’action du bouton, puis invoquons la méthode -validateMenuItem: pour effectuer une validation finale. Si la valeur finale de validated diffère de l’état d’activation du bouton, -setEnabled: le met à jour. Pour tester cette classe, nous créons une application simple qui comprend un NSTextView dans une fenêtre et trois boutons. Ces boutons envoient -cut:, -copy: et -paste: au premier répondeur (voir Figure 18.3). Figure 18.3 Utilisation des boutons avec validation.
254
18.3
Les design patterns de Cocoa
Exemples dans Cocoa
Les chaînes de répondeurs sont un élément central de la conception de Cocoa. Les événements du clavier et de la souris sont distribués par l’objet NSApplication et envoyés dans la chaîne de répondeurs appropriée. Les événements du clavier sont transmis à la chaîne de répondeurs de la fenêtre active, tandis que ceux de la souris peuvent être distribués à différentes fenêtres selon la position de la souris et le type de l’événement. Par exemple, les déplacements de la souris sont dirigés vers la fenêtre active, tandis que les événements de défilement vont à la fenêtre qui se trouve sous le pointeur de la souris. Une version étendue de la chaîne de répondeurs, qui comprend celle de la fenêtre active, celle de la fenêtre principale et les instances partagées de NSApplication et de NSDocumentController, permet de distribuer les messages d’action envoyés à une cible nil. C’est dans les articles de menu que les actions envoyées à nil sont les plus fréquentes, puisque de nombreuses commandes de menu doivent opérer sur la vue et/ou la fenêtre qui possède le focus. Chaque fois qu’une action doit changer dynamiquement sa cible en fonction du contexte de l’application, la meilleure solution est généralement d’utiliser une action envoyée à nil. Le copier-coller, la sélection de la police et de la couleur, la manipulation de la règle et l’annuler-rétablir sont autant d’actions qui sont communément envoyées à la chaîne de répondeurs étendue. Grâce à la chaîne de répondeurs, l’implémentation de la validation automatique des articles de menu est simple. L’article de menu et l’objet ciblé sont totalement découplés, car aucun d’eux n’a connaissance de l’autre jusqu’au moment de la validation. Le code de validation peut ainsi rester simple et bien organisé. Les menus contextuels exploitent également la chaîne de répondeurs. Si une vue ne veut pas générer un menu contextuel, cette opportunité est donnée à sa vue supérieure. Ce principe remonte la chaîne de répondeurs. Ainsi, certaines vues peuvent offrir des menus contextuels très spécifiques, tandis que d’autres fournissent des menus plus généraux. L’aide contextuelle fonctionne de la même manière, offrant à chaque objet de la chaîne de répondeurs l’opportunité d’apporter une aide. Toutes ces fonctionnalités contextuelles n’étaient pas présentes dans les premières versions de Cocoa ou de ses prédécesseurs. La puissance et la flexibilité de la chaîne de répondeurs ont permis de les implémenter de manière élégante, ce qui permet généralement aux développeurs de les adopter avec peu ou pas de travail supplémentaire. Il est fort probable que les futures fonctionnalités d’AppKit exploiteront également les chaînes de répondeurs pour que les développeurs les adoptent facilement et rapidement.
Chapitre 18
18.4
Chaîne de répondeurs
255
Conséquences
Cocoa se fonde sur la flexibilité de la distribution des messages le long d’une chaîne pour proposer automatiquement des fonctionnalités applicatives aussi variées que la distribution des événements du clavier et de la souris, la validation des articles de menu, l’annuler-rétablir, le copier-coller, la sélection de la police, les menus contextuels et l’aide contextuelle. Grâce à la chaîne de répondeurs, toutes les applications Cocoa graphiques bénéficient directement, ou en écrivant très peu de code, de ces fonctionnalités, ainsi que d’autres. Les développeurs peuvent inclure ces fonctionnalités dans leurs applications avec peu d’efforts, voire aucun. Il leur est également possible de personnaliser les chaînes de répondeurs de manière à simplifier la mise en œuvre de comportements contextuels propres aux applications.
19 Mémoire associative Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Mémoire associative fait partie des plus anciens et des plus utilisés dans le développement de logiciels. Il permet d’accéder rapidement et facilement à des données en utilisant les clés correspondantes. La mémoire associative promeut la flexibilité et l’efficacité du stockage à l’exécution.
19.1
Motivation
Voici les objectifs de la mémoire associative : n
Stocker efficacement des données quelconques associées à des objets.
n
Promouvoir la flexibilité en retardant jusqu’à l’exécution la sélection des données auxquelles accéder.
n
Offrir des possibilités d’extension au niveau de l’instance, non de la classe.
n
Contourner les limites du langage Objective-C qui obligent à passer par des sousclasses pour ajouter des variables d’instance.
19.2
Solution
Dans le framework Foundation de Cocoa, les principales classes qui mettent en œuvre la mémoire associative sont NSDictionary et NSMutableDictionary. Une instance de NSDictionary associe des clés à des valeurs de type objet. La méthode -objectForKey:
258
Les design patterns de Cocoa
permet de retrouver un objet précédemment stocké dans un dictionnaire. Elle retourne l’objet qui est associé à la clé indiquée. NSMutableDictionary, une sous-classe de NSDictionary, offre la méthode -setObject:forKey: pour créer de nouvelles associations dans le dictionnaire. Lorsque des clés et des valeurs sont ajoutées ou retirées d’un dictionnaire altérable, la mémoire allouée au stockage des objets grandit et diminue de manière automatique. Si -setObject:forKey: est invoquée avec une clé déjà présente dans le dictionnaire, l’objet associé à cette clé est remplacé par le nouvel objet. Les clés sont uniques et présentes en un seul exemplaire dans chaque dictionnaire. Les objets mémorisés dans un dictionnaire sont retenus lorsqu’ils sont ajoutés à la collection et relâchés lorsqu’ils en sont retirés. Le Chapitre 10 détaille les implications de la retenue et de la libération des objets. Les clés ajoutées à un dictionnaire sont copiées. Autrement dit, tous les objets utilisés en tant que clés dans un dictionnaire doivent se conformer au protocole formel NSCopying déclaré dans NSObject.h. Par ailleurs, ces objets doivent implémenter les méthodes -isEqual: et -hash afin que deux objets considérés égaux par la méthode -isEqual: possèdent également la même valeur de hachage. Les méthodes -isEqual: et -hash sont déclarées dans la classe NSObject, qui fournit une implémentation de base fondée sur les adresses des objets. Autrement dit, deux objets sont égaux s’ils possèdent la même adresse et la valeur de hachage est calculée à partir de l’adresse. Selon les besoins, les classes dérivées de NSObject redéfinissent les mises en œuvre héritées de -isEqual: et de -hash. Par exemple, la comparaison des instances de la classe NSString se fonde sur les chaînes de caractères qu’elles contiennent, non sur leur adresse, et la valeur retournée par -hash est également calculée à partir des chaînes mémorisées. INFO La mise en œuvre de NSDictionary et des autres fonctionnalités de la mémoire associative proposées par Cocoa se fondent sur des tables de hachage. Les tables de hachage sont expliquées dans la plupart des ouvrages d’initiation aux structures de données. Vous en trouverez une excellente introduction à l’adresse http://ciips.ee.uwa.edu.au/~morris/Year2/PLDS210/ hash_tables.html et une explication élaborée à l’adresse http://www.cris.com/~Ttwang/ tech/inthash.htm.
Dans Cocoa, l’interface à la mémoire associative passe par une structure de données NSMapTable et les fonctions qui la manipulent. NSMapTable est utilisée dans l’exemple suivant car elle apporte une flexibilité légèrement supérieure à NSDictionary. Les dictionnaires copient toujours leurs clés, mais, dans l’exemple suivant, il est indispensable de les conserver sans les copier ni les retenir.
Chapitre 19
Mémoire associative
259
Simuler les variables d’instance Le pattern Catégorie (voir Chapitre 6) permet d’ajouter uniquement des méthodes à une classe. Les variables d’instance doivent être déclarées dans l’interface de la classe principale. L’exemple suivant montre comment utiliser le pattern Mémoire associative pour simuler l’ajout d’une variable d’instance à la classe Cocoa NSObject. La catégorie donne accès à un libellé différent pour chaque instance de NSObject ou de toute classe dérivée de NSObject. Les instances auxquelles aucun libellé n’est affecté ne consomment pas d’espace mémoire supplémentaire. La catégorie suivante déclare les méthodes -mySetLabel: et -myLabel : #import @interface NSObject (MYSimulateIVar) - (void)setMyLabel:(NSString *)aString; - (NSString *)myLabel; @end
Les méthodes définies dans cette catégorie sont des accesseurs. Le pattern Accesseur est important et est décrit au Chapitre 10. Le principal objectif des accesseurs est de canaliser toutes les références aux variables d’instance dans quelques méthodes, en général seulement deux. Dans cet exemple, l’utilisation des accesseurs permet de cacher aux programmeurs qui utilisent la classe NSObject le fait que les libellés ne soient pas des variables d’instance. Les accesseurs placent un écran entre les utilisateurs d’une classe et son implémentation. #import "MYSimulateIvar.h" @implementation NSObject (MYSimulateIVar) // static NSMapTable *_MYSimulatedIVarMapTable = NULL; + (NSMapTable *)_mySimulatedIVarMapTable // { if(NULL == _MYSimulatedIVarMapTable) { _MYSimulatedIVarMapTable = NSCreateMapTable( NSNonRetainedObjectMapKeyCallBacks, NSObjectMapValueCallBacks, 16); } return _MYSimulatedIVarMapTable; }
260
Les design patterns de Cocoa
- (void)dealloc // Implémentation potentiellement risquée. { NSMapRemove([[self class] _mySimulatedIVarMapTable], self); NSDeallocateObject(self); } - (void)setMyLabel:(NSString *)aString // { NSString *newLabel = [aString copy]; NSMapInsert([[self class] _mySimulatedIVarMapTable], self, newLabel); [newLabel release]; } - (NSString *)myLabel // { return NSMapGet([[self class] _mySimulatedIVarMapTable], self); } @end
L’implémentation de la catégorie MYSimulateIVar recèle plusieurs éléments importants. La méthode de classe +_myRefCountMapTable permet d’accéder à la structure de données NSMapTable qui contient les libellés associés aux instances de NSObject. Cette méthode n’est pas déclarée dans l’interface de la catégorie, car il s’agit d’un détail d’implémentation privé. À la première invocation de +_myRefCountMapTable, la structure de données est initialisée de manière à stocker des objets non retenus pour les clés et des objets retenus pour les valeurs. Il est essentiel que les clés ne soient pas retenues car, dans le cas contraire, il serait impossible de désallouer correctement les instances de NSObject qui possèdent des libellés. La table est initialisée avec un espace suffisant pour seize couples clé-valeur, mais ce nombre est arbitraire. La mémoire réservée à la table augmente automatiquement lors de l’ajout des clés et des valeurs. La méthode -dealloc implémentée dans la catégorie remplace la version fournie par NSObject. Elle supprime tout couple clé-valeur associé à une instance lors de la désallocation de celleci. Dans ce cas, nous pouvons changer la mise en œuvre de -dealloc de NSObject car, depuis Mac OS X 10.5, la documentation de cette méthode stipule qu’elle ne fait rien à part appeler NSDeallocateObject(). Si Apple vient à changer l’implémentation de -dealloc dans la classe NSObject, le fait que la catégorie court-circuite cette implémentation pourrait avoir des effets secondaires indésirables. Pour compléter la prise en charge des libellés associés aux objets, il est nécessaire d’ajouter les fonctions de codage et de décodage afin qu’ils puissent être stockés avec les autres données lors du codage des objets. Le Chapitre 11 présente un exemple d’uti-
Chapitre 19
Mémoire associative
261
lisation des accesseurs dans la mise en œuvre des méthodes de codage et de décodage. La copie des libellés doit également être prise en charge au moment de la copie des objets. Une technique générale est décrite au Chapitre 12. Enfin, cet exemple se limite à l’ajout de libellés aux objets. Toutefois, nous pouvons proposer une catégorie pour stocker n’importe quelles données avec chaque objet. Pour cela, il suffit de modifier l’exemple de manière à utiliser des dictionnaires de couples clé-valeur, avec les méthodes -mySetUserInfo: et -myUserInfo en remplacement des méthodes -mySetLabel: et -myLabel. - (void)mySetUserInfo:(NSDictionary *)aDictionary // { NSDictionary *newDictionary = [aDictionary copy]; NSMapInsert([[self class] _mySimulatedIVarMapTable], self, newDictionary); [newDictionary release]; } - (NSDictionary *)myUserInfo // { return NSMapGet([[self class] _mySimulatedIVarMapTable], self); }
Le dictionnaire associé à chaque objet peut contenir autant de couples clé-valeur que souhaité. Pour conserver la possibilité de mémoriser des libellés, il suffit simplement d’enregistrer une chaîne de libellé associée à une clé de type @"Label" dans chaque dictionnaire d’informations utilisateurs (UserInfo). Objective-C ne prend pas en charge les véritables variables de classe, mais la technique décrite permet de les simuler. Une implémentation généralisée des variables de classe simulées peut se fonder sur la mémoire associative. Le nom de la classe peut servir de clé afin d’obtenir un dictionnaire pour cette classe. Le nom de la variable de classe peut ensuite être utilisé comme clé pour retrouver la valeur réelle.
19.3
Exemples dans Cocoa
De nombreuses classes Cocoa, notamment NSAttributedString, NSFileManager, NSNotification et NSProcessInfo, exploitent le pattern Mémoire associative. Il peut servir à simuler des variables d’instance, mais l’inverse est également possible. Cocoa utilise le système de codage clé-valeur pour donner accès aux variables d’instance de n’importe quel objet comme si elles étaient simulées à l’aide de ce pattern. La mémoire associative constitue aussi la base du système d’archivage de Cocoa (voir Chapitre 11).
262
Les design patterns de Cocoa
Nous l’avons déjà mentionné, la classe NSDictionary peut servir à mettre en œuvre la mémoire associative pour des propriétés quelconques des objets NSNotification et NSFileManager. NSNotification déclare la méthode -userInfo, qui retourne un dictionnaire contenant des clés et des valeurs arbitraires. La méthode -fileAttributesAtPath:traverseLink: de NSFileManager retourne un dictionnaire qui stocke le sousensemble des attributs possibles d’un fichier sous forme de couples clé-valeur. Le dictionnaire des attributs de mise en forme d’un texte contenu dans les instances de la classe NSAttributedString est un autre exemple notable. Chaque chaîne peut avoir des attributs différents et l’ensemble des attributs possibles n’est pas fini. En utilisant un dictionnaire pour mémoriser ces attributs, une application est en mesure de définir ses propres attributs sans passer par la création d’une sous-classe de NSAttributedString. La méthode -environment de la classe NSProcessInfo retourne un dictionnaire de couples clé-valeur qui correspondent aux variables d’environnement d’un processus en cours d’exécution. Une fois encore, puisque l’ensemble des noms de variables et des valeurs n’est pas fini, le pattern Mémoire associative représente la solution parfaite. Gestion de la mémoire par comptage des références Cocoa utilise le pattern Mémoire associative pour stocker le compteur nécessaire à la gestion de la mémoire par comptage des références. L’exemple suivant décrit la catégorie MYRefCounted de NSObject qui utilise la mémoire associative pour conserver un compteur de références, à la manière dont il est implémenté dans Cocoa. Le code illustre la technique de base et souligne certains avantages et inconvénients de la mémoire associative : #import @interface NSObject (MYRefCounted) - (int)retainCount; - (id)retain; - (void)release; @end
Les méthodes -retainCount, -retain et -release sont au cœur de la mise en œuvre de la gestion de la mémoire par comptage des références dans Cocoa. Une autre méthode cruciale, -autorelease, et la classe NSAutoreleasePool, utilisées pour la libération automatique, ne sont pas présentées ici mais sont décrites au Chapitre 10. #import "MYRefCounted.h" @implementation NSObject (MYRefCounted)
Chapitre 19
Mémoire associative
// static NSMapTable *_MYRefCountMapTable = NULL; + (NSMapTable *)_myRefCountMapTable // Donner accès à la table qui mémorise les compteurs de références. { if(NULL == _MYRefCountMapTable) { _MYRefCountMapTable = NSCreateMapTable( NSNonRetainedObjectMapKeyCallBacks, NSIntMapValueCallBacks, 16); } return _MYRefCountMapTable; } - (int)retainCount // Retourner le compteur de références courant du récepteur. { int result = 1; // Le compteur d’un récepteur absent // de la table est égal à 1. void *tableValue = NSMapGet( [[self class] _myRefCountMapTable], self); if(NULL != tableValue ) { // Si le récepteur est dans la table, son compteur correspond // à la valeur mémorisée. result = (int)tableValue; } return result; } - (id)retain // Augmenter le compteur de références du récepteur. { // Mémoriser la valeur augmentée dans la table. NSMapInsert([[self class] _myRefCountMapTable], self, (void *)([self retainCount] + 1)); return self; } - (void)release // Diminuer le compteur de références du récepteur et // le désallouer s’il atteint zéro. { int currentRetainCount = [self retainCount]; if(1 == currentRetainCount) { // Le compteur de références va atteindre zéro, le désallouer. // Il est inutile de retirer le récepteur de la table car,
263
264
Les design patterns de Cocoa
// si son compteur de références est égal à un, il n’est pas // dans la table. [self dealloc]; } else if(2 == currentRetainCount) { // Retirer le récepteur de la table pour indiquer que // son compteur de références est égal à 1. NSMapRemove([[self class] _myRefCountMapTable], self); } else { // Mémoriser la valeur décrémentée dans la table. NSMapInsert([[self class] _myRefCountMapTable], self, (void *)(currentRetainCount - 1)); } } @end
Les objets qui ne se trouvent pas dans _MYRefCountMapTable ont un compteur de références implicite égal à un. Par exemple, les objets nouvellement alloués ne sont pas stockés dans la table et leur compteur de références est donc égal à un. Par ailleurs, hormis la mémoire nécessaire aux variables d’instance, aucun autre espace supplémentaire n’est utilisé. En se basant sur l’hypothèse qu’à tout moment la plupart des objets ont un compteur de références égal à un, ce système utilise efficacement la mémoire. Chaque fois qu’un objet est retenu en invoquant la méthode -retain, son compteur de références augmente et est placé dans la table. Chaque fois qu’un objet est relâché à l’aide de la méthode -release, le compteur de références associé stocké dans la table est diminué. Si le compteur de références est en passe d’atteindre la valeur un, l’association est retirée de la table. Lorsqu’il va arriver à zéro, l’objet est désalloué immédiatement. Codage clé-valeur Jusque-là, ce chapitre s’est principalement intéressé à la simulation des variables d’instance à l’aide du pattern Mémoire associative. Le codage clé-valeur (KVC, Key Value Coding) de Cocoa fait exactement l’inverse. Il donne accès aux variables d’instance d’un objet en utilisant une sémantique semblable à la mémoire associative. Le codage clé-valeur permet d’accéder aux propriétés d’un objet indirectement en utilisant des clés de type chaînes de caractères à la place des accesseurs ou des références directes aux variables d’instance. Cette technique a été imaginée pour simplifier l’interaction des langages de script avec les objets Cocoa, mais elle trouve un intérêt dans de nombreux contextes. Les deux principales méthodes de mise en œuvre du codage clé-valeur sont -valueForKey et -setValue:forkey:. La méthode -setValue:forKey: utilise la chaîne de caractères indiquée comme une clé permettant d’identifier un accesseur ou une variable d’instance. Si une méthode ou une variable correspondante est trouvée, la
Chapitre 19
Mémoire associative
265
valeur de la variable est fixée à celle indiquée. La méthode -valueForKey: retourne une valeur obtenue en invoquant un accesseur ou en accédant directement à la variable d’instance identifiée par la clé. Les implémentations Cocoa de -setValue:forKey: et de -valueForKey: tentent tout d’abord d’utiliser une méthode accesseur dont le nom correspond à la clé. L’accesseur set doit être de la forme -set:, où est la chaîne utilisée comme clé. La première lettre de la clé est mise en majuscule, si ce n’est déjà le cas, de manière à respecter la convention Cocoa de nommage des méthodes, c’est-à-dire la première lettre de tous les mots en majuscule, excepté le premier. Par exemple, si -setValue:forKey: est appelée avec la chaîne @"label" comme clé, elle tente d’utiliser un accesseur nommé -setLabel: pour fixer la valeur. Pour obtenir une valeur avec -valueForKey:, cette méthode tente d’utiliser un accesseur nommé -. Dans ce cas, la première lettre de la clé est convertie en minuscule si nécessaire, afin que le nom de la méthode commence par une minuscule. Par exemple, l’invocation de -valueForKey: avec la clé @"label" conduit à l’appel de la méthode -label, si elle existe. Le fonctionnement du codage clé-valeur est très sophistiqué. Il se replie sur l’utilisation des accesseurs qui commencent par le caractère "_", puis, si tout le reste a échoué, sur l’accès direct aux variables d’instance dont les noms dérivent des chaînes utilisées comme clés. Ce système permet aux programmes d’interagir avec des objets comme s’ils étaient des dictionnaires qui associent leurs propriétés à des clés. Le codage clévaleur est également décrit aux Chapitres 29 et 30. Grâce au codage clé-valeur, les langages de script peuvent accéder, à l’exécution, aux valeurs enregistrées dans des objets en utilisant les noms de ces valeurs. Mais, sans doute plus important encore, il sert de base aux bindings Cocoa. Le codage clé-valeur applique le pattern Mémoire associative à chaque objet Cocoa de manière que les variables ou les accesseurs d’une instance d’objet soient sélectionnés à l’exécution par l’intermédiaire de clés données sous forme de chaîne de caractères.
19.4
Conséquences
La mémoire associative est flexible et permet d’implémenter des fonctionnalités qui n’ont pas été anticipées. L’association d’un dictionnaire à chaque instance de NSObject ouvre des utilisations illimitées. Toutefois, l’accès aux valeurs stockées dans un dictionnaire ou dans une table de correspondance n’est pas aussi efficace qu’un accès direct à des variables d’instance. En général, le code d’un programme peut accéder à une variable d’instance en une seule instruction machine, mais la mémoire associative exige plusieurs appels de méthodes ou de fonctions, des calculs de valeurs de hachage et des accès indicés à la mémoire. Le stockage d’une valeur demande de la mémoire pour la
266
Les design patterns de Cocoa
clé et la valeur. Si chaque objet utilise une zone de stockage associée, la quantité de mémoire nécessaire à l’ensemble des clés et des valeurs dépasse largement celle du stockage des valeurs dans des variables d’instance. Si le stockage des valeurs est rare, l’utilisation de la mémoire associative peut se révéler très avantageuse. Au lieu de stocker des variables inutilisées dans chaque instance, la mémoire est réservée uniquement lorsque les valeurs sont réellement employées. Les exemples de ce chapitre utilisent la mémoire associative avec les catégories, mais cette technique s’applique à de nombreux cas. En réalité, les inconvénients liés au remplacement des méthodes comme -dealloc par des méthodes de catégorie sont importants. Lorsque la création d’une sous-classe est envisageable, l’ajout des variables d’instance dans cette sous-classe constitue probablement le meilleur choix. L’une des solutions les plus flexibles pour la mémoire associative consiste à utiliser un objet NSDictionary comme variable d’instance. C’est l’approche retenue par la classe Cocoa NSNotification pour enregistrer des données arbitraires avec chaque instance. La classe NSFileManager emploie une technique comparable pour associer aux fichiers des informations spécifiques au système de fichiers. Dans le cas de NSFileManager, la flexibilité est obligatoire car chaque fichier géré peut être stocké sur un système de fichiers différent avec des attributs propres à ce système. L’utilisation d’un dictionnaire d’attributs permet de conserver uniquement les attributs pertinents. La mémoire associative est également utile pour raccourcir les noms des méthodes. Lorsqu’une méthode complexe prend plusieurs arguments, il peut être intéressant de l’implémenter avec un seul argument NSDictionary. Si certains des arguments sont facultatifs, il peut être tentant de créer de nombreuses méthodes de commodité avec des noms plus courts qui laissent de côté ces arguments facultatifs. En utilisant la mémoire associative pour passer les arguments, il est possible d’éviter la prolifération des méthodes. Les clés facultatives sont simplement omises lors du remplissage du dictionnaire et la méthode peut fournir des valeurs par défaut pour les clés manquantes. Grâce à la mémoire associative, les émetteurs peuvent également ne pas être impactés par les futures modifications. Si une version ultérieure de la méthode accepte de nouveaux arguments, la modification du message n’est pas obligatoire. Il suffit d’ajouter de nouveaux couples clé-valeur au dictionnaire. Si des arguments sont retirés, les clés superflues du dictionnaire sont ignorées. Cette approche a toutefois pour inconvénient d’imposer du code supplémentaire dans l’émetteur de manière à configurer et à remplir une instance de NSDictionary.
20 Invocation Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Les invocations constituent une technique qui permet de conserver l’état des messages, des arguments et des valeurs de retour. Elles permettent de découpler totalement l’émetteur d’un message et son récepteur, qui peuvent se trouver dans des processus différents ou être temporellement séparés. Les invocations sont mises en œuvre par la classe Cocoa NSInvocation et sont utilisées dans les objets distribués, l’annuler-rétablir et la planification du traitement périodique des événements. Les applications les utilisent pour mettre en place une grande diversité de comportements flexibles et dynamiques. Les invocations sont une implémentation généralisée du pattern réputé Commande (http://fr.wikipedia.org/wiki/Commande_(patron_de_conception)).
20.1
Motivation
L’idée est d’obtenir un mécanisme de capture des messages afin qu’ils puissent être stockés, retardés, redirigés ou traités et manipulés comme des objets. De nouveaux messages doivent pouvoir être construits et envoyés à l’exécution, sans impliquer le compilateur.
20.2
Solution
Les programmeurs Objective-C ne doivent pas oublier qu’envoyer un message à un objet n’équivaut pas à invoquer une méthode. Lorsqu’un objet reçoit un message, une
268
Les design patterns de Cocoa
méthode est généralement invoquée pour traiter ce message. Toutefois, ce n’est pas toujours le cas. Par exemple, si un objet n’implémente pas la méthode qui correspond au message, aucune méthode ne peut alors être invoquée et une exception d’exécution est lancée. En raison de la flexibilité du moteur d’exécution d’Objective-C, il est possible que des messages soient retardés, redirigés vers d’autres récepteurs ou même ignorés. Les messages Objective-C sont comparables aux messages du monde réel. Par exemple, une secrétaire peut recevoir des messages pour un directeur. Ils peuvent être traités par la secrétaire, donnés tels quels au directeur ou modifiés avant d’être transmis. Un message particulièrement important peut être copié et envoyé à plusieurs destinataires. En Objective-C, tous ces scénarios sont possibles. Le Chapitre 27 explique comment les mettre en œuvre. Au centre de cette flexibilité se trouve le besoin de traiter un message comme s’il s’agissait d’un objet que l’on peut créer et manipuler. La classe NSInvocation représente un message Objective-C. Une instance de NSInvocation encapsule tous les attributs d’un message Objective-C. Elle connaît le récepteur du message, le nom du message (le sélecteur et la signature de méthode), ainsi que les arguments du message. Après avoir invoqué une instance de NSInvocation, la valeur de retour du message (si elle existe) peut être obtenue à partir de cette instance. Signature de méthode De nombreux développeurs sont un peu perturbés par la manière de nommer les messages. Un nom de message est constitué de deux parties : le sélecteur et la signature de méthode. Ces deux composantes sont indispensables pour configurer correctement des objets NSInvocation. Le sélecteur correspond au nom du message sans les informations de type. Par exemple, countOfObjectsOfType: est un sélecteur. Objective-C propose également le type de données SEL, qui est un identifiant unique représentant un sélecteur. La directive Objective-C @selector() permet d’obtenir un SEL. La plupart des programmeurs pensent que le sélecteur est le nom du message, ce qui est vrai dans de nombreux cas. Toutefois, les sélecteurs ne fournissent aucune information de type. Pour construire un message complet, le type de chaque argument et le type de la valeur de retour doivent être connus. Ces informations de type représentent une signature de méthode. La classe NSMethodSignature encapsule ces informations. Pour obtenir une instance de NSMethodSignature, on demande généralement à une sous-classe de NSObject la signature correcte donnée à un certain sélecteur. Cette demande est nécessaire car le même sélecteur peut avoir une signature différente selon l’objet qui répond. Par exemple, prenons deux méthodes ayant le même sélecteur mais des signatures différentes et un message qui invoque l’une d’elles :
Chapitre 20
Invocation
269
- (int)countOfObjectsOfType:(id)objectType; // Définie dans ClasseA. - (NSValue *)countOfObjectsOfType:(int)objectType; // Définie dans ClasseB. value = [myObject countOfObjectsOfType:aType];
Si le récepteur, myObject, est de type id, ce message déclenche un avertissement du compilateur car il ne sait pas quelle signature de méthode utiliser lors de la construction du message. Normalement, le développeur utilisera un typage statique sur le récepteur pour indiquer au compilateur le type de l’objet qui recevra le message. Il est également possible de forcer le type lors de l’envoi du message. Les deux solutions suivantes permettent de supprimer l’avertissement du compilateur : // Si vous devez utiliser un type id, alors indiquez le type / lors de l’envoi du message. id myObject; value = [(ClassA *)myObject countOfObjectsOfType:aType]; // Sinon, utilisez un typage statique à la place du type id // générique pour lever l’ambiguïté. ClassA *myObject; value = [myObject countOfObjectsOfType:aType];
Toutefois, à l’exécution, le récepteur est connu et il est possible de lui demander simplement la signature correcte du message : NSMethodSignature* mySignature = [myObject methodSignatureForSelector:@selector(countOfObjectsOfType:)];
Puisque l’initialiseur désigné des instances de NSInvocation a besoin d’une signature de méthode, vous ne pouvez pas créer des invocations sans tout d’abord obtenir une signature de méthode. Puisque l’objet NSMethodSignature connaît également le type attendu de la valeur de retour, celui-ci est nécessaire pour obtenir la valeur de retour à partir de l’instance de NSInvocation. Utiliser des objets NSInvocation Pour envoyer un message avec un NSInvocation, il faut tout d’abord créer une instance de cette classe et la configurer. Une fois que l’instance a été configurée, elle peut être invoquée au moment opportun pour déclencher l’envoi d’un message Objective-C. La valeur de retour du message peut ensuite être obtenue à partir de l’objet NSInvocation. Les instances de NSInvocation peuvent être invoquées à plusieurs reprises, avec, si nécessaire, des modifications de la cible, des arguments ou même du sélecteur. Pour illustrer la création et l’utilisation de NSInvocation, nous allons écrire une application simple qui manipule des chaînes de caractères. L’utilisateur pourra saisir une chaîne, choisir l’opération à réaliser, fournir les arguments appropriés et cliquer sur un bouton pour envoyer un message qui effectuera l’opération sur la chaîne et affichera la valeur de retour. Une classe, nommée InvocationController, est nécessaire. Voici son interface :
270
Les design patterns de Cocoa
@interface { IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet }
InvocationController : NSObject NSTextField NSPopUpButton NSTextField NSTextField NSTextField
*receiver; *message; *argument1; *argument2; *result;
- (IBAction)inputChanged:(id)sender; - (IBAction)sendMessage:(id)sender; @end
La Figure 20.1 présente l’interface utilisateur employée par cet exemple, y compris les connexions à l’objet contrôleur.
Figure 20.1 L’interface de l’exemple pour NSInvocation.
La liste déroulante est configurée avec un NSMenuItem pour chaque opération reconnue. L’intitulé de chaque article du menu correspond au sélecteur du message à envoyer. Il est extrêmement important d’écrire cet intitulé de manière correcte. La valeur du champ TAG d’un élément de la liste indique le nombre d’arguments requis par le message. Par
Chapitre 20
Invocation
271
exemple, puisque la méthode -lowercaseString ne prend aucun argument, l’intitulé est "lowercaseString" et le paramètre tag vaut "0". La Figure 20.2 présente tous les éléments du menu déroulant pour notre exemple.
Figure 20.2 Les différentes options proposées par la liste déroulante.
La méthode -inputChanged: du contrôle active ou désactive simplement les champs de saisie des arguments en fonction de l’option sélectionnée par l’utilisateur. - (IBAction)inputChanged:(id)sender { int numberOfArguments = [[message selectedItem] tag]; [argument1 setEnabled:NO]; [argument2 setEnabled:NO]; switch (numberOfArguments) { case 2: [argument2 setEnabled:YES]; case 1: [argument1 setEnabled:YES]; case 0: default: break; } }
À présent que l’interface est en place, il est temps de donner du corps à cet exemple. La méthode -sendMessage: de l’objet contrôleur doit récupérer les informations de l’interface, les convertir en une instance de NSInvocation, invoquer celle-ci et afficher la valeur de retour dans l’interface.
272
Les design patterns de Cocoa
Puisque le sélecteur correspond à l’intitulé de l’article de menu, il est nécessaire de convertir un NSString en un SEL, ce que permet la fonction NSSelectorFromString(). À partir du récepteur et du sélecteur, il est possible d’obtenir la signature de méthode, qui sert ensuite à créer une instance de NSInvocation : NSString *receivingString = [receiver stringValue]; NSString *messageString = [message titleOfSelectedItem]; SEL selector = NSSelectorFromString(messageString); NSMethodSignature *methodSignature = [receivingString methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
Pour configurer une invocation, la première étape consiste à lui indiquer le récepteur du message (la cible) et le sélecteur. Puisque l’invocation a été créée avec la signature de méthode, elle connaît les informations de type, mais a également besoin de connaître le sélecteur : [invocation setTarget:receivingString]; [invocation setSelector:selector];
// L’argument 0 est "self". // L’argument 1 est "_cmd".
Chaque méthode Objective-C possède deux arguments cachés. Le premier et le plus utilisé est self. Le second, _cmd, contient le sélecteur qui a invoqué la méthode. Autrement dit, il est techniquement possible d’écrire l’implémentation d’une méthode qui est invoquée par des sélecteurs différents. L’implémentation détermine le sélecteur employé en examinant _cmd. Cela fonctionne tant que chaque sélecteur a la même signature de méthode. Cette possibilité est particulièrement utile pour la construction dynamique de classes dans un programme en cours d’exécution, par exemple pour établir un pont entre Objective-C et des langages de script. Ces arguments cachés sont importants pour la construction des instances de NSInvocation car ils sont requis par les méthodes. Si vous oubliez d’appeler -setTarget: et -setSelector:, l’invocation ne fonctionnera pas. L’étape suivante est de configurer les arguments de l’invocation. Pour cela, nous utilisons la méthode -setArgument:atIndex: de NSInvocation. Les messages envoyés peuvent avoir zéro, un ou deux arguments, ce qui est précisé par le paramètre tag de l’option sélectionnée. Puisque les arguments masqués self et _cmd occupent les positions 0 et 1 dans la liste des arguments, le premier argument de la méthode correspond en réalité à l’argument 2. De plus, avec la méthode -setArgument:atIndex:, il faut utiliser des pointeurs sur des pointeurs d’objet à la place des pointeurs d’objet. Voici le code : int numberOfArguments = [[message selectedItem] tag]; if (numberOfArguments > 0) { NSString *argumentString1 = [argument1 stringValue]; [invocation setArgument:&argumentString1 atIndex:2];
Chapitre 20
Invocation
273
if (numberOfArguments > 1) { NSString *argumentString2 = [argument2 stringValue]; [invocation setArgument:&argumentString2 atIndex:3]; } }
Par défaut, les objets d’invocation ne retiennent pas leurs arguments. Si les arguments sont des objets qui pourraient être libérés avant que l’invocation n’envoie son message, alors, le message -retainArguments doit lui être envoyé. Puisque notre invocation d’exemple sera utilisée immédiatement, il est inutile d’envoyer -retainArguments. L’invocation étant créée et tous ses arguments étant configurés, le message -invoke peut lui être envoyé. La valeur de retour est ensuite obtenue à l’aide de la méthode -getReturnValue:, qui prend en argument un pointeur sur void*. La valeur de retour est placée dans ce pointeur, mais la signature de méthode doit être consultée pour savoir comment interpréter les données. Voici le code d’invocation du message et d’interprétation de la valeur de retour : [invocation invoke]; void *returnValue = NULL; [invocation getReturnValue:&returnValue]; const char *returnType = [methodSignature methodReturnType]; if (returnType) { switch (returnType[0]) { case ‘@': [result setObjectValue:(id)returnValue]; break; case ‘i': [result setIntValue:(int)returnValue]; break; default: break; } }
L’interprétation de la valeur de retour se fonde sur la directive Objective-C @encode(), comme décrite dans la documentation du langage. Puisque ces informations dépendent de l’implémentation, il est plus prudent de tester tout code qui les utilise, à l’instar de l’instruction switch de notre exemple. Les valeurs de retour des méthodes qui peuvent être invoquées sont obligatoirement de type int (“i”) ou id (“@”). Par conséquent, seuls ces deux cas sont examinés. Un code plus robuste prendrait en charge tous les types reconnus par @encode() ou lancerait des exceptions pour les types non gérés. La Figure 20.3 illustre l’exécution de notre exemple. Un programme réel ne placerait sans doute pas les noms de méthodes directement dans une liste déroulante, mais utiliserait un dictionnaire ou tout autre mécanisme permettant de rechercher des noms de méthodes afin de proposer des options avec des intitulés plus parlants.
274
Les design patterns de Cocoa
Figure 20.3 L’exemple d’invocation en cours d’exécution.
Utiliser des minuteurs La plupart des programmeurs Cocoa manipuleront rarement directement des objets NSInvocation comme dans l’exemple précédent. Cela dit, la classe NSInvocation est employée dans l’implémentation de nombreuses parties de Cocoa. C’est par exemple le cas dans la classe NSTimer. Un minuteur (timer) prend en arguments une cible, un message à envoyer à cette cible et une durée. Lorsque la temporisation est écoulée, le minuteur envoie le message à la cible. Cela permet de créer des événements répétitifs, comme une boucle d’animation. Par exemple, une boucle d’animation qui affiche une nouvelle image vingt-quatre fois par seconde peut utiliser une temporisation de 1/24 seconde (0,0417 seconde). Lorsque cette durée est écoulée et que le message doit à nouveau être envoyé, le minuteur se déclenche. Pour illustrer ce fonctionnement, nous créons un chronomètre simple qui compte d’un nombre jusqu’à un autre, à la fréquence indiquée par l’utilisateur. Comme dans l’exemple précédent, un objet contrôleur est requis. Voici l’interface de la classe TimerController : @interface { NSTimer IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet IBOutlet int }
TimerController : NSObject NSTextField NSTextField NSTextField NSTextField NSButton NSButton NSButton
*myTimer; *startCount; *endCount; *interval; *currentCount; *startButton; *continueButton; *endButton; count;
Chapitre 20
-
Invocation
275
(void)startTimer; (void)stopTimer; (IBAction)beginTimer:(id)sender; (IBAction)continueTimer:(id)sender; (IBAction)endTimer:(id)sender; (void)count:(id)userInfo;
@end
L’interface contrôlée par cet objet est présentée à la Figure 20.4.
Figure 20.4 L’interface du minuteur.
L’implémentation du contrôleur réalise l’initialisation de base dans les méthodes -init et -awakeFromNib de manière à configurer l’interface et à fixer la valeur initiale du compteur. La méthode -dealloc met en œuvre un arrêt propre. Les actions d’Interface Builder sont de simples méthodes qui invoquent -startTimer et -stopTimer : - (id)init { if(nil != (self = [super init])) { count = 0; } return self; }
276
Les design patterns de Cocoa
- (void)awakeFromNib { count = [startCount intValue]; [currentCount setIntValue:count]; [self stopTimer]; } - (void)dealloc { [self stopTimer]; } - (IBAction)beginTimer:(id)sender { count = [startCount intValue]; [currentCount setIntValue:count]; [self startTimer]; } - (IBAction)continueTimer:(id)sender { if (!myTimer) { [self startTimer]; } } - (IBAction)endTimer:(id)sender { [self stopTimer]; }
Le minuteur doit envoyer un message pour incrémenter le compteur. Il est configuré pour que le message -count: soit envoyé au contrôleur chaque fois que la temporisation est terminée. La méthode -count: incrémente simplement le compteur et actualise l’interface, excepté lorsque le compteur a atteint la valeur finale, auquel cas elle arrête le minuteur. Voici le code correspondant : - (void)count:(id)userInfo { if (count >= [endCount intValue]) { [self stopTimer]; } else { count++; [currentCount setIntValue:count]; } }
À présent que l’interface est configurée et que le minuteur dispose d’une méthode à invoquer, il est possible d’écrire les méthodes de démarrage et d’arrêt.
Chapitre 20
Invocation
277
Puisqu’un seul minuteur ne peut s’exécuter à la fois, le contrôleur commence par arrêter tout minuteur actif. Il crée ensuite un nouveau minuteur et actualise l’interface pour activer uniquement les boutons valides. Le cœur de cette méthode est constitué d’un seul message qui crée le minuteur. En général, un minuteur est créé et ajouté ensuite à un NSRunLoop, par une procédure en deux étapes. Si vous souhaitez ajouter le minuteur à la boucle d’exécution courante, ce qui constitue le cas le plus fréquent, vous pouvez créer un minuteur "planifié" (scheduled), en une seule étape. Voici le code de création et de démarrage du minuteur : - (void)startTimer { if (myTimer) { [self stopTimer]; } myTimer = [NSTimer scheduledTimerWithTimeInterval: [interval doubleValue] target:self selector:@selector(count:) userInfo:nil repeats:YES]; [myTimer retain]; [startButton setEnabled:NO]; [continueButton setEnabled:NO]; [endButton setEnabled:YES]; }
Lors de la configuration du minuteur, nous précisons la période, le message à envoyer et son fonctionnement à répétition. Bien entendu, un minuteur dont la fonction de répétition est activée enverra le message chaque fois que la temporisation sera écoulée, jusqu’à ce qu’il soit arrêté, tandis qu’un minuteur dont ce fonctionnement est désactivé enverra le message une seule fois. Le message à envoyer par le minuteur doit respecter une signature de méthode très précise : il retourne void et prend un seul argument. L’argument est un objet nommé userInfo quelconque. Lorsque vous créez un minuteur, vous choisissez l’objet qui sera envoyé. C’est la méthode invoquée par le minuteur qui détermine l’utilisation de l’objet userInfo. En général, il est ignoré, mais il sert parfois à passer des données à la méthode invoquée. Si plusieurs minuteurs invoquent la méthode, l’objet userInfo peut servir à identifier le minuteur à l’origine de l’invocation. Si plusieurs données doivent être passées, userInfo peut être un dictionnaire, exploitant alors le pattern Mémoire associative (voir Chapitre 19). Bien que des messages soient construits et envoyés à la volée par le minuteur, nous n’avons écrit aucun code qui utilise NSInvocation. L’inconvénient est que la méthode invoquée lors du déclenchement du minuteur doit avoir une signature de méthode très précise. Si cela ne convient pas, il est possible de créer des minuteurs qui enverront n’importe quel message. Lors de la création du minuteur, au lieu de préciser une cible,
278
Les design patterns de Cocoa
un sélecteur et un objet contenant des informations utilisateurs, vous pouvez simplement passer un objet NSInvocation à la méthode +scheduledTimerWithTimeInterval: invocation:repeats:. Si cette approche est plus flexible, elle a également pour inconvénient, comme nous l’avons vu précédemment, d’exiger du code supplémentaire pour créer l’instance de NSInvocation. La dernière méthode de l’objet contrôleur arrête le minuteur. Pour stopper un minuteur à répétition, il suffit d’envoyer un message -invalidate. Au cours de la procédure d’invalidation, le minuteur se retire lui-même de la boucle d’exécution. Puisque les minuteurs sans répétition se retirent automatiquement de la boucle d’exécution après leur déclenchement, il est inutile de les invalider. La méthode -stopTimer du contrôleur invalide simplement le minuteur et actualise l’interface : - (void)stopTimer { [myTimer invalidate]; [myTimer release]; myTimer = nil; [startButton setEnabled:YES]; if ((count > [startCount intValue]) && (count < [endCount intValue])) { [continueButton setEnabled:YES]; } else { [continueButton setEnabled:NO]; } [endButton setEnabled:NO]; }
Retarder l’envoi d’un message Il est parfois utile de repousser à un moment ultérieur l’envoi d’un message. Dans ce cas, un NSTimer sans répétition peut être employé. Pour simplifier ce comportement, la classe Cocoa NSObject fournit les méthodes -performSelector:withObject:afterDelay: et -performSelector:withObject:afterDelay:inModes. La première envoie le message lorsque la boucle d’exécution se trouve dans son mode par défaut. Pour que le message soit envoyé dans une boucle modale ou dans un autre mode d’exécution particulier, la seconde méthode doit être utilisée. Les deux méthodes créent et configurent l’instance adéquate de NSTimer. Elles sont également utilisables avec n’importe quelle instance d’une sous-classe de NSObject pour envoyer un message en différé. L’utilisation d’une temporisation nulle constitue un cas intéressant. Le message est bien retardé, mais uniquement jusqu’au prochain passage dans la boucle d’exécution. Cette solution est utile lorsqu’un message doit être envoyé après la fin du traitement de l’évé-
Chapitre 20
Invocation
279
nement courant. L’invocation lors de la prochaine boucle d’exécution est un idiome fréquemment employé par les développeurs Cocoa. Par exemple, prenons le cas d’un bouton-poussoir. Lorsque l’utilisateur clique dessus, son apparence change. Supposons que l’action invoquée par un clic sur le bouton déclenche l’apparition d’un panneau d’alerte. Si la fonction NSRunAlertPanel() normale est invoquée depuis le code de traitement du bouton-poussoir, celui-ci conserve son apparence enfoncée pendant que le panneau d’alerte est affiché à l’écran. À la place, si une exécution retardée avec une temporisation nulle est utilisée, le traitement du clic se termine, le bouton reprend son apparence normale et alors seulement le panneau d’alerte est affiché. Vous pouvez facilement constater la différence entre ces deux approches à la Figure 20.5. Sur la gauche, la fonction NSRunAlertPanel() est appelée immédiatement. Sur la droite, une exécution retardée est utilisée. Le code des deux méthodes d’action invoquées par les deux boutons est très simple : - (IBAction)openAlert:(id)sender { NSRunAlertPanel(@"Alert", @"Voici une alerte.", @"OK", nil, nil); } - (IBAction)openDelayedAlert:(id)sender { [self performSelector:@selector(openAlert:) withObject:sender afterDelay:0.0]; }
Figure 20.5 L’exécution retardée permet au bouton de reprendre son apparence normale.
280
20.3
Les design patterns de Cocoa
Exemples dans Cocoa
Cocoa se sert énormément des invocations, même si leur utilisation n’est pas toujours évidente au premier abord. Elles sont employées dès qu’un message Objective-C doit être manipulé et permettent également de mémoriser, de retarder, de renvoyer et de rediriger des messages. Elles sont mises en œuvre par la classe NSInvocation. La classe NSTimer utilise des instances de NSInvocation, mais peut créer ses propres objets d’invocation. La plupart des développeurs peuvent ainsi éviter de travailler directement avec la classe NSInvocation. NSObject implémente des méthodes -performSelector:... qui, dans certains cas, permettent même de ne pas manipuler directement des instances de NSTimer. La classe NSInvocationOperation dérivée de NSOperation, introduite par Mac OS X 10.5, peut être utilisée comme une implémentation générique des opérations. Les ponts entre Objective-C et les langages Ruby et Python se fondent également sur les invocations. Les invocations sont notamment utilisées dans Cocoa pour l’annuler-rétablir de l’architecture de document et les objets distribués. La manière dont ces deux technologies exploitent les invocations est suffisamment importante pour qu’elle soit détaillée au Chapitre 27.
20.4
Conséquences
Les invocations encapsulent une méthode Objective-C de manière à la traiter comme un objet. En utilisant les invocations, un développeur peut créer et modifier dynamiquement des messages Objective-C. Les messages peuvent être mémorisés et envoyés ultérieurement ou de manière répétitive. Ils peuvent également être dupliqués, capturés et transmis à d’autres objets, qui peuvent se trouver dans d’autres applications (voir Chapitre 27).
21 Prototype Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Un prototype est un objet que l’on copie pour implémenter certaines fonctionnalités d’une application. La copie d’un objet existant apporte souvent une flexibilité supérieure à l’allocation et l’initialisation de nouvelles instances. Le pattern Prototype évite de figer dans le code des relations entre objets. Par exemple, la classe Cocoa NSMatrix affiche une grille d’objets. Lorsque des lignes et des colonnes sont ajoutées à une matrice, de nouveaux objets sont créés pour remplir chaque cellule. NSMatrix ne serait pas très flexible si elle n’acceptait qu’une sorte d’objet. Au contraire, elle vous permet de préciser un objet prototype qu’elle copie autant de fois que nécessaire pour remplir la grille. Si vous fournissez un bouton prototype, vous obtenez une grille de boutons. Si vous fournissez un champ de saisie prototype, vous obtenez une grille de champs de saisie.
21.1
Motivation
Voici les objectifs des prototypes : n
Réduire les dépendances entre les objets qui créent de nouvelles instances et les types de ces instances.
n
Permettre un contrôle à l’exécution du type de l’objet créé au lieu de préciser cette information à la compilation.
282
21.2
Les design patterns de Cocoa
Solution
La caractéristique fondamentale des objets prototypes est de pouvoir être copiés. Cocoa définit les protocoles NSCopying et NSCoding que les objets implémentent pour garantir leur interopérabilité avec des outils comme Interface Builder et des classes comme NSMatrix. INFO Le pattern Copie et le protocole associé NSCopying sont décrits au Chapitre 12. Ce chapitre se focalise sur les contraintes de la copie dans le pattern Prototype.
Le protocole NSCopying, détaillé dans la section Core Library > Cocoa > Objective-C Language > NSCopying Protocol Reference de la documentation Xcode, définit la méthode -copyWithZone:. Les instances d’une classe qui implémente -copyWithZone: peuvent être copiées. La méthode -copyWithZone: doit retourner un objet dont l’état est identique à celui de l’objet qui a reçu le message. Deux techniques sont généralement employées pour implémenter -copyWithZone:. La première et la plus fréquente retourne une copie superficielle. Une copie superficielle contient exactement les mêmes valeurs que l’objet copié. Autrement dit, si l’objet d’origine contient un pointeur sur une zone de mémoire, la copie superficielle contient un autre pointeur sur le même emplacement mémoire. Seul le pointeur est copié ; les deux pointeurs désignent le même emplacement mémoire. L’autre technique implique une copie profonde. Une copie profonde contient de vraies copies des valeurs contenues dans l’original. Par exemple, si l’original possède un pointeur sur un objet, la copie profonde contient un pointeur sur une copie de cet objet. Il est préférable d’utiliser le pattern Prototype avec des objets qui effectuent des copies profondes, car les objets copiés doivent souvent être indépendants de l’original. Par exemple, les objets copiés à partir d’une bibliothèque d’Interface Builder doivent continuer à fonctionner bien après que cet outil a été fermé. Néanmoins, la plupart des classes Cocoa implémentent le protocole NSCopying de manière à retourner des copies superficielles. La mise en œuvre de la copie profonde peut être complexe. L’objet copié peut envoyer des messages -copy aux objets qu’il référence par des pointeurs, mais, si la méthode -copyWithZone: d’un ou de plusieurs de ces objets retourne une copie superficielle, le résultat sera un mélange de copies superficielles et de copies profondes. La copie résultante ne sera donc pas totalement indépendante. Le protocole NSCoding et les classes NSArchiver et NSUnarchiver de Cocoa constituent une solution lourde mais simple pour créer des copies profondes à partir de graphes
Chapitre 21
Prototype
283
d’objets interdépendants. Si l’objet copié et tous les objets référencés se conforment au protocole NSCoding, l’implémentation suivante de la méthode -copyWithZone: génère une copie profonde : - (id)copyWithZone:(NSZone *)aZone // Retourner une copie profonde du récepteur. { id result; // S’archiver et se désarchiver immédiatement pour créer // une copie profonde. result = [NSKeyedUnarchiver unarchiveObjectWithData: [NSKeyedArchiver archivedDataWithRootObject:self]]; // Retourner un objet retenu car, par convention, l’appelant // est censé libérer les objets copiés. [result retain]; return result; }
Interface Builder emploie cette technique pour copier des objets à partir de ses bibliothèques. Lorsqu’un objet est déposé dans un fichier .nib en cours de construction, il est tout d’abord archivé à partir d’une instance prise dans la bibliothèque et désarchivé ensuite de manière à créer une copie qui sera éditée. Lorsque l’application en construction est sauvegardée dans un fichier .nib, tous les objets interconnectés sont à nouveau archivés. Un fichier .nib n’est rien d’autre qu’une archive d’objets. Lorsqu’il est chargé dans l’application en cours d’exécution, ses objets sont désarchivés et reprennent leur état qui a été défini dans Interface Builder. Le Chapitre 11 détaille le pattern Archivage et désarchivage et décrit les classes Cocoa NSKeyedArchiver et NSKeyedUnarchiver. L’archivage suivi du désarchivage représente une technique brutale pour créer des copies profondes, mais elle fonctionne parfaitement avec Interface Builder car cet outil doit pouvoir manipuler pratiquement n’importe quel objet. Interface Builder ne possède pas suffisamment d’informations sur les objets qu’il copie pour employer de manière fiable une autre technique. La classe NSMatrix travaille uniquement avec des sousclasses de NSCell et celle-ci implémente le protocole NSCopying en utilisant une autre approche, la fonction NSCopyObject(). La fonction NSCopyObject() est décrite dans la section Core Library > Cocoa > Data Management > Foundation Functions Reference de la documentation Xcode. Pour de plus amples informations concernant la copie des objets, consultez l’article Implementing Object Copy dans la section Core Library > Cocoa > Objective-C Language > Memory Management Programming Guide for Cocoa. NSCopyObject() produit une copie superficielle d’un objet en recopiant la mémoire occupée par l’original. Après avoir effectué la copie superficielle, la méthode -copyWithZone: de NSCell copie les
284
Les design patterns de Cocoa
différents attributs, comme le texte ou l’image de l’original, en envoyant des messages -copy aux objets référencés. Ce mélange de copies superficielle et profonde fonctionne avec les sous-classes de NSCell fournies par Cocoa, mais cette solution risque d’être complexe à mettre en œuvre avec vos propres sous-classes. Tant que les attributs ajoutés dans vos sous-classes sont des variables d’instance sans être des pointeurs, leur copie est implicitement prise en charge. L’appel à la fonction NSCopyObject() dans NSCell copie automatiquement les nouveaux attributs avec ceux hérités. En revanche, si vous ajoutez des variables d’instance qui pointent sur d’autres objets ou utilisez des attributs qui ne sont pas des variables d’instance (voir Chapitre 10), vous devez redéfinir l’implémentation de -copyWithZone: fournie par NSCell. INFO La classe MYLabelBarCell créée au Chapitre 3 peut servir de cellule prototype à une matrice sans avoir besoin de redéfinir l’implémentation héritée de -copyWithZone: car le seul attribut ajouté est un réel.
21.3
Exemples dans Cocoa
Les objets proposés dans les bibliothèques d’Interface Builder sont des prototypes. Lorsqu’un objet est glissé depuis une bibliothèque vers une fenêtre d’application, il est copié avec sa configuration et son état courants. L’ensemble des objets disponibles dans Interface Builder n’est pas limité. De nouvelles bibliothèques peuvent être créées à tout moment. La copie des objets depuis les bibliothèques vers des applications permet une extension continue de l’outil Interface Builder sans avoir besoin de le recompiler. Tant que les objets d’une bibliothèques peuvent être copiés par archivage et désarchivage, Interface Builder est capable de les manipuler. La classe Cocoa NSMatrix utilise une instance de la classe NSCell prototype pour définir le stockage et la présentation des valeurs. NSMatrix implémente les fonctionnalités de base d’une feuille de calcul ou d’une interface utilisateur de type grille. Lorsque la matrice doit ajouter des lignes ou des colonnes, elle copie simplement la cellule prototype autant de fois que nécessaire pour remplir les nouveaux emplacements. La classe NSMatrix ne présente aucune dépendance avec les cellules qu’elle utilise. N’importe quelles classes dérivées de NSCell, même celles qui n’existaient pas au moment où la matrice a été compilée, peuvent être utilisées dans une matrice. Par ailleurs, en configurant le prototype, il est possible de préciser indirectement l’état initial de toutes les cellules de la matrice ; chaque cellule est initialisée avec l’état du prototype copié. Grâce au pattern Prototype, NSMatrix est une classe extrêmement flexible et ses possibilités de réutilisation sont importantes. La Figure 3.1 montre une matrice remplie d’instances de la classe MYLabeledBarCell. L’exemple suivant illustre la configuration
Chapitre 21
Prototype
285
d’une matrice de manière à copier une instance de la classe prototype MYLabeledBarCell dès que de nouvelles cellules sont requises. Utiliser des instances de MYLabeledBarCell comme prototypes La classe MYLabeledBarCell développée au Chapitre 3 est prête à être utilisée avec le pattern Prototype. Elle dérive de NSCell, qui est conforme au protocole NSCopying. Puisque NSCell appelle la fonction NSCopyObject() dans son implémentation de la méthode -copyWithZone:, la variable d’instance barValue ajoutée dans MYLabeledBarCell est automatiquement copiée avec celles héritées de NSCell. Le code suivant crée une nouvelle instance de NSMatrix et une nouvelle instance de MYLabeledBarCell qui sert de cellule prototype pour la matrice. Celle-ci est ensuite passée à une vue défilante en tant que vue document. Pour tester ce code, vous devez créer une instance de MYLabeledBarCellTestController dans Interface Builder et connecter son outlet scrollView à une vue défilante. /* MYLabeledBarCellTestController */ #import @interface MYLabeledBarCellTestController : NSObject { IBOutlet NSScrollView *scrollView; } @end -----------File MYLabeledBarCellTestController.m #import “MYLabeledBarCellTestController.h” #import “MYLabeledBarCell.h” @implementation MYLabeledBarCellTestController // Une simple classe pour tester MYLabeledBarCell dans une matrice. // Créez une instance de cette classe dans Interface Builder. // Connectez l'outlet scrollView à une vue défilante. static const int _MYInitialNumRows = 75; static const int _MYInitialNumColumns = 5; - (void)awakeFromNib // Cette méthode est appelée automatiquement lorsqu'un objet est chargé // depuis un fichier nib d'Interface Builder. { NSMatrix *newMatrix; MYLabeledBarCell *prototype = [[MYLabeledBarCell alloc] initTextCell:@”Prototype......................................”];
286
Les design patterns de Cocoa
// Fixer la valeur de la barre prototype. Toutes les copies ont // initialement la même valeur. [prototype setBarValue:0.15f]; // Allouer et initialiser une nouvelle matrice en précisant le prototype // à utiliser. newMatrix = [[NSMatrix alloc] initWithFrame:[scrollView bounds] mode:NSRadioModeMatrix prototype:prototype numberOfRows:_MYInitialNumRows numberOfColumns:_MYInitialNumColumns]; // L'objet prototype a été alloué dans cette méthode et doit donc être // libéré ou autolibéré. Puisque la matrice qui l'utilise l'a déjà // retenu, il est plus efficace et sûr de le libérer maintenant. [prototype release]; prototype = nil; // Installer la matrice comme vue document de la vue défilante. [scrollView setDocumentView:newMatrix]; // Indiquer à la matrice de se redimensionner pour contenir toutes // ses cellules. [newMatrix sizeToCells]; // La matrice a été allouée dans cette méthode et doit donc être libérée // ou autolibérée. Puisque la vue défilante l'a déjà retenue, elle peut // être libérée maintenant. [newMatrix release]; newMatrix = nil; } @end
Utiliser des instances de MYColorLabeledBarCell comme prototypes La classe MYColorLabeledBarCell suivante dérive de MYLabeledBarCell. Elle montre comment redéfinir correctement -copyWithZone: lorsque des attributs objets sont ajoutés dans une sous-classe de NSCell. Chaque instance de MYColorLabeledBarCell contient un pointeur sur une instance de NSColor et utilise cette couleur pour dessiner la barre. #import “MYLabeledBarCell.h” @interface MYColorLabeledBarCell : MYLabeledBarCell { NSColor *barColor; // La couleur de la barre. } // Accesseurs. - (void)setBarColor:(NSColor *)aColor; - (NSColor *)barColor; @end ------------
Chapitre 21
Prototype
287
#import “MYColorLabeledBarCell.h” @implementation MYColorLabeledBarCell // Redéfinir l’initialiseur désigné. - (id)initTextCell:(NSString *)aString { self = [super initTextCell:aString]; if(nil != self) { [self setBarColor:[NSColor blueColor]]; } return self; } - (void)drawBarInRect:(NSRect)aRect // Dessiner une barre qui remplit une partie d’un aRect précisé par barValue. // La couleur de la barre est donnée par [self barColor]. { aRect.size.width *= barValue; [[self barColor] set]; NSRectFill(aRect); } - (id)copyWithZone:(NSZone *)aZone { id result = [super copyWithZone:aZone]; // Bidouille nécessaire car NSCell utilise NSCopyObject(). // On accède directement à la variable d’instance copiée pour // la fixer à nil. result->barColor = nil; [result setBarColor:[self barColor]]; return result; } // Accesseurs. - (void)setBarColor:(NSColor *)aColor { [aColor retain]; [barColor release]; barColor = aColor; } - (NSColor *)barColor { return barColor; } @end
288
Les design patterns de Cocoa
Pour utiliser une instance de MYColorLabeledBarCell en tant que cellule prototype pour une matrice, une mise en œuvre appropriée de -copyWithZone: est essentielle. Les lignes suivantes extraites de cette méthode fixent la variable d’instance barColor de la copie à nil avant que -setBarColor: ne soit invoquée. // Bidouille nécessaire car NSCell utilise NSCopyObject(). // On accède directement à la variable d’instance copiée pour // la fixer à nil. result->barColor = nil;
La variable barColor de la copie doit être fixée à nil de manière à éviter une erreur de mémoire fatale. La fonction NSCopyObject() appelée par l’implémentation NSCell de -copyWithZone: copie le pointeur contenu dans la variable barColor du prototype, mais la couleur elle-même n’est pas retenue ou copiée. La deuxième ligne de code de la méthode -setBarColor: de MYColorLabeledBarCell libère l’ancienne couleur pointée par barColor avant d’affecter la nouvelle couleur au pointeur. Si barColor n’est pas fixée à nil avant d’appeler -setBarColor:, l’objet couleur du prototype est libéré sans qu’aucun message -retain correspondant n’ait été envoyé. Cette libération supplémentaire risque de provoquer une erreur d’accès à la mémoire. Le Chapitre 10 détaille les règles de retenue et de libération des objets. Cette mise en œuvre de la méthode -copyWithZone: de MYColorLabeledBarCell est rendue obligatoire par les règles de la gestion de la mémoire dans Cocoa. L’accès direct à la variable d’instance de la copie est une entorse malheureuse mais nécessaire aux bonnes pratiques de la programmation orientée objet. En général, la fonction NSCopyObject() ne doit être employée qu’avec les classes qui ne contiennent aucun pointeur. L’utilisation de NSCopyObject() par NSCell est fâcheuse et complique inutilement la création de sous-classes de NSCell pour ajouter des variables d’instance qui pointent sur des objets. INFO Les difficultés liées à la gestion de la mémoire pour les copies de NSCell sont évitées en utilisant le ramasse-miettes automatique proposé par Objective-C 2.0.
21.4
Conséquences
La copie des objets est souvent une opération aussi coûteuse que la création de nouvelles instances avec +alloc et -init. C’est particulièrement le cas pour les copies profondes. À des fins d’optimisation, la classe NSMatrix libère rarement les copies de sa cellule prototype, même si le nombre des cellules contenues diminue. Les copies superflues sont conservées pour le cas où elles redeviendraient utiles, ce qui évitera d’en
Chapitre 21
Prototype
289
créer de nouvelles. Toutefois, cette optimisation a, dans de nombreux cas, l’inconvénient d’occuper inutilement des zones de mémoire alors qu’elles pourraientt servir à d’autres usages. Lorsque vous utilisez le pattern Prototype avec vos propres classes, il est indispensable de documenter le comportement que les objets prototypes doivent prendre en charge. Par exemple, l’objet utilisé comme cellule prototype pour une instance de NSMatrix doit être une sous-classe de NSCell et doit implémenter le protocole NSCopying de manière à produire des copies indépendantes. Tous les objets des bibliothèques d’Interface Builder doivent se conformer au protocole NSCoding.
22 Poids mouche Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Poids mouche permet de réduire la quantité de mémoire et/ou le coût processeur liés à l’utilisation des objets. Les avantages de la programmation orientée objet sont parfois amoindris par le surcoût de l’utilisation des objets, notamment lorsqu’un grand nombre d’instances sont employées à la fois. Grâce au pattern Poids mouche, il est possible de partager des instances de manière à réduire leur nombre tout en préservant les avantages de l’orienté objet. Les classes qui implémentent ce pattern sont des poids mouche.
22.1
Motivation
Voici les trois principales raisons de l’usage des poids mouche dans Cocoa : n
Ils encapsulent des données non objets afin qu’elles puissent être utilisées dans les contextes où des objets sont requis.
n
Ils diminuent les besoins en espace mémoire lorsque l’application exige un grand nombre d’instances.
n
Ils jouent le rôle de remplaçants pour d’autres objets.
22.2
Solution
Certaines applications emploient de grandes quantités d’objets. Prenons par exemple une feuille de calcul qui contient 100 lignes et 100 colonnes. Si chaque cellule de la
292
Les design patterns de Cocoa
feuille est représentée par un objet séparé, il faut créer 10 000 instances de cet objet. Si la feuille contient 2 000 lignes et 2 000 colonnes, il faut 4 millions d’instances. Le pattern Poids mouche peut être appliqué de plusieurs manières à la mise en œuvre de la feuille de calcul. Si l’on suppose que chaque cellule est représentée par une instance de la classe SpreadsheetCell, plusieurs optimisations sont possibles. Tout d’abord, si des cellules de la feuille sont vides, une même instance de SpreadsheetCell configurée comme une cellule vide peut représenter toutes ces cellules. Lorsque la valeur d’une cellule vide est fixée, l’instance partagée de SpreadsheetCell est remplacée par une nouvelle instance qui contient cette valeur. Par ailleurs, la plupart des informations associées à chaque cellule sont mémorisées séparément. Par exemple, sur 4 millions de cellules, il est probable qu’une dizaine de formats de cellule différents, comme le soulignement, la police et la couleur, soient utilisés. Ces informations de mise en forme sont factorisées dans la classe SpreadsheetCellFormat. Chaque instance de SpreadsheetCell pointe sur l’une des instances de SpreadsheetCellFormat parmi la dizaine existante au lieu de stocker ces informations de manière redondante. Enfin, si de nombreuses cellules contiennent la même valeur et ont le même format, elles peuvent être remplacées par une seule instance de SpreadsheetCell, à la manière des cellules vides. Dans le tableur, les classes SpreadsheetCell et SpreadsheetCellFormat sont des poids mouche. Des instances de SpreadsheetCell sont partagées pour réduire le nombre total d’instances nécessaires. Les informations mémorisées par chaque instance de SpreadsheetCell sont minimisées en plaçant certaines d’entre elles dans des instances partagées de SpreadsheetCellFormat.
22.3
Exemples dans Cocoa
Cocoa utilise le pattern Poids mouche pour atteindre trois objectifs : encapsuler des valeurs non objets, réduire l’encombrement mémoire et remplacer d’autres objets. Encapsuler des valeurs non objets La classe Cocoa NSNumber est un poids mouche. Chaque instance de NSNumber contient un nombre dont le type est l’un des types de données numériques du langage C, comme char, short, int, long, float ou double. NSNumber est également compatible avec le type de données BOOL d’Objective-C. NSNumber et sa super-classe, NSValue, placent des enveloppes objets autour des types de données non objets afin qu’ils puissent être utilisés dans des contextes où Cocoa attend des objets. Par exemple, la classe NSArray ne peut stocker que des objets. Par conséquent, si vous devez placer des nombres réels dans un NSArray, vous devez les envelopper dans la classe NSNumber.
Chapitre 22
Poids mouche
293
INFO Dans de nombreux langages orientés objet, tout, y compris les nombres, est représenté par des objets. Objective-C est un hybride entre le C ANSI/ISO standard et les langages orientés objet. Il permet, depuis le code, un accès direct aux types non objets fournis par C et Cocoa apporte des objets qui encapsulent ces types de données C pour conserver l’approche orientée objet. Cependant, cette approche hybride nécessite parfois des conversions entre les types objets et les types non objets. Mais elle permet de bénéficier de la pleine puissance de l’ordinateur lors de l’utilisation des types de données C et les bibliothèques C existantes peuvent être utilisées dans du code Objective-C.
Outre NSValue et NSNumber, d’autres classes, comme NSDecimalNumber, NSDate, NSCalendarDate, NSString, NSURL, NSFileHandle, NSPipe et NSAffineTransform, enveloppent des valeurs non objets simples ou des structures de données. Par exemple, tout en prenant en charge l’internationalisation dans le traitement des chaînes, NSString contient en réalité des tableaux C ordinaires de caractères Unicode. NSFileHandle et NSPipe enveloppent les types de données utilisés pour les descripteurs de fichiers Unix sous-jacents. NSAffineTransform enveloppe un tableau 2 × 3 de variables C double utilisées pour la mise en œuvre des transformations 2D comme la rotation. Réduire l’encombrement mémoire Un programme Cocoa peut contenir simultanément un grand nombre d’instances de NSNumber. Cocoa optimise la mémoire occupée par ces instances en les partageant. Chaque fois que vous appelez [NSNumber numberWithInt:0];, vous pouvez obtenir la même instance. Cocoa gère un cache des instances de NSNumber récemment ou fréquemment utilisées. Lorsque vous demandez un nouveau NSNumber qui contient la même valeur qu’une instance présente dans le cache, vous recevez cette instance, non une nouvelle. Le partage des instances de NSNumber est possible uniquement parce que cette classe est inaltérable. Autrement dit, après que l’instance a été créée, la valeur qu’elle contient ne peut pas être modifiée. Imaginez ce qui se passerait si vous pouviez modifier la valeur mémorisée par une instance partagée de NSNumber. Une instance contenant la valeur 300 pourrait être partagée en de nombreux endroits pour représenter différentes informations, comme le nombre de chaînes de télévision disponibles et le solde d’un compte bancaire. Si la valeur de l’instance partagée passait à 45 suite au changement du mode d’accès aux chaînes, quelqu’un pourrait constater une baisse inattendue du solde de son compte. D’autres poids mouche Cocoa, comme NSFont et NSColor, placent des instances inaltérables dans un cache et les réutilisent. Les couleurs standard utilisées dans les interfaces utilisateurs de Mac OS X sont fournies par la classe NSColor au travers de méthodes comme [NSColor redColor];. Chaque appel à +redColor retourne la même instance partagée de NSColor. De manière comparable, la mise en forme d’un texte, même très
294
Les design patterns de Cocoa
complexe, n’utilise probablement pas plus de quelques polices à la fois. Chaque fois que vous créez une instance de NSFont avec un nom et une taille de police particuliers, elle est placée dans le cache. Si vous demandez une nouvelle instance avec la même police et la même taille, l’instance retournée vient du cache. Les performances de la plupart des applications sont ainsi améliorées grâce au nombre réduit des lectures des polices à partir du disque dur. La classe Cocoa NSCell est également un poids mouche. Les instances de NSCell font partie du sous-système Vue lorsque le pattern MVC est utilisé. Elles dessinent des composants d’interface utilisateur avec l’aide de la classe NSView. NSMatrix est une sorte de NSView que l’on peut comparer à la feuille de calcul décrite à la section précédente. NSMatrix utilise des instances du poids mouche NSCell pour dessiner chaque cellule au lieu d’employer des sous-vues plus lourdes (voir Chapitre 16). Les avantages de l’utilisation de NSCell à la place des sous-vues sont très importants. Chaque instance de NSCell contient une valeur relativement simple, comme un pointeur sur un NSString ou un pointeur sur un NSImage. Elle utilise donc vingt octets, à comparer aux quatre-vingts octets nécessaires à une instance minimale de NSView. Les instances de NSCell sont affichées avec un coût processeur très faible par rapport aux NSView. Une matrice de 2 000 lignes et de 2 000 colonnes contient 4 millions d’instances de NSCell, qui occupent 80 Mo à la place des 320 Mo requis par 4 millions d’instances de NSView. La classe NSMatrix fait une utilisation minimale du pattern Poids mouche. Elle mémorise des cellules pour chaque ligne de chaque colonne, mais les bénéfices n’en sont pas moins présents. La classe Cocoa NSTableView est également comparable à une feuille de calcul, mais elle exploite le pattern Poids mouche beaucoup mieux que la classe NSMatrix. NSTableView stocke une seule instance de NSCell pour chaque colonne et les informations de mise en forme enregistrées dans cette cellule sont appliquées à chaque ligne de la colonne. Par conséquent, toutes les lignes d’une colonne ont la même apparence. Pour dessiner les lignes d’une colonne, NSTableView change la valeur contenue dans l’instance de NSCell associée à la colonne pour qu’elle corresponde à la valeur de la ligne, demande à la cellule de se dessiner à l’emplacement approprié et répète la procédure pour la ligne suivante. Un NSTableView de 2 000 lignes et 2 000 colonnes a besoin de seulement 2 000 instances de NSCell, c’est-à-dire une pour chaque colonne. La réutilisation de la même instance de NSCell pour chaque ligne d’une colonne n’est possible qu’avec une classe NSCell altérable ; la valeur de chaque instance peut être modifiée à tout moment. Cette approche est totalement opposée à la manière dont est employé le poids mouche NSNumber inaltérable. Les classes NSTableView et NSMatrix n’exploitent pas le pattern MVC de la même manière. Une instance de NSTableView ne stocke pas réellement les valeurs qu’elle affi-
Chapitre 22
Poids mouche
295
che. Pour se redessiner, chaque instance de NSTableView demande à un autre objet, sa source de données, de lui fournir les valeurs requises. La source de données se trouve généralement dans le contrôleur et les données qu’elle fournit sont habituellement dans le modèle. Quel que soit le nombre de lignes et de colonnes gérées par l’instance de NSTableView, elle demande à sa source de données uniquement les valeurs qui correspondent aux lignes et aux colonnes visibles. Les sources de données sont décrites au Chapitre 15. Le Chapitre 29 détaille l’utilisation du pattern MVC avec NSTableView. Remplacer d’autres objets Les poids mouche jouent souvent le rôle de remplaçants temporaires pour d’autres objets plus lourds. Par exemple, l’affichage d’un texte dans une interface utilisateur est potentiellement une opération complexe. Un seul bloc de texte contient souvent plusieurs polices, des couleurs, des soulignements, des alignements, des indentations, etc. La modification du texte est encore plus complexe. Ses attributs peuvent être changés, du texte peut être inséré ou supprimé, provoquant le repositionnement du texte existant, la sélection courante et le point d’insertion doivent être pris en compte et des fonctionnalités comme la correction orthographique doivent être envisagées. Cocoa fournit la classe NSTextView pour l’affichage et la saisie du texte. En réalité, NSTextView ne représente qu’une partie du sous-système de gestion du texte, qui comprend la mise en forme, l’enregistrement, la correction orthographique et d’autres fonctions apportées par des classes connexes. Vous pouvez l’imaginer, il n’est pas envisageable d’utiliser des instances séparées de NSTextView et le sous-système de gestion du texte chaque fois qu’un libellé doit être affiché à côté d’un bouton. Cocoa utilise NSCell et ses sous-classes comme remplaçants poids mouche au système complexe de gestion du texte. Chaque instance de NSWindow fournit une seule instance de NSTextView, appelée "éditeur de champ", qui est utilisée par les cellules de la fenêtre. Les instances de NSCell partagent l’éditeur de champ pour l’affichage ou la modification du texte. Cette conception fonctionne car les utilisateurs éditent une seule cellule à la fois. Lorsqu’un utilisateur commence à modifier du texte dans une instance de NSCell, la hiérarchie des vues (voir Chapitre 16) est temporairement modifiée afin que la cellule soit remplacée par l’éditeur de champ, qui prend en charge l’édition. Une fois l’édition terminée, l’éditeur de champ est retiré de la hiérarchie et la cellule est réaffichée.
22.4
Conséquences
L’utilisation du pattern Poids mouche fait toujours l’objet d’un compromis entre la simplicité, la mémoire utilisée et les performances. L’utilisation d’un poids mouche complique immanquablement la conception. Par exemple, l’existence et l’usage de l’éditeur de champ de Cocoa avec des instances de NSCell ont bien souvent été sources
296
Les design patterns de Cocoa
de questions et de confusion chez les programmeurs qui s’initiaient aux frameworks. Même le plus simple des poids mouche, comme NSNumber, augmente la complexité des applications par rapport à une simple utilisation des types de données C. Pour s’en convaincre, il suffit de comparer : double double double
firstFactor = 37.059; secondFactor = -18.112; sum = firstFactor + secondFactor;
à: NSNumber *firstFactor = [NSNumber numberWithDouble:37.059]; NSNumber *secondFactor = [NSNumber numberWithDouble:-18.112]; NSNumber *sum = [NSNumber numberWithDouble: [[firstFactor doubleValue] + [secondFactor doubleValue]];
Le pattern Poids mouche permet généralement d’optimiser l’occupation mémoire et les performances. En remplaçant des instances de NSView par des instances de NSCell, la mémoire allouée est plus réduite et les performances du tracé sont meilleures dans la plupart des cas, mais tout cela a un coût. Les cellules ne prennent pas en charge les fonctionnalités d’affichage avancées, comme les masques, les systèmes de coordonnées transformés et les calques Core Animation pour l’animation et les effets spéciaux. Les poids mouche peuvent également diminuer les performances. L’allocation et l’initialisation d’une instance de NSNumber prennent des centaines ou des milliers de fois plus de cycles processeur que l’initialisation et le stockage d’un type de données C. Heureusement, les processeurs modernes sont suffisamment rapides pour que l’allocation des instances de NSNumber n’ait pas un impact significatif sur les performances de l’application, et l’une des forces d’Objective-C est de toujours vous permettre de revenir aux types de base si nécessaire. Enfin, l’optimisation de l’occupation mémoire est de moins en moins importante, car les ordinateurs sont de plus en plus rapides et la mémoire, de moins en moins onéreuse. NSView et NSCell étaient utilisées sur des ordinateurs équipés de 8 Mo de RAM. Les machines équipées de plus de 1 Go de RAM sont aujourd’hui monnaie courante. Avec une quantité de mémoire disponible au moins cent vingt-cinq fois plus importante qu’au moment où la classe NSCell a été largement utilisée pour la première fois, de nombreux programmeurs se demandent à juste titre si cet emploi de NSCell n’est pas révolu. La classe Cocoa NSCollectionView est arrivée avec Mac OS X 10.5. Elle affiche une collection d’instances de NSView dans une grille, à la manière dont NSMatrix affiche une grille de NSCell. Puisque l’utilisation des poids mouche augmente la complexité, vous devez éviter la création de nouvelles classes poids mouche dans vos applications, excepté si l’optimisation de la mémoire est un besoin réel ou si vous devez remplacer des objets lourds.
23 Décorateur Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Décorateur permet d’ajouter aux objets des fonctionnalités communes réutilisables en utilisant la composition au lieu de créer des sous-classes. Les décorateurs peuvent être ajoutés ou configurés à l’exécution, contrairement aux sous-classes, qui sont définies à la compilation. La classe Cocoa NSScrollView est une parfaite illustration du pattern Décorateur. Le défilement permet aux utilisateurs de contrôler la partie visible d’un objet lorsque celui-ci est trop grand pour être intégralement affiché dans une vue. Au lieu de réimplémenter le mécanisme de défilement dans chaque objet graphique, Cocoa apporte cette fonctionnalité en décorant les objets avec une instance de NSClipView, qui, à son tour, est décorée avec une instance de NSScrollView. NSClipView masque certaines parties de la vue qu’elle décore. NSScrollView décore la vue de rognage (NSClipView) et fournit des instances de NSScroller selon les besoins. NSScrollView coordonne la vue de rognage et les barres de défilement pour contrôler la partie visible de l’objet graphique. La Figure 23.1 illustre la combinaison type des objets employés par Cocoa pour mettre en œuvre le défilement graphique. Les instances de NSScrollView, de NSClipView et de NSScroller décorent une instance de NSImageView en ajoutant une bordure, un découpage et des barres de défilement pour que l’utilisateur puisse contrôler le défilement. Le pattern Décorateur est un cas particulier du pattern Hiérarchie décrit au Chapitre 16.
298
Les design patterns de Cocoa
Une instance de NSClipView révèle une partie de la “vue de document” embarquée, qui, dans ce cas, est une instance de NSImageView.
Instances de NSScroller.
Instance de NSScrollView : la vue de rognage (NSClipView) et les barres de défilement sont des sous-vues.
Figure 23.1 Décoration d’une instance de NSImageView par des instances de NSScrollView, de NSClipView et de NSScroller.
23.1
Motivation
Les programmeurs veulent souvent ajouter plusieurs possibilités à un objet existant. Si elles sont ajoutées via des sous-classes, le nombre de classes nécessaires explose très rapidement. L’héritage définit une relation "est un" entre une sous-classe et sa classe mère. Par exemple, un objet RulerText hypothétique est un objet Text auquel a été ajoutée une prise en charge des règles graduées qui indiquent les dimensions et les tabulations. Un objet BorderedRulerText est un objet RulerText auquel a été ajoutée une bordure. La Figure 23.2 présente la hiérarchie d’héritage de la classe BorderedRulerText. Figure 23.2 La hiérarchie d’héritage d’une classe BorderedRulerText hypothétique.
Text
RulerText
BorderedRulerText
Chapitre 23
Décorateur
299
Que se passe-t-il si une application a besoin d’un objet qui met en œuvre un texte défilant, avec une bordure et une règle ? Dans ce cas, une sous-classe ScrollingBorderedRulerText doit être créée. Si vous souhaitez bénéficier du défilement, mais sans inclure la bordure, il faut une nouvelle classe ScrollingRulerText. Si la règle n’est pas utile, la classe ScrollingBorderedText doit être créée. La hiérarchie d’héritage qui correspond aux différentes variantes de texte, avec et sans les bordures, avec le défilement et avec les règles, est représentée à la Figure 23.3. Certains frameworks pourraient tenter de proposer une hiérarchie plus simple en utilisant l’héritage multiple, mais cette solution modifie uniquement les relations d’héritage, sans nécessairement réduire le nombre de classes. Figure 23.3 La hiérarchie d’héritage étendue avec d’autres combinaisons et permutations.
RulerText
BorderedRulerText
ScrollingBorderedRulerText
ScrollingRulerText Text
ScrollingText
BorderedText
ScrollingBorderedText
Le design pattern Décorateur permet d’atteindre les objectifs suivants : n
Personnaliser le comportement de l’application en utilisant la composition à la place de l’héritage.
n
Apporter la flexibilité à l’exécution ; des fonctionnalités peuvent être ajoutées et retirées dynamiquement à l’exécution.
n
Ajouter des fonctionnalités à des instances individuelles, non à des classes.
n
Réduire le nombre de classes requises.
23.2
Solution
Contrairement à l’héritage, la composition définit des relations "a un" entre des objets. Le pattern Décorateur utilise des relations "a un" implicites. Lorsque vous indiquez qu’une instance de NSClipView décore sa vue de document, vous précisez en réalité que l’instance de NSClipView a une vue document. La vue document peut être une instance de NSTextView ou de n’importe quelle autre vue Cocoa. Chaque instance de NSScrollView décore une instance de NSClipView et, par conséquent, a une vue de rognage. Une instance de NSScrollView peut être décorée par des instances de NSRulerView. La Figure 23.4 illustre une instance de NSTextView décorée par des instances de NSScrollView, de NSRulerView et de NSScroller.
300
Les design patterns de Cocoa
Figure 23.4 Composition d’une instance de NSTextView avec plusieurs décorateurs.
Puisque la composition est généralement établie et contrôlée à l’exécution, toutes les combinaisons et les permutations imaginables des vues de document, avec et sans les règles, le défilement et les autres caractéristiques, existent et peuvent être modifiées dynamiquement. Si vous le souhaitez, l’application peut ajouter ou retirer une règle au cours de l’exécution, probablement suite à la sélection d’un article de menu. L’ajout d’une règle graduée à une instance particulière d’une vue de défilement n’a aucun effet sur les autres instances des vues de défilement. NSScrollView fournit la méthode -(void)setDocumentView:(NSView *)aView pour fixer depuis le code la vue document de la vue de rognage. La vue document peut être n’importe quelle sous-classe de NSView. Autrement dit, vous pouvez utiliser vos propres vues et un nombre quelconque de vues inférieures. Pour placer une vue ou une collection de vues dans une vue défilante à l’aide d’Interface Builder, sélectionnez les vues et utilisez l’article de menu LAYOUT > EMBED OBJECTS IN > SCROLL VIEW.
23.3
Exemples dans Cocoa
Bien que Cocoa applique principalement le pattern Décorateur à des objets du soussystème Vue dans une architecture MVC, son utilisation n’est en rien limitée à ce contexte. La classe Cocoa NSAttributedString fait partie du sous-système Modèle et décore des instances de NSString avec des attributs permettant d’indiquer des polices, des styles de paragraphe, des tabulations, des images embarquées et d’autres données fournies par l’utilisateur. Elle prend en charge de nombreux attributs définis par des standards, comme RTF (Rich Text Format), HTML (HyperText Markup Language) et le
Chapitre 23
Décorateur
301
format .doc de Microsoft. NSAttributedString ne modifie pas la chaîne décorée, mais mémorise simplement les informations complémentaires aux côtés de la chaîne. Cette approche est comparable à la manière dont NSClipView ne modifie pas sa vue document, mais contrôle simplement sa partie visible. Le Tableau 23.1 recense les principales classes Cocoa qui servent de décorateurs. Tableau 23.1 : Principales classes Cocoa employées comme décorateurs
Classe
Rôle
NSAttributedString
Décorer une instance de NSString avec des attributs quelconques, comme une police, une couleur ou un soulignement.
NSBox
Décorer une instance de NSView avec des bordures facultatives et un libellé.
NSClipView
Décorer une instance de NSView en découpant (masquant) des parties de la vue.
NSRulerView
Décorer une instance de NSView en ajoutant des règles graduées configurables qui indiquent les dimensions de la vue et qui permettent à l’utilisateur de placer des repères et de les déplacer.
NSScrollView
Décorer une instance de NSClipView en ajoutant des barres de défilement facultatives qui indiquent et ajustent la partie visible de la vue décorée par une vue de rognage.
NSSplitView
Décorer des instances de NSClipView avec une barre de séparation que l’utilisateur peut faire glisser pour masquer ou révéler des parties des vues décorées.
NSTableHeaderView
Décorer une instance de NSTableView avec des intitulés de colonnes facultatifs et fournir aux utilisateurs un mécanisme permettant de redimensionner et de contrôler les colonnes de la table.
NSTabView
Décorer des instances de NSClipView avec une bordure et des onglets afin que les utilisateurs puissent choisir les vues décorées qui sont visibles.
Vues accessoires De nombreux panneaux Cocoa standard vous permettent d’ajouter vos propres décorateurs. Par exemple, les classes NSSavePanel, NSFontPanel, NSColorPanel, NSAlert, NSRulerView, ABPeoplePickerView et NSSpellChecker proposent toutes une méthode -(void)setAccessoryView:(NSView *)aView pour ajouter n’importe quelle vue en tant que "vue accessoire" affichée sur le panneau. La vue accessoire décore le panneau
302
Les design patterns de Cocoa
et, puisque vous pouvez fournir n’importe quelle vue, vous pouvez ajouter des boutons ou les autres éléments d’interface utilisateur nécessaires à l’application. Les panneaux se redimensionnent automatiquement pour tenir compte des vues accessoires. Vous pouvez utiliser les vues accessoires pour ajouter des fonctionnalités aux panneaux standard sans créer des sous-classes de ces panneaux. Apple fournit un exemple d’utilisation des vues accessoires dans la section Core Library > Cocoa > Application File Management > Managing Accessory Views de la documentation Xcode. À partir de Mac OS X 10.5, les classes Cocoa NSPrintPanel et NSPageLayout proposent une nouvelle manière de gérer les vues accessoires via la méthode -(void)addAccessoryController:(NSViewController *)accessoryController. Cette nouvelle méthode remplace les implémentations obsolètes de -setAccessoryView: dans les panneaux d’impression et de mise en page. Ce changement d’interface peut indiquer dans quelle voie Apple dirige Cocoa. La classe NSViewController fait partie du soussystème Contrôleur dans une application MVC. Le passage d’une utilisation directe des vues accessoires à une utilisation de contrôleurs des vues accessoires promeut un usage régulier du pattern MVC et indique clairement où doit être implémenté le code du contrôleur qui intervient entre les vues accessoires et la logique de l’application. NSViewController fournit également une prise en charge des bindings (voir Chapitre 32).
23.4
Conséquences
Les relations d’héritage de la programmation orientée objet sont puissantes, mais elles représentent également la première source de couplage dans la conception. Elles sont établies statiquement au moment de la compilation et affectent toutes les instances des sous-classes. La composition fondée sur des relations "a un" constitue souvent une alternative flexible à la création de sous-classes. L’extension d’un objet à l’aide de la composition est dynamique et peut s’appliquer aux instances. La composition permet d’ajouter plusieurs fonctionnalités au même objet sans déclencher une explosion du nombre de classes. D’autres frameworks exigent que les décorateurs présentent la même interface, par exemple des méthodes publiques, que l’objet décoré. Cette restriction ne s’applique pas à Cocoa. Le dynamisme d’Objective-C et le pattern Type anonyme permettent de déterminer dynamiquement les possibilités d’une vue document d’une vue de rognage, ellemême sous-vue d’une vue défilante. De manière comparable, la classe NSRulerView est capable de travailler avec n’importe quelle sorte de NSView, car elle détermine à l’exécution les méthodes qui sont implémentées.
Partie IV Patterns qui masquent la complexité La programmation orientée objet a, entre autres, pour objectif de masquer la complexité aux programmeurs. Ils n’ont pas besoin de connaître les détails d’implémentation de tous les objets utilisés. S’ils devaient les connaître, ils ne pourraient réutiliser que quelques objets avant d’être submergés par la complexité induite. C’est également l’objectif des patterns qui impliquent plusieurs objets. Les patterns décrits dans cette partie masquent la complexité et les détails d’implémentation afin que les programmeurs puissent se focaliser sur la résolution des problèmes. Voici les chapitres de cette partie du livre : 24. Bundle ; 25. Regroupement de classes ; 26. Façade ; 27. Mandataire et Renvoi ; 28. Gestionnaire ; 29. Contrôleur.
24 Bundle Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Un bundle est une collection qui regroupe du code exécutable et les ressources associées, comme des images, des fichiers audio, des chaînes de caractères et des fichiers .nib. Idéalement, un bundle doit pouvoir contenir simultanément plusieurs versions des ressources afin que le code exécutable puisse être utilisé avec la version qui correspond, par exemple, à la langue ou aux préférences culturelles de l’utilisateur. Le pattern Bundle fournit un mécanisme permettant d’organiser et de charger dynamiquement du code exécutable et des ressources. À l’instar d’autres design patterns, le pattern Bundle se retrouve dans de nombreux environnements de développement orientés objet comme Cocoa. Le langage Java met en œuvre ce pattern au travers des fichiers JAR (Java ARchive), qui réunissent des classes Java compilées et des ressources dans un même fichier compressé. Les fichiers JAR sont faciles à copier et à télécharger, car la complexité de l’organisation des ressources est masquée par le fichier. Toutefois, pour les fichiers JAR, aucun standard ne définit l’organisation des ressources autres que le code. Par conséquent, il est difficile de partager ces fichiers entre des applications développées séparément. Le langage C# de Microsoft et les outils de développement associés regroupent les ressources, comme les fichiers audio, les images et les fichiers texte, dans des "assemblages" à l’aide du programme resgen.exe. Les assemblages sont inclus dans le fichier exécutable de l’application ou ajoutés comme des plugins pour créer un seul fichier qui contient les ressources et le code exécutable. Toutefois, la plupart des développeurs ne
306
Les design patterns de Cocoa
fournissent que des chemins vers les ressources au moment de la compilation des assemblages. Les chemins sont placés dans l’exécutable de l’application, mais les fichiers des ressources existent séparément et les utilisateurs ne doivent pas oublier de les télécharger ou de les copier avec l’application.
24.1
Motivation
Le pattern Bundle vise à atteindre les objectifs suivants : n
Conserver ensemble le code exécutable et les ressources associées même lorsque plusieurs versions et plusieurs fichiers sont concernés.
n
Implémenter un mécanisme de plugin flexible qui permet de charger dynamiquement du code exécutable et des ressources.
24.2
Solution
Les frameworks Cocoa et le framework non orienté objet d’Apple, Carbon, implémentent tous deux le pattern Bundle en utilisant des répertoires du système de fichiers dans lesquels se trouvent les fichiers, le code et les ressources. Ces répertoires sont appelés bundles et définissent une organisation hiérarchique standard des fichiers. Dans Mac OS X, les bundles constituent le mécanisme recommandé pour organiser les fichiers des applications, des frameworks et des plugins, quels que soient le langage de programmation et le framework employés. La hiérarchie de répertoires d’un bundle pour Mac OS X est définie à la section Core Library > Core Foundation > Resource Management > Bundle Programming Guide > Anatomy of a Modern Bundle de la documentation Xcode. Chaque bundle doit posséder un fichier Info.plist qui, entre autres, contient une chaîne d’identification unique du bundle. Ce fichier peut être ouvert dans n’importe quel éditeur de texte pour consulter les informations concernant le bundle, sans avoir à charger celui-ci dans une application. La Figure 24.1 présente un exemple de bundle d’une application Cocoa tel qu’il peut être vu dans Finder. Le dossier Contents contient toutes les ressources du bundle. S’y trouvent le fichier Info.plist, qui décrit le bundle, et les dossiers MacOS et Resources. Le dossier MacOS contient l’exécutable de l’application, le dossier Resources, les fichiers d’Interface Builder, les images, les chaînes de caractères et les autres ressources utilisées par l’application. Les versions localisées des ressources sont placées dans des dossiers dont les noms se terminent par l’extension .lproj. Tous ces fichiers se trouvent à des endroits très précis dans le bundle. La documentation mentionnée précédemment présente l’organisation attendue pour tous les types de bundles définis par Apple.
Chapitre 24
Bundle
307
Figure 24.1 L’organisation d’un bundle d’application type telle que présentée par Finder.
L’application Finder de Mac OS X et les composants standard d’une interface utilisateur, comme les panneaux OUVRIR et ENREGISTRER, sont en mesure de masquer à l’utilisateur le fait qu’un répertoire de bundle contient de nombreux fichiers et de lui présenter le bundle comme un seul fichier. Les répertoires présentés comme des fichiers uniques sont des paquets et la plupart des bundles sont également des paquets. Toutefois, Finder permet d’examiner le contenu d’un paquet, en sélectionnant l’article de menu AFFICHER LE CONTENU DU PAQUET, qui apparaît en cliquant le paquet du bouton droit, ou en le cliquant du bouton gauche avec la touche Ctrl enfoncée. Les utilisateurs peuvent également connaître la nature exacte des paquets depuis l’interface en ligne de commande d’Unix. Voici les avantages de l’utilisation par Mac OS X des paquets et des répertoires pour mettre en œuvre les bundles : n
Les répertoires d’un bundle contiennent d’autres répertoires et des fichiers ordinaires, qui peuvent être examinés avec les outils de visualisation standard et modifiés avec l’application adaptée au type du fichier.
n
Les utilisateurs peuvent déplacer, copier ou supprimer les répertoires d’un bundle, comme n’importe quels autres répertoires du système de fichiers.
n
L’utilisateur lambda ne risque pas de modifier ou de supprimer par inadvertance les fichiers d’un paquet car il ne les verra probablement jamais.
308
Les design patterns de Cocoa
n
La hiérarchie standard d’un bundle accepte la présence de plusieurs localisations linguistiques ou culturelles des fichiers de ressources et simplifie la suppression des ressources localisées inutiles pour gagner de la place.
n
Tout comme ils peuvent contenir plusieurs versions des ressources, ils peuvent également inclure plusieurs versions du code exécutable afin qu’un bundle d’application puisse fonctionner indifféremment sur les ordinateurs de type PowerPC et Intel.
n
Les bundles ne dépendent pas de fonctionnalités particulières du système de fichiers, comme les sections de ressources ou les extensions du système de fichiers. Ils peuvent être enregistrés sur des serveurs de fichiers et des ordinateurs autres que des Mac qui utilisent différents systèmes de fichiers.
La distribution des bundles d’applications Mac OS X sur CD-ROM est très simple. Il suffit de copier le bundle sur le CD-ROM et de laisser les utilisateurs le recopier à partir du CD-ROM sur leur disque dur. Toutefois, le téléchargement des bundles via un réseau présente parfois certains problèmes. Il est préférable de compresser le bundle de manière à réduire le temps de téléchargement et à éviter que les utilisateurs ne récupèrent que certaines parties du bundle. Pour cela, une solution consiste à créer une archive compressée du bundle en utilisant l’option FICHIER > COMPRESSER de Finder ou un outil équivalent comme gzip. Un bundle compressé est comparable à un fichier JAR, excepté qu’il contient une hiérarchie standard de répertoires Mac OS X. Une autre solution est de créer une image disque Mac OS X compressée. Les images disques sont des fichiers ayant l’extension .dmg. Lorsque l’utilisateur double-clique sur un fichier .dmg dans Finder, ce fichier est monté comme un disque extractible. Pour l’utilisateur, le fichier .dmg monté ressemble à un CD-ROM monté et il peut copier les bundles à partir de ce fichier sur son disque dur, comme il le ferait à partir d’un CD-ROM. Vous n’êtes pas toujours obligé d’utiliser les bundles pour vos applications. Il est possible de créer des programmes Cocoa en ligne de commande avec le framework Foundation Kit et de produire un programme exécutable autonome sans aucun bundle. Toutefois, les applications Cocoa qui utilisent le framework Application Kit sont presque toujours livrées comme des bundles et les frameworks Cocoa sont eux-mêmes enregistrés dans des bundles. Lors de la construction des applications ou d’un autre bundle, Xcode place automatiquement les ressources standard, comme les images et les fichiers audio, .nib et .strings, à l’endroit approprié. Cette opération est effectuée au cours de l’étape COPY BUNDLE RESOURCES (voir Figure 24.2). Si d’autres fichiers de ressources doivent être copiés dans le bundle, ils peuvent être ajoutés à cette étape ou une nouvelle étape de construction COPY FILES peut être définie dans le projet.
Chapitre 24
Bundle
309
Figure 24.2 L’étape de copie des ressources du bundle dans Xcode.
24.3
Exemples dans Cocoa
Cocoa prend en charge les bundles par l’intermédiaire de la classe NSBundle. Chaque application fondée sur Application Kit possède au moins un bundle, le bundle principal, auquel on accède à l’aide du message [NSBundle mainBundle]. La classe NSApplication charge automatiquement à partir du bundle le fichier .nib d’Interface Builder qui contient le menu principal de l’application. NSApplication est un singleton (voir Chapitre 13) et les fichiers .nib sont des archives d’objets (voir Chapitre 11). La classe NSBundle est utilisée pour charger dynamiquement du code exécutable et des ressources. L’extrait de code suivant obtient le chemin sur le système de fichiers d’une ressource de type image nommée myImage.tiff présente dans le bundle principal. NSString *pathToImage = [[NSBundle mainBundle] pathForResource:@"myImage" ofType:@"tiff"];
Grâce à NSBundle, vous n’avez plus besoin de figer les chemins des ressources dans l’application. S’il existe plusieurs versions de la ressource pour différentes localisations, la méthode -(NSString *)pathForResource:(NSString *)name ofType: (NSString *)extension de NSBundle retourne automatiquement le chemin de la version appropriée en fonction de la langue courante de l’utilisateur et de ses préférences de localisation. Si la ressource n’est pas trouvée, la méthode retourne nil. Notez que la
310
Les design patterns de Cocoa
chaîne qui précise l’extension passée à -pathForResource:ofType: ne doit pas inclure le caractère '.'. Pour accéder à une version précise d’une ressource, la méthode -(NSString *)pathForResource:(NSString *)name ofType:(NSString *)extension inDirectory: (NSString *)subpath forLocalization:(NSString *)localizationName de NSBundle permet d’obtenir le chemin de la version qui correspond à la localisation indiquée dans le sous-répertoire indiqué du bundle. Si cette version n’existe pas, la méthode retourne le chemin de la correspondance la plus proche ou nil si elle ne la trouve pas. La classe NSBundle est déclarée dans le framework Foundation, mais le framework Application Kit l’étend de plusieurs manières à l’aide des catégories. Il ajoute des méthodes qui simplifient le chargement des fichiers .nib d’Interface Builder, des fichiers audio et des images. Les méthodes les plus utilisées sont +(BOOL)loadNibNamed:(NSString *)aNibName owner:(id)owner, -(NSString *)pathForSoundResource:(NSString *)name et -(NSString *)pathForImageResource:(NSString *) name. Il est inutile de préciser une extension de fichier avec -pathForSoundResource: et -pathForImageResource:. Les ressources peuvent être enregistrées dans n’importe quel format audio ou d’image reconnu. NSBundle recherchera la ressource et retournera le chemin. Outre le bundle principal, vous pouvez accéder à celui du code exécutable qui définit les classes de l’application. L’extrait de code suivant retourne l’instance de NSBundle qui encapsule le bundle du framework Foundation : // Retourner le bundle qui contient le code exécutable // de la classe NSString. return [NSBundle bundleForClass:[NSString class]];
Si l’un de vos objets a besoin de charger des ressources, une solution consiste à utiliser [NSBundle bundleForClass:[self class]] dans les méthodes d’instance qui chargent les ressources. Si votre classe est compilée et liée directement dans une application, le bundle obtenu à l’exécution sera le bundle principal de l’application. Cependant, si vous décidez ultérieurement de placer la classe et ses ressources associées dans un framework ou un plugin, le bundle obtenu à l’exécution sera ce framework ou ce plugin. La méthode +bundleForClass: permet d’éviter les dépendances avec l’emplacement du code exécutable. Vous pouvez obtenir un tableau de tous les bundles actuellement chargés dans l’application, autres que ceux des frameworks, en invoquant la méthode +(NSArray *)allBundles de NSBundle. Pour obtenir tous les bundles des frameworks, utilisez la méthode +(NSArray *)allFrameworks.
Chapitre 24
Bundle
311
Charger dynamiquement du code exécutable Le chargement explicite du code présent dans le bundle principal d’une application ou des bundles de frameworks n’est pas obligatoire, car il est chargé automatiquement au démarrage de l’application. Pour charger dynamiquement un bundle dans une application, commencez par créer une instance de NSBundle via la méthode +(NSBundle *)bundleWithPath:(NSString *) fullPath. Par exemple, voici comment charger le bundle myPlugin.bundle à partir du répertoire de l’application : NSBundle
*bundle = nil;
// Obtenir une liste des chemins vers les emplacements standard // de l’application sur le système de fichiers. NSArray *bundleSearchPaths = NSSearchPathForDirectoriesInDomains( NSApplicationSupportDirectory, NSUserDomainMask, YES); NSString NSEnumerator
*currentPath = nil; *pathEnumerator = [bundleSearchPaths objectEnumerator];
// Rechercher dans bundleSearchPaths le premier bundle nommé // @"myPlugin.bundle". while (nil == bundle && nil != (currentPath = [pathEnumerator nextObject])) { currentPath = [currentPath stringByAppendingPathComponent: @"myPlugin.bundle"]; bundle = [NSBundle bundleWithPath:currentPath]; } return bundle;
// Retourner le bundle ou nil.
La simple création d’une instance de NSBundle pour encapsuler un répertoire de bundle ne charge pas automatiquement le code contenu dans le bundle. La classe NSBundle patiente jusqu’à ce que le code du bundle soit requis. Pour forcer la liaison du code exécutable du bundle dans l’application, une solution consiste à invoquer la méthode -load de NSBundle, qui retourne YES en cas de succès. La méthode -principalClass force également la liaison du code exécutable. Elle retourne l’objet classe qui correspond à la classe principale du bundle. La classe principale du bundle peut être précisée au moment de la construction du bundle avec Xcode ou en modifiant la clé "Principal class" dans le fichier Info.plist inclus avec chaque bundle. Si la classe principale n’est pas définie, la méthode -principalClass retourne la première classe qui se trouve dans le code exécutable du bundle. Le fichier Info.plist contient des couples clé-valeur qui fournissent des informations concernant le bundle. La méthode -infoDictionary de NSBundle retourne les clés et
312
Les design patterns de Cocoa
les valeurs obtenues à partir du fichier Info.plist. Le code suivant récupère les informations concernant un bundle, sans charger son code exécutable dans l’application : NSBundle
*bundle = [NSBundle bundleWithPath:somePath];
NSDictionary
*infoDictionary = [bundle infoDictionary];
Dès lors que le code exécutable d’un bundle a été chargé, toute classe définie dans ce bundle est accessible au travers de la méthode -classNamed: de NSBundle. Par exemple, l’objet classe correspondant à une classe nommée MYApplicationPlugin peut être chargé en invoquant [someBundle classNamed:@"MYApplicationPlugin"]. Apple propose un très bon exemple de système de plugins complet basé sur les bundles à l’adresse http://developer.apple.com/samplecode/BundleLoader/index.html. Vous pouvez déterminer si du code a été chargé dynamiquement à partir d’une instance de NSBundle en invoquant la méthode -isLoaded. NSBundle fournit la méthode -unload pour tenter de décharger le code exécutable d’un bundle. Elle retourne YES en cas de succès. Vous ne devez pas décharger le code exécutable qui contient des classes ou des catégories Objective-C. La documentation d’Apple précise qu’il est de la responsabilité de l’appelant de vérifier qu’aucun objet ou structure de données en mémoire ne fait référence au code en cours de déchargement. Toutefois, après que des classes et des catégories Objective-C ont été installées dans le moteur d’exécution d’Objective-C, il est peu réaliste de rechercher et de retirer toutes les dépendances avec le code chargé. Si une classe ou une catégorie est déchargée alors qu’un autre code ou que le moteur d’exécution dépend encore du code précédemment chargé, l’application risque fortement de se terminer sur une erreur. Avant Mac OS X 10.5, la méthode -unload ne faisait rien et retournait toujours NO.
24.4
Conséquences
La mise en œuvre du pattern Bundle dans Cocoa conserve ensemble le code exécutable et les ressources connexes et vous permet d’éviter de figer les chemins des ressources dans le code de l’application. Grâce à une hiérarchie de répertoires de bundle standard et à des outils de développement appropriés, comme Xcode, Mac OS X simplifie la création des bundles. Néanmoins, cette utilisation d’une hiérarchie de répertoires pour stocker les ressources et le code présente des avantages et des inconvénients. Question avantages, les utilisateurs peuvent visualiser le contenu d’un bundle et modifier les ressources avec les applications standard associées au type de chaque ressource. Toutefois, le revers de la médaille est que cela permet aux utilisateurs d’inspecter, de modifier ou de supprimer à tout moment les ressources de votre application.
25 Regroupement de classes Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Regroupement de classes permet de présenter une interface simple à une implémentation sous-jacente complexe. Ce pattern est habituellement utilisé pour masquer aux développeurs d’applications les détails des optimisations mises en place par les frameworks. Il fournit une classe publique utilisée dans le code de l’application, mais, lorsque celle-ci alloue des instances de cette classe publique, le framework retourne généralement des instances de sous-classes privées de cette classe publique. Le framework se fonde sur des informations fournies à l’exécution pour choisir la sousclasse privée. Le pattern Regroupement de classes est parfois appelé Fabrique abstraite car la classe publique est abstraite. Autrement dit, aucune instance de la classe publique n’est jamais créée et cette classe fabrique des instances d’autre classes. Par exemple, la classe Cocoa NSData constitue l’interface publique d’un regroupement de classes dont l’objectif est d’encapsuler efficacement des données binaires. L’objet retourné par la méthode -(id)initWithContentsOfMappedFile:(NSString *)path de NSData est en réalité une instance d’une sous-classe cachée de NSData. Cette sousclasse cachée exploite les fonctionnalités de la mémoire virtuelle de Mac OS X pour mapper les données du fichier dans l’espace d’adressage virtuel. À tout moment, seule une petite partie des données binaires stockées par l’objet sont présentes en RAM. Les données restantes se trouvent sur le disque. Lorsque des données sont requises, le système de mémoire virtuelle les lit automatiquement à partir du fichier et invalide les données inutilisées. Cette solution évite de charger en RAM des données inutiles ou de stocker des données qui ne sont plus nécessaires. Les données peuvent toujours être récupérées à partir du fichier source en cas de besoin. À l’opposé, l’objet retourné par la
314
Les design patterns de Cocoa
méthode -(id)initWithBytes:(const void *)bytes length:(NSUInteger)length de NSData peut contenir uniquement l’espace nécessaire sur le tas pour que les performances des accès aléatoires et que l’utilisation de la mémoire soient optimales. Le code d’accès aux données binaires ne change pas, quelle que soit la sous-classe de NSData instanciée. En général, vous n’avez pas besoin de savoir ou de vous préoccuper de l’implémentation d’un regroupement de classes. Dans le cas de NSData, les concepteurs du framework font des compromis vis-à-vis de l’allocation en mémoire virtuelle et sur le tas. Les décisions peuvent varier selon la version de Mac OS X ou la quantité de RAM installée dans le système. L’application utilise les méthodes relativement simples déclarées par la classe NSData sans s’occuper de la complexité de l’implémentation.
25.1
Motivation
Dans un framework, un regroupement de classes a pour premier objectif de cacher aux programmeurs la complexité de certains détails de son implémentation. Des concepts simples nécessitent parfois des implémentations complexes, que ce soit pour des raisons de flexibilité ou d’optimisation. Un regroupement de classes présente des interfaces simples qui correspondent aux concepts simples et masquent la complexité réelle de la mise en œuvre. Les frameworks cachent les détails d’implémentation et dissimulent des classes de manière à réduire le nombre de classes auxquelles les programmeurs sont confrontés. Le regroupement de classes laisse aux concepteurs du framework la possibilité de modifier son implémentation sans remettre en cause la compatibilité avec le code existant. Ils peuvent ajouter et retirer des sous-classes privées sans perturber le code applicatif.
25.2
Solution
L’implémentation du regroupement de classes dans Cocoa se fonde sur le pattern Création en deux étapes (voir Chapitre 3). Ce pattern sépare l’allocation de la mémoire de son initialisation. La création d’une nouvelle instance se fait généralement avec du code semblable aux exemples suivants : // Garder les messages +alloc et –init dans la même expression. id newInstance = [[SomeClass alloc] init]; // Exemple comparable avec un type plus précis. SomeClass *anotherNewInstance = [[SomeClass alloc] init]; // Cet exemple utilise un initialiseur plus complexe. NSError *errorLoadingContents = nil; NSString *contentOfFile = [[NSString alloc] initWithContentsOfFile:@"/usr/share/dict/words" encoding:NSUTF8StringEncoding error:&errorLoadingContents];
Dans la création en deux étapes, un pointeur sur une zone mémoire allouée à une nouvelle instance non initialisée est retourné par +alloc. Cette nouvelle instance est ensuite initialisée par l’une des variantes de la méthode -(id)init.
Chapitre 25
315
Regroupement de classes
INFO La méthode +alloc de NSObject appelle la "méthode primitive" +(id)allocWithZone: (NSZone *)aZone. Les méthodes primitives correspondent au petit nombre de méthodes de chaque classe à partir desquelles toutes les autres méthodes sont implémentées. Par exemple, NSCharacterSet ajoute une seule méthode primitive, -(BOOL)characterIsMember: (unichar)aCharacter, à celles héritées de NSObject. Toutes les autres méthodes de la classe NSCharacterSet sont implémentées par des appels à -characterIsMember: ou à d’autres méthodes héritées. Apple a établi une convention pour les méthodes primitives de manière que vous puissiez facilement créer des sous-classes du framework. Vos sous-classes peuvent redéfinir les méthodes primitives tout en étant assurées que les messages qui invoquent les méthodes non primitives finiront par invoquer vos versions.
La classe d’interface publique du regroupement de classes redéfinit la méthode +(id)allocWithZone:(NSZone *)aZone de manière à n’allouer aucun espace mémoire. À la place, +allocWithZone: retourne un pointeur sur un objet partagé qui utilise le pattern Poids mouche (voir Chapitre 22). Lorsque l’une des variantes de la méthode -init est ensuite envoyée à l’objet partagé, celui-ci utilise les arguments de l’initialiseur (s’ils existent) pour déterminer quelle sous-classe privée de la classe d’interface publique du regroupement de classes doit être allouée, initialisée et retournée. La procédure d’allocations et d’initialisation avec un regroupement de classes est illustrée à la Figure 25.1. Code de l’application
Code du framework
Classe d’interface publique du regroupement de classes
+allocWithZone:
+allocWithZone:
Retour -init
Instance poids mouche partagée -init
Une nouvelle instance d’une sous-classe de la classe d’interface publique
Retour
Figure 25.1 Une séquence type d’envoi de message lors de la création d’une nouvelle instance via un regroupement de classes.
TEMPS
Retour Une sous-classe de la classe d’interface publique
316
Les design patterns de Cocoa
Lorsqu’un regroupement de classes est utilisé, le pointeur retourné par la méthode d’initialisation n’est pas identique à celui retourné par le message +alloc précédent. Par exemple, NSString représente la classe d’interface publique d’un regroupement de classes. L’expression [NSString alloc] retourne un pointeur sur une instance partagée d’une classe privée nommée NSPlaceholderString. L’objet retourné par la méthode -(id)initWithString:(NSString *)string de NSPlaceholderString est une instance de la classe privée NSCFString. Le petit programme suivant et la sortie résultante montrent chaque étape du processus d’allocation et d’initialisation d’une instance par l’intermédiaire du regroupement de classes NSString : main() { // Tout d’abord, l’allocation. NSString *newInstance = [NSString alloc]; NSLog(@"Après +alloc :\n\tValeur du pointeur : %p\n\tClasse : %@", newInstance, [newInstance description]); // Ensuite, l’initialisation. newInstance = [newInstance initWithString:@"chaîne idiote"]; NSLog(@"Après -initWithString :\n\tValeur du pointeur : %p\n\t Classe : %@", newInstance, [newInstance description]); } -----------2009-11-13 19:47:48.321 ClassClusterInstantiation[261:10b] Après +alloc : Valeur du pointeur : 0x103390 Classe : NSPlaceholderString 2009-11-13 19:47:48.324 ClassClusterInstantiation[261:10b] Après -initWithString : Valeur du pointeur : 0x2040 Classe : NSCFString
Apple encourage les programmeurs à utiliser une même expression composée pour allouer et initialiser de nouvelles instances : newInstance = [[NSString -alloc] initWithString:@"chaîne idiote"];
Il est important de stocker la valeur retournée par l’initialiseur, car, comme le montre l’exemple précédent, cette valeur peut être un objet différent de celui qui a reçu le message d’initialisation. Le code suivant génère une erreur : // Tout d’abord, l’allocation. NSString *newInstance = [NSString alloc]; // Erreur : newInstance est initialisée mais l’objet résultant est perdu. [newInstance initWithString:@"chaîne idiote"];
ATTENTION Certains détails non documentés, comme l’existence des classes NSPlaceholderString et NSCFString, sont des détails d’implémentation qui peuvent changer entre les versions de
Chapitre 25
Regroupement de classes
317
Mac OS X. Ce chapitre mentionne NSPlaceholderString et NSCFString comme exemples de l’implémentation du regroupement de classes par Cocoa. Vous ne devez pas les utiliser dans votre propre code.
Créer un regroupement de classes Étant donné qu’un regroupement de classes a pour objectif de cacher la complexité, un exemple complet et réutilisable d’un tel regroupement est trop long et trop complexe pour être présenté dans un chapitre. Par conséquent, seul le squelette du code est donné. Dans son implémentation la plus simple, un regroupement de classes comprend deux classes : une super classe abstraite et une sous-classe concrète. Pour mettre en œuvre un regroupement dont la classe abstraite est MYClassCluster, définissez une interface semblable à la suivante : #import @interface MYClassCluster : NSObject // Initialiseurs. - (id)initForType:(MYType)type; - (id)initWithData:(NSData *)data; // Méthodes primitives. // Méthodes dérivées. @end
Dans l’implémentation, les méthodes d’initialisation doivent libérer l’instance allouée de la classe abstraite et créer ensuite une instance de la sous-classe concrète souhaitée, qui sera retournée. Les méthodes primitives sont généralement vides dans la classe abstraite, car elles doivent être implémentées par les sous-classes concrètes. Certains développeurs préfèrent qu’elles lancent des exceptions afin de signaler tout oubli à l’auteur d’une sous-classe. Enfin, les implémentations des méthodes dérivées doivent appeler les méthodes primitives pour effectuer leur travail. Voici un code qui présente les méthodes d’initialisation d’un regroupement de classes, avec deux sous-classes concrètes, MYSubclassA et MYSubclassB. #import "MYClassCluster.h" @implementation MYClassCluster - (id)initForType:(MYType)type { [self release]; self = nil; if (/* On doit utiliser MySubclassA. */) {
318
Les design patterns de Cocoa
self = [[MYSubclassA alloc] initForType:data]; } else if (/* On doit utiliser MySubclassB. */) { self = [[MYSubclassB alloc] initForType:data]; } return self; } - (id)initWithData:(NSData *)data { [self release]; self = nil; if (/* On doit utiliser MySubclassA. */) { self = [[MYSubclassA alloc] initWithData:data]; } else if (/* On doit utiliser MySubclassB. */) { self = [[MYSubclassB alloc] initWithData:data]; } return self; } // Méthodes primitives – elles lancent toutes des exceptions ! // Méthodes dérivées. @end
La plupart des regroupements de classes possèdent plus de deux méthodes d’initialisation. En général, les différents initialiseurs retournent des sous-classes concrètes différentes. Dans un but d’optimisation, les implémentations des regroupements de classes dans les frameworks Cocoa utilisent une autre classe intermédiaire pour éviter l’allocation et libèrent immédiatement l’instance de la super-classe abstraite. Cette classe intermédiaire est à mi-chemin entre un poids mouche et un singleton. Une instance est créée pour chaque zone mémoire de l’application et la mémoire associative permet de l’obtenir. La super-classe abstraite redéfinit +allocWithZone: de manière à retourner la classe intermédiaire pour la zone concernée. INFO Les classes intermédiaires doivent être créées dans la zone par défaut de l’application, mais elles possèdent une variable d’instance qui pointe sur la zone à laquelle elles sont associées afin de savoir dans quelle zone allouer les instances. C’est pourquoi elles n’interfèrent pas avec la destruction d’une zone.
Chapitre 25
Regroupement de classes
319
La classe intermédiaire doit implémenter toutes les méthodes d’initialisation définies par la super-classe abstraite, car tous les messages d’initialisation lui seront envoyés. Elle alloue et initialise un objet de la sous-classe concrète appropriée et le retourne. La logique de sélection de la sous-classe du regroupement de classes est ainsi déplacée dans la classe intermédiaire. Puisque les méthodes d’initialisation de la super-classe abstraite ne doivent jamais être appelées, elles doivent être modifiées de manière à lancer des exceptions lorsqu’une classe intermédiaire est utilisée. Une bonne manière de lancer une exception dans un initialiseur ou une méthode primitive consiste à envoyer le message [self doesNotRecognizeSelector: _cmd];. Cela garantit que le nom de la méthode problématique est journalisé. Il est également possible d’utiliser NSAssert(@"message", NO); pour fournir un message précis. Pour des exemples détaillés de la création d’un regroupement de classes, y compris les classes intermédiaires, consultez l’article disponible à l’adresse http://www.cocoadev.com/index.pl?ClassClusters.
25.3
Exemples dans Cocoa
Les classes collections du framework Foundation, comme NSString, NSData, NSArray, NSSet, NSDictionary et leurs versions mutables, constituent les interfaces publiques les plus visibles des regroupements de classes. Parmi les autres classes d’interface des regroupements de classes de ce framework, on trouve également NSAttributedString, NSNumber, NSNotification, NSPipe, NSScanner et NSCharacterSet. Depuis Mac OS X 10.5, la classe NSManagedObject de Core Data est l’interface publique d’un regroupement de classes. L’existence des regroupements de classes affecte rarement le code applicatif, qui se contente de les utiliser, mais il peut être difficile de créer correctement des sous-classes des interfaces publiques d’un regroupement de classes. Les applications créent souvent des sous-classes de NSManagedObject, mais les autres classes d’interface publique en sont rarement l’objet. Créer une sous-classe d’une classe d’interface publique Pour créer une nouvelle sous-classe concrète d’une classe d’interface publique abstraite, voici les règles à suivre : n
La nouvelle classe doit redéfinir les méthodes primitives de la super-classe.
n
La nouvelle classe doit redéfinir toutes les méthodes d’initialisation de la superclasse ou des exceptions risquent d’être lancées.
320
n
Les design patterns de Cocoa
Chaque initialiseur de la nouvelle classe doit invoquer l’initialiseur désigné de la super-classe, qui est toujours -init ou -initWithCoder: pour l’interface abstraite d’un regroupement de classes.
Les méthodes primitives mettent en œuvre les fonctionnalités de base communes à toutes les classes d’un regroupement. La documentation Apple d’une classe identifie les méthodes primitives d’un regroupement de classes. Les autres méthodes déclarées sont implémentées à partir de ces méthodes primitives. L’existence des méthodes primitives permet de réduire le nombre de méthodes à redéfinir dans chaque sous-classe. La mise en œuvre des méthodes primitives permet de garantir que les autres méthodes héritées, hormis les initialiseurs, continueront de fonctionner correctement. L’implémentation des méthodes non primitives est autorisée, mais elle n’est pas obligatoire. Elle a généralement lieu lorsqu’une sous-classe apporte des optimisations par rapport aux implémentations par défaut. Un regroupement de classes nécessite un traitement particulier des méthodes d’initialisation. En général, les classes d’interface publique n’implémentent pas les initialiseurs déclarés car aucune instance de ces classes n’est jamais créée. À la place, la classe intermédiaire d’un regroupement de classes met en œuvre les initialiseurs pour allouer, initialiser et retourner des instances des classes privées du regroupement. Lorsque vous créez une sous-classe d’une classe d’interface publique, vous devez implémenter tous les initialiseurs déclarés, ou une exception risque d’être lancée si l’un des initialiseurs non implémentés est invoqué. À l’opposé, lorsque vous créez des sous-classes de classes qui ne font pas partie d’un regroupement, vous n’êtes pas obligé d’implémenter les initialiseurs hérités. Avec Mac OS X 10.5 et les versions ultérieures, aucun mécanisme commode ne permet de faire en sorte que les frameworks retournent des instances de votre sous-classe. Par exemple, si vous créez une classe dérivée de NSString, vous pouvez allouer et initialiser des instances de cette sous-classe dans votre propre code. Vous pouvez même en passer des instances en argument aux méthodes du framework et obtenir un code parfaitement opérationnel. En revanche, vous ne pouvez pas obliger les méthodes du framework à retourner des instances de cette sous-classe. Lorsque vous envoyez un message comme -(NSString *)bundlePath de NSBundle ou -(NSString *)stringByAppendingString:(NSString *)aString de NSString, le framework retourne des instances des sous-classes cachées de NSString à la place d’instances de votre sous-classe. INFO Avant Mac OS X 10.5, vous pouviez utiliser la méthode +(void)poseAsClass:(Class) aClass de NSObject pour informer le moteur d’exécution d’Objective-C que votre sousclasse devait être utilisée dans tous les cas où la classe d’interface publique du regroupement de classes était censée l’être. Il était donc possible de forcer les frameworks à retourner des
Chapitre 25
Regroupement de classes
321
instances de votre sous-classe. La méthode +poseAsClass: est obsolète dans le moteur d’exécution 32 bits d’Objective-C fourni avec Mac OS X 10.5 et elle a totalement disparu du moteur d’exécution 64 bits. Apple ne propose aucun remplaçant à +poseAsClass:.
Avant de créer une sous-classe d’une classe d’interface publique d’un regroupement de classes, vous devez envisager d’autres alternatives. Si vous souhaitez ajouter des méthodes à une classe, les catégories d’Objective-C peuvent représenter la meilleure solution (voir Chapitre 6). Si vous devez ajouter des variables et des méthodes d’instance, la mémoire associative (voir Chapitre 19) permet de simuler l’existence de variables d’instance supplémentaires sans créer des sous-classes. La composition, ou relations "a un", est également une autre alternative. Vous pouvez sans doute atteindre vos objectifs en créant une nouvelle classe qui a une instance de la classe d’interface publique et d’autres attributs. Par exemple, avez-vous réellement besoin d’une nouvelle sous-classe de NSDictionary ou votre nouvelle classe MYAwsomeDictionary peut-elle simplement utiliser une instance de NSDictionary pour stocker le contenu de votre nouveau type de dictionnaire ? Les décorateurs (voir Chapitre 23) montrent comment utiliser la composition. Par exemple, la classe Cocoa NSAttributedString décore les instances de NSString en lui ajoutant des attributs, comme la police de caractères, la couleur ou le style de soulignement. Si votre objectif est de conserver une instance d’un regroupement de classes synchronisée avec les autres éléments de votre application, vous devez pouvoir utiliser les notifications (voir Chapitre 14) ou les bindings Cocoa (voir Chapitre 32). La sous-classe MYShortString de NSString Aux développeurs qui demandent comment créer une sous-classe d’un regroupement de classe, la réponse est en général "ne le faites pas, c’est une perte de temps". Le travail nécessaire dépasse souvent les bénéfices obtenus et d’autres patterns proposent des alternatives à l’héritage. Il peut néanmoins exister de bonnes raisons de créer une sous-classe d’un regroupement de classes, notamment pour améliorer ses performances lorsqu’un profilage a révélé qu’une classe crée un goulot d’étranglement dans l’application. Toutefois, avec la maturité croissante de Cocoa, même ces bonnes raisons disparaissent. Bien qu’ils aient été avertis, la plupart des développeurs souhaitent savoir comment créer une sous-classe d’un regroupement de classes, ne serait-ce que pour satisfaire leur curiosité. Voici un exemple qui illustre le processus et montre également pourquoi vous ne voudrez sans doute pas prendre cette voie. À l’époque de Mac OS X 10.0, le profilage d’une application Cocoa particulièrement importante a révélé qu’une grande partie du temps processeur était passée dans l’allocation et la désallocation d’instances de NSString. Les chaînes étaient généralement très
322
Les design patterns de Cocoa
courtes, mais des centaines ou des milliers d’entre elles étaient allouées et désallouées chaque seconde. La première exigence de l’application concernait le traitement des chaînes de caractères. Une solution permettant d’éviter l’allocation dynamique des chaînes devait donc être trouvée. Elle a pris corps dans la classe MYShortString décrite ici et disponible dans l’archive des codes sources de cet ouvrage. MYShortString ne prend en charge que les chaînes courtes. Des instances de MYShortString sont allouées en fonction des besoins, mais elles sont rarement désallouées. À la place, elles sont ajoutées dans un cache. Lorsqu’une nouvelle instance de MYShortString est requise, l’une des instances inutilisées présentes dans le cache est retournée au lieu d’en allouer une nouvelle.
Les méthodes primitives de NSString sont -(NSUInteger)length et -(unichar)characterAtIndex:(NSUInteger)index. Conformément aux règles de création d’une sous-classe d’un regroupement de classes, MYShortString doit implémenter -length et -characterAtIndex:, ainsi que les initialiseurs de NSString qui pourraient être invoqués. Le code suivant déclare l’interface de la classe MYShortString. #import #define _MYMAX_SHORT_STRING_LENGTH (40) @interface MYShortString : NSString { // Tampon de stockage de la chaîne courte. unichar _myBuffer[_MYMAX_SHORT_STRING_LENGTH+1]; // Nombre de caractères individuels, sans l’octet nul de terminaison. NSUInteger _myLength; } // Allocateur redéfini. + (id)allocWithZone:(NSZone *)aZone; // Nettoyer la ressource partagée. + (void)cleanup; // Statistiques de réutilisation. + (NSUInteger)numberOfAvailableInstances; + (NSUInteger)totalNumberOfInstances; // Initialiseur désigné redéfini. - (id)init; // Autres initialiseurs. - (id)initWithCharacters:(const unichar *)characters length:(NSUInteger)length; - (id)initWithUTF8String:(const char *)nullTerminatedCString; - (id)initWithString:(NSString *)aString; - (id)initWithFormat:(NSString *)format, ...; - (id)initWithFormat:(NSString *)format arguments:(va_list)argList;
Chapitre 25
Regroupement de classes
323
- (id)initWithFormat:(NSString *)format locale:(id)locale, ...; - (id)initWithFormat:(NSString *)format locale:(id)locale arguments:(va_list)argList; - (id)initWithCString:(const char *)bytes length:(NSUInteger)length; - (id)initWithCString:(const char *)bytes; // NSCoding. - (void)encodeWithCoder:(NSCoder *)encoder; - (id)initWithCoder:(NSCoder *)decoder; // NSCopying. - (id)copyWithZone:(NSZone *)aZone; // NSMutableCopying. - (id)mutableCopyWithZone:(NSZone *)aZone; // Méthodes primitives de NSString redéfinies. - (NSUInteger)length; - (unichar)characterAtIndex:(NSUInteger)index; // Méthodes de NSString redéfinies pour les performances. - (void)getCharacters:(unichar *)buffer; - (void)getCharacters:(unichar *)buffer range:(NSRange)aRange; @end
Le code suivant montre comment sont implémentées +allocWithZone: et -release dans MYShortString de manière à réutiliser des instances : #import "MYShortString.h" @implementation MYShortString #define _MYMaxNumberOfCachedInstance (10000) // Collection d’instances partagées. static MYShortString *_MYShortStringCache[_MYMaxNumberOfCachedInstance]; // Nombre d’instances réutilisables actuellement disponibles. static NSUInteger _MYAvailableInstances = 0; // Nombre d’instances actuellement allouées. static NSUInteger _MYTotalNumberOfInstances = 0; // Utilisé pour désactiver le cache pendant son nettoyage. static BOOL _MYCacheIsDisabled = NO; + (void)cleanup; // Libérer toutes les instances présentes dans le cache. { _MYCacheIsDisabled = YES; // Empêcher l’utilisation du cache pendant // la libération des instances.
324
Les design patterns de Cocoa
NSUInteger i; for(i = 0; i < _MYAvailableInstances; { [_MYShortStringCache[i] release]; _MYShortStringCache[i] = nil; } _MYAvailableInstances = 0;
i++)
_MYCacheIsDisabled = NO; } + (NSUInteger)numberOfAvailableInstances // Retourner le nombre d’instances de MYShortString réutilisables. { return _MYAvailableInstances; } + (NSUInteger)totalNumberOfInstances // Retourner le nombre total d’instances de MYShortString allouées. { return _MYTotalNumberOfInstances; } // Allocateur redéfini. + (id)allocWithZone:(NSZone *)aZone { MYShortString *result = nil; if(_MYAvailableInstances > 0 && aZone == NSDefaultMallocZone()) { // Réutiliser une instance disponible. _MYAvailableInstances—; result = _MYShortStringCache[_MYAvailableInstances]; _MYShortStringCache[_MYAvailableInstances] = nil; } else { // Créer une nouvelle instance (impossible d’utiliser +alloc ici // sans obtenir une récursion infinie). result = NSAllocateObject([self class], 0, aZone); _MYTotalNumberOfInstances++; } return result; } - (void)release // Redéfinie pour placer les instances inutilisées dans le cache. { if([self retainCount] == 1 && _MYAvailableInstances < _MYMaxNumberOfCachedInstance && !_MYCacheIsDisabled && [self zone] == NSDefaultMallocZone()) {
Chapitre 25
Regroupement de classes
325
_MYShortStringCache[_MYAvailableInstances] = self; _MYAvailableInstances++; } else { [super release]; } } - (void)dealloc // Nettoyage. { _MYTotalNumberOfInstances—; [super dealloc]; }
MYShortString redéfinit la méthode -release de manière à stocker les instances inutilisées en vue de leur réutilisation ultérieure, au lieu de les désallouer. Dès qu’elle le peut, la méthode +allocWithZone: réutilise les instances stockées au lieu d’en créer de nouvelles. Le nombre d’instances réutilisables disponibles est fourni par la méthode +numberOfAvailableInstances. Le nombre total d’instances allouées est donné par la méthode +totalNumberOfInstances. La méthode +cleanup libère toutes les instances présentes dans le cache. ATTENTION La solution employée pour mettre les instances de MYShortString dans le cache en vue de leur réutilisation est incompatible avec le ramasse-miettes d’Objective-C 2.0.
La classe MYShortString implémente certains initialiseurs de NSString en se repliant sur la mise en œuvre fournie par le regroupement de classes. La principale raison de ce repli dans certains cas vient du fait que MYShortString ne peut pas stocker des chaînes plus longues que MYMAX_SHORT_STRING_LENGTH (40). Si l’utilisateur de MYShortString essaie de stocker une chaîne trop longue, une instance de NSString capable de prendre en charge cette chaîne est retournée. Le fragment de code suivant poursuit l’implémentation de MYShortString en montrant les initialiseurs : // Initialiseur désigné redéfini. - (id)init; { if(nil != (self = [super init])) { _myBuffer[0] = 0; _myLength = 0; } return self; }
326
Les design patterns de Cocoa
// Autres initialiseurs. - (id)initWithCharacters:(const unichar *)characters length:(NSUInteger)length; { NSParameterAssert(NULL != characters); id
result = nil;
if(nil != (self = [self init])) { if(length < _MYMAX_SHORT_STRING_LENGTH) { memcpy(_myBuffer, characters, (length * sizeof(unichar))); _myLength = length; _myBuffer[_myLength] = 0; result = self; } else { [self release]; self = nil; result = [[NSString alloc] initWithCharacters:characters length:length]; } } return result; } - (id)initWithUTF8String:(const char *)nullTerminatedCString; { NSParameterAssert(NULL != nullTerminatedCString); return [self initWithCString:nullTerminatedCString encoding:NSUTF8StringEncoding]; } - (id)initWithString:(NSString *)aString; { id result = nil; const int length = [aString length]; if(nil != (self = [self init])) { result = self; if(length < _MYMAX_SHORT_STRING_LENGTH) { NSRange copyRange = NSMakeRange(0, length); [aString getCharacters:_myBuffer range:copyRange]; _myBuffer[length] = ‘\0'; _myLength = length;
Chapitre 25
Regroupement de classes
} else { [self release]; self = nil; result = [[NSString alloc] initWithString:aString]; } } return result; } - (id)initWithFormat:(NSString *)format, ...; { NSParameterAssert(nil != format); id va_list
result = nil; args;
va_start(args, format); result = [self initWithFormat:format locale:nil arguments:args]; va_end(args); return result; } - (id)initWithFormat:(NSString *)format arguments:(va_list)argList; { NSParameterAssert(nil != format); return [self initWithFormat:format locale:nil arguments:argList]; } - (id)initWithFormat:(NSString *)format locale:(id)locale, ...; { NSParameterAssert(nil != format); id va_list
result = nil; args;
va_start(args, locale); result = [self initWithFormat:format locale:locale arguments:args]; va_end(args); return result; } - (id)initWithFormat:(NSString *)format locale:(id)locale arguments:(va_list)argList; { NSParameterAssert(nil != format); [self release]; self = nil;
327
328
Les design patterns de Cocoa
return [[NSString alloc] initWithFormat:format locale:locale arguments:argList]; } - (id)initWithCString:(const char *)bytes length:(NSUInteger)length; { NSParameterAssert(NULL != bytes); id
result = nil;
if(nil != (self = [self init])) { if(length < _MYMAX_SHORT_STRING_LENGTH) { int i; for(i = 0; i < length; i++) { _myBuffer[i] = bytes[i]; } _myLength = length; _myBuffer[_myLength] = 0; result = self; } else { [self release]; self = nil; result = [[NSString alloc] initWithCString:bytes length:length]; } } return result; } - (id)initWithCString:(const char *)bytes; { NSParameterAssert(NULL != bytes); NSUInteger
length = strlen(bytes);
return [self initWithCString:bytes length:length]; }
MYShortString doit implémenter des méthodes primitives de NSString, c’est-à-dire -length et -characterAtIndex:. Le code suivant montre comment : // Méthodes primitives de NSString redéfinies. - (NSUInteger)length; { return _myLength; }
Chapitre 25
Regroupement de classes
329
- (unichar)characterAtIndex:(NSUInteger)index; { if(index >= _myLength) { [NSException raise:NSRangeException format:@""]; } return _myBuffer[index]; } // Méthodes de NSString redéfinies pour les performances. - (void)getCharacters:(unichar *)buffer; { NSParameterAssert(NULL != buffer); memcpy(buffer, _myBuffer, ((_myLength) * sizeof(unichar))); } - (void)getCharacters:(unichar *)buffer range:(NSRange)aRange; { NSParameterAssert(NULL != buffer); if((aRange.length + aRange.location) > _myLength || aRange.location < 0) { [NSException raise:NSRangeException format:@""]; } else { memcpy(buffer, &_myBuffer[aRange.location], (aRange.length * sizeof(unichar))); } }
La classe MYShortString n’est pas tenue de redéfinir les méthodes -getCharacters: et -getCharacters:range: héritées. La classe NSString fournit des implémentations de ces méthodes à partir des méthodes primitives. Toutefois, ces deux méthodes sont fréquemment appelées par d’autres méthodes de NSString et, dans un but d’optimisation, les versions mises en œuvre par MYShortString évitent de nombreux appels inutiles aux méthodes primitives. Enfin, NSString se conforme aux protocoles NSCoding, NSCopying et NSMutableCopying en implémentant leurs méthodes de la manière suivante : // NSCoding. - (void)encodeWithCoder:(NSCoder *)encoder; { [encoder encodeValueOfObjCType:@encode(NSUInteger) at:&_myLength]; [encoder encodeArrayOfObjCType:@encode(unichar) count:_myLength at:_myBuffer]; }
330
Les design patterns de Cocoa
- (id)initWithCoder:(NSCoder *)decoder; { self = [self init]; [decoder decodeValueOfObjCType:@encode(NSUInteger) at:&_myLength]; [decoder decodeArrayOfObjCType:@encode(unichar) count:_myLength at:_myBuffer]; return self; } // NSCopying. - (id)copyWithZone:(NSZone *)aZone; { id result = nil; if(aZone == [self zone]) { result = [self retain]; } else { result = [[MYShortString allocWithZone:aZone] initWithString:self]; } return result; } // NSMutableCopying. - (id)mutableCopyWithZone:(NSZone *)aZone; { return [[NSMutableString allocWithZone:aZone] initWithString:self]; } @end
La classe MYShortString peut être utilisée directement dans du code applicatif. Les instances sont créées par [MYShortString alloc] et initialisées en invoquant l’un des initialiseurs fournis. Toutefois, les classes Cocoa qui retournent des chaînes ne bénéficient pas automatiquement de la classe MYShortString. Par exemple, l’invocation de la version héritée par MYShortString de la méthode +stringByAppendingString: retourne une instance de l’une des sous-classes concrètes privées de NSString définies par Cocoa à la place d’une instance de MYShortString. Leçons à tirer de MYShortString La classe MYShortString a été développée à l’origine pour améliorer les performances en évitant les allocations et les désallocations répétées des instances de NSString. À l’époque de Mac OS X 10.0, cet objectif était atteint, mais cette période est révolue. Le programme de test fourni dans l’archive des codes sources de cet ouvrage mène à une conclusion surprenante : la mise en œuvre de MYShortString est en pratique beaucoup
Chapitre 25
Regroupement de classes
331
plus lente que les sous-classes privées de NSString fournies par le framework. Dans Mac OS X 10.5, soit Apple a optimisé l’allocation et la désallocation au point que la classe MYShortString est devenue inutile, soit les frameworks utilisent déjà une optimisation supérieure à celle de MYShortString. Vous le constatez, il faut éviter de créer vos propres sous-classes des classes d’interface publique d’un regroupement de classes. Il est difficile d’obtenir un résultat meilleur que celui apporté par les frameworks. Par ailleurs, même si votre sous-classe conduit aujourd’hui à des performances meilleures que celles des frameworks, il ne faut pas oublier qu’ils continuent à s’améliorer. En utilisant votre propre sous-classe, vous risquez d’empêcher vos applications de bénéficier automatiquement des futures améliorations des frameworks. NSManagedObject est un cas particulier, car il faut en créer des sous-classes pour utiliser efficacement Core Data. Le Chapitre 30 inclut une section concernant la dérivation de NSManagedObject dans le but d’ajouter un comportement personnalisé, comme un traitement supplémentaire lorsqu’une nouvelle instance de votre sous-classe de NSManagedObject est insérée dans le modèle.
25.4
Conséquences
Le pattern Regroupement de classes permet d’offrir des interfaces simples à des problèmes conceptuellement simples, quelle que soit la complexité de la solution sousjacente. Il réduit le nombre de classes auxquelles les programmeurs sont confrontés. En revanche, il complique la création des sous-classes. Au cours du débogage des applications Cocoa, vous risquez de rencontrer des classes qui ne sont pas familières ou même non documentées. Il s’agit généralement de classes concrètes non documentées définies à l’intérieur d’un regroupement de classes. Vous ne devez pas oublier que toutes les classes d’un regroupement de classes dérivent des classes d’interface publique et que vous pouvez vous fonder sur les fonctionnalités et la sémantique de ces dernières. Le pattern Regroupement de classes complexifie également la mise en œuvre de l’archivage et du désarchivage (voir Chapitre 11). Pendant le codage d’un objet dans une archive, il est possible de remplacer cet objet par un autre. Chaque classe concrète privée d’un regroupement de classes code le nom de sa classe d’interface publique pour deux raisons. Premièrement, l’existence de la sous-classe privée est un détail d’implémentation qui ne doit pas être exposé dans l’archive et peut évoluer dans les futures versions du framework. Deuxièmement, la classe privée concrète à utiliser peut varier selon l’ordinateur qui code l’objet et celui qui le décode. Par exemple, deux ordinateurs différents peuvent utiliser des versions différentes du framework ou ne pas être pourvus de la même quantité de mémoire.
26 Façade Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Le pattern Façade a pour objectif de limiter le couplage entre les objets qui interagissent avec un sous-système complexe et ceux qui implémentent ce sous-système. Il masque la complexité. Comme exemple du monde réel, examinons les interactions d’un client qui commande une pizza par téléphone. En tant que client, il communique avec l’employé qui prend les commandes, mais de nombreuses personnes sont impliquées dans l’exécution de cette commande. La Figure 26.1 illustre les relations possibles entre les personnes, les entreprises et les services qui lui permettent de recevoir sa pizza. En tant que client, il n’interagit pas, normalement, avec tous les employés de la pizzeria. Le preneur de commandes représente une interface publique simplifiée, ou façade, pour faciliter sa demande. Suite à certaines circonstances inhabituelles, il pourrait être amené à communiquer directement avec le manager ou à contester une transaction de paiement auprès de la société de cartes de crédit ou de la banque. Dans ces cas, il peut contourner la façade pour des interactions plus compliquées. L’analogie entre le design pattern Façade et la commande d’une pizza par téléphone est immédiate. Le pattern est utilisé dans Cocoa pour simplifier les interactions de votre code avec des sous-systèmes complexes, comme la gestion du texte, sans vous empêcher d’accéder aux détails lorsque cela se révèle nécessaire. L’idée est d’avoir des interactions courantes simples, tout en permettant les interactions complexes.
334
Les design patterns de Cocoa
Figure 26.1 Les relations impliquées dans la commande d’une pizza.
Client
Compagnie de cartes de crédit ou banque
Fournisseurs d’ingrédients
Façade prise de commandes (caissier)
Pizzaiolo
Manager
Plongeur
26.1
Interface simplifiée pour les clients
Livreur
Carte et GPS
Motivation
Le pattern Façade est appliqué de manière à atteindre les objectifs suivants : n
Fournir une interface simple à un sous-système complexe.
n
Limiter le couplage entre les objets qui utilisent un sous-système et ceux qui l’implémentent.
Le pattern est utilisé lorsque les interactions sophistiquées avec un sous-système complexe sont rares mais doivent néanmoins rester possibles.
26.2
Solution
Dans les pages d’un livre, il est difficile de donner un code simple qui implémente le design pattern Façade puisque l’objectif de celui-ci est de masquer la complexité. Un exemple utile devrait présenter toute la complexité cachée afin de montrer l’utilité du pattern. Dans notre illustration du concept, nous allons faire un compromis. L’exemple suivant utilise le service web de génération de graphiques proposé par Google pour produire l’application de la Figure 26.2. Le code d’utilisation du service est suffisamment compliqué pour montrer l’intérêt d’une façade simple. La classe MYDirectoryChartGenerator, donnée dans le code suivant, présente une interface très simple. La méthode +sharedGenerator permet d’accéder à une instance partagée de cette classe. La méthode -(NSImage *) chartForDirectory:(NSString *)
Chapitre 26
Façade
335
Figure 26.2 L’application affiche des graphiques générés par Google.
directoryPath est invoquée sur l’instance partagée pour demander à Google de générer un graphique illustrant la taille relative des fichiers présents dans le répertoire indiqué par directoryPath. #import @interface MYDirectoryChartGenerator : NSObject { } + (MYDirectoryChartGenerator *)sharedGenerator; - (NSImage *)chartForDirectory:(NSString *)directoryPath; @end
MYDirectoryChartGenerator comprend deux méthodes utilitaires qui ne sont pas déclarées dans l’interface publique de la classe. La méthode -(NSString *)delimitedFileNamesForDirectory:(NSString *)directoryPath retourne une chaîne qui contient les noms des fichiers du répertoire directoryPath, avec les délimiteurs demandés par le service web de Google. La méthode -(NSString *)delimitedFileSizesForDirectory:(NSString *)directoryPath retourne également une chaîne délimitée qui contient les tailles des fichiers du répertoire directoryPath. Dans les deux cas, la fonction Unix popen() est utilisée pour exécuter les commandes du shell qui fournissent les informations nécessaires sur les fichiers. Cette fonction est un exem-
336
Les design patterns de Cocoa
ple d’équivalent non orienté objet du pattern Façade. Elle expose aux programmes C l’univers complexe et parfois obscur des commandes du shell Unix, tout en restant extrêmement simple à utiliser. #import "MYDirectoryChartGenerator.h" @implementation MYDirectoryChartGenerator + (MYDirectoryChartGenerator *)sharedGenerator { static MYDirectoryChartGenerator *sharedInstance = nil; if(nil == sharedInstance) { sharedInstance = [[MYDirectoryChartGenerator alloc] init]; } return sharedInstance; } - (NSString *)delimitedFileNamesForDirectory:(NSString *)directoryPath { NSMutableString *fileNames = [NSMutableString string]; NSString *fileNamesCommand = [NSString stringWithFormat:@"ls %@\n", directoryPath]; FILE *pipe = popen([fileNamesCommand UTF8String], "r"); int currentChar; while(EOF != (currentChar = fgetc(pipe))) { [fileNames appendFormat:@"%c", currentChar]; } pclose(pipe); pipe = NULL; // Ajouter les délimiteurs exigés par le service web de Google. [fileNames replaceOccurrencesOfString:@"\n" withString: @"|" options:NSLiteralSearch range: NSMakeRange(0, [fileNames length])]; // Supprimer le dernier délimiteur car Google ne l’apprécie pas. [fileNames deleteCharactersInRange:NSMakeRange( [fileNames length]-1, 1)]; return fileNames; } - (NSString *)delimitedFileSizesForDirectory:(NSString *)directoryPath { NSMutableString *fileSizes = [NSMutableString string]; NSString *fileSizesCommand = [NSString stringWithFormat: @"ls -l %@ | awk ‘{print $2}'\n", directoryPath]; FILE *pipe = popen([fileSizesCommand UTF8String], "r"); int currentChar;
Chapitre 26
Façade
337
while(EOF != (currentChar = fgetc(pipe))) { [fileSizes appendFormat:@"%c", currentChar]; } pclose(pipe); pipe = NULL; // Ajouter les délimiteurs exigés par le service web de Google. [fileSizes replaceOccurrencesOfString:@"\n" withString: @"," range:NSMakeRange(0, [fileSizes length])]; // Supprimer le dernier délimiteur car Google n’en veut pas. [fileSizes deleteCharactersInRange:NSMakeRange( [fileSizes length]-1, 1)]; return fileSizes; } - (NSImage *)chartForDirectory:(NSString *)directoryPath { NSString *names = [self delimitedFileNamesForDirectory:directoryPath]; NSString *sizes = [self delimitedFileSizesForDirectory:directoryPath]; NSString *chartServiceURL = @"http://chart.apis.google.com/chart?"; NSString *chartCommand = [NSString stringWithFormat:@names, sizes]; NSURL *url = [NSURL URLWithString:[chartCommand stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; NSImage *chartImage = [[[NSImage alloc] initWithContentsOfURL:url] autorelease]; return chartImage; } @end
Dans cet exemple, disponible dans l’archive des codes sources de cet ouvrage, la classe MYDirectoryChartGenerator masque les détails de la lecture des noms de fichiers et de leur taille à partir d’un répertoire et ceux de l’utilisation du service web de Google pour générer une image. Un message -delimitedFileSizesForDirectory: équivaut à une page de code, mais la fonction popen() et le service web sont également à leur manière des exemples de la philosophie du pattern Façade. L’exemple de ce chapitre n’est pas lié à l’implémentation particulière des commandes du shell Unix ni au code de dessin d’un graphique 3D de Google.
26.3
Exemples dans Cocoa
Cocoa applique le pattern Façade principalement pour simplifier ou réduire le code nécessaire à l’utilisation de fonctionnalités sophistiquées. Les façades sont également employées pour découpler la logique du traitement des interactions de l’utilisateur et l’implémentation réelle de l’interface utilisateur.
338
Les design patterns de Cocoa
Les classes NSTextView et NSImage illustrent la capacité du pattern Façade à simplifier les interfaces de programmation. De même, la classe NSPersistentStoreCoordinator de Core Data fournit une interface simple qui encapsule des interactions potentiellement complexes entre plusieurs types de systèmes de stockage des données. Les classes NSColorPanel, NSOpenPanel, NSSavePanel et NSPrintPanel d’Application Kit isolent des panneaux correspondants votre code de représentation de l’interface utilisateur. Par exemple, s’il est important que votre code obtienne la couleur sélectionnée par un utilisateur au travers du panneau de couleurs standard de Cocoa, il ne doit pas dépendre de la manière dont l’utilisateur effectue son choix. En réalité, si les futures versions de Mac OS X proposent de nouvelles possibilités de sélection d’une couleur dans le panneau standard, votre code doit continuer à fonctionner sans modification. Façade pour le texte La classe NSTextView est l’une des mises en œuvre les plus convaincantes du pattern Façade dans Cocoa. Elle prend en charge tous les détails de l’affichage et de l’édition d’un texte mis en forme, avec plusieurs polices de caractères, styles de paragraphe, images incluses, prise en charge multilingue, couleurs, styles, tabulations, etc. Pour le programmeur, l’ajout d’un texte pour son affichage dans une vue de texte se fait simplement en envoyant le message -insertText:. L’objet NSTextView que vous faites glisser depuis une bibliothèque d’Interface Builder semble inclure un éditeur de texte sophistiqué. En réalité, le NSTextView fourni par la bibliothèque d’Interface Builder offre uniquement la configuration standard la plus courante. En effet, la plupart des applications n’ont besoin que de la configuration standard de NSTextView et le scénario commun est pris en charge sans ajout de code par un simple glisser-déposer au moment de la conception. La Figure 26.3 présente une version plus complète des composants Cocoa qui interviennent dans la mise en œuvre de l’affichage et de l’édition du texte. Lorsque vous avez besoin d’un contrôle précis sur la modification, l’affichage ou le traitement du texte, vous pouvez personnaliser chacun des composants du système Cocoa de prise en charge du texte. Des configurations extrêmement complexes sont possibles, mais, grâce au pattern Façade, vous êtes rarement, voire jamais, concerné par les détails. L’architecture du sous-système de gestion du texte dans Cocoa est détaillée dans la section Core Library > Cocoa > Text & Fonts > Text System Overview > Text System Architecture de la documentation Xcode. Façade pour les images La classe NSImage est une autre illustration du pattern Façade. La plupart des applications qui chargent des images peuvent être implémentées en utilisant les méthodes simples de NSImage, +(id)imageNamed:(NSString *)name ou -(id)initByReferencing
Chapitre 26
Façade
339
Figure 26.3 Votre code contrôleur
Les composants cachés derrière la façade de la classe NSTextView.
Interface simplifiée pour les opérations courantes Façade NSTextView
NSTextInput
NSTextContainer
NSTextAttachement Cell
NSTextTab
NSLayoutManager
NSTextAttachement
NSParagraphStyle
NSTextStorage
NSTypesetter
NSFont
File:(NSString *)filename. La classe NSImage constitue la façade d’un sous-système sophistiqué et flexible qui prend en charge le chargement, l’affichage et la conversion de plusieurs types d’images vectorielles ou bitmap. Lorsque vous utilisez NSImage, vous n’avez pas nécessairement besoin de connaître la représentation de l’image sousjacente, qui peut être au format PDF (Portable Document Format), EPS (Encapsulated PostsScript), TIFF (Tagged Image File Format), JPEG (Joint Photographic Experts Group), PNG (Portable Network Graphics), GIF (Graphics Interchange Format), DIB (Device Independent Bitmap) ou encore bien d’autres. La liste des formats reconnus est donnée à la section Core Library > Cocoa > Graphics & Imaging > Cocoa Drawing Guide > Introduction to Cocoa Drawing Guide > Images de la documentation Xcode.
Si vous souhaitez simplement charger et afficher une image dans votre application, vous n’avez pas besoin de connaître ou de vous préoccuper des détails des différents formats. La classe NSImage communique avec les autres objets et effectue les conversions de format nécessaires. Elle conserve automatiquement une trace des multiples représentations de la même image. Par exemple, elle peut placer dans un cache une version bitmap
340
Les design patterns de Cocoa
d’une image vectorielle. Pour afficher la meilleure représentation disponible d’une image qui a déjà été chargée, appelez la méthode -(void)compositeToPoint: (NSPoint)aPoint operation:(NSCompositingOperation)op. Si vous devez créer de nouvelles images directement dans le code, utiliser des données d’images avec OpenGL ou prendre en charge vos propres formats de données d’images, vous pouvez toujours manipuler les classes NSBitmapImageRep, NSCachedImageRep, NSCIImageRep, NSPDFImageRep, NSEPSImageRep, NSPICTImageRep ou NSCustomImageRep. Par exemple, la classe NSBitmapImageRep propose une méthode, dont le nom est parmi les plus longs de Cocoa, pour offrir un contrôle maximal au programmeur : -(id)initWithBitmapDataPlanes:(unsigned char **)planes pixelsWide:(NSInteger)width pixelsHigh:(NSInteger)height bitsPerSample:(NSInteger)bps samplesPerPixel:(NSInteger)spp hasAlpha:(BOOL)alpha isPlanar:(BOOL)isPlanar colorSpaceName:(NSString *)colorSpaceName bitmapFormat:(NSBitmapFormat)bitmapFormat bytesPerRow:(NSInteger)rowBytes bitsPerPixel:(NSInteger) pixelBits. Si vous avez besoin d’un tel contrôle, les options existent, mais, pour la vaste majorité des cas où elles ne vous concernent pas, NSImage se charge des détails. Pour de plus amples informations concernant le traitement des images dans Cocoa, consultez la section Core Library > Cocoa > Graphics & Imaging > Cocoa Drawing Guide > Introduction to Cocoa Drawing Guide de la documentation Xcode. Façace pour la persistance Les instances de la classe NSPersistentStoreCoordinator servent d’intermédiaires entre les données de votre application et leur représentation dans le système de stockage sous-jacent. D’un point de vue conceptuel, NSPersistentStoreCoordinator joue un rôle comparable à celui de NSImage. Tout comme NSImage cache les détails des formats de fichiers d’images, NSPersistentStoreCoordinator masque les détails des multiples formats de stockage des données. En utilisant NSPersistentStoreCoordinator, vous pouvez charger les données de l’application à partir d’un fichier XML et les enregistrer dans une base de données SQLite, sans aucun impact sur les données elles-mêmes. NSPersistentStoreCoordinator prend en charge le stockage des données de sorte que le type de stockage n’ait pas d’importance pour le code qui utilise les données. Vous trouverez de plus amples informations concernant l’architecture de Core Data à la section Core Library > Cocoa > Design Guidelines > Core Data Programmming Guide. Façades pour les actions de l’utilisateur Cocoa utilise le design pattern Façade pour découpler la logique de traitement des actions de l’utilisateur et l’implémentation réelle de l’interface utilisateur. Par exemple, la classe Cocoa NSColorPanel fournit aux programmeurs une interface simple, mais
Chapitre 26
Façade
341
prend en charge plusieurs interfaces sophistiquées de sélection d’une couleur. Le panneau standard reconnaît les niveaux de gris, ainsi que les formats RVB (Rouge Vert Bleu), CMJN (Cyan Magenta Jaune Noir) et TSL (Teinte Saturation Luminosité). Il propose d’effectuer un choix à partir d’une liste de couleurs de l’utilisateur, un cercle chromatique et même une boîte à crayons aux couleurs nommées (voir Figure 26.4). Figure 26.4 Le panneau standard des couleurs de Cocoa.
Chaque application Cocoa possède une seule instance de NSColorPanel, à laquelle il est possible d’accéder depuis le code en envoyant le message [NSColorPanel sharedColorPanel]. Malgré toutes les possibilités du panneau de couleurs, le code de votre application qui le manipule se contente généralement d’implémenter la méthode d’action -(void)changeColor:(id)sender de la manière suivante : - (void)changeColor:(id)sender { NSColor *color = [sender color];
// Obtenir la couleur choisie par // l’utilisateur.
// Utiliser la couleur. }
Lorsqu’un utilisateur choisit une couleur dans l’un des modes du panneau, l’instance de NSColorPanel envoie le message d’action -changeColor: en utilisant le pattern Outlet, cible et action (voir Chapitre 17). Puisque la cible du panneau de couleurs est généralement fixée à nil, le message -changeColor: suit la chaîne de répondeurs pour être traité (voir Chapitre 18). Dans la plupart des applications, la seule méthode de NSColorPanel utilisée est -(NSColor *)color.
342
Les design patterns de Cocoa
Votre code n’a généralement aucun lien direct avec les multiples modes de sélection de couleur et les interfaces utilisateurs fournies par le panneau de couleurs standard. Même si l’application a des besoins particuliers, vous pouvez toujours contrôler le panneau de couleurs sans créer un couplage direct avec les interfaces utilisateurs. Par exemple, vous pouvez configurer le panneau de couleurs pour qu’il accepte uniquement le format CMJN sans avoir besoin de connaître les détails du mécanisme de sélection d’une couleur dans ce format. Il arrive qu’Apple change l’interface utilisateur des panneaux standard, mais le code applicatif en est rarement perturbé car il ne dépend pas d’une interface précise. À l’instar de NSColorPanel, les classes NSOpenPanel, NSSavePanel et NSPrintPanel isolent le code applicatif des complexités des interfaces utilisateurs correspondantes.
26.4
Conséquences
Le design pattern Façade masque la complexité et diminue le couplage du code, mais il peut être mal employé. Lorsque la complexité d’un sous-système est souvent exploitée, une façade simplifiée présente peu d’intérêt et peut même augmenter le travail nécessaire à l’utilisation efficace du sous-système. Si l’interface de façade est trop complexe ou reprend de nombreux détails des classes masquées, elle n’apporte rien. Les façades sont souvent des singletons. Par exemple, Cocoa fournit une instance de NSColorPanel par application. Le pattern Regroupement de classes est parfois une alternative au pattern Façade. Les regroupements de classes masquent également une complexité d’implémentation aux utilisateurs du framework. L’utilisateur du regroupement de classes voit une interface publique relativement simple qui cache le fait que plusieurs sous-classes spécialisées implémentent l’interface en fonction des besoins.
27 Mandataire et Renvoi Au sommaire de ce chapitre U U U U
Motivation Solution Exemples dans Cocoa Conséquences
Un mandataire (proxy) est un objet qui en remplace un autre. Il est utilisé dans le cas où un objet demandé n’est pas immédiatement disponible. Par exemple, lorsque des objets distribués communiquent par invocation de méthodes entre des applications séparées, un objet réel dans une application peut être représenté par un mandataire dans une autre. Les messages envoyés au mandataire sont transmis sur le réseau et reçus par l’objet destinataire réel. Les valeurs de retour de celui-ci sont renvoyées sur le réseau et retournées par le mandataire. Le renvoi est une fonctionnalité du moteur d’exécution d’Objective-C qui permet à un objet de capturer les messages qui lui sont envoyés et de les passer ensuite à un autre objet. L’implémentation des mandataires se fonde sur le renvoi. Ce dernier est plus généralement appliqué à la mise en œuvre du système annuler-rétablir de Cocoa. Le concept de messages d’ordre supérieur, expliqué plus loin dans ce chapitre, utilise également le renvoi, qui se fonde lui-même sur les invocations.
27.1
Motivation
Le pattern Mandataire permet d’envoyer des messages à un objet qui se trouve séparé de l’émetteur, que ce soit dans le temps ou l’espace. Les mandataires peuvent également contrôler l’accès aux objets ou en modifier le comportement. Le pattern Renvoi simplifie la capture des messages sous forme d’invocations afin qu’ils puissent être retransmis, retardés, répétés, stockés ou modifiés.
344
27.2
Les design patterns de Cocoa
Solution
Le renvoi est une caractéristique du langage Objective-C qui fait partie du distributeur des messages intégré au moteur d’exécution. Lorsqu’un message est envoyé à un objet qui n’offre pas la méthode correspondante, le moteur d’exécution donne à cet objet l’opportunité de prendre en charge le message avant de lancer une exception. Implémenter le renvoi NSObject déclare des patrons de méthodes (voir Chapitre 4) que vous pouvez redéfinir pour personnaliser le renvoi des messages. Pour renvoyer un message, le moteur d’exécution commence par invoquer le patron de méthode -methodSignatureForSelector: afin d’obtenir la signature de méthode qui permettra de créer une instance de NSInvocation. Ensuite, le patron de méthode -forwardInvocation: est invoqué avec, en argument, la nouvelle instance de NSInvocation créée. En redéfinissant l’implémentation de -forwardInvocation: fournie par NSObject, vous pouvez obtenir un comportement personnalisé. L’implémentation par défaut de NSObject envoie le message -doesNotRecognizeSelector: de manière à lancer une exception. Il faut redéfinir non seulement la méthode -forwardInvocation:, mais également -methodSignatureForSelector: pour obtenir un comportement de renvoi correct.
Par exemple, supposons que la classe MYClass souhaite renvoyer les messages qu’elle ne comprend pas à une instance de MYHelperClass. Pour cela, elle peut utiliser un code semblable au suivant : - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if ([myHelperClassInstance respondsToSelector:aSelector]) { return [myHelperClassInstance methodSignatureForSelector:aSelector]; } else { return [super methodSignatureForSelector:aSelector]; } } - (void)forwardInvocation:(NSInvocation *)invocation { SEL aSelector = [invocation selector]; if ([myHelperClassInstance respondsToSelector:aSelector]) { [invocation invokeWithTarget:myHelperClassInstance]; } else { [self doesNotRecognizeSelector:aSelector]; } }
Chapitre 27
Mandataire et Renvoi
345
Puisqu’une grande partie du code Objective-C sert à vérifier si un objet répond à un sélecteur avant d’envoyer le message, il est également préférable de redéfinir la méthode -respondsToSelector:. Voici le code correspondant dans notre exemple : - (BOOL)respondsToSelector:(SEL)aSelector { if ([myHelperClassInstance respondsToSelector:aSelector]) { return YES; } else { return [super respondsToSelector:aSelector]; } }
Le comportement personnalisé défini par l’implémentation de -forwardInvocation: peut évidemment faire plus qu’un simple renvoi, par exemple ignorer des messages ou déclencher un autre traitement avant ou après le renvoi d’un message. L’instance de NSInvocation peut être stockée ou modifiée et peut servir à construire un message qui sera transmis via le réseau. Les objets mandataires constituent la principale utilisation du renvoi. Mandataires Un mandataire est un objet qui, en général, ne fait rien par lui-même. Il est lié à un autre objet pour lequel il sert de mandataire. La majeure partie des messages envoyés à un mandataire finit par passer au travers du mécanisme de renvoi du moteur d’exécution. Pour cela, le mandataire se doit d’implémenter uniquement le strict minimum des méthodes. La Figure 27.1 illustre la séquence d’envoi des messages et de retour des valeurs au travers d’un mandataire. Figure 27.1 Le mandataire est placé entre l’émetteur et le récepteur d’un message.
1 Émetteur
2 4
Mandataire
3
Récepteur
La classe NSProxy est employée pour la mise en œuvre des mandataires. Contrairement à la plupart des classes Objective-C qui dérivent de NSObject, NSProxy ne possède pas de super-classe. Puisque NSProxy implémente aussi peu de méthodes que possible, un grand nombre de méthodes de NSObject, dont les développeurs supposent l’existence, comme -class, -superclass et même -init, ne sont pas implémentées par NSProxy. C’est pourquoi la plupart des messages sont quasiment assurés d’atteindre l’implémentation de la méthode -forwardInvocation: du mandataire.
346
Les design patterns de Cocoa
La classe MYJunction suivante implémente un mandataire qui renvoie les messages d’action issus de l’interface utilisateur vers chaque objet d’une liste. Cela permet de dépasser une limitation du design pattern Cible et action, qui, normalement, ne prend en charge qu’une seule cible. Le mandataire est la cible et il renvoie chaque message d’action reçu à plusieurs autres objets. MYJunction utilise un NSMutableArray pour stocker les cibles vers lesquelles sont renvoyés les messages d’action. L’interface publique déclare deux méthodes. La première ajoute une cible, tandis que la seconde permet d’obtenir une instance partagée. La classe MYJunction n’étant pas un véritable singleton, il est possible d’en créer plusieurs instances. L’instance partagée existe uniquement pour des questions de commodité lors de son utilisation dans une application de test, comme nous le verrons plus loin. Voici la déclaration de l’interface : #import @interface MYJunction : NSProxy { NSMutableArray *targets; } + (MYJunction *)sharedJunction; - (void)addTarget:(id)anObject; @end
La méthode +sharedJunction crée une instance lorsqu’elle est appelée pour la première fois et retourne ensuite cette même instance lors des appels ultérieurs, à la manière du code utilisé pour créer un singleton : + (MYJunction *)sharedJunction { static MYJunction *sharedJunction = nil; if (!sharedJunction) { sharedJunction = [[MYJunction alloc] init]; } return sharedJunction; }
Pour gérer la liste des cibles, les méthodes -init, -dealloc et -addTarget: sont nécessaires. La méthode -init crée la liste des cibles, tandis que -dealloc la supprime. La méthode -addTarget: ajoute simplement une nouvelle cible à la liste. - (id)init { targets = [[NSMutableArray alloc] init]; return self; }
Chapitre 27
Mandataire et Renvoi
347
- (void)dealloc { [targets release]; [super dealloc]; } - (void)addTarget:(id)anObject { [targets addObject:anObject]; }
Vous remarquerez que la méthode -init n’appelle pas [super init]. En effet, sa super-classe, NSProxy, n’offre aucune méthode -init. Nous avons opté pour un NSMutableArray car nous voulons que l’ordre de réception des messages par les cibles soit celui dans lequel elles sont ajoutées à l’instance de MYJunction via la méthode -addTarget:. Une mise en œuvre réellement robuste devrait probablement vérifier qu’une cible n’est pas ajoutée à la liste si elle s’y trouve déjà, à moins que l’envoi multiple d’un message à une cible soit un comportement souhaité. En utilisant un NSMutableSet pour mémoriser les cibles, nous garantissons leur unicité, sans garantir l’ordre de renvoi des messages aux cibles. Le cœur de l’implémentation du mandataire se trouve dans la mise en œuvre du renvoi des messages. Le code suivant parcourt toutes les cibles, en envoyant à chacune le message. Par ailleurs, si l’émetteur du message fait partie des cibles, le message ne lui est pas retransmis. Puisque cet objet s’intéresse particulièrement au renvoi des messages cible/action, il fait quelques suppositions sur la signature de méthode. Chaque message possède deux arguments cachés, self et _cmd. Par conséquent, le premier argument visible est en réalité le troisième. L’implémentation suivante de -forwardInvocation: ignore les messages qui n’ont pas de troisième argument et suppose que le troisième argument est un objet, normalement l’émetteur d’un message d’action. Dans un code de production, il faudrait vérifier que le troisième argument est réellement un objet. - (void)forwardInvocation:(NSInvocation *)anInvocation { if ([[anInvocation methodSignature] numberOfArguments] > 2) { for (id target in targets) { id messageSender = nil; [anInvocation getArgument:&messageSender atIndex:2]; if (messageSender != target) { [anInvocation invokeWithTarget:target]; } } } }
348
Les design patterns de Cocoa
Ce code ne vérifie pas si une cible donnée peut répondre au message. Une amélioration possible serait d’envoyer le message uniquement aux cibles qui sont capables d’y répondre. Une autre amélioration serait de conserver une indication d’envoi réussi du message à l’une des cibles et de lancer une exception si aucune d’elles n’y répond. Pour que le renvoi fonctionne correctement, une implémentation de -methodSignatureForSelector: est également nécessaire. Dans ce cas, nous faisons une autre supposition. Puisqu’une seule signature de méthode peut être retournée, la première cible qui peut répondre à un sélecteur est utilisée pour générer cette signature. Si certaines cibles ont une signature de méthode différente, notre implémentation risque de provoquer des erreurs d’exécution. Une fois encore, puisque l’objectif est le renvoi de messages cible/ action qui ont tous la même signature, notre hypothèse ne porte pas à conséquence. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { for (id target in targets) { if ([target respondsToSelector:aSelector]) { return [target methodSignatureForSelector:aSelector]; } } return nil; }
Enfin, l’implémentation de -conformsToProtocol: et de -respondsToSelector: est généralement obligatoire pour que le mandataire soit utilisable dans un plus grand nombre de cas. La version suivante retourne YES si l’une des cibles retourne YES. - (BOOL)conformsToProtocol:(Protocol *)aProtocol { for (id target in targets) { if ([target conformsToProtocol:aProtocol]) { return YES; } } return NO; } - (BOOL)respondsToSelector:(SEL)aSelector { for (id target in targets) { if ([target respondsToSelector:aSelector]) { return YES; } } return NO; }
Chapitre 27
Mandataire et Renvoi
349
Ce code suffit à rendre le mandataire opérationnel. Toutefois, pour l’utiliser dans une application réelle, nous devons encore ajouter du code. L’application d’exemple ouvre quatre fenêtres identiques, chacune avec un NSSlider et un NSTextfield. Pour cela, une fenêtre est placée dans son propre fichier .nib, qui est chargé quatre fois. Tous les curseurs et les champs de saisie sont liés à l’aide d’une instance de MYJunction. Ainsi, la modification de la valeur de l’un provoque l’actualisation des sept autres. L’établissement des connexions de notre objet mandataire pose un petit problème à Interface Builder. Pour le résoudre, nous utilisons un objet assistant. Sa seule fonction est d’établir les connexions à l’instance de MYJunction après le chargement du fichier .nib. Il a besoin d’un outlet pour l’objet qu’il gère. @interface MYJunctionHelper : NSObject { IBOutlet id myObject; } @end
Lorsque l’objet assistant est chargé depuis un fichier .nib, il obtient l’instance partagée de MYJunction et ajoute son objet à la liste des cibles. Il fixe également la jonction en tant que cible de l’objet : #import "MYJunctionHelper.h" #import "MYJunction.h" @implementation MYJunctionHelper - (void)awakeFromNib { MYJunction *junction = [MYJunction sharedJunction]; [myObject setTarget:junction]; [junction addTarget:myObject]; } @end
L’utilisation de cet objet assistant est simple. Une instance est créée dans Interface Builder pour chaque contrôle qui envoie un message à la jonction. Puisque la fenêtre contient un curseur et un champ de saisie qui communiquent au travers de la jonction, deux instances de MYJunctionHelper sont créées. Chacune est connectée à l’un des contrôles. Enfin, chaque contrôle est connecté au premier répondeur et un message d’action est choisi. Pour l’application d’exemple, nous avons retenu le message -takeDoubleValueFrom:. La connexion est faite avec le premier répondeur car n’importe quel message d’action valide connu d’Interface Builder peut être choisi. Si la connexion se faisait avec un autre objet, alors, seuls les messages d’actions implémentés par cette cible pourraient être utilisés. La connexion au premier répondeur fixe en réalité la cible
350
Les design patterns de Cocoa
d’un contrôle à nil, mais cela n’a pas d’importance car l’objet assistant fixera la cible à la jonction dès que l’interface sera chargée dans l’application. Une dernière classe assure l’ouverture des quatre fenêtres lorsque le chargement de l’application est terminé. La classe JunctionAppController est instanciée dans le fichier Main.nib et joue le rôle de délégué de l’objet NSApplication. Elle comprend un NSMutableArray qui contient des NSWindowController pour les quatre fenêtres et implémente la méthode -openAllWindows: pour toutes les ouvrir à la fois. Voici son interface : #import @interface JunctionAppController : NSObject { NSMutableArray *windowControllers; } - (IBAction)openAllWindows:(id)sender; @end
Son implémentation se contente de gérer la création des contrôleurs de fenêtre et ouvre les quatre fenêtres après un court délai. Un délai égal à zéro reporte l’envoi du message -openAllWindows: jusqu’à la prochaine invocation de la boucle d’exécution. Autrement dit, les fenêtres s’ouvriront lorsque la boucle des événements de l’application démarrera. #import "JunctionAppController.h" #define NUMBER_OF_WINDOWS 4 @implementation JunctionAppController - (id)init { if(nil != (self = [super init])) { windowControllers = [[NSMutableArray alloc] init]; } return self; } - (void)dealloc { [windowControllers release]; [super dealloc]; } - (void)awakeFromNib { int i; for (i=0; i