Vous êtes sur la page 1sur 331

Rfrence

Les

design patterns en Ruby


Russ Olsen

Rseaux et tlcom Programmation

Gnie logiciel

Scurit Systme dexploitation

Version franaise par Laurent Julliard, Mikhail Kachakhidze et Richard Piacentini

Les design patterns en Ruby


Russ Olsen

Pearson Education France a apport le plus grand soin la ralisation de ce livre an de vous fournir une information complte et able. Cependant, Pearson Education France nassume de responsabilits, ni pour son utilisation, ni pour les contrefaons de brevets ou atteintes aux droits de tierces personnes qui pourraient rsulter de cette utilisation. Les exemples ou les programmes prsents dans cet ouvrage sont fournis pour illustrer les descriptions thoriques. Ils ne sont en aucun cas destins une utilisation commerciale ou professionnelle. Pearson Education France ne pourra en aucun cas tre tenu pour responsable des prjudices ou dommages de quelque nature que ce soit pouvant rsulter de lutilisation de ces exemples ou programmes. Tous les noms de produits ou marques cits dans ce livre sont des marques dposes par leurs propritaires respectifs.
Publi par Pearson Education France 47 bis, rue des Vinaigriers 75010 PARIS Tl. : 01 72 74 90 00 Mise en pages : TyPAO ISBN : 978-2-7440-4018-4 Copyright 2009 Pearson Education France Tous droits rservs ISBN original : 978-0-321-49045-2 Copyright 2008 Pearson Education, Inc. All rights reserved Titre original : Design Patterns in Ruby Traduit de lamricain par Laurent Julliard, Mikhail Kachakhidze et Richard Piacentini

Aucune reprsentation ou reproduction, mme partielle, autre que celles prvues larticle L. 122-5 2 et 3 a) du code de la proprit intellectuelle ne peut tre faite sans lautorisation expresse de Pearson Education France ou, le cas chant, sans le respect des modalits prvues larticle 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.

Karen, qui a permis tout ceci, et Jackson, qui y donne un sens.

Table des matires

Prface ldition franaise ............................................................................................ Prface ...............................................................................................................................

XIII XV

Avant-propos ..................................................................................................................... XVII qui est destin ce livre ? .......................................................................................... XIX Comment ce livre est-il organis ? ............................................................................. XIX Avertissement ............................................................................................................. XIX Le style de code utilis dans ce livre .......................................................................... XX propos de lauteur ......................................................................................................... XXIII Partie I Patterns et Ruby Chapitre 1. Amliorer vos programmes avec les patterns ........................................... The Gang of Four (la bande des quatre) ..................................................................... Patterns des Patterns ................................................................................................... Sparer ce qui change de ce qui reste identique .............................................. Programmer par interface et non par implmentation ..................................... Prfrer la composition lhritage ................................................................ Dlguer, dlguer, dlguer ........................................................................... Vous naurez pas besoin de a ......................................................................... Quatorze sur vingt-trois .............................................................................................. Patterns en Ruby ? ...................................................................................................... Chapitre 2. Dmarrer avec Ruby ................................................................................... Ruby interactif ............................................................................................................ Afcher Hello World .................................................................................................. Variables ..................................................................................................................... Fixnums et Bignums ................................................................................................... Nombres virgule ottante ........................................................................................ 3 4 5 5 6 7 11 12 14 16 19 20 20 22 24 25

VI

Les design patterns en Ruby

Il ny a pas de types primitifs ici ................................................................................ Mais, parfois, il ny a pas dobjet ............................................................................... Vrit, mensonges et nil ............................................................................................. Dcisions, dcisions ................................................................................................... Boucles ....................................................................................................................... Plus de dtails sur les chanes de caractres .............................................................. Symboles .................................................................................................................... Tableaux ...................................................................................................................... Tableaux associatifs .................................................................................................... Expressions rgulires ................................................................................................ Votre propre classe ...................................................................................................... Accs aux variables dinstance ................................................................................... Un objet demande : qui suis-je ? ................................................................................ Hritage, classes lles et classes mres ...................................................................... Options pour les listes darguments ............................................................................ Modules ...................................................................................................................... Exceptions .................................................................................................................. Threads ....................................................................................................................... Grer des chiers de code source spars .................................................................. En conclusion ............................................................................................................. Partie II Patterns en Ruby Chapitre 3. Varier un algorithme avec le pattern Template Method .......................... Faire face aux ds de la vie ...................................................................................... Sparer les choses qui restent identiques .................................................................... Dcouvrir le pattern Template Method ....................................................................... Mthodes daccrochage .............................................................................................. Mais o sont passes toutes les dclarations ? ........................................................... Types, scurit et exibilit ........................................................................................ Les tests unitaires ne sont pas facultatifs .................................................................... User et abuser du pattern Template Method ............................................................... Les Templates dans le monde rel .............................................................................. En conclusion ............................................................................................................. Chapitre 4. Remplacer un algorithme avec le pattern Strategy .................................. Dlguer, dlguer et encore dlguer ........................................................................ Partager les donnes entre lobjet contexte et lobjet stratgie ..................................

26 27 27 29 30 32 34 35 36 37 38 40 42 42 43 45 47 48 49 50

53 54 55 58 59 61 61 64 65 67 67 69 69 72

Table des matires

VII

Typage la canard une fois de plus ............................................................................ Procs et blocs .............................................................................................................. Stratgies rapides et pas trs propres .......................................................................... User et abuser du pattern Strategy .............................................................................. Le pattern Strategy dans le monde rel ...................................................................... En conclusion ............................................................................................................. Chapitre 5. Rester inform avec le pattern Observer .................................................. Rester inform ............................................................................................................ Une meilleure faon de rester inform ....................................................................... Factoriser le code de gestion du sujet ......................................................................... Des blocs de code comme observateurs ..................................................................... Variantes du pattern Observer ..................................................................................... User et abuser du pattern Observer ............................................................................. Le pattern Observer dans le monde rel ..................................................................... En conclusion ............................................................................................................. Chapitre 6. Assembler le tout partir des composants avec Composite .................... Le tout et ses parties ................................................................................................... Crer des composites .................................................................................................. Rafner le pattern Composite avec des oprateurs ..................................................... Un tableau comme composite ? .................................................................................. Une diffrence embarrassante .................................................................................... Pointeurs dans les deux sens ....................................................................................... User et abuser du pattern Composite .......................................................................... Les composites dans le monde rel ............................................................................ En conclusion ............................................................................................................. Chapitre 7. Accder une collection avec lItrateur .................................................. Itrateurs externes ....................................................................................................... Itrateurs internes ....................................................................................................... Itrateurs internes versus itrateurs externes .............................................................. Lincomparable Enumerable ...................................................................................... User et abuser du pattern Itrateur .............................................................................. Les itrateurs dans le monde rel ............................................................................... En conclusion ............................................................................................................. Chapitre 8. Effectuer des actions avec Command ........................................................ Lexplosion de sous-classes ........................................................................................ Un moyen plus simple ................................................................................................ Des blocs de code comme commandes ......................................................................

73 75 78 80 80 82 83 83 85 88 91 92 92 94 95 97 97 100 103 104 105 106 107 108 109 111 111 113 114 116 117 119 122 123 124 125 126

VIII

Les design patterns en Ruby

Les commandes denregistrement .............................................................................. Annuler des actions avec Command .......................................................................... Crer des les de commandes .................................................................................... User et abuser du pattern Command ........................................................................... Le pattern Command dans le monde rel ................................................................... Migration ActiveRecord .................................................................................. Madeleine ........................................................................................................ En conclusion ............................................................................................................. Chapitre 9. Combler le foss avec lAdapter ................................................................. Adaptateurs logiciels .................................................................................................. Les interfaces presque parfaites .................................................................................. Une alternative adaptative ? ........................................................................................ Modier une instance unique ..................................................................................... Adapter ou modier ? ................................................................................................. User et abuser du pattern Adapter ............................................................................... Le pattern Adapter dans le monde rel ....................................................................... En conclusion ............................................................................................................. Chapitre 10 . Crer un intermdiaire pour votre objet avec Proxy ............................ Les proxies la rescousse ........................................................................................... Un proxy de contrle daccs ..................................................................................... Des proxies distants .................................................................................................... Des proxies virtuels vous rendre paresseux ............................................................. liminer les mthodes ennuyeuses des proxies .......................................................... Les mthodes et le transfert des messages ...................................................... La mthode method_missing .......................................................................... Envoi des messages ......................................................................................... Proxies sans peine ........................................................................................... User et abuser du pattern Proxy .................................................................................. Proxies dans le monde rel ......................................................................................... En conclusion ............................................................................................................. Chapitre 11. Amliorer vos objets avec Decorator ....................................................... Dcorateurs : un remde contre le code laid .............................................................. La dcoration formelle ............................................................................................... Diminuer leffort de dlgation .................................................................................. Les alternatives dynamiques au pattern Decorator ..................................................... Les mthodes dencapsulation ........................................................................ Dcorer laide de modules ............................................................................ User et abuser du pattern Decorator ...........................................................................

127 130 132 133 134 134 135 138 139 140 142 144 145 147 147 148 149 151 152 153 155 156 157 158 159 160 160 163 164 165 167 167 172 173 174 174 175 176

Table des matires

IX

Les dcorateurs dans le monde rel ............................................................................ En conclusion ............................................................................................................. Chapitre 12. Crer un objet unique avec Singleton ..................................................... Objet unique, accs global .......................................................................................... Variables et mthodes de classe .................................................................................. Variables de classe ........................................................................................... Mthodes de classe .......................................................................................... Premire tentative de cration dun singleton Ruby ................................................... Gestion de linstance unique ........................................................................... Sassurer de lunicit ....................................................................................... Le module Singleton ................................................................................................... Singletons instanciation tardive ou immdiate ........................................................ Alternatives au singleton classique ............................................................................. Variables globales en tant que singletons ........................................................ Des classes en tant que singletons ................................................................... Des modules en tant que singletons ................................................................ Ceinture de scurit ou carcan ? ................................................................................. User et abuser du pattern Singleton ............................................................................ Ce sont simplement des variables globales, nest-ce pas ? ............................. Vous en avez combien, des singletons ? .......................................................... Singletons pour les intimes ............................................................................. Un remde contre les maux lis aux tests ....................................................... Les singletons dans le monde rel .............................................................................. En conclusion ............................................................................................................. Chapitre 13. Choisir la bonne classe avec Factory ....................................................... Une autre sorte de typage la canard ......................................................................... Le retour du pattern Template Method ....................................................................... Des mthodes factory avec des paramtres ................................................................ Les classes sont aussi des objets ................................................................................ Mauvaise nouvelle : votre programme a du succs .................................................... Cration de lots dobjets ............................................................................................. Des classes sont des objets (encore) ........................................................................... Proter du nommage .................................................................................................. User et abuser des patterns Factory ............................................................................ Les factories dans le monde rel ................................................................................ En conclusion ............................................................................................................. Chapitre 14. Simplier la cration dobjets avec Builder ............................................ Construire des ordinateurs ..........................................................................................

177 178 179 179 180 180 181 182 183 184 184 185 185 185 186 188 188 190 190 190 191 193 193 194 195 196 198 200 202 203 204 207 208 209 209 211 213 214

Les design patterns en Ruby

Des objets builder polymorphes ................................................................................. Les builders peuvent garantir la validit des objets .................................................... Rutilisation de builders ............................................................................................. Amliorer des objets builder avec des mthodes magiques ....................................... User et abuser du pattern Builder ............................................................................... Des objets builder dans le monde rel ........................................................................ En conclusion ............................................................................................................. Chapitre 15. Assembler votre systme avec Interpreter .............................................. Langage adapt la tche ........................................................................................... Dvelopper un interprteur ......................................................................................... Un interprteur pour trouver des chiers .................................................................... Retrouver tous les chiers ............................................................................... Rechercher des chiers par nom ..................................................................... Des grands chiers et des chiers ouverts en criture .................................... Des recherches plus complexes laide des instructions Not, And et Or ....... Crer un AST .............................................................................................................. Un analyseur syntaxique simple ...................................................................... Et un interprteur sans analyseur ? .................................................................. Dlguer lanalyse XML ou YAML ? ........................................................ Racc pour des analyseurs plus complexes ....................................................... Dlguer lanalyse Ruby ? ........................................................................... User et abuser du pattern Interpreter .......................................................................... Des interprteurs dans le monde rel .......................................................................... En conclusion ............................................................................................................. Partie III Les patterns Ruby Chapitre 16. Ouvrir votre systme avec des langages spciques dun domaine ...... Langages spciques dun domaine ........................................................................... Un DSL pour des sauvegardes de chiers .................................................................. Cest un chier de donnes, non, cest un programme ! ............................................ Dvelopper PackRat ................................................................................................... Assembler notre DSL ................................................................................................. Rcolter les bnces de PackRat .............................................................................. Amliorer PackRat ..................................................................................................... User et abuser des DSL internes ................................................................................. Les DSL internes dans le monde rel ......................................................................... En conclusion .............................................................................................................

216 219 219 220 221 222 223 225 226 226 229 229 230 231 232 234 234 236 237 238 238 238 239 240

245 245 246 247 248 250 251 252 254 254 256

Table des matires

XI

Chapitre 17. Crer des objets personnaliss par mta-programmation .................... Des objets sur mesure, mthode par mthode ............................................................ Des objets sur mesure, module par module ................................................................ Ajouter de nouvelles mthodes .................................................................................. Lobjet vu de lintrieur .............................................................................................. User et abuser de la mta-programmation .................................................................. La mta-programmation dans le monde rel .............................................................. En conclusion ............................................................................................................. Chapitre 18. Convention plutt que conguration ....................................................... Une bonne interface utilisateur... pour les dveloppeurs ............................................ Anticiper les besoins ....................................................................................... Ne le dire quune seule fois ............................................................................. Fournir un modle ........................................................................................... Une passerelle de messages ........................................................................................ Slectionner un adaptateur ......................................................................................... Charger des classes ..................................................................................................... Ajouter un niveau de scurit ..................................................................................... Aider un utilisateur dans ses premiers pas ................................................................. Rcolter les bnces de la passerelle de messages ................................................... User et abuser du pattern Convention plutt que conguration ................................. Convention plutt que conguration dans le monde rel ........................................... En conclusion ............................................................................................................. Conclusion ......................................................................................................................... Annexes Annexe A. Installer Ruby ............................................................................................... Installer Ruby sous Microsoft Windows .................................................................... Installer Ruby sous Linux ou un autre systme de type UNIX .................................. Mac OS X ................................................................................................................... Annexe B. Aller plus loin ................................................................................................ Design patterns ........................................................................................................... Ruby ........................................................................................................................... Expressions rgulires ................................................................................................ Blogs et sites Web ....................................................................................................... propos des traducteurs ................................................................................................. Index ..................................................................................................................................

257 258 259 261 264 265 266 269 271 273 273 274 274 275 277 278 281 283 284 284 285 286 287

291 291 291 292 293 293 294 295 296 297 299

Prface ldition franaise

Design Patterns In Ruby started out as a 900 word blog article that I wrote in one afternoon. I certainly never dreamed that those 17 or so paragraphs would lead to a book in English, let alone to a French edition. Perhaps this is not so surprising, because that is what Ruby is like: Ruby changes the odds, it makes the difcult easy and many impossible things possible. But the language is only half the story: Every programming language needs an enthusiastic user community to be successful. Here too Ruby has been specially blessed. Certainly I am grateful to Richard Piacentini, Laurent Julliard and Mikhail Kachakhidze for making this French edition possible.
Russ Olsen Virginia, April 2008

Cest avec un plaisir non dissimul que nous livrons aux lecteurs francophones cette traduction de louvrage de Russ Olsen sur les patrons de conception (design patterns) en Ruby. Et ce pour plusieurs raisons. Tout dabord parce que nous disposons dsormais dun ouvrage supplmentaire en franais qui met en avant le langage Ruby. En effet, si les publications sur le framework web Ruby on Rails sont aujourdhui lgion (plus de 20 titres en franais ce jour !), il nen va pas de mme pour le langage Ruby qui ne compte que quelques titres. Ensuite parce que ladaptation Ruby dun des ouvrages les plus clbres de lhistoire de linformatique, Design Patterns de Erich Gamma, Richard Helm, Ralph Johnson et John M. Vlissides (souvent appel "le GoF", abrviation de The Gang of Four ou "la bande des quatre", en rfrence aux quatre auteurs de louvrage) est une indniable marque de maturit du langage Ruby lui-mme, mais aussi de sa communaut. Les centaines de milliers de dveloppeurs qui ont dcouvert Rails au cours des deux dernires annes ont aussi pris conscience que derrire Rails se cache Ruby, un langage de programmation totalement orient objet, incroyablement agile et expressif quon peut utiliser dans de trs nombreux domaines (algorithmie, systme, rseau, modlisation, etc.).

XIV

Les design patterns en Ruby

Les patrons de conception sont dailleurs un thme rv pour mettre en lumire les qualits de ce langage cr en 1995 par un universitaire japonais, Yukihiro Matsumoto ("Matz" pour les intimes...). Vous pourrez notamment constater quel point lexpressivit et les capacits dynamiques du langage Ruby permettent de saffranchir des lourdeurs et du code fastidieux souvent ncessaire la mise en uvre de ces mmes patrons de conception dans dautre langages comme C++ ou Java. Ces qualits ont aussi inspir la communaut Ruby la cration de nouveaux patrons de conception comme les DSL (langages spciques dun domaine) ou "Convention plutt que Conguration", que vous dcouvrirez la n de louvrage. Avant de vous laisser la lecture de ce livre qui est dj une rfrence outre-Atlantique, nous aimerions vous mettre en garde et vous proposer un sujet de rexion. Lavertissement porte sur le caractre terriblement "addictif" du langage Ruby : ceux dentre vous qui sapprtent ctoyer Ruby pour la premire fois risquent fort den ressentir les effets secondaires, cest--dire une certaine aversion pour les langages typage statique utiliss aujourdhui dans lindustrie, comme C++, C# ou Java. Pour vous aider surmonter cette impression de clivage profond, nous terminerons en vous livrant quelques lments de rexion. Sun et Microsoft ont tout deux lanc le portage du langage Ruby sur leur machine virtuelle (respectivement Jruby et RubyDLR) et Apple livre dsormais Ruby et Rails en standard avec ces kits de dveloppement. Tous ont compris que dans certains domaines les atouts du langage Ruby permettent des gains de productivit considrables sans pour autant hypothquer ni lexistant ni la qualit des logiciels produits. Les langages dynamiques orients objet, apparus dans les annes 1970 avec Smalltalk, sont lorigine de la plupart des concepts de programmation utiliss aujourdhui. Longtemps clipss par les langages "poids lourd" (dans tous les sens du terme !) de lindustrie, les voici qui reviennent en force sur le devant de la scne et Ruby en est assurment lun des plus dignes reprsentants. Bonne lecture !

Laurent Julliard Richard Piacentini

Prface

Design patterns. Catalogue de modles de conceptions rutilisables, connu sous le nom affectueux de "livre du Gang of Four" (ou GoF), est le premier ouvrage de rfrence publi sur ce sujet, devenu depuis trs populaire. Avec plus dun demi-million dexemplaires vendus, cet ouvrage a sans doute inuenc la faon de penser et de coder de millions de programmeurs dans le monde entier. Je me rappelle distinctement le jour o jai achet mon premier exemplaire de ce livre la n des annes 1990. En partie cause des recommandations enthousiastes de mes amis, je lai considr comme une tape incontournable vers ma maturit en tant que programmeur. Jai aval le livre en quelques jours en essayant dinventer des applications pratiques pour chacun des patterns. Il est gnralement reconnu que la caractristique la plus utile des patterns rside dans le vocabulaire quils proposent. Ce vocabulaire permet aux programmeurs dexprimer des modles de conception dans les conversations quils peuvent avoir entre eux en cours de dveloppement. Cest particulirement vrai pour la programmation en paire (pair programming), la pierre angulaire de la programmation agile de type Extreme Programming, ainsi que pour dautres techniques agiles, o la conception est une activit quotidienne et collective. Il est fantastiquement pratique de pouvoir dire votre collgue "Je pense quici nous avons besoin dune stratgie" ou "Ajoutons cette fonctionnalit sous la forme dun observateur". Dans certaines entreprises, la connaissance de design patterns est mme devenue un moyen simple pour ltrer des candidats : "Quel est votre pattern prfr ?" "Hum... Factory ?" "Merci dtre venu, au revoir." Il est vrai que la notion de pattern prfr est assez trange. Votre pattern prfr doit tre par dnition celui qui est le plus adapt aux circonstances. Une des erreurs classiques faites par des programmeurs inexpriments qui commencent apprendre les patterns est dimplmenter un pattern comme une n en soi et non pas comme un outil. Pourquoi implmenter des patterns dans le but de samuser ?

XVI

Les design patterns en Ruby

Dans le monde des langages typage statique, limplmentation de design patterns prsente un certain nombre de ds techniques. Dans le meilleur des cas, vous allez utiliser des techniques de ninja pour dmontrer toute votre habilet au codage. Dans le pire des scnarii, vous vous retrouverez avec un tas de code gnrique totalement rpugnant. Pour des allums de la programmation comme moi, cela suft rendre le sujet sur les design patterns particulirement amusant. Est-ce que les design patterns du GoF sont difciles implmenter en Ruby ? Pas vraiment. Tout dabord, labsence de typage statique rduit le cot de vos programmes en terme de lignes de code. La bibliothque standard de Ruby permet dinclure les patterns les plus frquents en une ligne, alors que les autres sont essentiellement incorpors dans le langage mme. Par exemple, selon le GoF un objet Command encapsule du code qui sait effectuer une tche ou excuter un fragment de code un moment donn. Cette description correspond galement un bloc de code un Proc en Ruby. Russ a travaill avec Ruby depuis 2002 et il sait que la majorit des dveloppeurs Ruby expriments ont dj une bonne matrise des design patterns et de leurs applications. Daprs moi, son d principal consistait crire un livre qui soit la fois pertinent pour des programmeurs Ruby professionnels mais qui puisse aussi proter aux dbutants. Je pense quil a russi et je suis convaincu que vous serez daccord avec moi sur ce point. Prenons notre exemple dun objet Command : dans sa forme simple, il peut tre facilement implment avec un bloc Ruby, mais il suft dy ajouter de linformation sur son tat et un peu de code mtier et limplmentation devient tout de suite plus complexe. Russ nous fournit des conseils avertis spciques Ruby et prts tre appliqus. Ce livre prsente aussi lavantage dinclure de nouveaux design patterns spciques Ruby. Russ a identi et expliqu en dtail plusieurs modles de conception dont un de mes prfrs : Internal Domain Specic Languages (les langages internes spciques dun domaine). Je crois que sa vision de ce pattern comme lvolution du pattern Interpreter est une analyse de rfrence signicative et sans prcdent. Enn, je pense que ce livre sera extrmement bnque ceux qui dbutent leur carrire dans le monde Ruby ou qui migrent dautres langages, comme PHP, qui ne mettent pas autant laccent sur les pratiques de conception oriente objet. En dcrivant les design patterns, Russ a illustr des solutions essentielles aux problmes que lon rencontre quotidiennement dans le dveloppement de programmes denvergure en Ruby : ce sont des conseils dune valeur inestimable pour un dbutant. Je suis sr que ce livre sera le cadeau que joffrirai en priorit mes amis et collgues dbutants. Obie Fernandez, diteur de la srie Professional Ruby

Avant-propos

Un ancien collgue disait que les gros livres sur les design patterns tmoignent de linadquation dun langage de programmation. Il entendait par l que les patterns sont des idiomes courants dans le code et quun bon langage de programmation doit rendre leur implmentation extrmement simple. Un langage idal intgrerait les design patterns jusquau point de les rendre quasiment transparents. Pour vous donner un exemple extrme, la n des annes 1980 jai travaill sur un projet o lon produisait du code C orient objet. Oui, C et non pas C++. Voici comment nous avons russi cet exploit. Chaque objet (en ralit une structure en C) pointait vers un tableau de pointeurs de fonction. Pour utiliser lobjet nous retrouvions le tableau correspondant et nous appelions ses fonctions, en simulant ainsi des appels de mthodes. Ctait trange et pas trs propre, mais cela fonctionnait. Si nous y avions pens, nous aurions pu appeler cette technique "le pattern orientobjet". videmment, avec larrive de C++, puis de Java, notre modle sest incorpor si profondment au langage quil est devenu invisible. Aujourdhui, lorientation objet nest plus considre comme un pattern cest trop simple. Pourtant, beaucoup de choses demeurent encore complexes. Design patterns. Catalogue de modles de conceptions rutilisables, crit par Gamma, Helm, Johnson et Vlissides, qui a acquis une notorit bien mrite, fait partie dun programme de lecture obligatoire pour chaque ingnieur logiciel. Or limplmentation des modles dcrits dans cet ouvrage avec les langages rpandus (Java, C++ et peut-tre C#) ressemble beaucoup au systme " lancienne" que jai conu dans les annes 1980. Trop pnible. Trop verbeux. Trop enclin aux bugs. Le langage de programmation Ruby se rapproche davantage de lidal de mon vieil ami il facilite si bien limplmentation des patterns que la plupart du temps ceux-ci se fondent dans larrire-plan.

XVIII Les design patterns en Ruby

Cette facilit est due plusieurs facteurs :


m

Ruby est un langage dynamiquement typ. En supprimant le typage statique, Ruby rduit de faon signicative le surcot de code ncessaire la construction de la plupart des programmes, y compris ceux qui implmentent des patterns. Ruby a des fermetures lexicales. Il permet de passer des fragments de code avec leur environnement sans recourir la construction de classes et dobjets inutiles. Les classes Ruby sont de vrais objets. Le fait quune classe soit un objet comme un autre nous permet de manipuler une classe Ruby au moment de lexcution. On peut crer de nouvelles classes ou modier des classes existantes en ajoutant ou en supprimant ses mthodes. On peut mme cloner une classe et modier la copie sans toucher loriginal. Ruby prsente un modle lgant de la rutilisation de code. En plus de lhritage classique, Ruby permet de dnir des mixins qui fournissent un moyen simple mais exible dcrire du code partageable entre plusieurs classes.

Tout cela rend le code Ruby trs compact. En Ruby, tout comme en Java et C++, on peut implmenter des ides trs sophistiques, mais Ruby propose des moyens beaucoup plus efcaces de cacher les dtails de leur implmentation. Comme vous le verrez par la suite, de nombreux "design patterns" qui ncessitent dinterminables lignes de code gnrique dans les langages statiques traditionnels ne requirent quune ou deux lignes en Ruby. Vous pouvez transformer une classe en un singleton avec la simple commande include Singleton. Vous pouvez dlguer aussi facilement quhriter. Ruby vous donne les outils pour exprimer davantage de choses intressantes chaque ligne, ce qui rduit votre base de code. Il sagit non seulement dviter de taper sur le clavier mais aussi dappliquer le principe DRY (Dont Repeat Yourself). Je doute que quelquun dans le monde daujourdhui regrette mon vieux pattern orient-objet en C. Il fonctionnait, mais ma demand beaucoup defforts. De mme, les implmentations traditionnelles des nombreux "design patterns" fonctionnent, mais vous demandent beaucoup defforts. Ruby reprsente un vrai pas en avant car il suft de faire le travail une fois et de le dissocier de votre code principal. En rsum, Ruby permet de se concentrer sur des solutions des problmes concrets, au lieu de la tuyauterie. Je souhaite que ce livre vous montre comment y parvenir.

Avant-propos

XIX

qui est destin ce livre ?


Ce livre sadresse aux programmeurs qui souhaitent apprendre dvelopper des applications en Ruby. Les concepts de base de la programmation oriente objet doivent tre connus, mais vous naurez besoin daucune connaissance particulire en matire de design patterns. Vous pourrez les apprendre en lisant ce livre. Vous naurez pas besoin non plus dune matrise approfondie de Ruby pour tirer pleinement parti de ce livre. Une introduction rapide au langage vous est propose au Chapitre 2, les autres points spciques Ruby tant expliqus au l de louvrage.

Comment ce livre est-il organis ?


Le prsent ouvrage est divis en trois parties. La Partie 1 est constitue de deux chapitres dintroduction : le premier passe en revue lhistorique et les raisons qui ont prsid la naissance des design patterns et le second vous propose un tour dhorizon du langage Ruby sufsamment toff pour que vous "deveniez dangereux". La Partie 2, qui reprsente la majeure partie de ce livre, examine du point de vue Ruby un certain nombre de patterns du Gang of Four. Quels sont les problmes que rsout un pattern ? quoi ressemblent limplmentation traditionnelle celle fournie par le Gang of Four et celle en Ruby ? Le pattern est-il justi en Ruby ? Existe-t-il des alternatives en Ruby pour faciliter la solution ce problme ? Autant de questions auxquelles nous apportons des rponses dans cette seconde partie. La Partie 3 couvre trois patterns qui sont apparus avec lusage avanc de Ruby.

Avertissement
Je ne peux pas signer de mon nom un livre sur les design patterns sans rpter le mantra que je murmure depuis des annes : les design patterns sont des solutions "prcuites" aux problmes courants de programmation. Idalement, lorsque vous rencontrez le bon problme il suft dappliquer le bon design pattern et vous avez la solution. Cest cette premire partie de la phrase attendre de rencontrer le "bon problme" qui semble poser problme certains ingnieurs. On ne peut pas considrer quun pattern est appliqu correctement si le problme que ce pattern est cens rsoudre nexiste pas. Lusage imprudent de tous les patterns pour rgler des problmes qui nen sont pas est lorigine de la mauvaise rputation que se sont taills les design patterns dans certains milieux. Je me permets dafrmer quen Ruby il est plus facile dcrire un adaptateur qui utilise une factory pour obtenir un proxy dun builder, qui cre son tour une commande pour coordonner lopration en vue dadditionner deux plus deux.

XX

Les design patterns en Ruby

Cest vrai, Ruby rend le processus effectivement plus facile, mais mme en Ruby cela na aucun sens. On ne peut non plus voir la construction de programmes comme un processus de recombinaison de patterns existants. Tout programme intressant a des sections uniques : du code qui est parfait pour un problme donn et aucun autre. Les design patterns sont l pour vous aider reconnatre et rsoudre des problmes de conception frquents et rptitifs. Lavantage des design patterns, cest que vous pouvez rapidement vacuer des problmes que quelquun a dj rsolus pour passer aux choses vritablement difciles, savoir le code spcique votre situation. Les patterns ne sont pas une potion magique pour rgler tous vos soucis de conception. Ils ne sont quune technique une technique trs utile que vous pouvez utiliser en dveloppant des programmes.

Le style de code utilis dans ce livre


Si la programmation en Ruby est si agrable, cest que le langage tente de seffacer. Sil existe plusieurs moyens senss dexprimer quelque chose, Ruby, en gnral, les propose tous :
# Une faon de lexprimer if (divisor == 0) puts Division by zero end # Encore une puts Division by zero if (divisor == 0) # Et une troisime (divisor == 0) && puts(Division by zero)

Ruby nessaie pas dinsister sur lutilisation mticuleuse de la syntaxe. Lorsque le sens de linstruction est clair, Ruby permet domettre des lments syntaxiques. Par exemple, dhabitude vous pouvez omettre des parenthses dans les listes darguments quand vous appelez une mthode :
puts(A fine way to call puts) puts Another fine way to call puts

Vous pouvez mme omettre des parenthses lorsque vous dnissez la liste darguments dune mthode ou dune expression conditionnelle :
def method_with_3_args a, b, c puts "Method 1 called with #{a} #{b} #{c}" if a == 0 puts a is zero end end

Avant-propos

XXI

Le problme de ces raccourcis vient du fait que leur usage excessif a tendance embrouiller les dbutants. Une majorit des programmeurs qui dbutent en Ruby seraient plus rassurs par
if file.eof? puts( Reached end of file ) end

ou mme
puts Reached end of file if file.eof?

que par
file.eof? && puts(Reached end of file)

Puisque ce livre se concentre plus sur la puissance et llgance inhrentes Ruby que sur la syntaxe, jai essay de faire en sorte que mes exemples ressemblent du code Ruby rel tout en restant comprhensibles par des dbutants. En pratique, cela signie que je prote des raccourcis les plus vidents mais que jvite volontairement des astuces plus radicales. Cela ne veut pas dire que je ne sois pas au courant ou que je ne soutienne pas lutilisation de la "stnographie" Ruby. Je suis simplement plus concentr sur la ncessit de faire passer le message de llgance conceptuelle de ce langage auprs des lecteurs dbutants. Vous aurez beaucoup doccasions dapprendre des raccourcis lorsque vous serez tomb fou amoureux de ce langage.

propos de lauteur

Russ Olsen est ingnieur logiciel avec son actif plus de vingt-cinq ans dexprience. Russ a dvelopp des logiciels dans des domaines aussi varis que la conception ou la production assiste par ordinateur, les systmes dinformation gographique, la gestion de documents et lintgration des systmes dinformation en entreprise. Il travaille actuellement sur des solutions de scurit et de dcouverte automatique de services SOA pour de grandes entreprises. Russ a contribu Ruby depuis 2002. Il est lauteur de ClanRuby, une tentative lpoque dajouter des fonctionnalits multimdias Ruby. Aujourdhui, il participe de nombreux projets open source y compris UDDI4R. Des articles techniques de Russ sont parus dans Javalobby, On Java chez OReilly ainsi que sur le site de Java Developers Journal. Russ est diplm de Temple University et habite en banlieue de Washington avec sa famille, deux tortues et un nombre indtermin de guppy. Russ est joignable par courrier lectronique russ@russolsen.com.

Partie I
Patterns et Ruby

1
Amliorer vos programmes avec les patterns
Cest drle, mais les "design patterns" me rappellent toujours une certaine picerie. Encore lycen, javais dcroch un premier boulot temps partiel. Pendant quelques heures les jours de la semaine et le samedi toute la journe, je donnais un coup de main dans un petit commerce local. Toutes les tches peu qualies telles que lapprovisionnement des tagres ou encore le balayage du plancher faisaient partie de mes responsabilits. Au dbut, la vie de cette petite picerie me paraissait un trange mlange dimages (je nai jamais apprci laspect dun foie cru), de sons (mon patron avait t instructeur dans le corps des marines amricains et il savait impressionner par sa voix) et dodeurs (je vous passe les dtails). Pourtant, plus je travaillais chez Conrad Market, plus ces vnements isols se regroupaient pour former des procdures comprhensibles. Le matin, il fallait ouvrir la porte dentre, teindre lalarme et afcher le panneau "Oui ! Nous sommes ouverts". Le soir, le processus tait invers. Entre les deux je moccupais dun million de choses : approvisionner les tagres, aider les clients trouver le ketchup tout et nimporte quoi. Au fur et mesure que je faisais connaissance avec mes homologues dans les autres commerces, je dcouvrais que leur mode de fonctionnement tait quasiment le mme. Cela illustre la faon dont les gens ragissent face aux problmes et la complexit de la vie. Nous improvisons et trouvons des solutions rapides de nouveaux problmes, mais pour rpondre aux problmes rcurrents nous dveloppons des procdures standardises. Il ne faut pas rinventer la roue, comme dit le proverbe.

Patterns et Ruby

The Gang of Four (la bande des quatre)


Rinventer la roue est un rel problme pour des ingnieurs logiciel. Personne naime faire et refaire la mme chose, mais lorsquon conoit des systmes il est parfois difcile de se rendre compte que le dispositif rotatoire de rduction de friction axe central que lon vient de raliser est effectivement une roue. La conception de logiciels peut devenir tellement complexe quil est facile de ne pas reconnatre les problmes types qui se prsentent nous de faon rpte et leurs solutions rcurrentes. En 1995, Erich Gamma, Richard Helm, Ralph Johnson et John Vlissides ont dcid de consacrer leur nergie un travail plus utile que la construction rpte de "roues". Sur la base du travail effectu entre autres par Christopher Alexander et Kent Beck, ils ont publi louvrage Design patterns. Catalogue de modles de conceptions rutilisables. Louvrage a t un succs instantan, rendant ses auteurs clbres (au moins dans le monde du dveloppement logiciel) sous le nom de "The Gang of Four" (le GoF ou la bande des quatre). Les membres du GoF ont accompli deux tches. Premirement, ils ont fait dcouvrir un grand nombre dingnieurs logiciel la notion des "design patterns" (modles de conception), un pattern tant une solution "prt--porter" un problme frquent de conception. Ils ont crit : "Nous devons regarder autour de nous et identier les solutions courantes aux problmes courants. Nous devons nommer chaque solution et dcrire les situations pour lesquelles son utilisation est approprie, ainsi que les cas o il faut opter pour une approche diffrente. Cette information sera consigne par crit an que la palette des modles de conception stoffe avec le temps." Deuximement, les membres du GoF ont identi, nomm et dcrit vingt-trois patterns initiaux, quils ont considrs comme des modles cls pour dvelopper des programmes orients objet propres et bien conus. Depuis la publication de Design Patterns, de nombreux livres ont suivi sur le sujet, dcrivant des patterns dans des domaines allant des microcontrleurs temps rel aux architectures dentreprise. Pourtant, les vingt-trois patterns recenss par le GoF sont rests au cur de la conception des logiciels orients objet en se concentrant sur des questions essentielles : comment les objets dans la plupart des systmes se rfrent lun lautre ? Comment doivent-ils tre coupls ? Quelles informations sur les autres objets doivent-ils avoir ? Comment remplacer des parties qui sont susceptibles de changer frquemment ?

Chapitre 1

Amliorer vos programmes avec les patterns

Patterns des Patterns


En rponse ces questions, les auteurs de Design Patterns ont formul plusieurs principes gnraux : des mta-design patterns. Pour moi, ces ides se rduisent quatre points :
m m m m

sparer ce qui change de ce qui reste identique ; programmer par interface et non par implmentation ; prfrer la composition lhritage ; dlguer, dlguer, dlguer.

Jy ajouterai un point qui ne gure pas dans Design Patterns, mais qui dnit souvent mon approche de la conception de programmes :
m

Vous naurez pas besoin de a.

Dans les sections suivantes, nous allons tudier comment chacun de ces principes conditionne la conception de logiciels. Sparer ce qui change de ce qui reste identique Le dveloppement de logiciels serait beaucoup plus simple si les choses ne changeaient pas. On pourrait crire des classes en tant sr quune fois termines elles continueront servir la mme tche. Mais le monde bouge, et dans le monde du dveloppement logiciel cest encore plus vrai. Les volutions du matriel informatique, des systmes dexploitation, des compilateurs ainsi que les corrections de bugs et les exigences qui varient perptuellement ont toutes un rle jouer. Lobjectif cl des ingnieurs logiciel est de dvelopper des systmes permettant de limiter les dgts. Dans un systme idal, tout changement serait local : on ne devrait jamais avoir besoin de revrier la totalit du code si le point A a t modi, ce qui vous a amen modier le point B, qui a dclench un changement de C, qui a eu une rpercussion sur Z. Comment atteindre ou au moins sapprocher du systme idal, dans lequel tout changement serait local ? Pour atteindre ce but il faut sparer les parties variables de celles qui restent identiques. Si vous pouvez identier quels aspects de votre systme sont susceptibles de changer, vous pourrez les isoler des parties plus stables. Il faudra toujours modier le code lorsque les besoins volueront ou lorsquon corrigera une anomalie, mais il y a peut-tre une chance pour que lon puisse contenir les modications dans ces portions de code que nous aurons isoles et protges an que le reste demeure inchang.

Patterns et Ruby

Mais comment maintenir cette quarantaine et prvenir la contamination des parties stables par des aspects variables ? Programmer par interface et non par implmentation crire du code faiblement coupl est toujours un bon dbut. Si nos classes sont conues pour une tche non triviale, elles doivent tre au courant lune de lautre. Mais que doivent-elles savoir lune de lautre exactement ? Le fragment de code Ruby suivant 1 cre une instance de classe Car et appelle la mthode dinstance drive :
my_car = Car.new my_car.drive(200)

Il est clair que ce code est fortement li la classe Car. Il continuera fonctionner si lon a besoin de manipuler un seul vhicule. Si les exigences changent et quun autre mode de transport doit tre gr (par exemple lavion), nous nous retrouvons face un problme. Pour matriser deux types de transport on pourrait par exemple se contenter dcrire lhorreur suivante :
# Grer des voitures et des avions if is_car my_car = Car.new my_car.drive(200) else my_plane = AirPlane.new my_plane.fly(200) end

Ce code nest pas seulement sale, mais il est aussi fortement coupl la fois aux voitures et aux avions. Cette structure pourrait tenir la route jusqu larrive dun bateau, ou dun train, ou dun vlo. Une meilleure solution serait de se rappeler les bases de la programmation oriente objet et dajouter une dose gnreuse de polymorphisme. Si les voitures, les avions et les bateaux implmentent une interface commune, le code peut tre amlior de faon suivante :
my_vehicle = get_vehicle my_vehicle.travel(200)

En plus dtre du bon code orient objet bien lisible, cet exemple dmontre le principe de programmation par interface.
1. Ne vous inquitez pas, si vous dmarrez avec Ruby. Le code utilis dans ce chapitre est trs basique. Le chapitre suivant prsente le langage plus en dtail. Il ne faut pas sinquiter si tout nest pas compltement clair pour le moment.

Chapitre 1

Amliorer vos programmes avec les patterns

Le code initial fonctionnait avec un seul type de vhicule une voiture , mais la nouvelle version gre tout objet de type Vehicle. Les dveloppeurs Java et C# suivent parfois ce conseil la lettre, puisque la dnition dinterfaces est un lment de ces langages. Ils extraient soigneusement toute la fonctionnalit principale dans plusieurs interfaces spares, qui peuvent ensuite tre implmentes par des classes concrtes. Gnralement, cest une bonne pratique, mais ce nest pas vraiment la philosophie sous-jacente au principe de programmation par interface. Lide consiste choisir le type le plus gnrique possible, ne pas appeler la voiture une voiture sil est possible de lappeler un vhicule, peu importe si Car et Vehicle sont des classes concrtes ou des interfaces abstraites. Cest mme mieux si lon peut choisir un supertype encore plus gnrique, par exemple un objet mobile. Comme vous le verrez par la suite, le langage Ruby, qui ninclut pas dinterfaces dans sa syntaxe1, vous encourage cependant programmer des interfaces utilisant des supertypes les plus gnriques possible. En crivant du code avec des types gnriques, par exemple en considrant tous les avions, les trains et les voitures comme des vhicules, nous rduisons le couplage de notre code. Au lieu davoir quarante-deux classes fortement lies des voitures, des bateaux et des avions, on nira peut-tre avec quarante classes qui ne manipuleront que la notion de vhicule. Il est probable que lon soit embt sil faut ajouter un nouveau type de vhicule correspondant aux deux classes qui restent, mais au moins on aura limit les dgts. Le fait dajouter seulement deux classes indique que nous avons russi le pari de sparer les parties variables (les deux classes) des parties stables (les quarante autres). Grce au faible couplage, nous diminuons considrablement le risque que la moindre modication dclenche une raction en chane dvastant la totalit du code. Nanmoins, programmer des interfaces nest pas la seule mesure prendre pour rendre le code rsistant aux changements. Il existe aussi la composition. Prfrer la composition lhritage Si votre introduction la programmation oriente objet a ressembl la mienne, vous avez d passer 10 minutes sur la dissimulation de linformation, 22 minutes sur les questions de visibilit et de porte et le reste du semestre sur lhritage. Une fois acquises les notions basiques des objets, champs et mthodes, le principal sujet intressant qui reste est lhritage, la partie la plus oriente objet de la programmation objet.
1. Le langage Ruby inclut des modules qui ressemblent des interfaces Java. Vous trouverez plus de dtails sur des modules Ruby au Chapitre 2.

Patterns et Ruby

Lhritage rend possible limplmentation sans peine, il suft de sous-classer la classe Widget pour obtenir comme par magie laccs toutes ses fonctionnalits. Lhritage semble tre la panace. Besoin dimplmenter une voiture ? Il suft de sousclasser Vehicle, qui lui-mme est de type MovableObject, etc. De mme, dautres branches telles que AirPlane et MotorBoat peuvent apparatre ct (voir Figure 1.1). chaque niveau nous protons des fonctionnalits de la classe mre.
Figure 1.1
Proter au maximum de lhritage
MovableObject

Vehicle

MotorBoat

Car

AirPlane

Toutefois, lhritage apporte son lot de dfauts. Lorsque vous dnissez une sous-classe dune classe existante, au lieu de crer deux entits spares vous obtenez deux classes qui sont intimement lies par limplmentation commune. Par nature, lhritage a tendance lier la classe lle et la classe mre. Il y a de fortes chances quune modication de comportement de la classe mre affecte le fonctionnement de la classe lle. De plus, la classe lle a un accs privilgi aux dtails dimplmentation de sa classe mre. Tout fonctionnement interne qui nest pas soigneusement cach est visible des sous-classes. Si lobjectif est de dvelopper des systmes faiblement coupls o le moindre changement ne provoque pas une raction en chane qui fait voler en clats tout le code, il ne faut pas se er compltement lhritage. Si lhritage pose autant de soucis, quelle est lalternative ? Eh bien, nous pouvons construire les comportements souhaits en ayant recours la composition. Au lieu de crer des classes qui hritent tous leurs talents de la classe mre, nous pouvons les laborer partir de composants mtier de base. Pour y arriver nous quipons nos objets avec des rfrences vers des objets-fournisseurs de fonctionnalits. Toute classe qui requiert ces fonctionnalits peut les appeler car elles sont encapsules dans lobjet-fournisseur. En bref, nous prfrons dnir un objet qui A une caractristique plutt quun objet qui EST de type donn.

Chapitre 1

Amliorer vos programmes avec les patterns

Pour reprendre lexemple prcdent, supposons que nous ayons une mthode qui simule une balade en voiture. La partie cl de cette balade est le dmarrage et larrt du moteur :
class Vehicle # Diffrentes fonctionnalits des vhicules... def start_engine # Dmarrer le moteur end def stop_engine # Arrter le moteur end end class Car < Vehicle def sunday_drive start_engine # Aller se balader et ensuite revenir. stop_engine end end

La logique de ce code est la suivante : notre voiture a besoin de dmarrer et darrter le moteur. Cette fonctionnalit sappliquera dautres vhicules, pourquoi ne pas factoriser le code li au moteur et le placer dans la classe commune Vehicle (voir Figure 1.2).
Figure 1.2
Factoriser le code li au moteur dans la classe mre
Vehicle start_engine() stop_engine()

Car

Cest bien, mais tous les vhicules ne sont pas forcment quips dun moteur. Une intervention chirurgicale sera ncessaire sur des classes qui reprsentent des vhicules non motoriss (un vlo ou un bateau voile). Par ailleurs, moins de faire attention lors du dveloppement de la classe Vehicle, les dtails lis au moteur seront disponibles pour la classe Car. Aprs tout, le moteur est gr par la classe Vehicle, et lobjet Car nest quune sorte de Vehicle. Ce nest pas conforme au principe de sparation des parties variables et statiques. Toutes ces difcults peuvent tre vites si le code du moteur est plac dans sa propre classe totalement indpendante et dissocie de la classe parent de Car.

10

Patterns et Ruby

class Engine # Code li au moteur def start # Dmarrer le moteur end def stop # Arrter le moteur end end

Si chaque objet Car contient une rfrence vers son propre Engine, nous pourrions aller nous promener en voiture grce la composition.
class Car def initialize @engine = Engine.new end def sunday_drive @engine.start # Aller se balader et ensuite revenir. @engine.stop end end

Lassemblage des fonctionnalits par composition (voir Figure 1.3) donne de nombreux avantages : les fonctions du moteur sont factorises dans une classe spare et sont prtes tre rutilises (encore une fois par le truchement de la composition !).
Figure 1.3
Assembler une voiture avec la composition
Vehicle

Car

Engine start() stop()

Qui plus est, nous avons galement simpli la classe Vehicle en supprimant les fonctions du moteur. Nous avons aussi amlior le niveau dencapsulation : lextraction des fonctions du moteur offre dsormais une sparation nette par interface entre chaque voiture et son moteur. Dans la version initiale, fonde sur lhritage, tous les dtails de limplmentation du moteur taient exposs toutes les mthodes de la classe Vehicle. Dans la

Chapitre 1

Amliorer vos programmes avec les patterns

11

nouvelle version, la voiture ne peut accder son moteur quau travers des fonctions publiques et probablement bien conues de linterface de la classe Engine. Nous avons galement ouvert une possibilit dutilisation dautres types de moteurs. La classe Engine pourrait devenir une classe abstraite dont nous pourrions driver plusieurs implmentations de moteurs, toutes utilisables par notre voiture (voir Figure 1.4).
Figure 1.4
Dsormais, la voiture peut avoir des moteurs diffrents
Car Engine

GasolineEngine

DieselEngine

Cerise sur le gteau notre voiture nest pas condamne une seule implmentation de moteur pendant toute sa vie. Les moteurs peuvent tre remplacs au moment de lexcution :
class Car def initialize @engine = GasolineEngine.new end def sunday_drive @engine.start # Aller se balader et ensuite revenir. @engine.stop end def switch_to_diesel @engine = DieselEngine.new end end

Dlguer, dlguer, dlguer Il y a une lgre diffrence fonctionnelle entre notre classe Car avec lobjet Engine spar et limplmentation initiale fonde sur lhritage. La classe initiale exposait les mthodes publiques start_engine et stop_engine. Il est videmment possible de faire de mme avec la dernire version de limplmentation en passant la responsabilit lobjet Engine :

12

Patterns et Ruby

class Car def initialize @engine = GasolineEngine.new end def sunday_drive start_engine # Aller se balader et ensuite revenir. stop_engine end def switch_to_diesel @engine = DieselEngine.new end def start_engine @engine.start end def stop_engine @engine.stop end end

Cette technique simple qui consiste "reler le bb" est connue sous le nom prtentieux de la dlgation. La mthode start_engine est appele sur lobjet Car. Lobjet dit "ce nest pas mon problme" et passe la main lobjet moteur. La combinaison de composition et de dlgation est une alternative puissante et exible lhritage. On prote de la plupart des avantages de lhritage tout en gardant beaucoup plus de exibilit et sans tre pnalis par des effets de bord. Cette facilit nest pas gratuite. La dlgation requiert un appel de mthode supplmentaire lorsquun objet passe la main un autre. Cet appel entrane un cot sur les performances, mais il reste cependant ngligeable dans la plupart des cas. La dlgation a aussi un autre cot : la ncessit dcrire du code basique savoir toutes ces mthodes ennuyeuses comme start_engine et stop_engine qui ne font que transfrer lappel lobjet nal capable de faire le traitement ncessaire. Heureusement, vous avez entre les mains un livre qui traite des design patterns en Ruby et, comme vous le verrez aux Chapitres 10 et 11, Ruby permet dviter dcrire ces mthodes ennuyeuses. Vous naurez pas besoin de a Assez parl des principes cits par le GoF en 1995. Jaimerais ajouter cette liste formidable un autre principe que je trouve critique pour dvelopper et livrer de vrais systmes. Ce principe de conception vient du monde de lExtreme Programming et est lgamment rsum en You Aint Gonna Need It (YAGNI ou "vous nen aurez pas

Chapitre 1

Amliorer vos programmes avec les patterns

13

besoin"). Le principe YAGNI stipule que vous ne devez pas implmenter des fonctionnalits ni introduire de la exibilit si vous nen avez pas un besoin immdiat. Pourquoi ? Parce quil est fort probable que vous nen ayez pas besoin plus tard non plus. Un systme bien conu est un systme qui sadapte avec lgance aux corrections de bugs, aux changements des exigences, au progrs continu de la technologie ainsi qu des remises plat invitables. Selon le principe YAGNI, il faut se concentrer sur des besoins immdiats et dvelopper prcisment le niveau de exibilit dont on est sr davoir besoin. Sans cette certitude, il vaut mieux reporter limplmentation de la fonctionnalit jusquau moment o elle devient ncessaire. Si la fonction nest pas indispensable, ne limplmentez pas, consacrez plutt votre temps et votre nergie lcriture des fonctions ncessaires dans linstant. la base du principe YAGNI on trouve un principe souvent vri qui dit que nous avons tendance nous tromper quand nous tentons danticiper nos besoins futurs. Lorsquon ajoute une nouvelle fonction ou un niveau de exibilit avant que le besoin ne se manifeste, on fait un double pari. Premirement, on parie que cette fonction sera nalement utilise. Si aujourdhui vous implmentez votre couche de persistance de manire indpendante de la base de donnes, vous faites le pari quun jour vous aurez utiliser une autre base de donnes. Si vous internationalisez linterface utilisateur, vous pariez quun jour vous aurez des utilisateurs ltranger. Comme le disait Yogi Berra (selon certaines sources), les prdictions sont difciles surtout lorsquelles concernent le futur. Sil savre que vous ne passerez jamais une autre base de donnes ou que votre application ne sera pas distribue ailleurs que dans votre pays dorigine, tout le travail effectu en amont et toute la complexit supplmentaire nauront servi rien. Le second pari que vous faites en dveloppant avant lheure est encore plus risqu. Lorsque vous implmentez une fonctionnalit ou ajoutez un niveau de exibilit trop tt, vous considrez que votre solution est bonne et que vous savez rsoudre un problme qui ne sest pas encore prsent. Vous pariez que votre couche de persistance indpendante de la base de donnes et installe avec tant damour sera adapte au systme effectivement retenu pour remplacer lancienne : "Quoi ? Le marketing veut que lon supporte xyzDB ? Je nen ai jamais entendu parler !" Vous pariez que votre solution dinternationalisation pourra supporter toute langue que vous serez amen grer : "Oh l l ! Je ne me suis pas rendu compte quil fallait supporter une langue qui scrit de droite gauche..."

14

Patterns et Ruby

Voici une faon de voir les choses : mis part la possibilit de recevoir un coup sur la tte, vous ne deviendrez pas plus bte avec le temps. Nous apprenons des choses et gagnons en intelligence chaque jour qui passe, et cest encore plus vrai dans des projets logiciel. On peut tre certain davoir une vision plus claire des contraintes, des technologies et de la conception la n dun projet qu son commencement. Lorsque vous dveloppez une fonction dont vous navez pas encore besoin, vous tes coupable de programmer btement. Attendez le moment o le besoin se manifeste et vous serez probablement en tat de mieux comprendre le besoin et la faon dy rpondre. Le but des design patterns est de rendre vos systmes plus exibles et plus adaptables aux changements. Mais, au l du temps, lusage des design patterns sest retrouv associ un courant particulirement virulent de "surengineering" visant produire du code inniment exible au risque de devenir incomprhensible et parfois mme dfectueux. Lutilisation approprie des design patterns est lart de concevoir un systme sufsamment exible pour rpondre vos besoins daujourdhui, pas plus. Un pattern est une technique utile plutt quune n en soi. Les design patterns peuvent vous aider dvelopper un systme qui fonctionne, mais le systme ne fonctionnera pas mieux si vous appliquez toutes les combinaisons imaginables des vingt-trois patterns du GoF. Votre code marchera mieux si vous restez concentr sur les tches accomplir aujourdhui.

Quatorze sur vingt-trois


Les patterns prsents dans louvrage Design Patterns sont des outils pour construire des logiciels. Tout comme les outils que lon achte la quincaillerie, ils ne sont pas adapts pour toutes les situations. Certains sont comme votre marteau prfr, indispensables pour toutes vos tches ; dautres sont comme le niveau laser que lon ma offert pour mon anniversaire, parfaits lorsque vous en avez besoin, ce qui narrive que rarement. Dans ce livre, nous allons aborder quatorze des vingt-trois patterns du GoF. En faisant le choix des patterns couvrir, jai essay de me concentrer sur les pratiques les plus rpandues et les plus utiles. Par exemple, je nimagine pas coder sans utiliser des itrateurs (voir Chapitre 7), jinclus donc ce pattern sans hsiter. Je me suis galement pench vers des patterns qui se transforment lors du passage en Ruby. Une fois de plus, litrateur est un bon exemple : il mest impossible de vivre sans, mais les itrateurs en Ruby diffrent beaucoup de ceux en Java et C++. Pour vous donner un aperu de ce qui vous attend, voici une vue densemble des patterns du GoF traits dans ce livre :
m

Chacun des patterns essaie de rsoudre un problme. Admettons par exemple que votre code fasse toujours la mme chose, sauf ltape 44. Parfois, ltape 44 doit

Chapitre 1

Amliorer vos programmes avec les patterns

15

sexcuter dune certaine manire et parfois dune autre. Vous aurez alors probablement besoin du pattern Template Method.
m

Peut-tre est-ce la totalit de lalgorithme qui doit varier et pas seulement ltape 44. Vous avez une tche bien dnie effectuer, mais il existe plusieurs faons de le faire. Il conviendrait sans doute dencapsuler ces techniques ou algorithmes dans un objet Stratgie. Et si vous avez une classe A qui doit rester au courant des vnements intervenant dans la classe B ? Mais en vitant le couplage des deux classes car un jour viendra o la classe C (ou mme la classe D) verra le jour et exprimera le mme besoin. Il faudra alors considrer lutilisation du pattern Observateur. Parfois, il faut grer une collection en tant quun seul objet. Il doit tre possible de supprimer, dplacer ou copier un chier isol ou faire les mmes oprations sur un rpertoire entier. Si vous devez dvelopper une collection qui se comporte comme un objet individuel, vous avez probablement besoin du pattern Composite. Imaginez que vous crivez du code pour dissimuler une collection dobjets, mais que vous ne voulez pas la cacher compltement : lutilisateur doit avoir accs aux objets en squence sans savoir o ni comment ils sont stocks. Vous avez sans doute besoin du pattern Itrateur. Parfois, nos instructions doivent tre prsentes comme une sorte de carte postale : "Chre base de donnes, quand tu recevras ceci, jaimerais que tu supprimes la ligne 7843." Les cartes postales sont rares dans le code, mais le pattern Command est conu sur mesure pour ce genre de situation. Que faire lorsque vous avez un objet qui fait ce quil doit faire mais dont linterface est totalement inadquate ? Il peut sagir ici dune profonde incohrence ou plus simplement dun objet qui utilise la mthode write sur un objet qui nomme cette mthode save. Dans cette situation, le GoF recommande le pattern Adaptateur. Vous avez peut-tre le bon objet sous la main, mais il est l-bas quelque part sur le rseau et vous ne voulez pas que le client ait soccuper de son emplacement. Ou peut-tre essayerez-vous de retarder le plus possible la cration dun objet ou encore dimplmenter un contrle daccs. Dans ces circonstances, optez pour le pattern Proxy. Il est parfois ncessaire de rajouter certaines responsabilits un objet la vole, au moment de lexcution. Si vous disposez dun objet avec un certain nombre de

16

Patterns et Ruby

fonctionnalits mais qui de temps en temps doive prendre des responsabilits supplmentaires, il serait judicieux dutiliser le pattern Dcorateur.
m

Vous pouvez avoir besoin dun objet unique dont il nexiste quune seule instance disponible pour tous les utilisateurs. Cela ressemble fort au pattern Singleton. Maintenant, imaginez que vous crivez une classe destine tre tendue. Pendant que vous tes en train de coder joyeusement la classe parent, vous vous rendez compte quelle doit instancier un nouvel objet. Seulement, la classe lle sera au courant du type dobjet crer. Vous avez probablement besoin du pattern Factory Method (Fabrication). Comment crer des familles dobjets compatibles ? Imaginez que vous ayez un systme de conception de voitures. Tous les types de moteurs ne sont pas compatibles avec toutes les sortes de carburant ou les systmes de refroidissement. Comment sassurer que lon ne nisse pas par dvelopper une voiture Frankenstein ? Cest peut-tre loccasion dutiliser une classe ddie la cration de ces objets et de lappeler Abstract Factory. Supposons que vous instanciez un objet dune telle complexit quun volume important de code soit ncessaire pour grer sa construction. Ou, pire encore, le processus de la construction est variable selon les circonstances. Vous avez probablement besoin du pattern Builder. Avez-vous dj eu le sentiment de ne pas utiliser le bon langage de programmation pour rsoudre votre problme ? Cela pourrait paratre fou, mais peut-tre que vous devriez vous arrter et dvelopper un Interprteur pour le langage spcique capable de fournir une solution simple.

Patterns en Ruby ?
Voil en quelques lignes la thorie des patterns1. Mais pourquoi Ruby ? Ne sagit-il pas dun quelconque langage de script seulement appropri pour des tches dadministration systme et des interfaces graphiques Web ? En un mot : non. Ruby est un langage orient objet, lgant et universel. Il nest pas parfait pour toutes les situations par exemple, si vous avez besoin de trs haute performance, il faut, au
1. videmment, je ne fais que gratter la surface dun sujet vaste et passionnant. Jetez un il sur lannexe B, Aller plus loin, pour plus dinformation.

Chapitre 1

Amliorer vos programmes avec les patterns

17

moins pour linstant, choisir un autre langage. Toutefois, Ruby est plus que convenable pour un grand nombre de tches. Ce langage a une syntaxe concise mais trs expressive et incorpore un modle de programmation riche et sophistiqu. Vous verrez dans les prochains chapitres que Ruby a sa propre faon de faire les choses, ce qui change notre approche des diffrents problmes de programmation, y compris ceux abords par des patterns classiques du GoF. Par consquent, il nest pas tonnant que la combinaison de Ruby et des design patterns classiques mne vers des dveloppements nouveaux et non traditionnels. Parfois, Ruby est sufsamment diffrent pour offrir des solutions totalement innovantes. ce propos, trois nouveaux patterns attirent lattention avec la popularit rcente de Ruby. Cest la raison pour laquelle je conclus le catalogue des patterns par les modles suivants :
m

Internal DomainSpecic Language (DSL), une technique trs dynamique pour construire des petits langages spcialiss ; Mta-programmation, une technique de cration dynamique des classes et des objets au moment de lexcution ; Convention plutt que conguration, un remde contre les maux de conguration (principalement XML).

Commenons...

2
Dmarrer avec Ruby
Jai dcouvert Ruby grce mon ls de 8 ans et son amour pour une gentille souris jaune lectriquement charge1. lpoque, en 2002, mon ls passait son temps libre jouer un jeu vido dont le but tait de trouver et dapprivoiser diffrentes cratures magiques, y compris le rongeur nergtique. Un jour, jai eu limpression de voir une ampoule sallumer au-dessus de sa tte et jimaginais ses penses : "Mon papa est programmeur. Le jeu auquel je joue, le truc qui parle des les magiques et des tres merveilleux, est un programme. Mon papa fait des programmes. Mon papa peut mapprendre faire un jeu !" Eh bien, peut-tre ! Aprs une dose de harclement et de pleurnicheries que seuls les parents de jeunes enfants peuvent rellement comprendre, jai entrepris dapprendre la programmation mon ls. Tout dabord, nous avions besoin dun langage de programmation simple, clair et facilement comprhensible. Aprs une courte recherche jai trouv Ruby. Mon ls, comme le font souvent les enfants, est rapidement pass autre chose, mais Ruby avait trouv un nouvel adepte. De fait, ctait un langage propre, clair et simple parfait pour apprendre. Mais, en tant que dveloppeur professionnel qui a conu des systmes dans toutes sortes de langages allant de lassembleur Java, jai vu davantage : Ruby est concis, sophistiqu et drlement puissant. Ruby est galement trs classique. Les composants de base du langage sont des vieux engrenages connus de tous les programmeurs. Ruby possde tous les types de donnes courants : des chanes de caractres, des entiers, des nombres virgule ottante ainsi que des tableaux et nos vieux amis vrai et faux. Les bases de Ruby sont familires et ordinaires, mais la faon dont le langage est structur au plus haut niveau nous apporte une joie inattendue.
1. Heureusement que la folie des Pokmon sest calme depuis.

20

Patterns et Ruby

Si vous matrisez dj les bases de Ruby, si vous avez dj crit quelques classes et savez comment extraire le troisime caractre dune chane ou comment calculer 2 puissance 437, vous pouvez passer directement au Chapitre 3. Cette section sera toujours l pour vous secourir si le besoin sen fait sentir. Ce chapitre vous est destin si vous dbutez en programmation Ruby. Le but de cette section est de vous donner le plus brivement possible un aperu des notions de base du langage. Au fond, Alan Turing avait raison lorsquil disait : "Une fois acquis un certain niveau de complexit, tous les langages de programmation deviennent quivalents." Si vous connaissez un autre langage rpandu, les bases de Ruby ne vous poseront aucun problme, vous allez "rapprendre" les choses que vous connaissez dj. Jespre qu la n de ce chapitre vous connatrez du langage juste ce quil faut pour devenir dangereux.

Ruby interactif
La faon la plus simple dexcuter du code Ruby1 consiste utiliser la console interactive irb. Aprs avoir dmarr irb, vous pouvez saisir du code Ruby et voir le rsultat instantanment. Pour lancer irb, il suft de taper irb dans le terminal. Lexemple ciaprs dmarre irb et additionne 2 et 2 laide de Ruby :
$ irb irb(main):001:0> 2+2 => 4 irb(main):002:0>

La console interactive Ruby est un moyen formidable pour faire des essais petite chelle. Rien naide plus explorer un nouveau langage que la possibilit de voir le rsultat immdiatement.

Afcher Hello World


Maintenant que Ruby est oprationnel, ltape suivante tombe sous le sens : crire votre premier programme. Voici lincontournable "Hello World" en Ruby :
# # Premier programme traditionnel pour tous les langages # puts(hello world)

1. Voyez lannexe A si vous avez besoin dinstructions pour installer Ruby sur votre systme.

Chapitre 2

Dmarrer avec Ruby

21

Vous pouvez simplement dmarrer irb et entrer ce code de manire interactive. Une autre alternative est dcrire le programme avec un diteur de texte et de lenregistrer dans un chier, par exemple sous le nom hello.rb. Ensuite, vous pouvez excuter votre programme laide de linterprteur Ruby, commodment appel ruby :
$ ruby hello.rb

Les deux techniques donnent un rsultat identique :


hello world

Rien quen lisant le programme hello world, vous pouvez apprendre beaucoup sur un langage de programmation. Par exemple, on dcouvre que la mthode puts afche des choses. On voit galement que les commentaires commencent par le caractre # et continuent jusqu la n de la ligne. Les commentaires peuvent occuper toute la ligne, comme dans lexemple ci-dessus, ou on peut galement les placer aprs le code :
puts(hello world) # Afficher bonjour

Labsence de point-virgule la n de chaque instruction est un autre point remarquable. En rgle gnrale, une instruction Ruby se termine par un saut de ligne. Les programmeurs Ruby ont tendance utiliser des points-virgules uniquement pour sparer des instructions multiples sur la mme ligne, et ce dans les rares cas o ils dcident dentasser plusieurs instructions sur une ligne :
# # Usage valide, mais atypique dun point-virgule en Ruby # puts(hello world); # # Un peu plus de rigueur. Utilisation toujours rare dun point-virgule # puts(hello ); puts(world)

Lanalyseur syntaxique de Ruby est sufsamment intelligent pour poursuivre son analyse sur la ligne suivante lorsquune instruction est clairement inacheve. Par exemple, le code ci-aprs fonctionne bien, car lanalyseur syntaxique dduit de loprateur + la n de la ligne que linstruction continue sur une deuxime ligne :
x = 10 + 20 + 30

Une instruction peut indiquer explicitement avec une barre oblique inverse quelle stend sur la ligne suivante :
x = 10 \ + 10

22

Patterns et Ruby

On voit ici merger un principe rcurrent de la philosophie Ruby qui consiste aider lutilisateur lorsquil a besoin dassistance et seffacer en toute autre circonstance. Selon cette philosophie, Ruby permet domettre les parenthses si elles naident pas clarier une liste darguments :
puts hello world

Pour des raisons de clart, la plupart des exemples de ce livre incluent les parenthses dans les appels de mthodes sauf si aucun argument nest fourni, et dans ce cas les parenthses vides sont omises. Dans le programme "hello world" nous avons entour la chane de caractres de guillemets simples, mais les guillemets doubles peuvent tout aussi bien tre utiliss :
puts("hello world")

Le rsultat obtenu avec des guillemets simples ou doubles est le mme, mais avec une petite subtilit. Les guillemets simples ont pour effet dafcher littralement ce que vous voyez car Ruby ne fait que trs peu dinterprtations sur de telles chanes de caractres. Ce nest pas le cas des guillemets doubles, qui provoquent un traitement classique en amont : \n est converti en un caractre de saut de ligne, \t devient une tabulation. Si la chane abc\n compte cinq caractres (les deux derniers sont la barre oblique inverse et la lettre "n"), la longueur de la chane "abc\n" nest que de quatre caractres (le dernier caractre tant le saut de ligne). Finalement, vous avez probablement remarqu que la chane hello world que nous avons passe loprateur puts ninclut pas de \n. Pourtant, cet oprateur a ajout un saut de ligne la n du message. En ralit, loprateur puts est assez intelligent. Il rajoute un saut de ligne au texte de sortie sil en manque un. Ce comportement nest pas forcment souhait pour le formatage de prcision, mais il est parfaitement adapt pour les exemples que vous trouverez dans ce livre.

Variables
Les noms des variables ordinaires en Ruby commencent par une lettre minuscule ou un tiret bas (nous verrons quelques variables inhabituelles plus tard) 1. Le premier caractre peut tre suivi par des lettres minuscules ou majuscules, des tirets bas ainsi que des chiffres. Les noms des variables sont sensibles la casse et la seule limite la longueur
1. Dans la plupart des cas, Ruby considre le tiret bas comme un caractre minuscule.

Chapitre 2

Dmarrer avec Ruby

23

des variables Ruby est votre imagination. Tous les noms ci-aprs reprsentent des noms de variables valides :
m m m m m m m m

max_length maxLength numberPages numberpages a_very_long_variable_name _flag column77Row88 ___

Les deux noms max_length et maxLength voquent un point important : les noms en casse mixte sont parfaitement accepts en Ruby et, pourtant, les dveloppeurs Ruby ont tendance ne pas les utiliser. La pratique la plus rpandue parmi les programmeurs Ruby bien levs est de sparer_des_mots_par_des_tirets_bas. Puisque les noms des variables sont sensibles la casse, numberPages et numberpages sont deux variables diffrentes. Enn, le dernier nom dans la liste comprend seulement trois tirets bas. Cest certes une pratique valide mais viter1. Assemblons maintenant nos chanes de caractres et nos variables :
first_name = russ last_name = olsen full_name = first_name + + last_name

Cet exemple illustre trois affectations basiques : la chane de caractres russ est attribue la variable first_name, la valeur olsen est attribue last_name et full_name reoit la concatnation de mon prnom et de mon nom spars par une espace. Vous avez peut-tre remarqu quaucune des variables nest dclare dans cet exemple. Rien ne dclare que la variable first_name est et restera toujours une chane de caractres. Ruby est un langage dynamiquement typ, ce qui signie que les variables ne possdent pas de type xe. En Ruby vous pouvez faire apparatre un nom de variable comme par magie et lui affecter une valeur. La variable adoptera le type de la valeur
1. Une autre bonne raison de ne pas nommer des variables uniquement avec des tirets bas est le fait quirb a dgain plus vite que vous. irb affecte la variable _ (un tiret bas) la valeur de la dernire expression value.

24

Patterns et Ruby

quelle reoit. Non seulement cela, mais la variable peut contenir des valeurs radicalement diffrentes aux diffrents moments de lexcution du programme. Au dbut du programme, la valeur de pi peut tre le nombre 3,14159; ensuite, au sein du mme programme la valeur peut prendre une rfrence vers un algorithme mathmatique complexe et, encore plus tard, elle peut devenir une chane de caractres "apple". Dans ce livre nous allons revisiter le typage dynamique plusieurs reprises (et nous commencerons au chapitre suivant, si vous tes impatient). Pour linstant, il faut retenir que les variables adoptent les types de leurs valeurs. En plus des variables habituelles que nous venons de voir, Ruby supporte les constantes. Une constante ressemble une variable sauf que son nom commence par une lettre majuscule :
POUNDS_PER_KILOGRAM = 2.2 StopToken = end FACTS = Death and taxes

Le principe dune constante est de recevoir une valeur qui ne change pas. Ruby nest pas particulirement rigoureux en ce qui concerne ce comportement. On peut modier la valeur dune constante mais au prix dun avertissement :
StopToken = finish (irb):2: warning: already initialized constant StopToken

Par prcaution, vous devez viter de changer les valeurs des constantes.

Fixnums et Bignums
Vous ne serez pas tonn dapprendre que Ruby supporte les oprations arithmtiques. En Ruby on peut additionner, soustraire, multiplier et diviser comme dhabitude :
x = 3 y = 4 sum = x+y product = x*y

En Ruby, un certain nombre de rgles sappliquent aux nombres. Il y a deux types de base : des entiers et des nombres virgule ottante. videmment, les entiers ne contiennent pas de partie dcimale : 1 ; 3 ; 6 ; 23 ; 77 et 42 sont tous des entiers, alors que 7.5 ; 3.14159 et 60000.0 sont des nombres virgule ottante. La division des entiers en Ruby ne rserve aucune surprise : divisez deux entiers et vous obtiendrez un entier, la partie dcimale sera tronque (et non pas arrondie !) :

Chapitre 2

Dmarrer avec Ruby

25

6/3 7/3 8/3 9/3

# # # #

donne 2 fait toujours 2 2 une fois de plus et finalement 3!

Les entiers de taille raisonnable tous ceux qui peuvent tre reprsents sur 31 octets sont de type Fixnum. Les entiers plus grands sont de type Bignum. Un Bignum peut contenir tout nombre arbitrairement gigantesque. tant donn quon passe dun type lautre de faon quasiment transparente, vous pouvez considrer quon ne fait aucune distinction entre les deux :
2 437 2**437 1234567890 1234567890/1234567890 # # # # # # Un Fixnum Un Fixnum Trs certainement un grand Bignum Encore un Bignum Divisez deux Bignums, et vous obtenez 1 un Fixnum

Ruby fournit les astuces daffectations classiques permettant de raccourcir des expressions. Par exemple, a = a+1 devient a += 1:
a = 4 a+ = 1 a- = 2 a* = 4 a/ = 2 # # # # est est est est devenu devenu devenu devenu 5 3 12 6

Malheureusement (NdT : ou heureusement !), en Ruby il ny a pas doprateurs dincrmentation (++) et de dcrmentation (--).

Nombres virgule ottante


Si seulement le monde tait aussi prcis que les entiers ! Mais pour faire face la complexit du monde rel Ruby fournit des nombres virgule ottante ou, en terminologie Ruby, des floats. Un float se distingue facilement, cest un nombre avec une partie dcimale :
3.14159 -2.5 6.0 0.0000000111

On peut additionner, soustraire et multiplier des floats pour obtenir les rsultats attendus. Les floats obissent aux rgles traditionnelles de la division :
2.5 + 3.5 # egale 6.0 0.5*10 # egale 5.0 8.0/3.0 # egale 2.66666666

26

Patterns et Ruby

Il ny a pas de types primitifs ici


Vous ntes pas oblig de me croire sur parole en ce qui concerne les types de toutes ces espces de nombres. Demandez plutt Ruby en utilisant la mthode class :
7.class 888888888888.class 3.14159.class # Renvoie class Fixnum # Renvoie class Bignum # Renvoie class Float

Cette syntaxe peut paratre quelque peu trange, mais cest un aperu dun aspect profond et important : en Ruby, tout absolument tout est objet. Lorsque nous crivons 7.class, nous utilisons la syntaxe bien connue oriente objet pour appeler la mthode class sur un objet. Dans ce cas prcis lobjet reprsente le nombre sept. Les nombres en Ruby disposent dun large ventail de mthodes :
3.7.round 3.7.truncate -123.abs 1.succ # # # # renvoie 4.0 renvoie 3 renvoie 123 Successeur, ou le nombre suivant, 2

Contrairement Java, C# et de nombreux langages rpandus, Ruby na pas de types primitifs. Ce sont des objets purs et durs. Le fait que tout en Ruby est objet conditionne en grande partie llgance du langage. Par exemple, lorientation objet universelle de Ruby est le secret derrire la conversion facile entre Fixnum et Bignum. Si on trace la hirarchie de classes dun objet Ruby quelconque vers sa classe parent, puis vers la classe parent du parent et tous les autres anctres, on nira par atteindre la classe Object. Grce cette ascendance commune, tout objet Ruby hrite dun minimum de mthodes, une sorte de kit de survie. La mthode class que nous avons appele ci-dessus provient de cette source. On peut galement dcouvrir si lobjet est une instance dune classe donne :
hello.instance_of?(String) # vrai

Ou sil est nil :


hello1.nil? # faux

Lune des mthodes de la classe Object les plus utilises est probablement to_s, qui retourne une reprsentation de lobjet sous forme dune chane de caractres. Cest une mthode quivalente de la mthode Java toString avec le nom commodment raccourci :
44.to_s hello.to_s # retourne une chane de deux caractres 44 # une conversion pas trs impressionnante # qui retourne hello

Chapitre 2

Dmarrer avec Ruby

27

Lorientation objet totale de Ruby a aussi certaines implications sur les variables. Puisque tout en Ruby est objet, il nest pas tout fait correct dafrmer que linstruction x=44 affecte la valeur 44 la variable x. En ralit, la variable x reoit une rfrence vers lobjet qui reprsente le nombre qui suit 43.

Mais, parfois, il ny a pas dobjet


Si tout est objet, quarrive-t-il si lon na pas vraiment dobjet ? Dans ce cas Ruby fournit un objet spcial qui reprsente lide de ne pas avoir dobjet, dtre compltement dpourvu dobjet. Cette valeur spciale est nil. Dans la section prcdente, nous avons vu que tout en Ruby est objet, ce qui est exact : nil est un vrai objet Ruby tout comme "hello world" ou 43. On peut par exemple obtenir la classe de nil :
puts(nil.class)

Le rsultat est prvisible :


NilClass

Malheureusement, nil est prdestin vivre sa vie tout seul : il ny a quune unique instance de NilClass (appele nil), et aucune autre instance de NilClass ne peut tre cre.

Vrit, mensonges et nil


Ruby supporte la panoplie classique des oprateurs boolens. Nous pouvons par exemple dterminer si deux expressions sont gales, si lune est infrieure ou suprieure lautre.
1 == 1 # vrai 1 == 2 # faux russ == smart # malheureusement, faux (1 < 2) # vrai (4 > 6) # et non a = 1 b = 10000 (a > b) # pas question

Nous avons aussi infrieur ou gal et son cousin suprieur ou gal :


(4 >= 4) # oui! (1 <= 2) # vrai aussi

28

Patterns et Ruby

Tous les oprateurs de comparaison sont valus comme lun de ces deux objets true ou false. Tout comme nil, true et false sont des instances uniques de leurs classes respectives : true est la seule instance de TrueClass et false est la seule instance de (vous lavez devin) FalseClass. trangement, TrueClass et FalseClass sont des sous-classes directes dObject. On aurait pu sattendre trouver une classe BooleanClass quelque part mais, hlas, elle nexiste pas ! Ruby offre aussi un oprateur and. Il en a mme plusieurs :
(1 == 1) and (2 == 2) # vrai (1 == 1) and (2 == 3) # faux

On pourrait galement crire :


(1 == 1) && (2 == 2) # vrai (1 == 1) && (2 == 3) # faux

Les rsultats des deux sont quivalents. Gnralement, les oprateurs and et && sont des synonymes1. Les oprateurs or et || sont assortis avec and et && et provoquent un rsultat attendu2 :
(1 (2 (1 (2 == == == == 1) 1) 1) 1) or || or || (2 (7 (3 (3 == 2) > 10) == 2) == 2) # # # # oui non oui non

Enn, Ruby fournit un oprateur classique not et son jumeau ! :


not (1 == 2) ! (1 == 1) not false # vrai # faux # vrai

Il faut bien tenir compte du fait quen Ruby toute expression peut tre value de manire boolenne. Nous pouvons mlanger des chanes de caractres avec des entiers ou encore des dates pour obtenir un boolen. Les rgles dvaluation sont trs simples : false et nil sont valus false. Toute autre expression provoque le rsultat true. Par consquent, les expressions suivantes sont parfaitement valides :
true and fred # vrai, puisque fred nest pas nil ni false fred && 44 # vrai, puisque fred et 44 sont tous les deux true nil || false # faux, puisque nil et false svaluent false

1. Pas compltement. Loprateur && a une priorit plus forte par rapport and. La mme rgle sapplique || et or. 2. Il existe en Ruby des oprateurs & et | remarquez quils ne consistent quen un seul caractre. Ce sont des oprateurs logiques bit bit, qui sont trs utiles dans certaines situations, mais probablement pas dans vos instructions quotidiennes de comparaison.

Chapitre 2

Dmarrer avec Ruby

29

Si vous venez du monde de C ou C++, vous trouverez choquant le fait que zro en Ruby svalue true dans les expressions boolennes (car zro nest pas nil ni false). Aussi surprenant que cela puisse paratre, lexpression
if 0 puts(Zero is true!) end

afche
Zero is true!

Dcisions, dcisions
Lexemple prcdent tait un aperu de linstruction if qui vient avec son else facultatif :
age = 19 if (age >= 18) puts(You can vote!) else puts(You are too young to vote.) end

Comme vous pouvez le voir, chaque instruction if doit tre obligatoirement ferme par end. Dans le cas o il y a plus dune condition, on peut utiliser elsif :
if(weight < 1) puts(very light) elsif(weight < 10) puts(a bit of a load) elsif(weight < 100) puts(heavy) else puts(way too heavy) end

Il faut noter que le mot cl elsif est un seul mot de cinq lettres. Ce nest pas else if, ni elseif et encore moins elif. Ruby essaie toujours de rendre le code le plus concis possible. Vu que les parenthses autour des instructions if et elsif najoutent pas de valeur au code, elles sont facultatives :
if weight < 1 puts(very light) elsif weight < 10 puts(a bit of a load)

30

Patterns et Ruby

elsif weight < 100 puts(heavy) else puts(way too heavy) end

Il existe un idiome particulier si vous avez prendre une dcision quant lexcution dune seule instruction Ruby. Dans ce cas vous pouvez tout simplement appliquer loprateur if la n de la ligne :
puts(way too heavy) if weight >= 100

Loprateur unless est linverse de loprateur if : le corps de linstruction ne sexcute que si la condition est value false. Tout comme avec loprateur if, unless peut avoir une forme longue :
unless weight < 100 puts(way too heavy) end

ou une forme courte :


puts(trop lourd) unless weight < 100

Boucles
Ruby possde deux sortes de boucles. Tout dabord, il y a la boucle while classique, qui doit toujours se terminer par end comme une expression if. La boucle suivante
i = 0 while i < 4 puts("i = #{i}") i = i + 1 end

afche ceci :
i i i i = = = = 0 1 2 3

Le frre ennemi de while est until, qui est plus ou moins identique while avec la seule diffrence que la boucle continue tre excute jusquau moment o la condition devient true. Lexemple prcdent peut tre crit de faon suivante :
i = 0 until i >= 4 puts("i = #{i}") i = i + 1 end

Chapitre 2

Dmarrer avec Ruby

31

Une boucle for en Ruby peut tre utilise par exemple pour accder aux lments dun tableau en squence :
array = [first, second, third] for element in array puts(element) end

tonnamment, les boucles for sont rares dans les vrais programmes Ruby. Un programmeur Ruby est plus susceptible dcrire ce code quivalent :
array.each do |x| puts(x) end

Cette boucle lallure bizarre est dcrite de manire dtaille au Chapitre 7. Pour linstant, voyez la syntaxe each comme une manire alternative dcrire des boucles for. Si la boucle doit tre interrompue, on peut utiliser linstruction break :
names = [george, mike, gary, diana] names.each do |name| if name == gary puts(Break!) break end puts(name) end

Si vous excutez ce code, il nafchera jamais gary:


george mike Break!

Enn, on peut omettre lexcution dune itration laide de linstruction next :


names.each do |name| if name == gary puts(Next!) next end puts(name) end

Ce code nafchera jamais gary mais continuera lexcution de la boucle :


george mike Next! diana

32

Patterns et Ruby

Plus de dtails sur les chanes de caractres


Essayons de nous familiariser davantage avec les chanes de caractres puisque nous les utilisons dj. Comme nous lavons vu prcdemment, les chanes de caractres peuvent tre construites la fois avec des guillemets simples et des guillemets doubles :
first = Mary had second = " a little lamb"

Nous avons galement appris que le signe plus est un oprateur de concatnation, donc
poem = first + second

svalue :
Mary had a little lamb

Les chanes disposent de tout un ventail de mthodes. On peut par exemple obtenir la longueur dune chane :
puts(first.length) # Affiche 8

On peut aussi convertir une chane en majuscules ou en minuscules :


puts(poem.upcase) puts(poem.downcase)

Ce code afche
MARY HAD A LITTLE LAMB mary had a little lamb

Le comportement des chanes de caractres en Ruby ressemble celui des tableaux : on peut affecter un caractre prcis une chane par son index, comme un lment dun tableau. Si lon excute
poem[0] = G puts(poem)

le rsultat sera un pome trs diffrent :


Gary had a little lamb

De mme, on peut accder des caractres particuliers dans une chane, mais avec un petit souci : Ruby ne possde pas de type de caractre spcial. Par consquent, si lon extrait des caractres dune chane en Ruby, on obtient un nombre entier, le code du caractre. Voyez lexemple suivant :
second_char = poem[1] # le caractre second_char est gal 97, # le code ASCII de la lettre a

Chapitre 2

Dmarrer avec Ruby

33

Heureusement, on peut aussi rinsrer des caractres et, nalement, nous navons peuttre pas caus de dommages :
poem[0] = 67 # 67 est le code ASCII de la lettre C

Maintenant, le propritaire de lagneau est Cary. Les chanes de caractres entoures de guillemets doubles ont en Ruby une caractristique spciale que nous allons souvent rencontrer dans les exemples de ce livre. En plus de remplacer les \n par des sauts de ligne et les \t par des tabulations, lorsque linterprteur Ruby trouve #{expression} lintrieur dune chane entre doubles guillemets, il remplace lexpression par sa valeur. Par exemple, si lon affecte une valeur la variable n
n = 42

on peut simplement linsrer dans une chane de caractres


puts("The value of n is #{n}.")

pour obtenir
The value of n is 42.

Cette fonctionnalit (qui sappelle linterpolation de chanes de caractres) ne se limite pas une seule expression par chane, et les expressions ne se limitent pas simplement des noms de variables. Nous pouvons trs bien crire
city = Washington temp_f = 84 puts("The city is #{city} and the temp is #{5.0/9.0 * (temp_f-32)} C")

le rsultat afch sera


The city is Washington and the temp is 28.8888888888889 C

Les guillemets simples sont parfaits pour des chanes de caractres relativement courtes, qui tiennent sur une ligne, mais ils sont peu commodes pour des expressions plusieurs lignes. Pour remdier cela, Ruby fournit un autre moyen dexprimer des chanes de caractres littrales :
a_multiline_string = %Q{ The city is #{city}. The temp is #{5.0/9.0 * (temp_f-32)} C

Dans cet exemple, tout ce qui se trouve entre %Q{ et } est une chane de caractres. Si votre chane commence par %Q { comme ci-dessus, Ruby la considre comme une chane entoure des doubles guillemets et fait toute linterprtation en consquence. Si

34

Patterns et Ruby

lon utilise %q{ (remarquez que "q" est minuscule), le texte nest trait que de manire minimaliste, correspondant une chane entre des guillemets simples 1. Enn, si vous venez du monde de Java ou C#, un srieux pige conceptuel vous attend en Ruby. Les chanes en C# et Java sont immuables : une fois cre, la chane de caractres ne peut jamais tre modie. Ce nest pas le cas de Ruby. En Ruby, une chane de caractres est modiable tout instant. Crons deux rfrences la mme chane de caractres pour illustrer notre propos :
name = russ first_name = name

Si lon crivait du code Java ou C#, on pourrait manipuler first_name linni, en tant certain que sa valeur ne peut jamais tre modie. Contrairement ces langages, si lon changeait la valeur de name :
name[0] = R

on modierait galement first_name, qui nest quune rfrence au mme objet de type chane de caractres. En afchant les deux variables
puts(name) puts(first_name)

on obtient la valeur modie :


Russ Russ

Symboles
Une polmique autour des avantages des chanes immuables existe depuis bien longtemps. Les chanes de caractres taient modiables en C et C++, puis immuables en Java et C#, et sont ensuite redevenues modiables en Ruby. Les chanes transformables ont sans aucun doute des avantages, mais le fait de les rendre modiables laisse une lacune vidente : que faire sil faut reprsenter un identicateur interne plutt que des donnes ?
1. En vrit, nous disposons de beaucoup plus doptions. Nous pouvons par exemple choisir des parenthses "()" ou des chevrons "<>" au lieu des accolades que jutilise pour dlimiter des chanes. Ainsi, %q<une chane> est une chane de caractres parfaitement valide. On peut utiliser un caractre spcial de son choix pour commencer et terminer un chane, par exemple %Q-une chane-.

Chapitre 2

Dmarrer avec Ruby

35

Ruby fournit dans ce cas une classe spciale, le symbole. Les symboles Ruby sont des identicateurs immuables. Ils commencent toujours par un deux-points :
m m m

:a_symbol :an_other_symbol :first_name

Si vous ntes pas habitu aux symboles, ils peuvent paratre tranges au dbut. Il suft de retenir que les symboles sont plus ou moins des chanes de caractres immuables que les programmeurs Ruby utilisent en tant quidenticateurs.

Tableaux
Crer des tableaux Ruby se fait simplement en tapant au clavier une paire de crochets ou Array.new:
x = [] y = Array.new a = [neo, trinity, tank] # Un tableau vide # Encore un # Un tableau de trois lments

Les lments dun tableau Ruby sont numrs partir de zro :


a[0] # neo a[2] # tank

On peut se renseigner sur le nombre des lments dun tableau laide des mthodes length ou size. Les deux sont quivalentes :
puts(a.length) puts(a.size) # est 3 # est 3 aussi

Noubliez pas que le nombre dlments dun tableau Ruby nest pas xe. Un tableau crot dynamiquement si on lui affecte un nouvel lment la n :
a[3] = morpheus

Maintenant, le tableau compte quatre lments. Si lon ajoute un lment au tableau au-del de sa longueur courante, Ruby cre automatiquement les lments intermdiaires et leur attribue la valeur nil. Donc, le rsultat du code suivant
a[6] = keymaker puts(a[4]) puts(a[5]) puts(a[6])

36

Patterns et Ruby

est
nil nil keymaker

Loprateur << fournit un moyen simple dajouter un lment la n dun tableau :


a << mouse

Le principe du typage dynamique de Ruby sapplique galement aux tableaux. En Ruby, les tableaux ne sont pas limits un seul type dlment. Dans un seul tableau nous pouvons mlanger volont diffrents types dobjets :
mixed = [alice, 44, 62.1234, nil, true, false]

Enn, vu que les tableaux sont des objets normaux1, ils bncient dune riche palette de mthodes. Par exemple, on peut les trier par ordre ascendant :
a = [77, 10, 120, 3] a.sort # renvoie [3, 10, 77, 120]

ou les inverser :
a = [1, 2, 3] a.reverse # renvoie [3, 2, 1]

Il est important de savoir que les mthodes sort et reverse ne modient pas le tableau original, elles retournent une nouvelle instance trie. Si vous avez besoin de trier le tableau original, optez pour les mthodes sort! et reverse! :
a = [77, 10, 120, 3] a .sort! # a est maintenant [3, 10, 77, 120] a .reverse! # a est maintenant [120, 77, 10, 3]

La convention de nommage spciant quune mthode ne modie pas lobjet original tandis quune mthode! opre sur lobjet original nest pas limite aux tableaux. Elle est frquemment (mais malheureusement pas encore universellement) employe dans le langage Ruby.

Tableaux associatifs
Un tableau associatif Ruby est un ami intime dun tableau. On peut considrer les tableaux associatifs comme des tableaux qui acceptent tout objet en tant que cl de
1. Nous savons que les tableaux sont des objets Ruby puisque (tous ensemble !) en Ruby tout est objet.

Chapitre 2

Dmarrer avec Ruby

37

hachage. Mais, contrairement aux tableaux, les tableaux associatifs ne sont pas ordonns. On cre un tableau associatif avec une paire daccolades :
h = {} h[first_name] = Albert h[last_name] = Einstein h[first_name] # est Albert h[last_name] # est Einstein

Il existe une syntaxe raccourcie pour initialiser un tableau associatif. Le mme tableau associatif peut tre dni de manire suivante :
h = {first_name => Albert, last_name => Einstein}

Les symboles font dexcellentes cls de hachage. Ainsi, lexemple prcdent peut tre amlior ainsi :
h = {:first_name => Albert,:last_name => Einstein}

Expressions rgulires
Le dernier type Ruby que nous allons tudier est lexpression rgulire. Une expression rgulire en Ruby est place entre deux barres obliques :
/old/ /Russ|Russell/ /.*/

Les expressions rgulires permettent de faire des choses extrmement complexes, mais les principes de base de leur fonctionnement sont vraiment trs simples. Mme des connaissances supercielles en la matire vous aideront normment 1. En deux lignes, une expression rgulire est un modle qui correspond ou pas une chane de caractres donne. Par exemple, la premire des trois expressions rgulires ci-dessus correspondra uniquement la chane old, tandis que la deuxime correspondra aux deux variantes de mon prnom. La troisime expression correspondra nimporte quelle chane de caractres. Loprateur =~ de Ruby permet de vrier quune expression donne correspond une chane de caractres particulire. Loprateur =~ retourne soit nil (si aucune correspondance nest dtecte) soit lindice du premier caractre de la chane pertinente si une correspondance est trouve :
1. Si vous faites partie des gens qui ont russi viter dapprendre les expressions rgulires, je me permets de vous inciter prendre le temps dexplorer cet outil extrmement pratique. Vous pouvez commencer avec des livres mentionns lannexe B, Aller plus loin.

38

Patterns et Ruby

/old/ =~ this old house # 5 - lindice de old /Russ Russell/ =~ Fred # nil - Fred nest pas Russ ni Russell /.*/ =~ any old string # 0 - cette expression correspond # toute chane de caractres

Il existe aussi !~, cet oprateur permet de vrier que lexpression rgulire ne correspond pas une chane de caractres donne.

Votre propre classe


Ruby ne serait pas un langage orient objet si vous ne pouviez pas crer vos propres classes :
class BankAccount def initialize( account_owner ) @owner = account_owner @balance = 0 end def deposit( amount ) @balance = @balance + amount end def withdraw( amount ) @balance = @balance - amount end end

videmment, la syntaxe de dnition des classes en Ruby est aussi dpouille et laconique que le reste du langage. La dnition dune classe commence par le mot cl class suivi par le nom de la classe :
class BankAccount

Souvenez-vous quen Ruby les noms de constantes commencent toujours par une lettre majuscule. Selon la philosophie Ruby, le nom dune classe est donc une constante. Cela parat logique, car le nom dune classe fait toujours rfrence la mme chose, la classe. Par consquent, tous les noms de classes en Ruby doivent commencer par une lettre majuscule, ce qui explique le fait que notre classe sappelle BankAccount avec un "B" majuscule. La seule rgle absolue est que le nom de classe doit commencer par une lettre majuscule, mais il faut noter que les programmeurs Ruby nomment typiquement leurs classes en utilisant la casse mixte comme en Java. La premire mthode de notre classe BankAccount est la mthode initialize :
def initialize( account_owner ) @owner = account_owner @balance = 0 end

Chapitre 2

Dmarrer avec Ruby

39

La mthode initialize est la fois ordinaire et particulire. Elle est ordinaire dans la faon dont elle est construite : la ligne de dclaration de mthode dbute avec le mot cl def suivi du nom de la mthode et de la liste des arguments si elle est prsente. Notre mthode initialize prend ici un seul argument, account_owner. Ensuite vient le corps de la mthode ; dans ce cas particulier, ce sont deux instructions daffectation. La premire opration de notre mthode rcupre la valeur passe dans largument account_owner et laffecte ltrange variable @owner :
@owner = account_owner

Les noms commenant par @ dnotent des variables dinstance. Chaque instance de la classe BankAccount emportera avec elle sa propre copie de @owner. De mme, chaque instance de la classe BankAccount emportera sa propre copie de @balance initialise zro. Comme dhabitude, il ny a pas de dclaration des variables @owner et @balance, nous inventons leurs noms illico presto. Malgr le fait quinitialize soit dnie de la mme faon que toutes les autres mthodes, elle est particulire du fait de son nom. Ruby utilise la mthode initialize pour construire des nouveaux objets. Lorsque Ruby cre une nouvelle instance dune classe, la mthode initialize est appele an de prparer lobjet avant utilisation. Si lon ne dnit pas la mthode initialize dans sa classe, Ruby suivra la logique oriente objet : il remontera dans la hirarchie de classes jusqu ce quil trouve la mthode initialize ou quil arrive la classe Object. La classe Object dnit la mthode initialize (qui ne fait rien), ce qui garantit que la recherche sachvera ce niveau. En substance, initialize est un constructeur la faon Ruby. Pour crer une nouvelle instance de notre classe, nous appelons la mthode new. Cette mthode doit utiliser les mmes paramtres que la mthode initialize :
my_account = BankAccount.new(Russ)

Cette instruction alloue un nouvel objet BankAccount, appelle sa mthode initialize avec les arguments passs par new et affecte cette nouvelle instance de BankAccount la variable my_account. Notre classe BankAccount dispose de deux autres mthodes, deposit et withdraw, qui augmentent et rduisent respectivement le solde du compte. Mais comment accder la position du compte ?

40

Patterns et Ruby

Accs aux variables dinstance


Notre classe BankAccount semble prte tre utilise, mais il reste un problme : en Ruby les variables dinstance dun objet ne sont pas accessibles de lextrieur. Si on crait un objet BankAccount et quon essayait dobtenir la valeur de @balance, on aurait une surprise dsagrable. Lexcution de ce code
my_account = BankAccount.new (russ) puts(my_account.balance)

provoque lerreur
account.rb:8: undefined method balance ... (NoMethodError)

De mme, my_account.@balance ne fonctionne pas non plus. Les variables dinstance des objets Ruby ne sont tout simplement pas accessibles de lextrieur. Alors, que faire ? Nous pouvons dnir un accesseur :
def balance @balance end

Il faut noter quil manque une instruction de retour notre mthode balance. En labsence dune instruction de retour explicite, une mthode retourne la valeur de la dernire expression value, ce qui correspond dans notre cas @balance. Notre balance est dsormais accessible :
puts(my_account.balance)

Le fait domettre les parenthses autour de la liste des arguments vide nous donne lagrable impression daccder la valeur au lieu dappeler une mthode. On pourrait avoir besoin de modier la valeur de balance. La solution vidente serait dajouter BankAccount un mutateur :
def set_balance(new_balance) @balance = new_balance end

Le code ayant une rfrence vers une instance de BankAccount pourrait ensuite modier la valeur de balance :
my_account.set_balance(100)

Le problme, avec la mthode set_balance, cest sa laideur. Ce serait beaucoup plus clair si lon pouvait crire :
my_account.balance = 100

Chapitre 2

Dmarrer avec Ruby

41

Heureusement, cest possible. Lorsque Ruby reoit une telle expression daffectation, il la traduira en un bon vieil appel de mthode. Le nom de la mthode sera le nom de la variable suivi par le signe gal. La mthode aura un seul paramtre : la valeur de la partie droite de linstruction daffectation. Laffectation ci-dessus sera donc traduite en lappel de mthode suivant :
my_account.balance=(100)

Regardez bien le nom de cette mthode. Non, ce nest pas une syntaxe spcique, le nom de la mthode nit vraiment par le signe gal. Pour que tout cela fonctionne avec notre objet BankAccount, il suft de renommer le mutateur :
def balance=(new_balance) @balance = new_balance end

Maintenant, notre classe a bonne mine vue de lextrieur : le code utilisant BankAccount peut affecter et rcuprer la valeur de balance volont, sans se proccuper du fait quen ralit il appelle les mthodes balance et balance=. Malheureusement, lintrieur notre classe est lgrement verbeuse. Il semble que nous soyons condamns voir toutes ces mthodes ennuyeuses encombrer notre dnition de classe. Il nen est rien. Ruby va nouveau venir notre secours. Il savre que les accesseurs et mutateurs sont tellement frquents que Ruby fournit un raccourci formidable pour les crer. Au lieu dcrire chaque defname, etc., nous pouvons simplement ajouter une ligne notre classe :
attr_accessor:balance

Cette expression cre une mthode qui sappelle balance et qui ne fait rien dautre que retourner la valeur de @balance. Elle cre galement le mutateur balance=(new_value). Il est mme possible de crer des accesseurs multiples en une instruction :
attr_accessor:balance,:grace,:agility

Le code ci-dessus ajoute pas moins de six nouvelles mthodes sa classe dappartenance : ce sont les accesseurs et mutateurs pour chacune des trois variables dinstance 1. Des accesseurs en un clin dil.
1. Il existe ici une subtilit de terminologie : les variables commenant par le signe arobase lintrieur de la classe sont des variables dinstance. Lorsque vous crez des accesseurs et des mutateurs, elles deviennent des attributs de lobjet, do les noms attr_reader et attr_writer. En pratique, ces points de dtail de la terminologie ne dsorientent personne, donc on ny prte pas beaucoup dattention.

42

Patterns et Ruby

De la mme manire, si le monde extrieur doit pouvoir lire vos variables dinstance mais pas les modier, il vous suft de remplacer attr_accessor par attr_reader :
attr_reader:name

Dsormais, votre classe expose un accesseur, mais pas de mutateur. De mme, attr_writer cre seulement le mutateur, name= (new_value).

Un objet demande : qui suis-je ?


Parfois, une mthode a besoin dobtenir une rfrence vers lobjet courant, linstance laquelle la mthode appartient. Dans ce cas, on peut utiliser self, qui est toujours la rfrence vers lobjet courant :
class SelfCentered def talk_about_me puts("Hello I am #{self}") end end conceited = SelfCentered.new conceited.talk_about_me

Le rsultat de lexcution de ce code ressemble


Hello I am #<SelfCentered:0x40228348>

videmment, il est peu probable que votre instance de SelfCentered rside la mme adresse hexadcimale que la mienne, donc, votre rsultat sera lgrement diffrent.

Hritage, classes lles et classes mres


Ruby supporte lhritage simple, toutes les classes que lon cre ne peuvent avoir quun seul parent ou classe-mre. Si la classe parent nest pas spcie, votre classe devient automatiquement une classe lle de Object. Si vous voulez choisir une classe mre autre que Object, vous devez le spcier aprs le nom de la classe :
class InterestBearingAccount < BankAccount def initialize(owner, rate) @owner = owner @balance = 0 @rate = rate end def deposit_interest @balance += @rate * @balance end end

Chapitre 2

Dmarrer avec Ruby

43

Regardez bien la mthode initialize de InterestBearingAccount. Tout comme la mthode initialize de BankAccount, elle remplit les variables dinstance @owner et @balance ainsi que la nouvelle variable dinstance @rate. Le point cl est la correspondance des variables dinstance @owner et @balance de InterestBearingAccount avec celles de la classe BankAccount. En Ruby, une instance dobjet ne possde quun seul ensemble de variables dinstance visibles travers tout son arbre hirarchique. Si sur un coup de folie on dcidait de crer une classe lle de BankAccount et puis sa classe lle, etc. jusqu faire quarante classes et quarante classes lles, il ny aurait quune seule instance de @owner par instance de classe. Un aspect regrettable de notre classe InterestBearingAccount est le fait que sa mthode initialize affecte des valeurs des champs @owner et @balance, crant un doublon de la mthode initialize de BankAccount. Nous pouvons rendre le code plus propre en appelant la mthode initialize de la classe Account partir de la mthode initialize de InterestBearingAccount :
def initialize(owner, rate) super(owner) @rate = rate end

Notre nouvelle mthode initialize remplace le code dupliqu par lappel super. Lorsquune mthode appelle super, elle cherche appeler une mthode avec le mme nom dans sa classe mre. Leffet de lappel super dans la mthode initialize est dappeler la mthode initialize de la classe BankAccount. Si la mthode avec le mme nom nexiste pas dans la classe mre, Ruby continue remonter larbre de lhritage jusqu ce quil trouve la mthode ou arrive la n de la hirarchie. Dans le deuxime cas, une erreur surviendra. Contrairement beaucoup de langages orients objet, Ruby nassure pas automatiquement lappel des mthodes initialize dans toutes vos classes mres. Dans ce sens, Ruby considre initialize comme une mthode ordinaire. Si la mthode initialize de InterestBearingAccount nappelait pas super, la version dinitialize dans BankAccount ne serait jamais appele par InterestBearingAccount.

Options pour les listes darguments


Jusquici, les mthodes que nous avons cres pour dcorer nos classes fournissaient des listes darguments assez ordinaires. Il savre que Ruby fournit un grand nombre doptions pour les arguments de mthode. Par exemple, il est possible de spcier des valeurs par dfaut pour nos arguments :

44

Patterns et Ruby

def create_car( model, convertible=false) # ... end

On peut appeler create_car soit avec deux arguments soit avec un seul, dans le dernier cas la variable convertible reoit la valeur false par dfaut. Toutes les expressions suivantes sont des idiomes valides pour crer une classe Car :
create_car(sedan) create_car(sports car, true) create_car(minivan, false)

Si vous crivez des mthodes comme dans lexemple prcdent, les arguments qui acceptent des valeurs par dfaut doivent se trouver la n de la liste darguments. La possibilit de dnir des valeurs par dfaut donne beaucoup de exibilit mais, parfois, il est commode davoir encore plus de libert. Dans ce cas, on peut crer des mthodes acceptant un nombre arbitraire darguments :
def add_students(*names) for student in names puts("adding student #{student}") end end add_students( "Fred Smith", "Bob Tanner" )

Excutez le code ci-dessus et vous obtiendrez le rsultat suivant :


adding student Fred Smith adding student Bob Tanner

La mthode add_students fonctionne car les arguments sont emballs dans un tableau, cest ce quindique lastrisque. On peut mme mlanger les arguments ordinaires avec les tableaux darguments, du moment que le tableau est plac la n de la liste :
def describe_hero(name, *super_powers) puts("Name: #{name}") for power in super_powers puts("Super power: #{power}") end end

La mthode prcdente requiert au moins un argument, mais elle acceptera autant darguments que vous le souhaitez. Tous les appels ci-aprs sont donc valides :
describe_hero("Batman") describe_hero("Flash", "speed") describe_hero("Superman" , "can fly", "x-ray vision", "invulnerable")

Chapitre 2

Dmarrer avec Ruby

45

Modules
En complment des classes, Ruby fournit un deuxime moyen dencapsuler du code : les modules. Tout comme une classe, un module est un lot de mthodes et de constantes. Contrairement une classe, on ne peut jamais crer une instance dun module. Nanmoins, un module peut tre inclus dans une classe qui rcuprera ainsi les mthodes et constantes du module pour les rendre disponibles aux instances de la classe. Si vous tes un programmeur Java, vous pouvez considrer les modules comme des interfaces qui contiennent des fragments du code dimplmentation. Une dnition de module ressemble singulirement une dnition de classe. Voici lexemple dun module comprenant une mthode :
module HelloModule def say_hello puts(Hello out there.) end end

Une fois la mthode dnie, nous pouvons limporter dans nos classes avec linstruction include1 :
class TryIt include HelloModule end

Linstruction include a pour effet de rendre toutes les mthodes de module disponibles pour les instances de classe :
tryit = TryIt.new tryit.say_hello

Laccessibilit est rciproque : une fois le module inclus dans la classe, toutes les mthodes et les variables dinstance de la classe deviennent disponibles pour les mthodes du module. Par exemple, le module suivant comprend une mthode qui afche diffrentes informations concernant lobjet dans lequel le module se trouve. Il rcupre ces valeurs en appelant les mthodes name, title et department exposes par la classe hte :
module Chatty def say_hi puts("Hello, my name is #{name}") puts("My job title is #{title}") puts("I work in the #{department} department") end end

1. Vous dcouvrirez un autre moyen dutiliser les modules au Chapitre 12 : on peut simplement appeler leurs mthodes directement, sans les inclure dans des classes.

46

Patterns et Ruby

class Employee include Chatty def name Fred end def title Janitor end def department Maintenance end end

Le rsultat de lexcution est :


Hello, my name is Fred My job title is Janitor I work in the Maintenance department

Lorsque le module est inclus dans votre classe, il devient une sorte de classe secrte et particulire de sa classe mre (voir Figure 2.1). Mais, bien quune classe ne puisse avoir quune seule classe mre, elle peut inclure un nombre illimit de modules.
Figure 2.1
Un module import dans une classe
Object

Chatty say_hi()

Employee name() title() department

Lorsquune mthode est appele sur une instance de classe, Ruby vrie tout dabord si cette mthode est dnie dans cette classe mme. Si cest le cas, la mthode est appele. Par exemple, lorsque vous appelez la mthode name sur une instance de Employee, Ruby commence par chercher si la mthode est disponible dans la classe Employee et fait appel elle. Si la mthode nest pas disponible directement dans la classe, Ruby continue sa recherche au sein des modules inclus dans la classe. Par exemple, si vous appelez la mthode say_hi, Ruby, sil ne la trouve pas dans la classe Employee, continuera de chercher dans les modules inclus par Employee. Si plusieurs modules sont

Chapitre 2

Dmarrer avec Ruby

47

inclus dans la classe, Ruby effectue la recherche dans les modules en commenant par le dernier module inclus. Ici, notre classe Employee ninclut quun module ; Ruby trouvera et appellera la mthode say_hi du module Chatty. Si Ruby ne trouvait pas la mthode dans la classe Employee ou dans lun de ses modules, il continuerait la recherche dans la classe mre de la classe Employee puis dans ses modules. Les modules utiliss de la faon dcrite ci-dessus sont appels des mixins, ils sont l pour tre "mixs" ou mlangs avec des classes pour leur adjoindre leurs mthodes. Conceptuellement, les mixins ressemblent aux interfaces Java et C#. Tout comme une interface, un module permet des classes similaires de partager un ensemble de mthodes communes. La diffrence rside dans le fait quune interface reste compltement abstraite elle ne fournit aucune implmentation tandis quun module est fourni complet, avec limplmentation.

Exceptions
La plupart des langages actuels ont une facilit pour grer les malheurs de linformatique, qui parfois affectent mme le code le plus respectable. Ruby nest pas une exception. Lorsquun ennui survient dans un programme, linterprteur Ruby arrte lexcution et lve une exception. Lexception remonte la pile dappels comme une bulle dair jusqu ce que Ruby rencontre du code charg de grer lexception ou jusqu ce quil arrive la n de la pile. Dans le dernier cas, Ruby termine votre programme. On peut attraper les exceptions laide de la paire dinstructions begin/ rescue :
begin quotient =1/0 # Boom! rescue puts(Something bad happened ) end

Ruby intercepte toute exception pouvant se produire entre les instructions begin et rescue et rend immdiatement la main au code qui se trouve aprs linstruction rescue. Vous pouvez prciser les types derreurs que vous voulez grer en ajoutant la liste des classes dexception dans linstruction rescue :
begin quotient =1/0 # Boom! rescue ZeroDivisionError puts(You tried to divide by zero) end

48

Patterns et Ruby

Si vous vous retrouvez la source du problme plutt que dans le rle dune victime, vous pouvez lancer votre propre exception avec linstruction raise :
if denominator == 0 raise ZeroDivisionError end return numerator / denominator

Ruby fournit plusieurs raccourcis bien pratiques pour lancer des exceptions. Si votre instruction raise appelle une classe dexception, comme nous lavons fait dans lexemple prcdent, Ruby cre commodment une nouvelle instance de cette classe et lutilise en tant quexception. Inversement, lorsque vous dnissez une instruction raise avec une chane de caractres, Ruby instancie la classe RuntimeException et utilise la chane comme message incorpor dans cette exception :
irb(main):001:0> raise You did it wrong RuntimeError: You did it wrong

Threads
linstar de nombreux langages rcents, Ruby a un systme incorpor de ls dexcution ou threads. Les threads permettent vos programmes de faire plusieurs choses la fois1. Crer des threads en Ruby est simple : le constructeur de la classe Thread accepte un bloc qui devient le corps du thread. Lexcution du thread commence au moment de sa cration et continue jusqu la n du bloc. Voici un exemple de deux threads qui calculent la somme et le produit des dix premiers entiers :
threadl = Thread.new do sum=0 1.upto(10) {|x| sum = sum + x} puts("The sum of the first 10 integers is #{sum}") end thread2 = Thread.new do product=1 1.upto(10) {|x| product = product * x} puts("The product of the first 10 integers is #{product}") end thread1.join thread2.join

1. Si vous travaillez sur un systme monoprocesseur, les threads provoquent seulement lillusion de faire plusieurs choses la fois. En ralit, le systme avance lgrement sur une tche avant de passer la main la tche suivante. Mais les choses se passent si rapidement quil est la plupart du temps impossible de sapercevoir de la diffrence.

Chapitre 2

Dmarrer avec Ruby

49

Vous pouvez attendre la n de lexcution dun thread en utilisant la mthode join :


thread1.join thread2.join

Bien que le code multitche puisse tre trs puissant, il est aussi trs dangereux. Permettre deux threads ou plus de modier la mme structure de donnes simultanment est en gnral un moyen redoutable dintroduire des bugs trs difciles trouver. Une bonne solution pour viter cela, ainsi que dautres situations de concurrence, et protger votre code contre laccs simultan est dutiliser la classe Monitor :
@monitor = Monitor.new @monitor.synchronize do # Accd par un thread la fois... end

Grer des chiers de code source spars


Les exemples de programmation que nous avons utiliss prsentent cet avantage dtre sufsamment courts pour tre logs dans un seul chier source. Malheureusement, la plupart des applications relles deviennent trop grandes pour un seul chier. La rponse logique consiste diviser le systme en plusieurs chiers contenant des fragments de code grables. Ds que votre systme est divis en plusieurs chiers, vous vous retrouvez face au problme de chargement de tous ces chiers. Selon le langage utilis, ce problme est rsolu de faons diffrentes. Java, par exemple, possde un systme complexe de chargement automatique de classes lorsque le programme en a besoin. Lapproche Ruby est diffrente. Les programmes Ruby doivent explicitement charger les chiers dont ils dpendent. Par exemple, si votre classe BankAccount est dnie dans account.rb et quelle doit tre utilise par la classe Portfolio, qui rside dans portfolio.rb, vous devez vous assurer que BankAccount est charg avant que Portfolio ne commence lutiliser. Cette tche est accomplie laide de linstruction require :
require account.rb class Portfolio # Uses BankAccount end

Linstruction require charge le contenu dun chier dans linterprteur Ruby. Cette instruction est assez intelligente : elle ajoute le sufxe .rb automatiquement. Les programmeurs Ruby criront donc plus simplement :
require account

50

Patterns et Ruby

Linstruction require retient galement les noms des chiers qui sont dj chargs et ne charge pas le mme chier deux fois. Si le chargement dun chier est demand plusieurs fois dans votre code, cela ne pose aucun problme. Puisque require gre si bien les chiers charger, les programmeurs demandent habituellement le chargement de tous les chiers ncessaires au dbut de chaque chier Ruby sans sinquiter des classes qui ont dj t charges par un autre chier. Tout ceci sapplique non seulement aux chiers que vous produisez, mais aussi aux chiers livrs avec la bibliothque standard de Ruby. Par exemple, si vous devez analyser des URL, vous pouvez simplement charger la classe URI fournie avec Ruby :
require uri yahoo = URI.parse(http://www.yahoo.com)

Une dernire prcision sur la saga require concerne les gemmes RubyGems. RubyGems est un systme de paquetage qui permet aux dveloppeurs de publier des bibliothques et applications Ruby sous la forme de paquets pratiques et faciles installer. Si vous voulez utiliser une bibliothque provenant dune gemme, par exemple la gemme nomme runt1, il faut commencer par inclure RubyGems :
require rubygems require runt

En conclusion
De hello_world aux modules et require, ce chapitre tait un cours de Ruby express. Fort heureusement, de nombreux lments de base de Ruby les nombres, les chanes de caractres et les variables sont relativement standard. Les spcicits du langage telles que les constantes qui ne sont pas trs constantes et le fait que zro soit true ne sont pas bouleversantes. ce stade, aprs avoir jet un il sur les bases du langage, nous arrivons dj avoir un aperu des raisons qui font de Ruby un langage trs agrable. La syntaxe est concise, mais pas cryptique. Tout ce que lon trouve dans un programme de la chane abc au nombre 42 en passant par des tableaux est objet. Dans les chapitres qui suivent, nous verrons des design patterns (motifs de conception) et nous comprendrons comment Ruby nous donne la possibilit dexprimer des choses extrmement puissantes de manire claire et laconique.

1. Au Chapitre 15, nous en apprendrons plus sur runt, qui est une bibliothque consacre la gestion du temps et des tches planies.

Partie II
Patterns en Ruby

3
Varier un algorithme avec le pattern Template Method
Imaginez que vous ayez un fragment de code complexe : un algorithme compliqu, un enchevtrement de code mtier ou un fragment sufsamment difcile pour vouloir lcrire une seule fois, rajouter des tests unitaires et le laisser en paix. Le problme est quen plein milieu de votre code complexe se trouve une partie qui doit tre variable. Parfois, elle est excute dune faon et parfois dune autre. Pire encore, vous tes presque certain que dans le futur cette partie devra prendre des responsabilits supplmentaires. Vous tes face au vieux problme "comment se protger contre les changements" que nous avons examin au Chapitre 1. Que faire ? Pour concrtiser davantage le scnario, imaginez que votre premier vrai projet Ruby consiste crire un gnrateur de rapports, cest--dire un programme qui produira des rapports davancement mensuels. Les rapports doivent tre joliment mis en forme en HTML et vous crivez donc quelque chose comme ceci :
class Report def initialize @title = Monthly Report @text = [ Things are going, really, really well. ] end def output_report puts(<html>) puts( <head>) puts(" <title>#{@title}</title>") puts( </head>) puts( <body>) @text.each do |line puts(" <p>#{line}</p>" ) end

54

Patterns en Ruby

puts( </body>) puts(</html>) end end

Il est clair que nous avons pris quelques liberts avec ce code pour lui conserver une certaine simplicit. Dans la vraie vie, le rapport ne serait pas cod en dur dans la classe, et nous ninsrerions pas du texte arbitraire dans un chier HTML sans vrier la prsence de balises "<" et ">". Ceci dit, le code prcdent prsente quelques bonnes caractristiques. Il est simple, facile utiliser et il produit effectivement du HTML :
report = Report.new report.output_report

Si vous devez juste gnrer du HTML basique, ce code, ou du code semblable, est tout ce dont vous avez besoin.

Faire face aux ds de la vie


Malheureusement, mme si les choses sont simples au dbut, elles le demeurent rarement. Quelques mois aprs avoir ni le chef-duvre de programmation prcdent, vous recevez une nouvelle demande : lobjet qui gre le formatage doit produire du texte simple en plus du HTML. Et avant la n de lanne on aura probablement besoin dun format PostScript et peut-tre RTF. Parfois, les solutions les plus simples sont les meilleures, donc vous abordez le problme de la faon la plus bte possible :
class Report def initialize @title = Monthly Report @text = [Things are going, really, really well.] end def output_report(format) if format ==:plain puts("*** #{@title} ***") elsif format ==:html puts ( <html> ) puts( <head>) puts(" <title>#{@title}</title>") puts( </head>) puts( <body>) else raise "Unknown format: #{format}" end @text.each do |line| if format ==:plain puts(line) else

Chapitre 3

Varier un algorithme avec le pattern Template Method

55

puts(" <p>#{line}</p>" ) end end if format ==:html puts( </body>) puts(</html>) end end end

Beurk ! Cette deuxime version fonctionne assurment, mais cest un vritable bazar. Le code traitant du texte simple est mlang avec le code qui concerne le HTML. Pire encore, lorsque vous ajouterez de nouveaux formats (il ne faut pas oublier la demande pour PostScript qui vient de surgir), vous serez oblig de retravailler la classe Report pour intgrer chaque nouveaut. Vu la faon dont le code est construit actuellement, avec lajout de chaque nouveau format vous courez le risque de perturber le bon fonctionnement des formats existants. Bref, notre premire tentative dajouter un nouveau format de sortie est en violation dun des principes fondamentaux des design patterns : elle mlange le code variable avec le code permanent.

Sparer les choses qui restent identiques


Un moyen de sortir de lembarras consiste remanier ce dsastre pour sparer le code qui gre les formats diffrents. La cl est de se rendre compte que, quel que soit le format le texte simple, le HTML ou le futur PostScript , le ux dexcution de Report demeure le mme : 1. Afcher len-tte en fonction du format spcique. 2. Afcher le titre. 3. Afcher chaque ligne du compte rendu. 4. Afcher les balises fermantes requises par un format donn. En gardant cette squence lesprit, retournons aux premires leons de la programmation oriente objet : dnir une classe abstraite avec des mthodes pour grer les tapes numres ci-dessus et laisser les dtails de chaque tape aux classes lles. En utilisant cette approche nous nous retrouvons avec une classe lle qui correspond chaque format de sortie. Voici notre nouvelle classe abstraite Report :
class Report def initialize @title = Monthly Report @text = [Things are going, really, really well.] end

56

Patterns en Ruby

def output_report output_start output_head output_body_start output_body output_body_end output_end end def output_body @text.each do |line| output_line(line) end end def output_start raise Called abstract method: output_start end def output_head raise Called abstract method: output_head end def output_body_start raise Called abstract method: output_body_start end def output_line(line) raise Called abstract method: output_line end def output_body_end raise Called abstract method: output_body_end end def output_end raise Called abstract method: output_end end end

Certes, la classe Report nest pas compltement abstraite. Nous pouvons thoriser sur les mthodes et classes abstraites, mais en vrit Ruby ne supporte aucune des deux. Lide des mthodes et des classes statiques nest pas tout fait compatible avec le mode de vie simple et dynamique de Ruby. Le mieux que lon puisse faire est de dclencher des exceptions dans le cas o un utilisateur essaie dappeler une de nos mthodes "abstraites". Une fois notre nouvelle implmentation de Report termine, nous pouvons dnir des classes lles de Report pour chacun des deux formats. Voici la classe qui traite le HTML :
class HTMLReport < Report def output_start puts(<html>) end

Chapitre 3

Varier un algorithme avec le pattern Template Method

57

def output_head puts( <head>) puts(" <title>#{@title}</title>") puts( </head>) end def output_body_start puts(<body>) end def output_line(line) puts(" <p>#{line}</p>") end def output_body_end puts(</body>) end def output_end puts(</html>) end end

Et voici la version qui traite le texte simple :


class PlainTextReport < Report def output_start end def output_head puts("**** #{@title} ****") puts end def output_body_start end def output_line(line) puts(line) end def output_body_end end def output_end end end

Lusage de nos nouvelles classes est simple :


report = HTMLReport.new report.output_report report = PlainTextReport.new report.output_report

Choisir le format est facile, il suft de slectionner la classe de formatage correspondant.

58

Patterns en Ruby

Dcouvrir le pattern Template Method


Flicitations ! Vous venez de redcouvrir ce qui constitue probablement le plus simple des patterns rpertoris par le GoF, le pattern Template Method. Comme vous le voyez la Figure 3.1, lide gnrale du pattern Template Method consiste construire une classe parent abstraite avec un squelette de mthode. Ce squelette (autrement appel template method) actionne le traitement variable en faisant appel aux mthodes abstraites, dont limplmentation est fournie par des classes lles. Les diffrences de traitement sont conditionnes par la slection des classes lles concrtes. Dans notre exemple, le plan de base comprend toutes les tapes ncessaires pour gnrer un rapport : afcher linformation de len-tte, le titre du rapport puis chaque ligne du texte. Dans ce cas, les implmentations de mthodes dans les classes lles se chargent dcrire le rapport dans le bon format : soit du texte simple soit du HTML. Si lon russit concevoir les tches correctement, on arrivera une sparation des parties statiques (lalgorithme principal exprim dans template method) et des parties variables (les dtails fournis par les classes lles). Les classes HTMLReport et PlainTextReport paraissent incompltes, cest une caractristique quelles partagent avec toutes les classes concrtes bien crites qui suivent le pattern Template Method. En tant que bonnes classes concrtes, HTMLReport et PlainTextReport surchargent toutes les deux des mthodes abstraites telles que output_line. Leur apparence fragmentaire provient du fait quelles ne surchargent pas la mthode cl template method output_report. Selon le modle Template Method, la classe abstraite contrle le traitement de haut niveau laide du pattern template method, les classes lles remplissant simplement les dtails.
Figure 3.1
Diagramme de classes du pattern Template Method
AbstractClass template_method() operation1() operation2() operation3() def template_method operation1() operation2() operation3() end

ConcreteClass1 operation1() operation2() operation3()

ConcreteClass2 operation1() operation2() operation3()

Chapitre 3

Varier un algorithme avec le pattern Template Method

59

Mthodes daccrochage
Si vous relisez PlainTextReport, vous remarquerez que cette classe surcharge les mthodes output_start et output_end ainsi que les mthodes qui concernent le corps du rapport, mais ces mthodes ne comprennent pas de code dimplmentation. Ceci parat raisonnable : contrairement au document HTML, un document en texte simple ne ncessite ni en-tte ni balise fermante. Cependant, il ny a aucune raison de dnir toutes ces mthodes dans une classe comme PlainTextReport, qui nen a pas besoin. Il est plus judicieux de fournir une implmentation par dfaut de ces mthodes dans la classe parent Report an que les classes lles puissent y avoir accs :
class Report def initialize @title = Monthly Report @text = [Things are going, really, really well.] end def output_report output_start output_head output_body_start @text.each do |line| output_line(line) end output_body_end output_end end def output_start end def output_head raise Called abstract method: output_head end def output_body_start end def output_line(line) raise Called abstract method: output_line end def output_body_end end def output_end end end

Les mthodes non abstraites qui peuvent tre surcharges par des classes concrtes du pattern Template Method sappellent des hooks (ou point daccroche). Une mthode

60

Patterns en Ruby

daccrochage (hook) permet des classes concrtes soit de surcharger limplmentation de base pour faire du traitement diffrenci, soit daccepter limplmentation par dfaut. Souvent, les classes mres dnissent des hooks dans le but de tenir les sousclasses concrtes au courant de ce qui se passe dans le code. Lorsque la classe Report appelle output_start, cela signie pour les sous-classes que "nous sommes prts pour afcher le rapport, si vous avez des choses faire, cest le moment de le faire". Les implmentations par dfaut de ces hooks informatifs sont frquemment vides. Leur raison dtre est dinformer les sous-classes de lavancement de lexcution sans les obliger surcharger des mthodes qui nont pour eux aucun intrt. Parfois, limplmentation par dfaut dun hook peut contenir du code. Dans notre exemple de la classe Report, nous avons une possibilit de grer le titre comme sil sagissait dune simple ligne de texte :
class Report def initialize @title = Monthly Report @text = [Things are going, really, really well.] end def output_report output_start output_head output_body_start @text.each do |line| output_line(line) end output_body_end output_end end def output_start end def output_head output_line(@title) end def output_body_start end def output_line(line) raise Called abstract method: output_line end def output_body_end end def output_end end end

Chapitre 3

Varier un algorithme avec le pattern Template Method

61

Mais o sont passes toutes les dclarations ?


Vu que ce chapitre dcrit notre premier patron de conception Ruby, cela vaut la peine de prendre un moment pour parler des types et de la scurit de type en Ruby. Si vous arrivez du monde des langages typage statique, vous vous demandez probablement comment notre classe Report et ses sous-classes peuvent tenir la route sans pratiquement effectuer aucune dclaration de type. Vous avez remarqu que nous ne dnissons nulle part dans la classe Report que @title est une chane de caractres et @text, un tableau de chanes de caractres. Suivant la mme logique, lorsque le code client cre un nouvel HTMLReport, nous ne prcisons jamais que la variable report contient une rfrence vers une instance de HTMLReport ou de Report ; nous crivons simplement :
report = HTMLReport.new

Ruby est dynamiquement typ, ce qui signie quaucune vrication nest faite pour sassurer quun objet particulier possde un anctre dun type particulier. La seule chose qui importe, cest que lobjet implmente les mthodes appeles par ses clients. Dans lexemple prcdent, la classe Report sattend ce que lobjet @text se comporte comme un tableau de chanes de caractres. Si @text ressemble un tableau de chanes de caractres savoir que lon peut en extraire sa troisime chane de caractres en appelant @text[2] , le type est considr comme correct quelle que soit sa classe relle. Lapproche du typage "je suis ce que je fais" est connu sous le nom "duck typing" o "typage la canard". Le nom provient dun vieux principe qui consiste dire que "si un objet ressemble un canard et quil couine comme un canard, alors, ce doit tre un canard". Une autre faon de voir le sujet est de considrer le fonctionnement du typage statique comme celui de laristocratie : les langages statiquement typs vous posent sans arrt des questions sur votre parent ou grand-parent ou, dans le cas des interfaces la Java, sur vos tantes et vos oncles. Dans les langages typage statique, larbre de hirarchie dun objet a un rle trs important, tandis que les langages dynamiquement typs sont des mritocraties. Ils sintressent aux mthodes que possde la classe plutt qu ses origines. Les langages dynamiquement typs ne se renseignent que rarement sur les anctres dun objet. Ils disent plutt : "Peu importe qui sont tes parents. Je veux seulement savoir ce que tu sais faire1."

Types, scurit et exibilit


Les gens qui sont habitus programmer avec des langages statiquement typs se demandent souvent comment tout cela peut fonctionner. Vous avez probablement
1. mon avis, ils le disent avec un accent dun conducteur de taxi new-yorkais.

62

Patterns en Ruby

limpression que le typage " la canard", si libre et si simple, mnera invitablement une catastrophe, que les programmes sarrteront brutalement de fonctionner force dessayer de formater du HTML sur un objet de type connexion une base de donnes ou bien de tenter de demander au nombre 42 de gnrer un rapport mensuel. Aussi tonnant que cela puisse paratre, des problmes de typage aussi atroces narrivent que rarement. Vous pouvez trouver des preuves de cette robustesse l o lon naurait jamais pens la trouver : dans le monde des programmes Java. La plupart des programmes Java crits avant larrive de Java 1.5 (ce qui couvre la majorit des programmes Java existants) utilisent des conteneurs du package java.util tels que HashMap et ArrayList. Les versions de ces conteneurs antrieures Java 1.5 nassuraient aucune scurit de types, et mme aprs larrive de la version 1.5 Java continue supporter les versions sans scurit de type pour des raisons de compatibilit ascendante. Malgr cette attitude cavalire envers la scurit des types, la plupart des programmes Java ne mlangent pas leurs objets socket avec des objets Employee et ne partent pas en vrille en essayant daugmenter le salaire dune connexion rseau. Les langages typage statique sont tellement omniprsents aujourdhui que lon se pose rarement la question cl : quel est le cot du typage statique ? Ma rponse est que le cot est trs lev. En terme deffort de programmation et dencombrement du code, le typage statique cote une fortune. Regardez un programme en Java ou C# et comptez le nombre dinstructions consacres la dclaration de variables et paramtres. Rajoutez la majorit des dnitions dinterfaces. Noubliez pas les agaantes instructions cast, lorsque vous essayez de convaincre le systme que cest vraiment un objet String. Rajoutez un bonus pour chaque dclaration gnrique complexe. Tout ce code trs encombrant nest pas gratuit1. Et il ne sagit pas seulement deffort de programmation. Bien quil soit cach, le cot du typage statique est bien rel : il remonte le couplage de votre systme un niveau bien plus lev que ncessaire. Voyez la mthode Java isEmpty() :
public boolean isEmpty(String s) { return s.length() == 0; }

1. Pour tre impartial, il me faut prciser que le typage statique est trs coteux en terme dencombrement de code cause de limplmentation des langages largement rpandus aujourdhui. Nanmoins, il existe un certain nombre de langages moins connus, tels quOCaml et Scala, qui grent le typage statique avec beaucoup moins de bruit.

Chapitre 3

Varier un algorithme avec le pattern Template Method

63

Et maintenant son jumeau en Ruby :


def empty?(s) s.length == 0 end

Au premier abord, les mthodes sont quivalentes. Souvenez-vous que la version Java ne fonctionne quavec les arguments de type java.lang.String. La mthode Ruby est sre de fonctionner avec des chanes de caractres, mais elle fonctionnera tout aussi bien avec des tableaux, des queues, des collections et des tableaux associatifs. En fait, la mthode empty? en Ruby fonctionnera avec tout argument disposant de la mthode length. Elle se moque du type exact de largument, et cest bien mieux ainsi. Compars au typage dynamique, ces arguments peuvent ne pas paratre intuitifs pour une personne habitue au typage statique. Si vous tes habitu au typage statique et au fait de tout dclarer, la construction des grands systmes ables peut vous sembler irraliste sans la vrication de type. Mais cest possible et il y a deux exemples vidents pour dmontrer cette possibilit. Ruby on Rails est de loin la preuve la plus agrante que lcriture de code able est possible avec un langage dynamiquement typ. Rails est constitu de dizaines de milliers de lignes de code Ruby, et Rails est extrmement stable. Si Rails ne vous convainc pas, pensez un autre grand corps de code Ruby qui est utilis tous les jours : la bibliothque standard livre avec Ruby. Cette bibliothque compte plus de 100 000 lignes crites en Ruby. Il est presque impossible dcrire un programme en Ruby sans faire appel cette bibliothque, et elle fonctionne. Lexistence de Ruby on Rails et de la bibliothque standard Ruby est la preuve quil est possible dcrire des grands volumes de code able avec un langage dynamiquement typ. La crainte que le typage dynamique engendre un code chaotique est largement infonde. Il arrive effectivement que des programmes Ruby sarrtent occasionnellement cause des problmes de types, mais la frquence de ces checs est sans rapport avec les efforts dploys par les langages statiquement typs pour viter une possibilit derreur aussi faible. Est-ce que cela signie que les langages dynamiquement typs sont meilleurs et que nous devrions compltement abandonner les langages typage statique ? Le dbat sur la question continue encore. Tout comme pour les autres questions relatives au gnie logiciel, il existe deux cts la mdaille. Le typage statique vaut la peine dans des grands systmes complexes construits par des quipes immenses. Mais le typage dynamique prsente un nombre davantages signicatifs : la taille des programmes en Ruby est bien moindre par rapport la taille de ses quivalents statiquement typs. Comme

64

Patterns en Ruby

nous lavons vu dans lexemple de la mthode empty? et le verrons encore dans les chapitres venir, le typage dynamique offre une norme exibilit. Si cela vous parat fou, laissez-moi vous guider dans la suite de ce livre et donnez sa chance cette folie dynamiquement type. Il nest pas impossible que vous soyez agrablement surpris.

Les tests unitaires ne sont pas facultatifs


Une des faons daccrotre vos chances dtre agrablement surpris consiste crire des tests unitaires. Quel que soit votre langage de travail, Java, C# ou Ruby, vous devez crire des tests unitaires. La deuxime blague la plus ancienne1 de la programmation est "a se compile donc a doit fonctionner". Effectuer des tests est primordial quel que soit le langage, mais dans les langages dynamiques tels que Ruby ils sont critiques. Ruby ne dispose pas dun compilateur pour effectuer une premire passe de vrication et vous donner une fausse impression de scurit. Le seul moyen de savoir si le programme fonctionne, cest dexcuter les tests. La bonne nouvelle est que les mmes tests pourront la fois dmontrer que votre code fonctionne et vous aider liminer les ventuels problmes de typage. Une autre bonne nouvelle : si vous savez utiliser JUnit, NUnit ou une autre bibliothque XUnit, alors, vous savez dj crire des tests unitaires en Ruby. Par exemple, la classe suivante vrie notre mthode empty? :
require test/unit require empty class EmptyTest < Test::Unit::TestCase def setup @empty_string = @one_char_string = X @long_string = The rain in Spain @empty_array = [] @one_element_array = [1] @long_array = [1, 2, 3, 4, 5, 6] end def test_empty_on_strings assert empty?(@empty_string) assert! empty?(@one_char_string) assert! empty?(@long_string) end

1. La blague la plus ancienne est le papillon de nuit mort, coll dans le journal derreurs.

Chapitre 3

Varier un algorithme avec le pattern Template Method

65

def test_empty_on_arrays assert empty?(@empty_array) assert! empty?(@one_element_array) assert! empty?(@long_array) end end

Test::Unit, dle ses racines XUnit, excute chaque mthode dont le nom commence par test en tant que test. Si votre classe de test a une mthode setup (cest le cas de la classe prcdente), cette mthode est excute avant chaque mthode de test. Si votre classe possde une mthode teardown (la classe prcdente ne la pas), elle est excute aprs chaque mthode de test. Test::Unit est quip de toute une collection de mthodes dassertion. Vous pouvez vrier quune valeur est true, ou appeler assert_equal pour sassurer de lgalit de deux objets. Si vous voulez tre sr que lobjet nest pas vide, utilisez assert_not_ nil. Lexcution des tests unitaires est trs facile. Si le test ci-dessus est plac dans le chier string_test.rb, on peut le dmarrer en excutant le chier comme un programme Ruby :
$ ruby empty_test.rb Loaded suite empty_test Started .. Finished in 0.000708 seconds. 2 tests, 6 assertions, 0 failures, 0 errors

Il est gratiant (et terriblement scurisant) de voir un test se terminer sans se plaindre.

User et abuser du pattern Template Method


Comme il est possible dcrire une implmentation able du pattern Template Method en Ruby, reste savoir comment nous y prendre. Procder par itrations est la meilleure tactique : commencez par une variante du code et continuez coder comme si vous aviez un seul problme rsoudre. Dans notre exemple, on pourrait commencer par le HTML :
class Report def initialize @title = MonthlyReport @text = [Things are going, really, really well.] end

66

Patterns en Ruby

def output_report puts(<html>) puts( <head>) puts( <title>#{@title}</title>) # output the rest of the report ... end end

On pourrait ensuite refactoriser la mthode qui deviendra la mthode template an quelle fasse appel dautres mthodes qui fourniront les parties variables de lalgorithme. Restez toujours focalis sur un cas prcis :
class Report # ... def output_report output_start output_title(@title) output_body_start for line in @text output_line(line) end output_body_end output_end end def output_start puts(<html>) end def output_title(title) puts( <head>) puts(" <title>#{title}</title>") puts( </head>) end # ... end

Enn, on pourrait crer une sous-classe pour le premier cas et placer toutes les implmentations spciques dedans. Maintenant, vous tes prt pour commencer coder le reste des variations. Comme mentionn au Chapitre 1, la pire erreur que vous puissiez faire est de pousser le bouchon trop loin en essayant de prvoir toutes les possibilits imaginables. Le pattern Template Method est optimal lorsquil est cod de faon concise chaque mthode abstraite et chaque hook doivent avoir une raison dexister. vitez de crer une classe template qui oblige ses sous-classes surcharger dinnombrables mthodes obscures dans une tentative dsespre de couvrir tous les cas de gure. Il est galement dconseill de crer une classe template incruste dune multitude de hooks que jamais personne ne surchargera.

Chapitre 3

Varier un algorithme avec le pattern Template Method

67

Les Templates dans le monde rel


On peut trouver un exemple classique du pattern Template Method dans WEBrick, une bibliothque lgre, crite compltement en Ruby, qui sert pour la cration des serveurs TCP/IP. La partie cl de WEBrick est la classe GenericServer, qui implmente tous les dtails de fonctionnement dun serveur rseau. GenericServer na videmment aucune ide de la faon dont vous voulez raliser votre serveur. Donc, pour utiliser GenericServer on ltend et on surcharge la mthode run :
require webrick class HelloServer < WEBrick::GenericServer def run(socket) socket.print(Hello TCP/IP world) end end

La mthode template incorpore dans GenericServer contient tout le code pour couter sur un port TCP/IP, accepter de nouvelles connexions et nettoyer le tout lorsque la connexion est interrompue. En plein milieu de tout ce code, prcisment au moment o une nouvelle connexion est cre, votre mthode run1 est appele. Il existe un autre exemple du pattern Template Method, qui est omniprsent au point den devenir difcilement visible. Pensez la mthode initialize, que nous utilisons pour construire nos objets. Tout ce que nous savons, cest que la mthode initialize est appele vers la n du processus de cration dobjets et que nous pouvons la surcharger dans nos classes pour implmenter une initialisation spcique. Cela ressemble fortement une mthode hook.

En conclusion
Dans ce chapitre, nous avons tudi en dtail notre premier design pattern, Template Method. Le pattern Template Method est simplement une faon prtentieuse de dire : si vous avez un algorithme variable, il peut tre implment en dplaant la partie statique dans une classe parent et en encapsulant les parties variables dans les mthodes dnies par un nombre de classes lles. La classe parent peut laisser les mthodes sans implmentation, dans ce cas les sous-classes sont obliges de fournir limplmentation.
1. Je sais tout cela parce que jai lu le code. Un des avantages des langages interprts est le fait que le code source de la bibliothque standard Ruby dort quelque part sur votre systme en attendant de vous apprendre un tas de choses. Je ne peux pas imaginer une meilleure faon dapprendre un nouveau langage de programmation que de lire du code qui fonctionne.

68

Patterns en Ruby

linverse, la classe parent peut dnir une implmentation par dfaut des mthodes que les sous-classes peuvent choisir de surcharger ou pas. tant donn que cest notre premier pattern, nous avons fait un dtour pour explorer lun des aspects les plus importants de la programmation en Ruby : le typage dynamique. Le typage la canard est un compromis : vous abandonnez la scurit de la vrication au moment de la compilation en change dune plus grande clart du code et dune exibilit dans la programmation. Nous verrons bientt que le pattern Template Method est une brique de base de la programmation oriente objet, elle-mme utilise par dautres patterns. Au Chapitre 13, par exemple, nous apprendrons que le pattern Factory Method nest quun template method destin crer de nouveaux objets. Le problme auquel rpond le pattern Template Method est galement assez rpandu. Au chapitre suivant, nous examinerons le pattern Strategy, qui propose une solution diffrente au mme problme. Cette solution ne sappuie pas sur lhritage de la mme manire que le pattern Template Method.

4
Remplacer un algorithme avec le pattern Strategy
Nous avons dmarr le chapitre prcdent avec la question : comment varier une partie de lalgorithme ? Comment permettre ltape 3 dans un processus de cinq tapes de faire parfois une chose et parfois une autre ? La rponse fournie au Chapitre 3 recourait au pattern Template Method : crer une classe parent avec une mthode Template, pour contrler le traitement gnral, et laisser les sous-classes soccuper des dtails. Si aujourdhui il nous faut un traitement particulier et demain un autre, nous dnissons alors deux sous-classes : une pour chaque variation. Malheureusement, le pattern Template Method prsente quelques dfauts dont la plupart proviennent de sa faon dutiliser lhritage. Comme nous lavons vu au Chapitre 1, les modles fonds sur lhritage ont un certain nombre dinconvnients importants. Quel que soit le soin que vous apportez la conception de votre code, les sous-classes seront troitement lies avec leurs classes parents, cest la nature mme de leur relation dhritage. Qui plus est, les techniques reposant sur lhritage, comme le pattern Template Method, limitent la exibilit au moment de lexcution. Une fois la variante de lalgorithme slectionne dans notre exemple, cest linstance de HTMLReport , il est difcile de changer davis. Pour modier le format de sortie, le pattern Template Method nous contraint crer un nouvel objet, par exemple PlainTextReport. Quelle est donc lalternative ?

Dlguer, dlguer et encore dlguer


Lalternative est de suivre le conseil du GoF mentionn au Chapitre 1 : prfrer la dlgation. Et si au lieu de crer une sous-classe pour chaque variante on extrayait le

70

Patterns en Ruby

fragment de code variable qui nous ennuie pour lencapsuler dans sa propre classe ? On pourrait dnir toute une famille de classes, une pour chaque variante. Voici notre code de formatage HTML de lexemple prcdent, transplant dans sa propre classe :
class Formatter def output_report( title, text ) raise Abstract method called end end class HTMLFormatter < Formatter def output_report( title, text ) puts(<html>) puts(<head>) puts(" <title>#{title}</title>") puts( </head>) puts( <body>) text.each do |line| puts(" <p>#{line}</p>" ) end puts( </body>) puts(</html>) end end

Notre classe de formatage du texte simple :


class PlainTextFormatter < Formatter def output_report(title, text) puts("***** #{title} *****") text.each do |line| puts(line) end end end

Maintenant que les dtails du formatage sont supprims de la classe Report, elle devient beaucoup plus simple :
class Report attr_reader:title,:text attr_accessor:formatter def initialize(formatter) @title = Monthly Report @text = [ Things are going, really, really, really well.] @formatter = formatter end def output_report @formatter.output_report( @title, @text ) end end

Chapitre 4

Remplacer un algorithme avec le pattern Strategy

71

Lutilisation de la classe Report est lgrement plus complique. Il faut maintenant fournir la classe Report un objet de formatage correspondant :
report = Report.new(HTMLFormatter.new) report.output_report

Les membres du GoF ont baptis la technique qui consiste extraire lalgorithme dans un objet spar du nom de pattern Strategy (voir Figure 4.1). Lide sous-jacente du pattern Strategy est de dnir une famille dobjets de stratgies ayant la mme mission. Dans notre exemple, la mission est le formatage du rapport. Non seulement les stratgies effectuent le mme travail, mais elles doivent prsenter exactement la mme interface. Dans notre exemple, les deux stratgies fournissent la mthode output_ report.
Figure 4.1
Le pattern Strategy
@strategy Context Strategy operation()

Strategy1 operation()

Strategy2 operation()

Puisque les stratgies ont une interface identique, lobjet utilisateur nomm par le GoF la classe de contexte peut les traiter comme des pices interchangeables. Peu importe quelle stratgie vous appelez, elles exposent la mme interface pour le monde extrieur et elles excutent la mme fonction. Mais il est important de choisir la bonne stratgie, car chacune delles fait le traitement diffremment. Dans notre exemple, lune des stratgies de formatage produit du HTML tandis que lautre afche du texte simple. Si lon faisait des calculs dimpts pour les rsidents des diffrents pays europens, on pourrait utiliser le pattern Strategy en fonction des rgles de calcul dimpts sur le revenu dans les diffrents tats : une stratgie pour calculer les impts des rsidents de la France et une autre pour lAllemagne. Le pattern Strategy possde de vrais avantages. Comme nous lavons vu dans lexemple du rapport, lextraction des stratgies dune classe permet datteindre une meilleure sparation des fonctionnalits. Avec le pattern Strategy nous librons la classe Report de toute responsabilit et de toute connaissance du format.

72

Patterns en Ruby

Le pattern Strategy est fond sur la composition et la dlgation plutt que lhritage, il est donc facile dalterner des stratgies au moment de lexcution. Il suft de remplacer lobjet stratgie :
report = Report.new(HTMLFormatter.new) report.output_report report.formatter = PlainTextFormatter.new report.output_report

Le pattern Strategy a une caractristique en commun avec le pattern Template Method. Les deux nous permettent de concentrer un seul endroit la dcision sur la variante de lalgorithme utiliser. Avec le pattern Template Method on prend la dcision lorsque lon slectionne une sous-classe concrte. Avec Strategy on choisit la classe stratgie au moment de lexcution.

Partager les donnes entre lobjet contexte et lobjet stratgie


Un atout signicatif du pattern Strategy est quun mur de donnes spare lgamment les objets contexte et stratgie, puisque leur code rside dans des classes diffrentes. La mauvaise nouvelle est que nous devons inventer un moyen de faire transiter travers ce mur les informations du contexte ncessaires la stratgie. Concrtement, nous avons deux options. Premirement, nous pouvons continuer avec lapproche adopte jusqualors : passer tout ce dont lobjet stratgie a besoin en argument lorsque lobjet contexte appelle des mthodes de la stratgie. Rappelez-vous notre exemple : lobjet Report passait toute linformation requise par le gestionnaire de format dans les arguments de la mthode output_report. Le passage des donnes a lavantage de garder le contexte et la stratgie parfaitement spars. Les stratgies exposent une interface, le contexte lutilise. Linconvnient de cette approche, cest que cela oblige transfrer beaucoup de donnes complexes entre le contexte et la stratgie sans aucune assurance quelles seront effectivement utilises. Deuximement, une stratgie peut rcuprer les donnes du contexte si lobjet contexte lui-mme est pass en argument par rfrence. Lobjet stratgie peut ensuite appeler les mthodes du contexte pour accder toutes les donnes ncessaires. Voici ce que nous pouvons faire dans notre exemple :
class Report attr_reader:title,:text attr_accessor:formatter def initialize(formatter) @title = Monthly Report @text = [Things are going, really, really well.]

Chapitre 4

Remplacer un algorithme avec le pattern Strategy

73

@formatter = formatter end def output_report @formatter.output_report(self) end end

Lobjet Report est pass par rfrence lobjet stratgie. Ensuite, la classe formatter appelle les mthodes title et text an de rcuprer les donnes ncessaires. Voici la classe HTMLFormatter refactorise pour accompagner cette classe Report qui se passe elle-mme en argument :
class Formatter def output_report(context) raise Abstract method called end end class HTMLFormatter < Formatter def output_report(context) puts(<html>) puts( <head>) puts(" <title>#{context.title}</title>") puts( </head>) puts( <body>) context.text.each do |line| puts(" <p>#{line}</p>") end puts( </body>) puts(</html>) end end

Alors que cette technique facilite la transmission de donnes entre le contexte et la stratgie, elle augmente galement le couplage entre les deux parties. Cela amplie le risque que les classes du contexte et les stratgies se mlangent.

Typage la canard une fois de plus


Le programme de formatage que nous avons crit rete lapproche adopte par le GoF concernant le pattern Strategy. Notre famille des stratgies de formatage est compose de la classe parent "abstraite" Formatter et des deux sous-classes HTMLFormatter et PlainTextFormatter. Toutefois, cette implmentation est "antiRuby". La classe Formatter ne sert rien, elle nexiste que pour dnir linterface commune pour les sous-classes de formatage. Il ny a pas danomalie au niveau du fonctionnement de cette approche, le programme fonctionne correctement. Nanmoins, ce type de code soppose la philosophie rubyesque du typage la canard. Les canards pourraient

74

Patterns en Ruby

afrmer (en couinant ?) que HtmlFormatter et PlainTextFormatter partagent dj une interface commune car tous deux implmentent la mthode output_report. Il ny a donc aucune raison de faire du sur-place et de crer une classe parent inutile. Nous pouvons nous dbarrasser de la classe Formatter en appuyant sur la touche "supprimer". Voici le code que lon obtient :
class Report attr_reader:title,:text attr_accessor:formatter def initialize(formatter) @title = Monthly Report @text = [Things are going, really, really well.] @formatter = formatter end def output_report() @formatter.output_report(self) end end class HTMLFormatter def output_report(context) puts(<html>) puts( <head>) # Imprimer le reste du rapport ... puts(" <title>#{context.title}</title>") puts( </head>) puts( <body>) context.text.each do |line| puts(" <p>#{line}</p>") end puts( </body>) puts(</html>) end end class PlainTextFormatter def output_report(context) puts("***** #{context.title} *****") context.text.each do |line| puts(line) end end end

Si nous comparons ce code celui de la version prcdente, nous verrons que la classe parent Formatter est limine sans provoquer de changements. Grce au typage dynamique nous obtenons toujours les rapports annonant que tout va bien. Malgr un fonctionnement correct des deux versions prsentes, le monde Ruby voterait sans ambigut pour la suppression de la classe Formatter.

Chapitre 4

Remplacer un algorithme avec le pattern Strategy

75

Procs et blocs
Il savre queffacer la classe parent nest pas la seule faon de refondre le pattern Strategy la Ruby. Mais avant de procder ltape suivante nous devons explorer un des aspects les plus intressants de Ruby : les blocs de code et lobjet Proc. En tant quutilisateur des langages de programmation orients objet, nous rchissons beaucoup sur des objets et comment ils interagissent. Mais notre vision des objets est quelque peu asymtrique. Nous pouvons facilement sparer les donnes dun objet on peut aisment extraire @text de lobjet report et lutiliser indpendamment de lobjet report. Pourtant, nous considrons que notre code est intimement li et insparable de lobjet auquel il est attach. videmment, ce nest pas la seule faon de fonctionner. Et si nous pouvions sortir des fragments de code et les passer comme sils taient des objets ? Ruby met notre disposition les moyens dy parvenir. Un Proc est un objet Ruby qui contient un fragment de code. La mthode lambda est la manire la plus courante de crer un Proc :
hello = lambda do puts(Hello) puts(I am inside a proc) end

En Ruby, le fragment de code se trouvant entre do et end se nomme un bloc1. La mthode lambda renvoie un objet Proc, qui est un conteneur pour le code entour de do et end. Notre variable hello est une rfrence vers lobjet Proc. Nous pouvons excuter le code incorpor dans lobjet Proc en appelant (quoi dautre ?) la mthode call. Si lon appelle la mthode call de notre objet Proc
hello.call

on obtient
Hello I am inside a proc

De faon extrmement utile, un objet Proc rcupre le contexte de son environnement dexcution au moment de sa cration. Toutes les variables visibles au moment de la cration dun Proc lui demeurent accessibles lexcution. Par exemple, il ny a quun seul nom de variable dans le code suivant :
name = John

1. Les autres noms de ce concept sont une fermeture lexicale et lambda, do le nom de notre mthode qui fabrique un Proc.

76

Patterns en Ruby

proc = Proc.new do name = Mary end proc.call puts(name)

lexcution, la variable name recevra la valeur "John" dans la premire instruction, ensuite, le Proc lui affectera la valeur "Mary". Enn, Ruby afchera "Mary". Si vous trouvez que do et end reprsentent trop de code taper, Ruby propose une syntaxe plus concise avec des accolades. Voici une faon plus rapide de crer le Proc "hello" :
hello = lambda { puts(Hello, I am inside a proc) }

Les programmeurs Ruby ont adopt une convention qui consiste utiliser do/end pour des blocs sur plusieurs lignes et des accolades pour ceux qui tiennent sur une ligne 1. Voici une version de notre exemple plus conforme la convention :
hello = lambda {puts(Hello, I am inside a proc)}

Les objets Proc ont beaucoup de points communs avec les mthodes. Par exemple, non seulement des objets Proc, tout comme des mthodes, sont des fragments de code, mais les deux peuvent retourner une valeur. Un Proc retourne toujours la dernire valeur value dans le bloc. Pour retourner une valeur partir dun objet Proc il faut donc sassurer quelle soit value par la dernire instruction de cet objet. La mthode call retransmet la valeur renvoye par un Proc. Par consquent, le code suivant
return_24 = lambda {24} puts(return_24.call)

afche
24

Vous pouvez galement dnir des paramtres passer votre objet Proc, bien que la syntaxe soit un peu trange. Au lieu dentourer la liste de paramtres des parenthses habituelles "()", on ouvre et ferme la liste par une barre verticale "|" :
multiply = lambda { |x, y| x * y}

1. En ralit, il existe une diffrence entre les oprateurs do/end et les accolades. Dans les instructions Ruby les accolades ont une priorit suprieure par rapport do/end, elles sont values avant le reste. Cette diffrence ne devient visible que lorsque lon commence omettre des parenthses optionnelles.

Chapitre 4

Remplacer un algorithme avec le pattern Strategy

77

Ce code dnit un objet Proc qui accepte deux paramtres, les multiplie et retourne le rsultat. Pour appeler un Proc avec des paramtres, on les passe simplement la mthode call :
n = multiply.call(20, 3) puts(n) n = multiply.call(10, 50) puts(n)

Lexcution de ce code afche le rsultat suivant :


60 500

La possibilit de placer des blocs de code diffrents endroits de vos programmes est tellement utile que Ruby dnit une syntaxe raccourcie spciale. Si lon veut passer un bloc dans une mthode, il suft de lajouter la n de lappel de cette mthode. La mthode pourra ensuite excuter le bloc laide du mot cl yield. Voici un exemple dune mthode qui afche un message, excute un bloc, puis afche un deuxime message :
def run_it puts("Before the yield") yield puts("After the yield") end

Le code ci-aprs appelle run_it. Remarquez que nous ajoutons simplement le bloc de code la n de lappel de mthode :
run_it do puts(Hello) puts(Coming to you from inside the block) end

Lorsque lon colle un bloc de code lappel de mthode, le bloc (qui est en ralit un objet Proc) est pass en tant quune sorte de paramtre invisible. Le mot cl yield excute ce paramtre. Par exemple, lexcution du code ci-dessus produit le rsultat suivant :
Before the yield Hello Coming to you from inside the block After the yield

Si le bloc pass la mthode accepte des paramtres, il faut les fournir lappel yield.

78

Patterns en Ruby

Par exemple, le code suivant


def run it with parameter puts(Before the yield) yield(24) puts(After the yield) end run_it_with_parameter do |x| puts(Hello from inside the proc) puts("The value of x is #{x}") end

afche
Before the yield Hello from inside the proc The value of x is 24 After the yield

Parfois, on a besoin de rendre le paramtre de type bloc explicite, cest--dire capturer le bloc pass la mthode sous forme dun objet Proc dans un vrai paramtre. Pour y parvenir, nous pouvons ajouter un paramtre spcial la n de notre liste darguments. Ce paramtre doit tre prcd par une esperluette. Sa valeur deviendra lobjet Proc cr partir du bloc de code que lon a pass lappel de mthode. Voici une version quivalente de la mthode run_it_with_parameter :
def run it with parameter(&block) puts(Before the call) block.call(24) puts(After the call) end

Lesperluette fonctionne galement lors de lutilisation inverse. Si lon a un objet Proc dans une variable et quil doive tre pass une mthode qui sattend recevoir un bloc de code, nous pouvons ajouter une esperluette devant lobjet Proc pour le convertir en un bloc :
my_proc = lambda {|x| puts("The value of x is #{x}")} run_it_with_parameter(&my_proc)

Stratgies rapides et pas trs propres


Quel est le rapport entre cette histoire de blocs et de Procs et le pattern Strategy ? Tout simplement, une stratgie est un fragment de code excutable qui sait accomplir une tche par exemple formater du texte et qui est encapsul dans un objet. Cette dnition doit vous rappeler quelque chose car elle correspond aussi celle dun Proc, un fragment de code incorpor dans un objet.

Chapitre 4

Remplacer un algorithme avec le pattern Strategy

79

Il est trivial de refondre notre programme de formatage pour quil se fonde sur la stratgie Proc. Ajouter une esperluette dans la mthode initialize, an quelle accepte un bloc de code en argument, et renommer la mthode output_report en call sont les seules modications apporter :
class Report attr_reader:title,:text attr_accessor:formatter def initialize(&formatter) @title = Monthly Report @text = [ Things are going, really, really well. ] @formatter = formatter end def output_report @formatter.call( self ) end end

Il y a un peu plus de travail pour construire les objets de formatage. Nous devons dsormais crer des objets Proc au lieu des instances de nos classes de formatage spciales :
HTML_FORMATTER = lambda do |context| puts ( <html> ) puts( <head>) puts(" <title>#{context.title}</title>") puts( </head>) puts( <body>) context.text.each do |line| puts(" <p>#{line}</p>" ) end puts( </body>) puts

Nous voil prts crer des rapports avec nos nouveaux objets de formatage fonds sur des Procs. Puisque nous avons un objet Proc et que le constructeur de la classe Report sattend recevoir un bloc de code, il faut ajouter une esperluette devant notre objet Proc lorsque lon instancie un nouveau Report :
report = Report.new &HTML_FORMATTER report.output_report

Pourquoi prendre la peine dadopter la stratgie fonde sur des Procs ? Premirement, nous ne sommes plus obligs de crer des classes spciales pour notre stratgie, il suft dencapsuler le code dans un objet Proc. Plus important encore, on peut maintenant crer une stratgie comme par magie en passant un bloc de code directement dans une mthode. Par exemple, voici notre code de traitement du texte simple remani pour devenir un bloc de code gnr la vole :

80

Patterns en Ruby

report = Report.new do |context| puts("***** #{context.title} *****") context.text.each do |line| puts(line) end end

Les blocs de code peuvent paratre bizarres si vous ny tes pas habitu. Il faut noter que ces blocs de code nous ont aids simplier grandement le pattern Strategy : nous avons condens une classe contexte, une classe parent de stratgie et quelques stratgies concrtes avec leurs instances associes en une classe contexte et quelques blocs de code. Est-ce que cela signie quil faut simplement oublier les stratgies fondes sur des classes ? Pas vraiment. Les stratgies en forme de blocs fonctionnent uniquement lorsque linterface de la stratgie est trs simple et quelle ne dispose que dune seule mthode. Aprs tout, la seule mthode que lon peut appeler sur un objet Proc est call. Si votre stratgie est plus sophistique, il ne faut pas hsiter dnir quelques classes. Mais si vos demandes peuvent tre satisfaites par une stratgie simple, un bloc de code est probablement la bonne solution.

User et abuser du pattern Strategy


La faon la plus simple de se tromper avec le pattern Strategy est de faire des erreurs dans linterface entre le contexte et la stratgie. Rappelez-vous que vous essayez de rcuprer une tche cohrente et plus ou moins autonome de lobjet contexte et de la dlguer la stratgie. Il faut bien veiller aux dtails de linterface entre le contexte et la stratgie ainsi qu leur couplage. Noubliez pas que le pattern Strategy ne vous sera pas utile si vous couplez le contexte avec la premire stratgie jusquau point de ne pas pouvoir insrer une deuxime ou troisime stratgie dans le systme.

Le pattern Strategy dans le monde rel


Lutilitaire rdoc, livr en standard avec votre distribution Ruby, contient quelques instances du pattern Strategy classique, fond sur des classes. Le but de rdoc est dextraire la documentation des programmes. part Ruby, rdoc est capable de traiter la documentation des programmes en C et (Dieu nous garde...) en FORTRAN. Lutilitaire rdoc utilise le pattern Strategy pour traiter chacun des langages de programmation : il contient un analyseur C, un analyseur Ruby et un analyseur FORTRAN. Chacun deux est une stratgie permettant de grer leur format dentre respectif. Lutilitaire rdoc vous offre galement un choix de formats de sortie. On peut gnrer la documentation en plusieurs variantes de HTML, XML ou en format utilis nativement

Chapitre 4

Remplacer un algorithme avec le pattern Strategy

81

par la commande Ruby ri. Vous avez probablement devin que chacun des formats de sortie est gr par sa propre stratgie. La relation entre les diffrentes stratgies de rdoc dmontre lattitude typique de Ruby envers lhritage. Cette relation est illustre la Figure 4.2.
Figure 4.2
Les gnrateurs de rdoc
HTMLGenerator RIGenerator

CHMGenerator

XMLGenerator

HTMLGeneratorInOne

Il existe quatre stratgies de sortie apparentes rdoc les appelle des gnrateurs et une stratgie indpendante (voir Figure 4.2). Les quatre stratgies apparentes gnrent des chiers similaires : du texte <stuff> entour des balises entre crochets </stuff>1. La dernire stratgie gnre les chiers de sortie pour la commande Ruby ri. Ce format ne ressemble ni du XML ni du HTML. Comme vous pouvez le voir dans le diagramme UML de la Figure 4.2, la relation entre les classes rete les choix de limplmentation : les classes qui grent le HTML, le CHM et le XML ont de par la nature de leurs formats beaucoup de code en commun et sont donc lies par lhritage. La classe RIGenerator produit quant elle un format de sortie totalement diffrent et na aucun lien avec la famille XML/HTML. Le code de rdoc ne cre donc pas de classe parent commune pour imposer la mme interface tous les gnrateurs. Chaque gnrateur implmente les bonnes mthodes et cest tout. Nous avons sous la main un bon exemple dun objet Proc utilis en tant que stratgie lgre. Cest notre bon vieux tableau. Si lon veut trier le contenu dun tableau Ruby, on appelle simplement la mthode sort :
a = [russell, mike, john, dan, rob] a.sort

Par dfaut, la mthode sort trie le tableau dans lordre "naturel" de ses lments. Et si nous voulions dnir un algorithme de tri diffrent ? Par exemple, si nous voulions trier
1. CHM est un type de HTML utilis par les chiers daide Microsoft. Notez que cest le code rdoc et pas moi qui suggre que XML est une sorte de HTML.

82

Patterns en Ruby

en fonction de la longueur des chanes de caractres ? Nous passons simplement une stratgie de comparaison en forme de bloc de code :
a.sort { |a,b| a.length <=> b.length }

La mthode sort appellera votre bloc lorsquelle devra comparer deux lments dun tableau. Le bloc doit retourner 1 si le premier lment est suprieur, 0 si les deux sont gaux et -1 si cest le second lment qui est suprieur. Cest exactement ce que fait loprateur <=> et ce nest pas une concidence.

En conclusion
Le pattern Strategy est un modle fond sur la dlgation. Son but est de proposer une solution au mme problme que celui rsolu par le pattern Template Method. Au lieu dextraire des parties variables de votre algorithme et de les encapsuler dans des sousclasses, vous implmentez chaque version de lalgorithme dans un objet spar. Ensuite, varier lalgorithme consiste fournir les diffrents objets stratgie lobjet contexte par exemple une stratgie pour produire du HTML, une autre pour gnrer des chiers PDF ; ou une stratgie pour calculer des impts en France et une autre pour des impts en Italie. Nous avons plusieurs options pour rcuprer les donnes ncessaires dans lobjet contexte et les transmettre lobjet stratgie. On peut passer toutes les donnes en paramtre lorsque lon appelle les mthodes de lobjet stratgie, ou simplement passer la stratgie une rfrence vers lobjet contexte entier. Les blocs de code Ruby, qui reprsentent essentiellement des fragments de code encapsuls dans un objet Proc, sont merveilleusement utiles pour crer rapidement des objets stratgie simples. Comme nous allons le voir dans les chapitres suivants, le pattern Strategy ressemble, au moins au premier regard, plusieurs autres patterns. Par exemple, dans le pattern Strategy nous avons un objet le contexte qui essaie daccomplir une tche. Pour y arriver nous devons lui fournir un deuxime objet la stratgie qui aide faire le traitement ncessaire. Vu de manire supercielle, le pattern Observer semble fonctionner de faon similaire : un objet fait un travail, mais au cours de lexcution il fait appel un deuxime objet que nous devons fournir. La diffrence entre les deux patterns est dans la nature de lintention. Le but du pattern Strategy est de donner au contexte laccs un objet qui est au courant dune variation de lalgorithme. Le but du pattern Observer est trs diffrent : lintention est... mais nous devrions peut-tre laisser cette distinction pour un autre chapitre, le suivant.

5
Rester inform avec le pattern Observer
Un des ds les plus ardus de la conception rside dans le dveloppement dun systme compltement coordonn, cest--dire un systme dans lequel chaque partie reste informe de ltat de lensemble du systme. Pensez un logiciel de type tableur : modier la valeur dune cellule naffecte pas seulement cette valeur particulire mais elle est aussi rpercute dans les totaux de la colonne, modie la hauteur dune des barres de lhistogramme et active le bouton "enregistrer". Un exemple encore plus simple : un systme de gestion de ressources humaines doit notier le dpartement comptabilit lorsque le salaire dune personne est modi. Construire ce type de systme est assez difcile. Ajoutez-y limpratif de garder le systme maintenable et la tche devient vraiment complique. Comment coordonner les composants indpendants dun grand systme logiciel sans augmenter le couplage entre les classes au point de rendre lensemble totalement ingrable ? Comment dvelopper un tableur qui afche des changements de donnes correctement sans coder en dur le lien entre le code ddition et le constructeur dhistogrammes ? Comment permettre lobjet Employee de passer les nouvelles concernant les changements de son salaire sans mlanger son code avec le composant de comptabilit ?

Rester inform
Une faon dapprocher le problme consiste se concentrer sur le fait que la cellule du tableau ainsi que lobjet Employee sont tous les deux lorigine dune notication. Fred est augment et son enregistrement Employee annonce au monde entier (ou au moins tous ceux qui sont intresss) : "Bonjour ! Il y a eu du changement ici !" Tout

84

Patterns en Ruby

objet qui sintresse ltat des nances de Fred doit stre enregistr auprs de lobjet Employee au pralable. Tout objet enregistr recevra en temps opportun les notications concernant les uctuations des gains de Fred. Comment exprimer tout ceci dans le code ? Voici la version de base de lobjet Employee. Il ne contient pas de code pour ractualiser dautres objets, pour linstant il soccupe uniquement de tenir jour les informations dun salari :
class Employee attr_reader:name attr_accessor:title,:salary def initialize( name, title, salary ) @name = name @title = title @salary = salary end end

Puisque le champ salary est modiable grce son accesseur attr_accessor, nos salaris peuvent tre augments1 :
fred = Employee.new("Fred Flintstone", "Crane Operator", 30000.0) # Augmenter Fred fred.salary = 35000.0

Ajoutons du code (assez naf) pour tenir le dpartement comptabilit inform des changements du salaire :
class Payroll def update( changed_employee ) puts("Cut a new check for #{changed_employee.name}!") puts("His salary is now #{changed_employee.salary}!") end end class Employee attr_reader:name,:title attr_reader:salary def initialize( name, title, salary, payroll) @name = name @title = title @salary = salary @payroll = payroll end

1. Le salaire de nos employs peut aussi tre rduit, mais nous allons fermer les yeux sur cette possibilit dsagrable.

Chapitre 5

Rester inform avec le pattern Observer

85

def salary=(new_salary) @salary = new_salary @payroll.update(self) end end

Nous pouvons maintenant changer la paie de Fred :


payroll = Payroll.new fred = Employee.new(Fred, Crane Operator, 30000, payroll) fred.salary = 35000

La comptabilit le saura aussitt :


Cut a new check for Fred! His salary is now 35000!

Puisque le dpartement comptabilit doit tre inform des changements du salaire, on ne peut pas utiliser la mthode attr_accessor pour le champ salary. Nous devons crire la main la mthode salary=.

Une meilleure faon de rester inform


Il y a un problme avec ce programme : le lien pour informer la comptabilit des changements du salaire est cod en dur. Et si nous devions tenir dautres objets par exemple dautres classes de la comptabilit au courant des nances de Fred ? La faon dont le code est conu actuellement nous obligerait modier la classe Employee nouveau, ce qui serait regrettable, car dans cette classe rien na rellement chang. Ce sont les autres classes celle de la comptabilit qui provoquent des changements dans Employee. Force est de constater que notre classe Employee semble tre trs peu rsistante aux changements. Nous devrions peut-tre prendre du recul et rsoudre ce problme de notications de manire plus gnrique. Comment peut-on sparer les parties variables les objets qui doivent apprendre la nouvelle sur les changements du salaire du fonctionnement interne de lobjet Employee ? Il semble que nous ayons besoin de btir un tableau des objets intresss par les dernires nouvelles de lobjet Employee. On pourrait crer ce tableau dans la mthode initialize :
def initialize( name, title, salary ) @name = name @title = title @salary = salary @observers = [] end

86

Patterns en Ruby

Nous avons aussi besoin du code qui informera tous les observateurs ds quun changement a lieu :
def salary=(new_salary) @salary = new_salary notify_observers end def notify_observers @observers.each do |observer| observer.update(self) end end

La partie variable la plus importante de notify_observers est observer.update (self). Ce fragment de code appelle la mthode update sur chaque observateur1 en lui annonant que quelque chose le salaire dans ce cas prcis a chang dans lobjet Employee. Il ne nous reste qu crire les mthodes pour ajouter et supprimer des observateurs de lobjet Employee :
def add_observer(observer) @observers << observer end def delete_observer(observer) @observers.delete(observer) end

Dsormais, tout objet qui sintresse aux changements du salaire de Fred peut senregistrer en tant quobservateur de lobjet Employee de Fred :
fred = Employee.new(Fred, Crane Operator, 30000.0) payroll = Payroll.new fred.add_observer( payroll ) fred.salary = 35000.0

Ce mcanisme nous permet de supprimer le couplage implicite entre la classe Employee et lobjet Payroll. Lobjet Employee ne se proccupe plus du nombre dobjets intresss par ses changements de salaire, il passe linformation tout objet qui sest dclar concern. Lobjet Employee fonctionnerait tout aussi bien avec un seul, plusieurs ou aucun observateur.

1. Rappelez-vous que array.each est la terminologie Ruby pour exprimer une boucle qui parcourt tous les lments dun tableau. Nous en apprendrons plus sur array.each au Chapitre 7.

Chapitre 5

Rester inform avec le pattern Observer

87

class TaxMan def update( changed_employee ) puts("Send #{changed_employee.name} a new tax bill!") end end tax_man = TaxMan.new fred.add_observer(tax_man)

Supposons que le salaire de Fred soit modi une fois de plus :


fred.salary = 90000.0

Maintenant, le dpartement comptabilit ainsi que linspection des impts seront tous les deux au courant :
Cut a new check for Fred! His salary is now 90000.0! Send Fred a new tax bill!

Les membres du GoF ont appel cette ide de construire une interface propre entre une source dinformation et ses consommateurs le pattern Observer (voir Figure 5.1). Ils ont nomm la classe gnratrice de linformation la classe sujet. Dans notre exemple, Employee est le sujet. Les observateurs sont des objets qui souhaitent recevoir linformation. Dans notre exemple, il y en a deux : Payroll et TaxMan. Lorsquun objet doit rester inform de ltat du sujet, il senregistre en tant quobservateur de ce sujet.
Figure 5.1
Le pattern Observer
Subject @observers[] add_observer(o) remove_observer(o) notify_observers() Employee update(subject)

Observer update(subject)

Il ma toujours sembl que le pattern Observer tait mal nomm. Lobjet observateur sattribue tout le mrite alors que le sujet effectue la plupart du travail. La responsabilit de conserver le suivi des observateurs choit au sujet. Le sujet est galement charg de tenir les observateurs au courant des changements qui se produisent. Dit autrement, il est bien plus difcile de publier et de distribuer un journal que de le lire.

88

Patterns en Ruby

Factoriser le code de gestion du sujet


Gnralement, limplmentation du pattern Observer en Ruby nest pas plus complique que notre exemple avec Employee : il suft davoir un tableau pour stocker les observateurs, une paire de mthodes pour grer le tableau et la mthode pour notier tout le monde lors des changements. Mais il y a srement un moyen de mieux faire et de ne pas rpter ce code chaque fois que lon souhaite rendre un objet "observable". On pourrait utiliser lhritage. Le rsultat de la factorisation du code pour grer le sujet est une petite classe parent fonctionnelle :
class Subject def initialize @observers=[] end def add_observer(observer) @observers << observer end def delete_observer(observer) @observers.delete(observer) end def notify_observers @observers.each do |observer| observer.update(self) end end end

Maintenant, Employee peut devenir une sous-classe de Subject :


class Employee < Subject attr_reader:name,:address attr_reader:salary def initialize( name, title, salary) super() @name = name @title = title @salary = salary end def salary=(new_salary) @salary = new_salary notify_observers end end

Cest une solution assez raisonnable. Dailleurs, Java a choisi daller prcisment dans cette voie avec sa classe java.util.Observable. Mais, comme nous lavons vu au Chapitre 1, lhritage peut tre une source de tracas. Le problme, avec lusage de

Chapitre 5

Rester inform avec le pattern Observer

89

Subject en tant que classe parent, cest limpossibilit davoir une classe parent diffrente. Ruby permet chaque classe davoir une classe mre unique. Lorsque la classe Employee choisit dtendre Subject, il ne lui reste aucune autre option. Si votre modle dobjets requiert quEmployee soit une sous-classe de DatabaseObject ou OrganizationalUnit, pas de chance, elle ne pourra plus devenir une sous-classe de Subject. Parfois, nous avons besoin de partager du code entre des classes totalement indpendantes et cela peut poser un problme. Notre classe Employee voudrait tre Subject, mais il est possible que des cellules du tableur souhaitent ltre aussi. Comment partager limplmentation de Subject sans gaspiller le droit davoir une classe parent ? La solution ce dilemme est dutiliser un module. Souvenez-vous quun module est un conteneur de mthodes et de constantes que lon peut partager entre des classes sans recourir lutilisation de la seule et unique classe mre. Aprs sa refonte en module notre classe Subject nest gure diffrente :
module Subject def initialize @observers=[] end def add_observer(observer) @observers << observer end def delete_observer(observer) @observers.delete(observer) end def notify_observers @observers.each do |observer| observer.update(self) end end end

Notre nouveau module Subject est trs simple dutilisation. On inclut le module et on appelle notify_observers lors des changements :
class Employee include Subject attr_reader:name,:address attr_reader:salary def initialize( name, title, salary) super() @name = name @title = title @salary = salary end

90

Patterns en Ruby

def salary=(new_salary) @salary = new_salary notify_observers end end

Linclusion du module Subject rend disponibles toutes ses mthodes la classe Employee : elle est dsormais prte agir comme un sujet du pattern Observer. Remarquez que nous devons appeler super() dans la mthode initialize de la classe Employee, ce qui a pour effet dappeler initialize du module Subject1. Dvelopper notre propre module Subject tait amusant et constituait un bon entranement lcriture des modules mixin. Mais est-ce que son utilisation est vraiment justie ? Probablement pas. La bibliothque standard Ruby comprend en effet un formidable module Observable qui fournit tous les outils ncessaires pour transformer vos objets en sujets du pattern Observer. Son usage ne diffre pas beaucoup de notre module Subject :
require observer class Employee include Observable attr_reader:name,:address attr_reader:salary def initialize( name, title, salary) @name = name @title = title @salary = salary end def salary=(new_salary) @salary = new_salary changed notify_observers(self) end end

Le module standard Observable propose une fonctionnalit que nous avons oublie dans notre version artisanale. Ce module vous oblige appeler la mthode changed avant dappeler notify_observers an de rduire le nombre de notications redondantes. La mthode changed affecte une variable boolenne pour indiquer quun changement a rellement eu lieu ; la mthode notify_observers nenvoie pas de
1. Lappel vers super() est un des rares cas en Ruby o les parenthses autour dune liste darguments vide sont obligatoires. Lappel effectu avec des parenthses, comme nous le faisons dans lexemple, provoque un appel sans arguments vers la mthode dans la classe parent. Si lon omet les parenthses, la classe parent sera appele avec la liste initiale des arguments, dans ce cas name, title, salary et payroll_manager.

Chapitre 5

Rester inform avec le pattern Observer

91

notications tant que la valeur de la variable nest pas true. Chaque appel notify_ observers rinitialise cette variable avec la valeur false.

Des blocs de code comme observateurs


Les blocs de code Ruby vont une fois de plus permettre une variation intressante sur le thme du pattern Observer. Le code est simpli de manire signicative lorsquon utilise un bloc comme couteur. tant donn que la classe Observable fournie avec la bibliothque Ruby ne supporte pas les blocs de code, une version lgrement modie de notre module Subject pourrait nalement nous tre utile :
module Subject def initialize @observers=[] end def add_observer(&observer) @observers << observer end def delete_observer(observer) @observers.delete(observer) end def notify_observers @observers.each do |observer| observer.call(self) end end end class Employee include Subject attr_accessor:name,:title,:salary def initialize( name, title, salary ) super() @name = name @title = title @salary = salary end def salary=(new_salary) @salary = new_salary notify_observers end end

Lavantage des blocs rside dans la simplicit du code car il nest plus ncessaire de mettre en place une classe spare pour nos observateurs. Pour en ajouter un, nous appelons simplement add_observer en lui passant un bloc de code :

92

Patterns en Ruby

fred = Employee.new(Fred, Crane Operator, 30000) fred.add_observer do |changed_employee| puts("Cut a new check for #{changed_employee.name}!") puts("His salary is now #{changed_employee.salary}!") end

Cet exemple passe un observateur sous la forme dun bloc de deux lignes lobjet Employee. Avant darriver dans lobjet Employee les deux lignes sont converties en un objet Proc, qui est prt jouer le rle dobservateur. Lorsque le salaire de Fred change, lobjet Employee appelle la mthode call de lobjet Proc, ce qui excute les deux commandes puts.

Variantes du pattern Observer


Les dcisions cls prendre lorsque lon implmente le pattern Observer concernent linterface entre le sujet et lobservateur. Pour rester simple, on peut adopter lapproche de notre exemple ci-dessus : dnir dans lobservateur une seule mthode qui prend un argument unique, le sujet. Les membres du GoF ont nomm cette stratgie la mthode pull, car les observateurs doivent demander au sujet tous les dtails sur ses changements. Une autre possibilit logiquement nomme la mthode push consiste envoyer les dtails des changements du sujet vers les observateurs :
observer.update(self,:salary_changed, old_salary, new_salary)

Nous pouvons mme dnir des mthodes de mise jour diffrentes pour diffrents vnements. Par exemple, nous pourrions disposer dune mthode pour dclencher les notications des changements du salaire
observer.update_salary(self, old_salary, new_salary)

et dune autre pour les changements dintitul


observer.update_title(self, old_title, new_title)

Lavantage de ce surcrot de dtails rside dans la diminution de la charge de travail ncessaire pour garder les observateurs informs des changements. Le dfaut du modle push tient au fait que tous les observateurs ne sont pas forcment concerns par tous les dtails et, dans ce cas, leffort dploy pour passer linformation est inutile.

User et abuser du pattern Observer


Le problme majeur du pattern Observer tourne autour des choix de la frquence et du moment choisis pour envoyer les notications. Parfois, le volume mme des notications peut devenir une difcult. Par exemple, un observateur pourrait senregistrer

Chapitre 5

Rester inform avec le pattern Observer

93

avec un objet sans se rendre compte que cet objet lance des milliers de notications par seconde. La classe sujet peut rduire ce risque en vitant de diffuser des mises jour redondantes. Une mise jour dun objet ne signie pas forcment quune vraie modication sest produite. Souvenez-vous de la mthode salary= de lobjet Employee. On ne devrait probablement pas notier les observateurs si aucun changement na eu lieu :
def salary=(new_salary) old_salary = @salary @salary = new_salary if old_salary!= new_salary changed notify_observers(self) end end

La cohrence du sujet lorsquil informe ses observateurs est un autre problme potentiel. Imaginez que nous compltions notre classe dexemple an quelle notie ses observateurs des changements de lintitul du poste dun employ ainsi que de son salaire :
def title=(new_title) old_title = @title @title = new_title if old_title!= new_title changed = true notify_observers(self) end end

Maintenant, imaginez que Fred reoive une promotion importante, donc une augmentation. On pourrait le coder de faon suivante :
fred = Employee.new("Fred", "Crane Operator", 30000) fred.salary = 1000000 # Attention! Etat incohrent! fred.title = Vice President of Sales

Le souci, cest que pendant un court instant Fred serait le grutier le mieux pay du monde, car son augmentation arriverait avant la modication de son intitul de poste. Cela serait ngligeable si tous nos observateurs ntaient pas lcoute et ntaient pas affects par cet tat incohrent. On peut remdier ce problme en retardant la notication jusquau moment o lensemble des changements rentre en cohrence :
# Ne pas informer les observateurs fred.salary = 1000000 fred.title = Vice President of Sales # Notifier les observateurs maintenant! fred.changes_complete

94

Patterns en Ruby

Un dernier point : faites attention aux observateurs qui se comportent mal. Nous avons utilis lanalogie dun sujet qui transmet des nouvelles un observateur, mais en ralit ce nest quun appel de mthode sur un autre objet. Que faire si lobservateur lance une exception en rponse la notication de laugmentation de Fred ? Simplement crire dans le log derreurs et continuer ou bien prendre des mesures plus nergiques ? Il nexiste pas de solution standard, tout dpend de votre application et de la conance que vous accordez vos observateurs.

Le pattern Observer dans le monde rel


Le pattern Observer nest pas difcile trouver dans le code Ruby. Par exemple, il est utilis dans ActiveRecord. Des clients dActiveRecord qui souhaitent rester au courant des crations, lectures, mises jour et suppressions dobjets dans la base de donnes peuvent dnir des observateurs de la faon suivante1 :
class EmployeeObserver < ActiveRecord::Observer def after_create(employee) # Un nouvel employ est enregistr end def after_update(employee) # Lenregistrement de lemploy est mis jour end def after_destroy(employee) # Lenregistrement de lemploy est supprim end end

Dans llgant exemple du pattern Convention plutt que Conguration (voir Chapitre 18), vous verrez quActiveRecord ne vous oblige pas enregistrer un observateur. Il dduit du nom de la classe EmployeeObserver que son rle consiste observer les objets Employee. On trouve un autre exemple dobservateur fond sur des blocs de code dans REXML, une bibliothque de traitement XML livre avec la bibliothque standard de Ruby. La classe REXML SAX2Parser est un analyseur de ux XML : il lit un chier XML et vous laisse libre dajouter des observateurs permettant dtre noti lorsquun lment XML particulier est trait.
1. La syntaxe ActiveRecord::Observer peut vous paratre trange car nous nen avons pas encore parl. Cette syntaxe indique que la classe Observer est dnie lintrieur dun module et il faut utiliser :: pour y accder.

Chapitre 5

Rester inform avec le pattern Observer

95

Malgr le fait que SAX2Parser supporte un style plus formel dobservateurs dnis dans des classes spares, vous pouvez galement lui passer des blocs de code qui serviront dobservateurs :
require rexml/parsers/sax2parser require rexml/sax2listener # # Crer un parser XML pour nos donnes # xml = File.read(data.xml) parser = REXML::Parsers::SAX2Parser.new( xml ) # # Ajouter des observateurs pour tre au courant des dbuts et fins # des lments # parser.listen(:start_element ) do |uri, local, qname, attrs| puts("start element: #{local}") end parser.listen(:end_element ) do |uri, local, qname| puts("end element #{local}") end # # Parser le XML # parser.parse

Passez un chier XML au programme ci-dessus et vous pourrez regarder passer les notications des lments du chier.

En conclusion
Le pattern Observer vous permet de dvelopper des composants qui restent au courant des activits des autres composants en vitant de coupler lensemble trop fortement pour se retrouver dans un code spaghetti. Linterface claire entre la source de linformation (le sujet) et les consommateurs de cette information (les observateurs) permet de transmettre les messages sans embrouiller le code. Le travail principal pour implmenter le pattern Observer est effectu dans la classe Observable. Ruby nous permet de factoriser ce mcanisme soit dans une classe parent soit dans un module, ce dernier tant la solution la plus courante. Linterface entre le sujet et lobservateur peut tre aussi complexe que ncessaire mais, si vous dveloppez un observateur simple, les blocs de code font trs bien laffaire. Comme mentionn au Chapitre 4, le pattern Observer et le pattern Strategy se ressemblent. Les deux prsentent un objet principal (le sujet dans le pattern Observer et le contexte dans le pattern Stratgie) qui met des appels vers un autre objet (un

96

Patterns en Ruby

observateur ou une stratgie). La principale diffrence rside dans lintention. Dans le cas de lObserver, nous informons lautre objet des vnements qui se produisent dans lobjet sujet. Dans le cas du pattern Strategy, nous dlguons notre objet stratgie une partie du traitement. Aussi, le pattern Observer remplit plus ou moins la mme fonction que les mthodes daccrochage (hooks) du pattern Template Method. Les deux permettent de tenir un objet inform des vnements en cours. La diffrence, cest bien sr que le pattern Template Method ninforme que des objets qui lui sont lis par hritage. Si la classe nest pas une de ses sous-classes, elle ne recevra aucune notication de la mthode daccrochage du pattern Template Method.

6
Assembler le tout partir des composants avec Composite
Lorsque jai eu 11 ou 12 ans, jai dvelopp une thorie de lunivers. Je venais dapprendre lexistence du systme solaire et des atomes, et les ressemblances entre les deux semblaient tre plus quune concidence. Notre systme solaire a des plantes qui tournent autour du soleil, tandis que latome (au moins au niveau de lcole lmentaire) a des lectrons qui tournent autour du noyau. Je me souviens que je me demandais si notre monde entier ntait pas juste un lectron quelconque dans un univers plus grand. Et peut-tre existait-il un adolescent fantastiquement petit qui vivait sa vie dans un monde qui faisait partie dun atome au bout de mon crayon. Malgr le fait que ma thorie se fondait sur de la physique errone, lide de choses construites partir de sous-ensembles similaires est un concept puissant qui peut tre appliqu lcriture de vos programmes. Dans ce chapitre nous allons nous intresser au pattern Composite, un motif de conception qui nous aide construire des objets complexes partir de sous-objets plus petits qui peuvent leur tour tre composs de sous-sous-objets encore plus petits. Nous verrons comment appliquer le pattern Composite des situations aussi diverses que des organigrammes et la mise en page dinterfaces graphiques. Et, qui sait, dans le cas improbable o ma vieille thorie de lunivers serait exacte, nous pourrions utiliser le pattern Composite comme un modle de lunivers.

Le tout et ses parties


Dvelopper des logiciels orients objet est un processus qui consiste combiner des objets relativement simples, tels que des entiers et des chanes de caractres, en objets

98

Patterns en Ruby

plus complexes, comme des dossiers personnels et des listes de chansons. Ces objets peuvent leur tour tre utiliss pour construire des objets encore plus intressants. Dordinaire, la n apparaissent quelques objets trs sophistiqus qui ne ressemblent plus du tout aux composants qui ont servi leur construction. Mais ce nest pas toujours le cas. Parfois, un objet complexe doit ressembler ses composants et se comporter exactement de la mme manire. Imaginez que vous travaillez dans la ptisserie La Chocolatine SARL. Vous devez dvelopper un systme informatique qui suit le processus de fabrication des gteaux au chocolat ChocOCroc. Un des prrequis indispensables est que votre systme surveille le temps de prparation dun gteau. videmment, faire un gteau est un processus assez complexe. Dabord, il faut prparer la pte, la placer dans un moule et ensuite mettre le moule au four. Une fois le gteau prpar il faut ventuellement le surgeler et le conditionner pour la vente. Prparer la pte est aussi un processus assez compliqu qui consiste mesurer la farine, ajouter des ufs et... bien sr, lcher la cuillre. Le processus de prparation dun gteau peut tre considr comme un arbre (voir Figure 6.1). La tche principale "faire un gteau" consiste en des sous-tches telles que faire cuire le gteau au four et lemballer, qui peuvent, elles aussi, tre divises en soustches. Il est clair que nous ne voulons pas diviser le processus de fabrication dun gteau linni ("Maintenant, ajoutez dans le rservoir le grain de farine numro 1 463 474..."). Il faut se contenter didentier les tches de bas niveau les plus fondamentales. Il est probablement judicieux de sarrter au niveau "ajouter des ingrdients secs" et "enfourner le gteau" et de modliser chacune de ces tapes dans une classe spare.
Figure 6.1
Larbre des tches de prparation dun gteau
Make Cake Manufacture Cake

Package Cake

Make Batter

Fill Pan

Bake

Frost

Box

Label

AddDryIngredients

AddLiquids

Mix

Chapitre 6

Assembler le tout partir des composants avec Composite

99

Il est vident que toutes les classes devront partager la mme interface qui leur permettra de retourner linformation sur leur dure. Dans votre projet La Chocolatine vous dcidez dutiliser une classe parent commune Task et de crer quelques sous-classes, une pour chaque tche basique : AddDryIngredientsTask, AddLiquidsTask et MixTask. Assez parl des tches simples. Comment sy prendre pour traiter des tches plus complexes telles que "prparer la pte" ou mme "fabriquer un gteau", qui consistent en des sous-tches plus petites ? Dune part, ces sous-tches sont parfaitement respectables. On pourrait vouloir savoir combien de temps est ncessaire pour prparer la pte, faire lemballage ou mme fabriquer le gteau entier exactement de la mme manire quavec des tches simples. Mais ces tches de haut niveau nont pas tout fait la mme nature que des tches simples : elles sont composes partir dautres tches. Vous aurez clairement besoin dun conteneur pour traiter ces tches complexes (ou devrions-nous dire composites). Il existe un autre point concernant les tches de haut niveau dont il faut tenir compte : elles sont construites partir dun certain nombre de sous-tches, mais de lextrieur elles ont la mme interface que nimporte quelle autre tche Task. Cette approche fonctionne deux niveaux. Premirement, le code utilisant lobjet MakeBatterTask nest pas oblig de se proccuper du fait que faire la pte est plus compliqu que, par exemple, mesurer la farine. Que ce soit simple ou complexe, tout est un objet Task. Cest la mme chose pour la classe MakeBatter, cette classe nest pas concerne par les dtails de ses sous-tches ; quelles soient simples ou complexes, pour MakeBatter ce sont simplement des objets Task. Cela nous amne au deuxime point lgant de cette technique : MakeBatter gre simplement une liste dobjets Task, donc chacune des sous-tches peut aussi consister en des sous-tches. Enn, nous pouvons construire un arbre de tches et de sous-tches aussi profond que ncessaire. Il savre que la situation o lon doit grouper des composants pour crer un nouveau supercomposant arrive assez frquemment. Pensez lorganisation dans des grandes entreprises. Toute entreprise est compose dindividus : des directeurs, des comptables et des ouvriers. Nous voyons rarement une grande entreprise comme une collection dindividus. Nous avons plutt une vision dun conglomrat de divisions qui sont spares en dpartements eux-mmes composs dquipes dans lesquelles travaillent des individus. Les dpartements et les divisions partagent beaucoup de caractristiques avec les employs. Par exemple, chacun deux reoit sa part de la masse salariale : Fred la

100

Patterns en Ruby

logistique peut tre largement sous-pay, mais cela peut aussi tre le cas du dpartement des relations publiques. Les employs et les dpartements ont un responsable hirarchique : Yves est le patron de Fred, tandis que Karine est la vice-prsidente du dpartement des relations publiques. Finalement, les dpartements peuvent quitter lentreprise tout aussi bien que des employs : Fred peut trouver un travail mieux pay alors que le dpartement des relations publiques peut tre vendu une autre entreprise. Du point de vue de la modlisation, les personnes dans une grande entreprise sont comparables des tches simples de la prparation dun gteau, tandis que les dpartements et les divisions sont des lments de plus haut niveau, semblables des tches plus complexes de la prparation dun gteau.

Crer des composites


Pour dsigner un systme o "lensemble a le mme comportement que ses parties", les membres du GoF ont cr le pattern Composite. Vous comprenez que vous avez besoin de ce pattern lorsque vous essayez de raliser une hirarchie ou un arbre dobjets et que vous ne voulez pas que le code client se demande constamment sil a affaire un seul objet o toute une branche dun grand arbre. Trois lments sont ncessaires pour construire le pattern Composite (voir Figure 6.2). Premirement, il vous faut une interface ou une classe parent commune pour tous les objets. Dans la terminologie du GoF, cette classe parent ou interface sappelle un composant. Posez-vous la question : "Quelles caractristiques communes auront mes objets basiques et mes objet de haut niveau ?" Lorsque lon prpare des gteaux, les tches simples comme mesurer la farine ainsi que les tches complexes comme prparer la pte prennent toutes deux un certain temps.
Figure 6.2
Le pattern Composite pattern
Component operation()

Leaf operation()

Composite @subcomponents[] operation()

Deuximement, vous avez besoin des objets feuilles, des tapes simples et indivisibles du processus. Dans lexemple avec le gteau, les feuilles sont des tches simples telles

Chapitre 6

Assembler le tout partir des composants avec Composite

101

que mesurer la farine ou ajouter des ufs. Dans lexemple de lorganisation, les feuilles taient des employs individuels. Les classes feuilles doivent, videmment, implmenter linterface Component. Troisimement, nous avons besoin dau moins une classe de haut niveau, nomme par le GoF composite. Lobjet composite est un composant, mais cest aussi un objet de haut niveau qui consiste en des sous-composants. Dans lexemple du gteau, les composites sont des tches complexes telles que prparer la pte ou prparer le gteau, ce sont des tches qui consistent en des sous-tches. Pour des organisations, les objets composites sont des dpartements et des divisions. Pour concrtiser cette discussion, jetons un il sur le processus de prparation dun gteau exprim en code. Commenons par la classe parent des composants :
class Task attr_reader:name def initialize(name) @name = name end def get_time_required 0.0 end end

Task est une classe parent abstraite dans le sens o elle nest pas complte. Elle conserve le nom de la tche et fournit une mthode vide get_time_required. Ajoutons deux classes feuilles :
class AddDryIngredientsTask < Task def initialize super(Add dry ingredients) end def get_time_required 1.0#1 minute pour ajouter la farine et le sucre end end class MixTask < Task def initialize super(Mix that batter up!) end def get_time_required 3.0# Remuer pendant 3 minutes end end

102

Patterns en Ruby

Les classes AddDryIngredientsTask et MixTask sont des sous-classes trs simples de Task. Elles exposent des implmentations de la mthode get_time_required. videmment, nous aurions pu continuer et dnir toutes les tches de base pour prparer un gteau, mais utilisons notre imagination et allons droit au but avec la tche composite :
class MakeBatterTask < Task def initialize super(Make batter) @sub_tasks = [] add_sub_task( AddDryIngredientsTask.new ) add_sub_task( AddLiquidsTask.new ) add_sub_task( MixTask.new ) end def add_sub_task(task) @sub_tasks << task end def remove_sub_task(task) @sub_tasks.delete(task) end def get_time_required time=0.0 @sub_tasks.each {|task| time += task.get_time_required} time end end

Alors que la classe MakeBatterTask apparat de lextrieur comme une tche simple ordinaire car elle implmente la mthode cl get_time_required , elle se compose en fait de trois sous-tches : AddDryIngredientsTask et MixTask, que nous avons dj vues, et la tche AddLiquidsTask, dont limplmentation est laisse votre imagination. La partie critique de MakeBatterTask est la faon dont cette tche gre la mthode get_time_required. Pour tre prcis, MakeBatterTask additionne les dures de chaque sous-tche. Puisque dans notre exemple culinaire nous avons plusieurs tches composites (emballage du gteau et la tche principale : prparation du gteau), il serait judicieux de factoriser les dtails de gestion des sous-tches dans une autre classe parent :
class CompositeTask < Task def initialize(name) super(name) @sub_tasks = [] end

Chapitre 6

Assembler le tout partir des composants avec Composite

103

def add_sub_task(task) @sub_tasks << task end def remove_sub_task(task) @sub_tasks.delete(task) end def get_time_required time=0.0 @sub_tasks.each { task| time += task.get_time_required} time end end

Notre classe MakeBatterTask est dsormais rduite ce qui suit :


class MakeBatterTask < CompositeTask def initialize super(Make batter) add_sub_task( AddDryIngredientsTask.new ) add_sub_task( AddLiquidsTask.new ) add_sub_task( MixTask.new ) end end

La profondeur illimite de larbre est la caractristique principale des objets composites. La classe MakeBatterTask na quun niveau de profondeur, mais ce ne sera jamais le cas en rgle gnrale. Lorsque nous terminons notre projet culinaire, nous devrons crer la classe MakeCake :
class MakeCakeTask < CompositeTask def initialize super(Make cake) add_sub_task( MakeBatterTask.new ) add_sub_task( FillPanTask.new ) add_sub_task( BakeTask.new ) add_sub_task( FrostTask.new ) add_sub_task( LickSpoonTask.new ) end end

Chacune des sous-tches de MakeCakeTask peut tre un composite comme cest le cas pour MakeBatterTask.

Rafner le pattern Composite avec des oprateurs


On pourrait rendre le code de notre composite encore plus lisible si lon remarquait le double rle des objets composites. Dune part, un objet composite est un composant, dautre part, cest une collection de composants. Notre implmentation fait que la

104

Patterns en Ruby

classe CompositeTask ne ressemble pas beaucoup des collections standard de Ruby, telles quun tableau ou un tableau associatif. Par exemple, ce serait agrable de pouvoir ajouter des tches CompositeTask laide de loprateur <<, comme si ctait un tableau :
composite = CompositeTask.new(example) composite << MixTask.new

Il savre que cest trs simple raliser si lon renomme la mthode add_sub_task :
def <<(task) @sub_tasks << task end

On pourrait aussi vouloir accder aux sous-tches en utilisant la syntaxe indicielle familire, comme dans le code suivant :
puts(composite[0].get_time_required) composite[1] = AddDryIngredientsTask.new

Ruby traduira lappel object[i] en un appel la mthode rpondant au nom trange [ ], qui accepte un seul paramtre, lindice. An que notre classe CompositeTask supporte cette opration, il suft dy ajouter la mthode :
def [](index) @sub_tasks[index] end

De la mme manire, lappel object[i] = value est traduit en un appel la mthode avec le nom encore plus trange []=, qui accepte deux paramtres, lindice et la nouvelle valeur :
def []=(index, new_value) @sub_tasks[index] = new_value end

Un tableau comme composite ?


On pourrait galement avoir le mme conteneur si notre CompositeTask tait une sousclasse dArray plutt que de Task :
class CompositeTask < Array attr_reader:name def initialize(name) @name = name end

Chapitre 6

Assembler le tout partir des composants avec Composite

105

def get_time_required time=0.0 each {|task| time += task.get_time_required} time end end

tant donn le typage dynamique de Ruby, cette approche fonctionnera. Lorsque lon transforme CompositeTask en une sous-classe dArray, on obtient par lhritage le conteneur et ses oprateurs associs [], []= et <<. Mais est-ce une bonne approche ? Je vote contre. CompositeTask est non pas une espce spcialise dun tableau mais une espce spcialise de Task. Si la classe CompositeTask doit tre lie par hritage avec une autre classe, cette classe devrait tre Task et non pas Array.

Une diffrence embarrassante


Toute implmentation du pattern Composite doit grer un problme dlicat. Nous avons commenc par dire que le but du pattern Composite est de rendre les objets feuilles plus ou moins indiscernables des objets composites. Je souligne le "plus ou moins", car il existe une diffrence incontournable entre un composite et un objet feuille : le composite doit grer ses enfants, ce qui signie probablement quil doit avoir une mthode pour rcuprer les objets enfants ainsi que les ajouter et les supprimer. Les classes feuilles nont videmment aucun enfant grer, puisque cest dans la nature mme des feuilles. Lapproche de ce problme est en grande partie une question de got. Dun ct, nous pouvons implmenter les objets composites et les feuilles diffremment. On pourrait par exemple quiper lobjet composite des mthodes add_child et remove_child (ou ses quivalents en utilisant la syntaxe des tableaux) et les omettre dans les classes feuilles. La logique sous-jacente est que les objets feuilles nont pas denfant et nont pas besoin de la mcanique de gestion des enfants. De lautre ct, notre objectif principal avec le pattern Composite est de rendre les objets feuilles et composites indiscernables. Si le code qui utilise votre composite doit savoir quune partie des composants les composites expose les mthodes get_ child et add_child tandis que les objets feuilles ne le font pas, alors, les objets composites et feuilles ne sont pas les mmes. Et si lon rajoutait les mthodes de gestion des enfants dans un objet feuille ? Que se passerait-il si quelquun les appelait ? Un appel remove_child nest pas trs grave car les objets feuilles nont pas denfant et il ny aura rien supprimer. Mais si lon appelait add_child sur un objet feuille ? Ignorer lappel ? Lever une exception ? Aucune des rponses nest parfaite.

106

Patterns en Ruby

Je rpte, cette dcision est principalement une question de got : concevoir des classes feuilles et composites diffrentes ou charger les classes feuilles avec des mthodes embarrassantes quelles ne savent pas grer. En ce qui me concerne, je prfre ne pas inclure ces mthodes dans les feuilles. Les objets feuilles ne peuvent pas grer des objets enfants et il faut ladmettre.

Pointeurs dans les deux sens


Jusqualors, nous avons considr le pattern Composite comme une solution strictement oriente "du haut vers le bas". Les objets composites conservent une rfrence vers leurs sous-objets, mais les composants enfants nont aucune information sur leurs parents. Il est donc facile de parcourir larbre de la racine vers les feuilles, mais linverse est trs difcile. Pour grimper vers le sommet de larbre nous devons ajouter dans chaque participant du pattern Composite une rfrence vers son parent. Le meilleur endroit pour ce code reste la classe composant. Ainsi, le code pour grer le parent peut tre centralis :
class Task attr_accessor:name,:parent def initialize(name) @name = name @parent = nil end def get_time_required 0.0 end end

Vu que la relation parent-enfant est gre dans la classe composite, cest lendroit logique o affecter la valeur lattribut parent :
class CompositeTask < Task def initialize(name) super(name) @sub_tasks = [] end def add_sub_task(task) @sub_tasks << task task.parent = self end def remove_sub_task(task) @sub_tasks.delete(task) task.parent = nil end

Chapitre 6

Assembler le tout partir des composants avec Composite

107

def get_time_required time=0.0 @sub_tasks.each { |task| time += task.get_time_required} time end end

Les rfrences vers les parents permettent de tracer le chemin de chaque composant vers le parent initial :
while task puts("task: #{task}") task = task.parent end

User et abuser du pattern Composite


La bonne nouvelle concernant le pattern Composite, cest quil nexiste quune seule erreur classique dans son implmentation, la mauvaise nouvelle, cest que les gens la font trs souvent. La faute qui survient si frquemment dans le pattern Composite est la supposition que larbre na quun unique niveau de profondeur, cest--dire que tous les composants enfants de lobjet composite sont des objets feuilles plutt que dautre composites. Pour illustrer ce faux pas, imaginez que nous ayons besoin de connatre le nombre des objets feuilles impliqus dans la prparation de gteaux. On pourrait simplement ajouter le code suivant dans la classe CompositeTask :
# # La mauvaise technique # class CompositeTask < Task # Beaucoup de code omis... def total_number_basic_tasks @sub_tasks.length end end

Cest faisable, mais ce nest pas bien. Cette implmentation ignore le fait que chacune de ces sous-tches pourrait tre elle-mme un norme composite avec de nombreuses sous-sous-tches. La bonne solution dans cette situation est de dnir la mthode total_num_of_tasks dans la classe composant :
class Task # Beaucoup de code omis... def total_number_basic_tasks 1 end end

108

Patterns en Ruby

Ensuite, on surcharge la mthode dans la classe composite avant de parcourir larbre rcursivement :
class CompositeTask < Task # Beaucoup de code omis... def total_number_basic_tasks total = 0 @sub_tasks.each {|task| total += task.total_number_basic_tasks} total end end

Souvenez-vous que la puissance du pattern Composite rside dans sa capacit dcrire des arbres de profondeur illimite. Ne faites donc pas tous ces efforts pour nalement sacrier cet avantage en crivant quelques lignes de code mal choisies.

Les composites dans le monde rel


Lorsque lon recherche des exemples rels du pattern Composite dans la base de code Ruby, les bibliothques de linterface graphique utilisateur sautent immdiatement aux yeux. Toutes les interfaces graphiques modernes supportent une palette de composants de base comme les tiquettes, les champs de texte et les menus. Ces composants graphiques de base ont beaucoup de points communs : que ce soit un bouton, une tiquette ou un lment de menu, ils ont tous une police de caractres, une couleur du premier et de larrire-plan, et ils occupent tous un certain espace sur lcran. videmment, les vraies interfaces modernes ne reprsentent pas simplement une collection de composants graphiques. Une vraie interface est une structure hirarchique : commencez par une tiquette et un champ, positionnez-les dune certaine faon, puis groupez-les en un seul lment visuel qui servira pour demander lutilisateur son prnom. Combinez cet lment dentre du prnom avec un lment similaire pour le nom de famille et le numro de scurit sociale. Arrangez-les dans un composant graphique encore plus grand et complexe. Si vous avez lu ce chapitre attentivement, le processus doit vous paratre familier : nous venons de dvelopper une interface graphique, un composite. On peut trouver un bon exemple de lutilisation de composites dans une bibliothque dinterfaces graphiques dans FXRuby. FXRuby est une extension Ruby qui amne dans le monde Ruby la bibliothque des interfaces graphiques open-source et cross-plateforme FOX. FXRuby fournit une grande slection de widgets dinterface graphique, en commenant par les prosaques FXButton et FXLabel jusquaux trs complexes FXColorSelector et FXTable. Vous pouvez galement construire vos propres objets, aussi complexes que vous voulez, avec FXHorizontalFrame et son cousin FXVerticalFrame. Ces deux classes jouent le rle de conteneurs qui permettent dajouter dautres

Chapitre 6

Assembler le tout partir des composants avec Composite

109

widgets pour crer un lment dinterface graphique uni. La diffrence entre ces deux classes rside dans la manire dont elles afchent leurs sous-lments : lune aligne les lments horizontalement, et lautre les place verticalement. Que ce soit horizontal ou vertical, les deux classes frames FOX sont des sous-classes de FXWindow, tout comme les autres widgets de base. Par exemple, voici une petite maquette dun logiciel ddition de texte laide de FXRuby :
require rubygems require fox16 include Fox application = FXApp.new("CompositeGUI", "CompositeGUI") main_window = FXMainWindow.new(application, "Composite", nil, nil, DECOR_ALL) main_window.width = 400 main_window.height = 200 super_frame = FXVerticalFrame.new(main_window, LAYOUT_FILL_X | LAYOUT_FILL_Y) FXLabel.new(super_frame, "Text Editor Application") text_editor = FXHorizontalFrame.new(super_frame, LAYOUT_FILL_X | LAYOUT_FILL_Y) text = FXText.new(text_editor, nil, 0, TEXT_READONLY | TEXT_WORDWRAP | LAYOUT_FILL_X | LAYOUT_FILL_Y) text.text = "This is some text." # La barre de boutons button_frame = FXVerticalFrame.new(text_editor, LAYOUT_SIDE_RIGHT | LAYOUT_FILL_Y) FXButton.new(button_frame, "Cut") FXButton.new(button_frame, "Copy") FXButton.new(button_frame, "Paste") application.create main_window.show (PLACEMENT_SCREEN) application.run

La totalit de linterface est une srie dobjets composites imbriqus. Au premier niveau de larbre se trouve FXMainWindow, qui contient exactement un lment enfant, un cadre vertical. Ce cadre a un autre enfant ; un cadre horizontal, qui son tour a... Vous avez sans doute compris le schma. Sinon consultez la Figure 6.3, qui est un bel exemple du pattern Composite que vous pouvez voir luvre sur vos crans.

En conclusion
Une fois la nature rcursive de ce pattern assimile, le pattern Composite devient trs simple. Parfois, nous avons besoin de modliser des objets qui peuvent se regrouper naturellement en composants plus grands. Les objets plus complexes sinscrivent dans

110

Patterns en Ruby

le pattern Composite sils partagent certaines caractristiques de leurs composants individuels : lensemble ressemble une de ses parties.
Figure 6.3
Une maquette dditeur de texte dvelopp avec FXRuby

Le pattern Composite permet de dvelopper des arbres dobjets profondeur illimite dans lesquels on peut grer chacun des nuds intrieurs les composites de la mme faon que les feuilles. Le pattern Composite est si fondamental quil nest gure surprenant de le voir rapparatre, parfois bien cach, dans dautres patterns. Comme nous le verrons au Chapitre 15, le pattern Interprteur nest quune version spcialise de Composite. Enn, il est difcile dimaginer le pattern Composite sans le pattern Itrateur. La raison pour laquelle les deux sont insparables va vous apparatre trs vite car le pattern Itrateur fait lobjet du chapitre suivant.

7
Accder une collection avec lItrateur
Au Chapitre 6, nous avons examin les composites des objets qui ressemblent de simples composants, mais qui sont en vrit composs eux-mmes dune collection de sous-composants. videmment, un objet na pas besoin dtre un composite pour avoir connaissance dune collection dautres objets. Un objet Employee peut avoir une collection de membres de sa famille, ou de numros de tlphone, ou, dans le cas dun cadre bien pay, des adresses de plusieurs rsidences luxueuses. Dans cette situation, il serait pratique de pouvoir accder ces sous-objets squentiellement sans pour autant connatre les dtails du mode de stockage utilis par le conteneur. Dans ce chapitre, nous allons explorer le pattern Itrateur, une technique qui permet un objet conteneur douvrir laccs sa collection de sous-objets. Nous verrons les deux types ditrateurs basiques et nous aurons enn une explication concernant ces tranges boucles each, que lon rencontre frquemment dans Ruby.

Itrateurs externes
Voici comment les membres du GoF formulent le but du pattern Itrateur : Fournir un moyen daccder de faon squentielle aux lments dun objet collection sans exposer sa reprsentation interne. Formul autrement, le pattern Itrateur propose au monde extrieur un pointeur mobile vers des objets stocks dans un conteneur de collection opaque. Si vous tes un programmeur Java vous connaissez probablement linterface java .util.Iterator et son frre an java.util.Enumeration.

112

Patterns en Ruby

Voici un exemple dutilisation typique dun itrateur Java :


ArrayList list = new ArrayList(); list.add("red"); list.add("green"); list.add("blue"); for( Iterator i = list.iterator(); i.hasNext();) { System.out.println( "item: " + i.next()); }

On trouve galement des itrateurs dans les endroits inattendus. Par exemple, on peut voir java.util.StringTokenizer comme un itrateur qui nous permet de parcourir tous les tokens dune chane de caractres. De la mme manire, JDBC propose la classe ResultSet, qui nous permet ditrer sur chaque ligne du rsultat renvoy pour une requte SQL. Ce style ditrateur est souvent appel litrateur externe. "Externe" indique que litrateur est un objet spar du conteneur de collection. Nous verrons bientt que ce nest pas le seul itrateur sur la liste. Mais voyons dabord quoi ressemblerait un itrateur externe en Ruby. Il est assez facile de construire des itrateurs externes la Java en Ruby. Une implmentation simple, quoique trop limite pour lusage rel, pourrait ressembler ceci :
class ArrayIterator def initialize(array) @array = array @index = 0 end def has_next? @index < @array.length end def item @array[@index] end def next_item value = @array[@index] @index += 1 value end end

ArrayIterator est une traduction directe de litrateur Java en Ruby, avec un ajout : la mthode pour retourner llment courant (trangement, cette fonction est absente dans limplmentation Java). Voici une faon dappliquer notre nouvel itrateur :
array = [red, green, blue] i = ArrayIterator.new(array)

Chapitre 7

Accder une collection avec lItrateur

113

while i.has_next? puts("item: #{i.next_item}") end

Lexcution de ce code provoque le rsultat attendu :


item: red item: green item: blue

En quelques lignes de code notre ArrayIterator nous fournit tout ce qui est ncessaire pour itrer sur nimporte quel tableau Ruby. En prime, grce au typage dynamique et exible de Ruby, ArrayIterator fonctionne avec toute classe qui expose la mthode length et qui peut tre indexe par un entier. String fait partie de ces classes, notre ArrayIterator conviendrait donc trs bien aux chanes de caractres :
i = ArrayIterator.new(abc) while i.has_next? puts("item: #{i.next_item.chr}") end

Excutez ce code et vous obtiendrez le rsultat suivant :


item: a item: b item: c

Le seul souci lorsquon utilise ArrayIterator avec des chanes, cest que la mthode [n] de la classe String renvoie le caractre n sous forme dun nombre, son code ASCII, do le besoin dappeler la mthode chr dans lexemple ci-dessus. tant donn la facilit de dveloppement dArrayIterator, il est tonnant que les itrateurs externes soient si rares en Ruby. Il savre que Ruby propose quelque chose de mieux, et ce quelque chose est fond sur nos vieux amis, les blocs de code et lobjet Proc.

Itrateurs internes
En y rchissant bien, le but dun itrateur est damener votre code vers chaque sousobjet dun conteneur. La solution des itrateurs externes traditionnels revient fournir une longue gaffe, lobjet itrateur, qui peut tre utilis pour rcuprer des sous-objets de la collection sans plonger dans les dtails de lobjet conteneur. Mais avec un bloc de code on peut passer la logique mtier lobjet conteneur. Ensuite, cet objet appellera ce code pour chacun de ses sous-objets. Vu que toutes les actions ditration se droulent lintrieur de lobjet conteneur, les itrateurs fonds sur des blocs de code sont nomms des itrateurs internes.

114

Patterns en Ruby

Dvelopper un itrateur interne pour des tableaux est trs simple. Il suft de dnir une mthode qui appelle le bloc de code ( laide de yield) pour chaque lment1 :
def for_each_element(array) i = 0 while i < array.length yield(array[i]) i += 1 end end

Pour utiliser notre itrateur interne on ajoute un bloc de code la n de lappel de mthode :
a = [10, 20, 30] for_each_element(a) {|element| puts("Llment est #{element}")}

Il savre que nous navons mme pas besoin de for_each_element car la classe Array propose un appel de mthode itrateur each. Tout comme notre mthode for_ each_element, each accepte un bloc de code en paramtre et appelle ce bloc pour chaque lment du tableau :
a.each {|element| puts("Llment est #{element}")}

Excutez une des deux versions du code prcdent et vous obtiendrez le rsultat suivant :
Llment est 10 Llment est 20 Llment est 30

La mthode each explique toutes ces tranges boucles each que vous voyez partout dans ce livre. Ces boucles sont non pas de vraies boucles incorpores dans le langage mais plutt des itrateurs internes.

Itrateurs internes versus itrateurs externes


Bien que les itrateurs internes et externes fassent essentiellement le mme travail en parcourant les lments dune collection, il faut tenir compte de certaines diffrences dordre pratique. Les itrateurs externes ont certainement leurs avantages. Par exemple, dans le cas ditrateur externe litration est actionne par le code client. Avec litrateur externe vous nappelez next que lorsque vous tes prt pour traiter llment suivant. Dans le cas dun itrateur interne, lobjet conteneur force sans cesse le bloc de code recevoir un lment aprs lautre.
1. Un vrai programme Ruby ajouterait probablement la mthode for_each_element la classe String, en Ruby cest trs facile raliser. Pour en savoir plus, voyez le Chapitre 9.

Chapitre 7

Accder une collection avec lItrateur

115

Dans la plupart des cas, cette diffrence est ngligeable. Mais que se passe-t-il si vous essayez de fusionner le contenu de deux tableaux ordonns dans un seul tableau ordonn ? Ce type dopration est assez simple avec un itrateur externe tel quArrayIterator : on cre un itrateur pour les deux tableaux dentre et on boucle en rcuprant la valeur infrieure dans un des itrateurs. Cette valeur est ensuite place dans le tableau de sortie :
def merge(array1, array2) merged = [] iterator1 = ArrayIterator.new(array1) iterator2 = ArrayIterator.new(array2) while( iterator1.has_next? and iterator2.has_next? ) if iteratorl.item < iterator2.item merged << iterator1.next_item else merged << iterator2.next_item end end # Charger les lments restants du tableau array1 while( iterator1.has_next?) merged << iterator1.next_item end # Charger les lments restants du tableau array2 while( iterator2.has_next?) merged << iterator2.next_item end merged end

Je ne suis pas sr de bien voir comment implmenter cette mme fonction avec des itrateurs internes. Le deuxime avantage des itrateurs externes rside dans leur position. Puisquils sont externes, ils sont partageables, on peut les passer dans dautres mthodes et objets. Bien entendu, cest une pe double tranchant : vous obtenez la exibilit, mais vous devez tre sr de votre code. Il faut faire particulirement attention aux multiples threads qui accdent un itrateur externe sans prcaution concernant les accs concurrents. Les atouts principaux des itrateurs internes sont la simplicit et la clart du code. Les itrateurs externes ont un lment de plus, lobjet itrateur. Dans notre exemple, avec les tableaux nous navons pas seulement le tableau et le code client, mais aussi lobjet ArrayIterator indpendant. Avec les itrateurs internes il ny a aucun objet itrateur grer ("Est-ce que jai dj appel next ?"), juste quelques lignes de code regroupes dans un bloc.

116

Patterns en Ruby

Lincomparable Enumerable
Si vous crez une classe conteneur quipe dun itrateur interne, vous devriez probablement envisager linclusion dun module mixin appel Enumerable. Enumerable est comme une publicit pour un gadget : pour ajouter Enumerable, il suft de sassurer que votre mthode ditrateur interne se nomme each et que les lments sur lesquels vous itrez possdent une implmentation raisonnable de loprateur de comparaison <=>. Pour ce prix modique Enumerable ajoute votre classe toute une gamme de mthodes pratiques. Le mixin Enumerable vous offre une panoplie de mthodes trs commodes comme, par exemple, include?(obj), qui retourne true si lobjet fourni en paramtre fait partie de votre collection, ou encore min et max, qui retournent exactement ce que lon attend deux. Le mixin Enumerable vous fournit aussi des mthodes plus exotiques comme all?, qui accepte un bloc et renvoie true si le bloc svalue true pour tous les lments. Comme la classe Array inclut le mixin Enumerable, nous pouvons crire une ligne de code trs simple qui renverra true si la longueur de chaque chane du tableau est infrieure quatre caractres :
a = [ joe, sam, george ] a.all? { element| element.length < 4}

La chane george est plus longue que quatre caractres, donc lappel all? dans cet exemple renvoie false. any? est une mthode semblable all?. Elle retourne true si le bloc renvoie true pour au moins un des lments de litrateur. Vu que la longueur de joe et sam est infrieure quatre caractres, le code suivant renvoie true :
a.any? {|element| element.length < 4}

Enn, Enumerable ajoute votre classe la mthode sort, qui ordonne et renvoie tous les lments du tableau. Pour comprendre la simplicit avec laquelle on peut ajouter toutes ces fonctionnalits nos propres classes, imaginez que vous ayez deux classes : une premire qui modlise un compte nancier et une seconde qui gre un portefeuille de comptes :
class Account attr_accessor:name,:balance def initialize(name, balance) @name = name @balance = balance end def <=>(other) balance <=> other.balance end end

Chapitre 7

Accder une collection avec lItrateur

117

class Portfolio include Enumerable def initialize @accounts = [] end def each(&bloc) @accounts.each(&bloc) end def add_account (account) @accounts << account end end

Nous incluons simplement le module mixin Enumerable dans Portfolio et dnissons la mthode each, et voil Portfolio quip de toute la panoplie des mthodes Enumerable. Par exemple, nous pouvons dsormais savoir de faon trs simple si au moins un des comptes dans le portefeuille contient 2 000 dollars ou plus :
my_portfolio.any? { account| account.balance > 2000}

Nous pouvons galement savoir si tous les comptes contiennent au moins 10 euros :
my_portfolio.all? {|account| account.balance > = 10}

User et abuser du pattern Itrateur


Itrateur gure parmi les patterns les plus rpandus et les plus pratiques mais il prsente nanmoins quelques pines prtes piquer limprudent. Le danger principal est le suivant : que se passe-t-il si lobjet conteneur change lorsque vous tes en train ditrer dessus ? Imaginez que vous parcouriez une liste et que juste avant darriver au troisime lment quelquun supprime cet lment de la liste. Quel serait le rsultat ? Est-ce que litrateur doit vous prsenter lobjet non existant ? Ou continuer vers le quatrime lment comme si de rien ntait ? Ou peut-tre lancer une exception ? Malheureusement, aucun des itrateurs dvelopps dans ce chapitre ne ragit particulirement bien ce genre de changement. Souvenez-vous que notre ArrayIterator externe fonctionnait en conservant lindice de llment courant. La suppression des lments que nous navons pas encore vus ne pose aucun problme, nanmoins, toute modication au dbut du tableau provoquera immanquablement des ravages dans lindexation. Crer une copie du tableau parcouru dans le constructeur de lobjet itrateur peut rendre ArrayIterator rsistant aux changements oprs sur ce tableau :

118

Patterns en Ruby

class ChangeResistantArrayIterator def initialize(array) @array = Array.new(array) @index = 0 end ...

Le nouvel itrateur cre une copie incomplte (shallow copy) du tableau la copie pointe vers le contenu dorigine, qui nest pas copi et parcourt le nouveau tableau. Grce ce nouvel itrateur nous obtenons une capture du tableau, rsistante aux changements, sur laquelle nous pouvons itrer. Les itrateurs internes sont sensibles au mme problme de modication concurrente que les itrateurs externes. Par exemple, cest probablement une trs mauvaise ide de faire ceci :
array=[red, green, blue, purple] array.each do color puts(color) if color == green array.delete(color) end end

Ce code afche
red green purple

En supprimant lentre green nous avons russi semer la pagaille dans lindexation, le rsultat tant que lentre blue nest pas afche. Les itrateurs internes peuvent galement oprer sur une copie indpendante de lobjet conteneur pour viter tout risque de modication en cours ditration comme ce que nous avons fait dans la classe ChangeResistantArrayIterator. Limplmentation pourrait ressembler au code suivant :
def change_resistant_for_each_element(array) copy = Array.new(array) i = 0 while i < copy.length yield(copy[i]) i += 1 end end

Chapitre 7

Accder une collection avec lItrateur

119

Pour rsumer, un programme plusieurs ls dexcution (threads) est un milieu particulirement dangereux pour des itrateurs. Il faut prendre toutes les prcautions habituelles pour sassurer quun thread ne tire pas le tapis sous les pieds de votre itrateur.

Les itrateurs dans le monde rel


Les itrateurs internes pour la plupart mais occasionnellement externes sont si frquents en Ruby quil est difcile de choisir par quel exemple commencer. En fait, les tableaux Ruby ont deux autres itrateurs internes en plus de each. La mthode reverse_each boucle sur des lments du tableau en commenant par le dernier pour remonter jusquau premier, alors que each_index appelle le bloc pass en argument sur chacun des indices du tableau au lieu de ses lments. La classe String possde une mthode each qui parcourt chaque ligne de la chane (oui, chaque ligne et non pas chaque caractre) ainsi que la mthode each_byte. Les objets String ont galement la formidable mthode scan, qui accepte en paramtre une expression rgulire et itre sur chaque occurrence trouve dans la chane. Par exemple, nous pouvons ainsi rechercher des mots commenant par la lettre p:
s = Peter Piper picked a peck of pickled peppers s.scan(/[Pp]\w*/) { |word| puts("Le mot est #{word}")}

Ce code afche beaucoup de mots en p :


Le Le Le Le Le Le mot mot mot mot mot mot est est est est est est Peter Piper picked peck pickled peppers

Vous ne serez pas tonn dapprendre que la classe Hash supporte elle aussi une riche palette ditrateurs. Nous avons each_key, qui appelle un bloc de code pour chaque cl du tableau associatif :
h = {nom => russ, yeux => bleu, sexe => mle} h.each_key {|key| puts(key)}

Ce code afche le rsultat suivant :


nom sexe yeux

La classe Hash offre galement la mthode each_value :


h.each_value { |value| puts(value)}

120

Patterns en Ruby

Ce code afche le rsultat suivant :


russ mle bleu

Enn, la mthode each classique est aussi disponible :


h.each {|key, value| puts("#{key} #{value}")}

La mthode each itre sur des paires cl/valeur du tableau associatif, le code afche donc le rsultat suivant :
nom russ sexe mle yeux bleu

Des itrateurs externes en Ruby sont plus difciles trouver. Lobjet IO constitue un spcimen intressant. La classe IO est une classe charge de grer des ux dentresortie. Cest un objet amphibie limplmentation lgante : il propose au choix un itrateur externe ou interne. On peut ouvrir un chier et lire chaque ligne avec un itrateur externe de faon classique :
f = File.open(names.txt) while not f.eof? puts(f.readline) end f.close

Lobjet IO expose galement la mthode each (aussi nomme each_line), qui implmente un itrateur interne renvoyant chaque ligne dun chier :
f = File.open(names.txt) f.each {|line| puts(line)} f.close

Les chiers non orients ligne peuvent tre traits par la mthode ditration each_byte :
f.each_byte {|byte| puts(byte)}

Si vos programmes font beaucoup doprations dentre et sortie, vous devriez probablement vous intresser de prs la classe Pathname. Pathname ambitionne de vous offrir tous les outils ncessaires la manipulation des dossiers et des chemins dun systme de chiers. On cre un objet Pathname en passant au constructeur le chemin concern :
pn = Pathname.new(/usr/local/lib/ruby/1.8)

En complment dun ventail de mthodes qui nont aucune relation avec des itrateurs, Pathname fournit litrateur each_filename, qui boucle sur des composants du chemin pass en argument.

Chapitre 7

Accder une collection avec lItrateur

121

Si vous excutez le code suivant :


pn.each_filename {|file| puts("File: #{file}")}

vous obtiendrez :
File: File: File: File: File: usr local lib ruby 1.8

Vous pouvez galement changer de dimension : la mthode each_entry itre sur le contenu du dossier vers lequel pointe lobjet Pathname. Donc si vous excutez
pn.each_entry { entry| puts("Entry: #{entry}")}

vous verrez le contenu de /usr/local/lib/ruby/1.81:


Entry: Entry: Entry: Entry: Entry: ... . .. i686-linux shellwords.rb mailread.rb

Enn, mon itrateur interne prfr est celui propos par le module ObjectSpace. ObjectSpace ouvre une fentre vers lunivers des objets prsents dans votre interprteur Ruby. Litrateur fondamental fourni par ObjectSpace est la mthode each_ object. Il itre sur tous les objets Ruby, cest--dire tout ce qui est charg dans votre interprteur Ruby :
ObjectSpace.each_object { object| puts("Object: #{object}")}

La mthode each_object accepte un argument facultatif qui peut tre une classe ou un module. Si largument est prsent, each_object itre uniquement sur les instances de cette classe ou de ce module. Eh oui, les sous-classes sont incluses ! Si je voulais afcher tous les nombres connus de mon interprteur Ruby, je pourrais faire ceci :
ObjectSpace.each_object(Numeric) {|n| puts("The number is #{n}")}

La capacit dintrospection de Ruby est assez sensationnelle. Avec ObjectSpace vous pouvez implmenter votre propre systme dinspection de la mmoire : il suft de crer un thread qui observe les objets intressants tout en afchant un rapport en consquence. Par exemple, une classe pourrait utiliser ObjectSpace pour retrouver toutes les
1. Vous verrez ce rsultat si vous utilisez un systme dexploitation UNIX et si Ruby est install dans /usr/local/lib.

122

Patterns en Ruby

instances delle-mme. Rails recourt ObjectSpace pour construire la mthode qui retrouve toutes les sous-classes dune classe donne :
def subclasses_of(superclass) subclasses = [] ObjectSpace.each_object(Class) do |k| next if!k.ancestors.include?(superclass) || superclass == k || to_s.include?(::) || subclasses.include?(k.to_s) subclasses << k.to_s end subclasses end

Si lon excute
subclasses_of(Numeric)

on recevra un tableau qui contient "Bignum", "Float", "Fixnum" et "Integer". Je le rpte, les capacits dintrospection de Ruby sont vraiment sensationnelles.

En conclusion
Dans ce chapitre nous avons explor deux formes fondamentales ditrateurs. La premire version, et probablement la plus courante, est litrateur externe : un objet qui pointe vers un lment dune collection. Dans le cas dun itrateur interne, au lieu de passer une sorte de pointeur, nous descendons le code de gestion des lments vers les sous-objets. Nous avons galement rencontr le module Enumerable, qui peut amliorer les possibilits ditration sur tout type de collection. Nous avons par ailleurs jet un coup dil sur le ct obscur des itrateurs, quand notre collection peut tre modie pendant que le processus ditration est en cours. Enn, nous avons effectu une visite guide avec ObjectSpace, qui peut parcourir les entrailles de linterprteur Ruby et nous montrer des choses que lon ne pensait jamais voir. Les itrateurs Ruby sont un excellent exemple de la beaut du langage. Ruby tire parti de la exibilit des objets Proc, des blocs de code et des itrateurs internes plutt que fournir des itrateurs externes spcialiss pour chaque classe conteneur. Les itrateurs internes sont trs faciles crire vous crez une seule mthode au lieu dune toute nouvelle classe , Ruby encourage donc les programmeurs choisir un itrateur optimal pour leurs besoins. La puissance de cette approche devient vidente avec la grande collection des itrateurs disponibles dans la bibliothque standard de Ruby, o lon peut obtenir tout de each_byte dans une chane de caractres each_object dans linterprteur Ruby lui-mme.

8
Effectuer des actions avec Command
Jai mentionn au Chapitre 1 que lorsque jtais lycen et mes dbuts luniversit je passais un temps considrable travailler dans un commerce local. Ce petit magasin essayait avec beaucoup de difcults de concurrencer des grands supermarchs et offrait donc des services introuvables au mgamarch local. Les clients pouvaient notamment nous appeler et dicter au tlphone leur liste de courses. Nous tions plus que contents de rassembler les haricots, le beurre et le saucisson et de les livrer gratuitement. Certains de nos clients avaient mme des commandes permanentes. Ils appelaient et demandaient quon leur livre leur commande habituelle. Ctait donc moi de prparer la livraison avec la liste des courses la main. Les listes des courses de ma jeunesse ressemblent beaucoup aux commandes qui ont donn leur nom au pattern Command qui nous occupe dans ce chapitre. Tout comme une liste de courses, une commande du pattern Command est une instruction destine dclencher une action assez spcique. Tout comme une liste, une commande peut tre excute tout de suite ou ultrieurement lorsquun vnement particulier se produit. Puisque le pattern Command est un des patterns les plus polyvalents couverts dans ce livre, la discussion qui suit va plutt ressembler une revue densemble. On commencera par lusage trs rpandu du pattern Command dans les interfaces utilisateur graphiques, puis nous verrons comment enregistrer les actions effectues ainsi que les actions qui restent faire. Enn, nous utiliserons le pattern Command pour annuler des actions dj excutes ou, a contrario, dfaire ce qui vient dtre fait.

124

Patterns en Ruby

Lexplosion de sous-classes
Imaginez que vous soyez en train de dvelopper SlickUI, un nouveau framework dinterface graphique. Vous crez des boutons magniques, des botes de dialogue exquises et des icnes tomber la renverse. Mais, une fois les lments graphiques de votre framework joliment dcors, vous vous retrouvez face un problme critique : comment mettre linterface en action pour quelle soit utile quelque chose ? Imaginez que votre classe bouton soit conue pour appeler la mthode on_button_ push ds que lutilisateur appuie sur le bouton lcran :
class SlickButton # # Beaucoup de code graphique et logique de gestion omis # ... # def on button push # # Faire quelque chose lorsque le bouton est appuy # end end

Que faut-il crire lintrieur de la mthode on_button_push ? Vous esprez que SlickUI deviendra extrmement populaire et quil sera utilis par des milliers de programmeurs partout dans le monde. Ils creront des millions dinstances de SlickButton. Une quipe de programmeurs dveloppera peut-tre un diteur de texte et elle aura besoin de boutons pour crer de nouveaux documents et pour enregistrer les documents en cours de rdaction. Une autre quipe de projet se concentrera plutt sur un utilitaire de gestion de rseau et elle aura donc besoin dun bouton pour ouvrir une connexion rseau. La difcult dans tout cela, cest quau moment o vous crivez la classe SlickButton vous navez aucune ide de toutes les fonctions que vos futurs clients vont pouvoir accrocher tous ces boutons. Une des solutions ce problme consiste recourir notre outil presque universel, mais lgrement discrdit : lhritage. Vous pourriez demander vos utilisateurs de sousclasser votre classe pour chaque type de bouton comme ceci :
class SaveButton < SlickButton def on button push # # Enregistrer le document courant... # end end

Chapitre 8

Effectuer des actions avec Command

125

class NewDocumentButton < SlickButton def on_button_push # # Crer un nouveau document... # end end

Malheureusement, une application avec une interface graphique complexe aura des dizaines ou mme des centaines de boutons et, par consquent, des dizaines ou des centaines de sous-classes de SlickButton, sans parler des autres lments graphiques de linterface comme les menus et les boutons radio. Qui plus est, lhritage est permanent. Et si vous vouliez que votre bouton fasse une action avant douvrir la feuille de calcul et une action juste aprs lavoir ouvert ? Si vous crivez une sous-classe de Button, soit vous avez besoin de deux sous-classes de Button spares soit vous tes oblig de coder la logique "Le chier est-il ouvert ?" dans une seule sous-classe de Button. Les deux techniques ne sont pas propres. Existe-t-il un moyen plus simple ?

Un moyen plus simple


La bonne approche ce problme consiste encapsuler laction excuter lorsquon clique sur un bouton ou sur un lment de menu. Autrement dit, il faut extraire le code de gestion de laction provoque par le bouton ou le menu dans un objet spar qui ne fait quattendre son excution. Lorsquil est excut, laction effectue la tche spcique lapplication. Ces actions encapsules sont des commandes du pattern Command. Pour appliquer le pattern Command notre exemple il suft de conserver la commande dans lobjet bouton :
class SlickButton attr_accessor:command def initialize(command) @command = command end # # Beaucoup de code mtier et graphique omis # ... # def on_button_push @command.execute if @command end end

Nous pouvons dnir des commandes diffrentes pour toutes les actions possibles de nos boutons :

126

Patterns en Ruby

class SaveCommand def execute # # Enregistrer le document courant... # end end

Et nous passons la commande concrte la cration du bouton :


save_button = SlickButton.new( SaveCommand.new )

Factoriser le code de laction dans son propre objet est lide essentielle du pattern Command. Ce pattern spare la partie variable, la tche accomplir lorsque le bouton est slectionn, de la partie statique, savoir la classe de bouton gnrique apporte par le framework graphique. Puisque la connexion entre le bouton et la commande stablit au moment de lexcution le bouton stocke simplement une rfrence vers la commande dclencher lorsquil est slectionn , il est facile de remplacer les commandes la vole et de changer le comportement du bouton au moment de lexcution. Comme vous pouvez le voir sur le diagramme UML (voir Figure 8.1), la structure du pattern Command est trs simple. Elle consiste en un certain nombre de classes qui partagent la mme interface.
Figure 8.1
Le pattern Command
Command execute()

ConcreteCommand1 execute()

ConcreteCommand2 execute()

Des blocs de code comme commandes


Nous avons vu quune commande nest quun objet qui encapsule un fragment de code spcialis. La seule raison dexister dune commande est dappeler ce code au bon moment. Cette description doit vous tre familire : cest la dnition assez prcise dun bloc de code Ruby ou dun objet Proc. Souvenez-vous quun objet Proc encapsule du code qui attend dtre excut. Le pattern Command se traduit trs naturellement en bloc de code. Voici notre classe SlickButton restructure pour employer des blocs de code :

Chapitre 8

Effectuer des actions avec Command

127

class SlickButton attr_accessor:command def initialize(&bloc) @command = bloc end # # Beaucoup de code mtier et graphique omis # ... # def on_button_push @command.call if @command end end

Pour crer notre nouveau SlickButton fond sur des blocs de code nous passons un bloc de code la cration du bouton :
new_button = SlickButton.new do # # Cration dun nouveau document... # end

Dans un monde Ruby fait de blocs de code et dobjets Proc, les classes codes la main comme SaveCommand sont-elles totalement dpasses ? Pas vraiment. Tout dpend de la complexit de la tche. Si vous avez besoin dune commande simple qui excute quelques actions trs claires, je vous recommande vivement dopter pour un objet Proc. En revanche, si votre tche est relativement complexe (comme le fait de garder beaucoup dinformation sur le contexte ou bien de ncessiter plusieurs mthodes), nhsitez pas crer une classe de commande spcique.

Les commandes denregistrement


Les boutons et les commandes qui les accompagnent constituent certes un bon exemple du pattern Command mais son usage est loin dtre limit aux interfaces graphiques. Par exemple, le pattern Command se rvle trs utile pour garder trace des actions effectues. Imaginez que vous dveloppiez un utilitaire dinstallation de logiciels. Un programme dinstallation a typiquement besoin de crer, copier, dplacer et parfois supprimer des chiers. Il est aussi probable que lutilisateur souhaite prendre connaissance des actions que linstalleur sapprte effectuer avant quelles ne soient lances ou bien savoir ce qua fait linstalleur aprs excution. Assurer le suivi de ces oprations est facile si les actions effectuer sont organises sous forme de commandes. Les commandes dinstallation vont contenir un certain nombre dinformations sur leur tat, nous allons donc les coder comme des classes spares selon le style classique du pattern Command. Commenons par dnir quelques commandes de manipulation

128

Patterns en Ruby

de chiers. Chacune delles implmente la mthode describe ainsi que la mthode execute. Pour commencer, voici la classe parent des commandes :
class Command attr_reader:description def initialize(description) @description = description end def execute end end

Ensuite, dnissons la commande permettant de crer un chier et dcrire son contenu partir dune chane de caractres :
class CreateFile < Command def initialize(path, contents) super("Create file: #{path}") @path = path @contents = contents end def execute f = File.open(@path, "w") f.write(@contents) f.close end end

Nous pouvons galement avoir besoin dune commande pour supprimer un chier :
class DeleteFile < Command def initialize(path) super("Delete file: #{path}") @path = path end def execute File.delete(@path) end end

Et peut-tre dune commande pour copier un chier dans un autre :


class CopyFile < Command def initialize(source, target) super("Copy file: #{source} to #{target}") @source = source @target = target end def excute FileUtils.copy(@source, @target) end end

Chapitre 8

Effectuer des actions avec Command

129

videmment, nous aurions pu tendre davantage les classes de commandes, par exemple pour renommer des chiers, modier les droits daccs ou crer des dossiers, mais arrtons-nous l pour le moment. Puisque nous essayons de garder un suivi de ce que nous nous apprtons faire ou de ce que nous avons dj fait, nous avons besoin dune classe pour rassembler toutes nos commandes. Hum ! une classe qui se comporte comme une commande, mais qui en ralit nest quune faade pour un certain nombre de sous-commandes. Cela ressemble fort un composite :
class CompositeCommand < Command def initialize @commands = [] end def add_command(cmd) @commands << cmd end def execute @commands.each { |cmd| cmd.execute} end def description description = @commands.each { |cmd| description += cmd.description + "\n"} description end end

Outre la satisfaction de mettre en uvre un pattern dj tudi, CompositeCommand nous permet dinformer lutilisateur de ce que nous faisons exactement avec son systme. On pourrait par exemple crer un nouveau chier, le copier dans un autre chier, et puis supprimer le premier :
cmds = CompositeCommand.new cmds.add_command(CreateFile.new(file1.txt, "hello world\n")) cmds.add_command(CopyFile.new(file1.txt, file2.txt)) cmds.add_command(DeleteFile.new(file1.txt))

Pour vritablement excuter toutes ces manipulations sur les chiers on appelle simplement :
cmds.execute

Lavantage majeur de cette technique rside dans sa capacit expliquer lutilisateur ce qui se passe tout moment que ce soit avant ou aprs lexcution des commandes. Par exemple, le code
puts(cmds.description)

130

Patterns en Ruby

afche
Create file: filel.txt Copy file: file1.txt to file2.txt Delete file: filel.txt

Annuler des actions avec Command


Permettre votre client (que ce soit un utilisateur ou un autre programme) dannuler des actions dj effectues est une demande classique. Aujourdhui, la fonction dannulation est une ncessit absolue pour tout diteur de texte, mais on la retrouve aussi ailleurs. Par exemple, la plupart des bases de donnes supportent le rollback de transactions, ce qui est un autre nom dune fonction dannulation. En fait, annuler des actions peut devenir une exigence partout o une srie de modications coteuses en effort est ralise par un humain (enclin lerreur) ou par un programme. La faon nave dimplmenter cette opration consiste garder en mmoire ltat avant le changement et restituer cet tat si le client dcide dannuler la modication. Le problme de cette approche, cest que les chiers texte et les documents modis (sans parler des bases de donnes) peuvent tre assez volumineux. Faire une copie complte de lensemble avant dapporter une modication peut trs vite devenir assez laid et surtout trs coteux en ressources. Le pattern Command peut galement tre utile dans cette situation. Une commande une encapsulation de code pour faire une action spcique pourrait aussi, avec quelques amliorations, dfaire une action. Lide est trs simple : chaque commande annulable que nous crons possde deux mthodes. Aux cts de la mthode habituelle execute, qui dclenche une action, nous ajoutons la mthode unexecute pour lannuler. Lorsque lutilisateur fait des changements nous crons une commande aprs lautre en les excutant immdiatement pour provoquer la modication. Mais ces commandes doivent tre stockes dans lordre dexcution quelque part dans une liste. Si lutilisateur change davis et dcide dannuler la modication, nous serons ainsi en mesure de retrouver la dernire commande dans la liste et nous pourrons appeler unexecute. Nous pouvons mme permettre lutilisateur de remonter aussi loin quil le souhaite dans lhistorique des actions en annulant des modications une par une. Refaire la modication, cest--dire la possibilit de changer davis une fois de plus et de rappliquer les changements qui viennent dtre annuls, sinscrit lgamment dans la mme veine de conception. Pour refaire les actions il suft de rexcuter les commandes en commenant par la dernire annule.

Chapitre 8

Effectuer des actions avec Command

131

Rendons lexplication un peu plus concrte et retournons lexemple de linstalleur. Supposons que la demande ne consiste pas simplement pouvoir expliquer ce que nous faisons avec le systme de lutilisateur mais que nous devions aussi offrir la possibilit de revenir en arrire si lutilisateur juge que linstallation est une mauvaise ide. On commence par ajouter la mthode unexecute la commande CreateFile :
class CreateFile < Command def initialize(path, contents) super "Create file: #{path}" @path = path @contents = contents end def execute f = File.open(@path, "w") f.write(@contents) f.close end def unexecute File.delete(@path) end end

La mthode unexecute porte bien son nom : elle supprime le chier cr par la commande execute. Ce que la mthode execute nous donne, la mthode unexecute nous le retire. Un d un peu plus srieux nous attend avec la commande DeleteCommand car elle est destructive par nature. Pour annuler lopration de suppression nous sommes obligs de sauvegarder le contenu du chier dorigine avant de le supprimer1. Dans un vrai systme, on copierait probablement le contenu du chier dans un rpertoire temporaire, mais pour lexemple contentons-nous de le stocker dans la mmoire :
class DeleteFile < Command def initialize(path) super "Delete file: #{path}" @path = path end def execute if File.exists?(@path) @contents = File.read(@path)

1. Le lecteur sagace (cest--dire vous) aurait dj compris que laction CreateFile pourrait aussi tre destructive. Il est possible que le chier que lon essaie de crer existe dj et soit cras par le nouveau. Dans un systme rel nous devons grer cette possibilit ainsi quun tas de questions lies aux droits daccs et de proprit du chier. An de prserver la simplicit des exemples, je vais ignorer toutes ces questions. Parfois, cest bien de se contenter dcrire des exemples.

132

Patterns en Ruby

end f = File.delete(@path) end def unexecute if @contents f = File.open(@path,"w") f.write(@contents) f.close end end end

Lajout de la mthode unexecute CopyFile soulve les mmes questions que DeleteFile. Avant de faire une copie laide de la mthode execute il faudrait vrier que le chier cible existe et, si cest le cas, sauvegarder son contenu. La mthode unexecute devra rtablir le contenu du chier sil existait ou bien leffacer sil nexistait pas auparavant. Enn, nous avons besoin dajouter la mthode unexecute la classe CompositeCommand :
class CompositeCommand < Command # ... def unexecute @commands.reverse.each { |cmd| cmd.unexecute } end # ... end

La mthode unexecute est gnralement linverse de la mthode execute, elle annule les sous-commandes. Remarquez que nous appelons la mthode reverse sur les tableaux de commandes avant ditrer dessus car pour annuler les actions il faut commencer par la dernire commande et remonter lhistorique vers la premire.

Crer des les de commandes


Le pattern Command peut galement tre utile dans les situations o il faut accumuler plusieurs oprations pour ensuite les excuter ensemble. Cest le mode de fonctionnement courant des installeurs. Un assistant dinstallation typique vous permet de dire "oui, je veux le programme de base, oui, je veux la documentation, mais je ne veux pas de chiers dexemples". Lorsque vous congurez linstalleur, il compose une sorte de liste des choses faire : copier le programme, copier la documentation, etc. la n lassistant vous donne la possibilit de changer davis. Les choses ne se font vritablement quaprs avoir cliqu sur le bouton dinstallation. Il est vident que cette liste de tches constitue l encore une liste de commandes.

Chapitre 8

Effectuer des actions avec Command

133

Une situation similaire se produit si une srie doprations doit tre excute et que chacune des oprations, excute seule, a un cot de dmarrage lev. Par exemple, une petite ternit est souvent ncessaire pour se connecter une base de donnes. Si un bon nombre doprations sont raliser sur la base de donnes on se retrouve alors face un choix dsagrable : (1) soit conserver une connexion ouverte constamment et gcher une ressource prcieuse, (2) soit dpenser le temps ncessaire pour ouvrir et fermer la connexion chaque opration. Dans ce cas de gure, le pattern Command offre une solution. Au lieu dexcuter chaque opration comme une tche indpendante, on accumule les commandes dans une liste. La connexion la base de donnes peut tre ouverte priodiquement pour excuter toutes les commandes accumules, ensuite, la liste est vide.

User et abuser du pattern Command


Le pattern Command prsente une particularit qui tend provoquer un usage excessif. Aussi concis que puisse tre un pattern Command en Ruby,
class FileDeleteCommand def initialize(path) @path = path end def execute File.delete(@path) end end fdc = FileDeleteCommand.new(foo.dat) fdc.execute

il nen demeure pas moins que rien nest plus simple que de faire les choses directement :
File.delete(foo.dat)

Lune des caractristiques cls du pattern Command tient sa capacit sparer la rexion de laction. Lorsque vous utilisez ce pattern, vous ne dites plus "fais ceci". Vous dites plutt "souviens-toi comment faire ceci" et ensuite, plus tard, "fais laction que tu as retenue". Mme la version lgre du pattern Command fonde sur des blocs de code Ruby ajoute un srieux niveau de complexit en raison de ce processus en deux tapes. Assurez-vous que cette complexit est justie avant de dgainer le pattern Command. Dans lhypothse o vous avez vraiment besoin du pattern Command, il faut vous assurer que vous avez bien pens tout avant de le faire fonctionner. Rchissez bien toutes les circonstances dans lesquelles votre objet commande peut se trouver tant

134

Patterns en Ruby

ltape de cration qu celle de lexcution. Oui, le chier principal est ouvert et lobjet crucial est initialis lorsque je cre la commande. Est-ce que ce sera encore le cas quand la commande sera excute ? Dhabitude, pour des commandes simples, prvoir correctement le contexte de cration et dexcution nest pas trs difcile. En gnral, il suft de sauvegarder tous les arguments de lopration dans lobjet commande. Mais les commandes annulables requirent une vigilance particulire. Beaucoup doprations sont destructives : elles effacent des donnes. Si vous envisagez de crer une commande annulable il faut trouver un moyen de prserver dans lobjet commande les donnes susceptibles dtre effaces lexcution. Cela vous permettra de restaurer ces donnes en cas dannulation.

Le pattern Command dans le monde rel


Comme lindique lintroduction de ce chapitre, on trouve souvent le pattern Command dans des frameworks dinterface graphique. Les frameworks Tk et FXRuby permettent tous les deux dassocier des commandes fondes sur des blocs de code avec des lments graphiques tels que boutons et autres entres de menu. Mais on peut galement voir le pattern Command dans dautres parties de la base de code Ruby. Migration ActiveRecord La fonctionnalit des migrations dActiveRecord1 est dote dune implmentation classique du pattern Command annulable. Les migrations ActiveRecord permettent au programmeur de dnir le schma de sa base de donnes indpendamment du moteur de base de donnes utilis. Ce code est crit en Ruby, naturellement. Les migrations Rails constituent un exemple tout fait pertinent pour ce chapitre car chaque partie du schma est organise comme une commande. La migration ci-aprs, par exemple, cre dans la base de donnes la table books :
class CreateBookTable < ActiveRecord::Migration def self.up create_table:books do |t| t.column:title,:string t.column:author,:string end end def self.down drop_table:books end end

1. Comme vous le savez, ActiveRecord est linterface des bases de donnes utilise par Rails.

Chapitre 8

Effectuer des actions avec Command

135

Notez que le code de cration gure dans la mthode up. La mthode up est quivalente la mthode execute, elle soccupe du travail de cration de la table books. Notre migration propose galement la mthode down, qui annule leffet de la commande la migration et supprime la table cre par la mthode up. Une application Rails typique dnit toute une liste des classes de migrations telles que la classe ci-dessus. Elles ajoutent des classes au fur et mesure des volutions de la base de donnes. La beaut des migrations tient au fait quil est possible davancer le schma ltape suivante ou de retourner ltat prcdent laide des mthodes up et down. Madeleine Un autre exemple remarquable du pattern Command dans du vrai code Ruby se trouve dans Madeleine. Madeleine est une implmentation Ruby de Prevayler, un projet qui a ses racines dans le monde Java, mais qui sest rpandu dans de nombreux autres langages. Madeleine est un framework transactionnel haute performance pour la persistance dobjets qui na pas besoin de mappage objet relationnel pour la simple raison quil nutilise pas de base de donnes relationnelle. Madeleine se e au package Ruby Marshal : un module Ruby qui permet de convertir des objets Ruby en octets et inversement, les octets en objets. Malheureusement, la sriation de vos objets dans des chiers nest pas une solution complte la persistance dapplications. Imaginez la lenteur de votre systme si vous deviez rcrire laffectation des siges pour lensemble des avions dune compagnie arienne chaque fois que quelquun souhaite changer de place dans un avion. Les choses se passeraient bien plus rapidement si lon pouvait nenregistrer que les changements : sauvegarder ltat initial de vos objets, et puis crire les modications au fur et mesure de ses volutions. Attendez, cela a lair trangement familier... Pour avoir une ide de Madeleine, dveloppons un systme simple de gestion des ressources humaines. Commenons par lomniprsente classe Employee :
require rubygems require madeleine class Employee attr_accessor:name,:number,:address def initialize(name, number, address) @name = name @number = number @address = address end def to_s "Employee: name: #{name} num: #{number} addr: #{address}" end end

136

Patterns en Ruby

Ensuite, nous allons crer une classe de gestion des employs. Cette classe conserve un tableau associatif des employs en utilisant leurs numros comme cls. La classe EmployeeManager nous permet dajouter un employ, de supprimer un employ existant, de modier son adresse et de retrouver son numro :
class EmployeeManager def initialize @employees = {} end def add_employee(e) @employees[e.number] = e end def change_address(number, address) employee = @employees[number] raise "No such employee" if not employee employee.address = address end def delete_employee(number) @employees.remove(number) end def find_employee(number) @employees[number] end end

Rien de trs sensationnel pour le moment, mais lintrigue va spaissir. Dnissons un ensemble des commandes, une pour chaque opration supporte par la classe EmployeeManager. Au dbut, nous avons AddEmployee : la commande pour insrer un nouvel Employee dans le tableau associatif EmployeeManager. Tout comme les autres commandes, la classe AddEmployee consiste en une mthode initialize qui stocke sufsamment dinformations pour pouvoir rpter la commande et la mthode execute qui excute vritablement la commande :
class AddEmployee def initialize(employee) @employee = employee end def execute(system) system.add_employee(@employee) end end

Les commandes de suppression, de changement dadresse et de recherche sont trs similaires :


class DeleteEmployee def initialize(number) @number = number end

Chapitre 8

Effectuer des actions avec Command

137

def execute(system) system.delete_employee(@number) end end class ChangeAddress def initialize(number, address) @number = number @address = address end def execute(system) system.change_address(@number, @address) end end class FindEmployee def initialize(number) @number = number end def execute(system) system.find_employee(@number) end end

Venons-en maintenant la partie intressante : crons un nouvel objet Madeleine en lui passant le nom du dossier o sauvegarder nos donnes ainsi quun bloc de code pour crer une nouvelle instance dEmployeeManager:
store = SnapshotMadeleine.new(employees) { EmployeeManager.new }

Nous avons galement besoin dun thread pour sauvegarder intervalles rguliers ltat des objets stocks. Notre thread demande que Madeleine enregistre ltat du systme sur le disque toutes les 20 secondes :
Thread.new do while true sleep(20) madeleine.take_snapshot end end

Pendant que ce thread est actif, nous pouvons commencer envoyer des commandes dans notre systme de ressources humaines :
richard = Employee.new(Richard, 1001,1 rue des Rails) laurent = Employee.new(Laurent,1002,34 avenue Ruby) store.execute_command(AddEmployee.new(richard)) store.execute_command(AddEmployee.new(laurent))

Lorsque Richard et Laurent se retrouvent dans le systme de stockage de Madeleine, on peut lancer quelques requtes :

138

Patterns en Ruby

puts(store.execute_command(FindEmployee.new(1001))) puts(store.execute_command(FindEmployee.new(1002)))

Le rsultat afch sera :


Employee: name: Richard num: 1001 addr: 1 rue des Rails Employee: name: Laurent num: 1002 addr: 34 avenue Ruby

On peut mme changer ladresse de Richard :


store.execute_command(ChangeAddress.new(1001, 55 rue Merb))

Madeleine est un formidable exemple du pattern Command. Au fur et mesure que les commandes (ajouter un employ ou changer une adresse prcise) arrivent dans le systme, Madeleine modie la reprsentation des donnes dans la mmoire laide de ce pattern. Mais la commande est galement crite dans un chier. Lorsque le systme sarrte, Madeleine est capable de restaurer ltat correct en lisant la dernire capture des donnes et en appliquant toutes les commandes en attente. Au bout dun intervalle donn 20 secondes dans notre exemple , on crit une nouvelle capture des donnes courantes et on nettoie toutes les commandes accumules sur le disque.

En conclusion
Avec le pattern Command nous construisons des objets qui savent accomplir des tches spciques. Le mot cl est ici "spcique". En effet, une instance de commande du pattern Command ne sait pas changer ladresse de nimporte quel employ, mais elle est capable de faire dmnager un employ prcis. Les commandes sont utiles pour maintenir une liste des actions excuter ou pour se souvenir des actions dj effectues. Vous pouvez galement lancer vos commandes dans le sens inverse pour annuler les modications apportes. Les commandes peuvent tre implmentes comme des classes compltes ou de simples blocs de code en fonction de leur complexit. Le pattern Command a beaucoup de points communs avec le pattern Observer. Les deux identient un objet une commande dans le cas de Command et un observateur dans le cas dObserver qui est appel par un autre participant du pattern. Cet objet que je passe au bouton graphique est-il une commande (cest--dire laction excuter lorsque le bouton est slectionn) ou bien est-ce un observateur qui attend la notication du changement de ltat du bouton ? La rponse est : cela dpend. Lobjet commande est capable de faire une action mais ne sintresse pas particulirement ltat de lobjet qui provoque son excution. Un observateur est au contraire concern par ltat du sujet, cest--dire lobjet qui la appel. Command ou Observer : vous de voir.

9
Combler le foss avec lAdapter
Tout comme la plupart des gens qui aiment bricoler des appareils lectroniques, je dispose, dans ma cave, dune bote remplie ras bord de pices lectroniques. Sur le haut de la bote se trouve mon dle adaptateur USB-PS2, qui me permet de brancher un clavier USB sur un vieux Pentium 250 caboss que je conserve encore pour une raison inconnue. Si lon fouille un peu, on trouve ensuite une couche de convertisseurs de port srie vers port parallle. Et au fond de la bote on tombe sur des petites pices dalimentation lectrique noires. Tous ces gadgets ont un point commun : ils permettent de connecter deux appareils qui aimeraient communiquer mais qui ne peuvent pas le faire pour cause de broches alignes diffremment, de taille de prise inadapte ou encore parce que le voltage de sortie de lun des appareils serait plus que sufsant pour envoyer lautre au paradis des gadgets. Bref, ce sont des adaptateurs ! Le monde des logiciels a besoin des adaptateurs davantage que le monde du matriel. Les logiciels ne possdent pas de caractristiques physiques : un dveloppeur ne stipule pas que les broches dun connecteur doivent tre espaces dexactement 1,5 mm. Comme les logiciels sont le fruit de nos ides et puisque nous pouvons coder des interfaces aussi vite que nos doigts tapent au clavier, nous, les dveloppeurs, avons des possibilits quasi illimites crer des objets incompatibles, des objets qui aimeraient communiquer, mais qui ny parviennent pas cause du dsaccord qui rgne parmi les interfaces. Dans ce chapitre nous examinerons des adaptateurs du monde logiciel. Nous verrons comment des adaptateurs nous aident combler le vide entre des interfaces discordantes. Nous apprendrons galement comment utiliser une des fonctionnalits les plus

140

Patterns en Ruby

surprenantes de Ruby la possibilit de modier des objets et des classes la vole, au moment de lexcution pour faciliter la tche de cration des adaptateurs.

Adaptateurs logiciels
Pour commencer, imaginons que nous disposons dune classe permettant de crypter des chiers :
class Encrypter def initialize(key) @key = key end def encrypt(reader, writer) key_index = 0 while not reader.eof? clear_char = reader.getc encrypted_char = clear_char ^ @key[key_index] writer.putc(encrypted_char) key_index = (key_index +1) % @key.size end end end

La mthode encrypt de la classe Encrypter ncessite deux chiers ouverts, lun en lecture, lautre en criture, ainsi quune cl dencryption. Cette mthode crit, octet par octet, la version crypte du chier dentre dans le chier de sortie. 1 Lusage de la classe Encrypter pour crypter un chier ordinaire est simple. Il suft douvrir les deux chiers et dappeler encrypt avec la cl secrte de votre choix :
reader = File.open(message.txt) writer = File.open(message.encrypted,w) encrypter = Encrypter.new(my secret key) encrypter.encrypt(reader, writer)

Et maintenant le pige : quarrive-t-il si les donnes encoder sont contenues dans une chane de caractres au lieu dun chier ? Dans ce cas, nous aurions besoin dun objet qui, vu de lextrieur, prendrait lallure dun chier et qui offrirait donc la mme interface quun objet Ruby IO mais qui, intrieurement, rcuprerait ses caractres dans une chane. Cest dun StringIOAdapter dont nous avons besoin :
1. La classe Encrypter applique un vnrable algorithme dencryption. Pour obtenir le texte crypt elle calcule le OU exclusif (parfois nomm XOR) pour chaque caractre du texte dentre avec le caractre correspondant de la cl. La cl est rpte autant de fois que ncessaire. Cet algorithme lgant est compltement autonome : pour dcrypter le texte il suft de rexcuter la procdure sur le texte crypt avec la mme cl.

Chapitre 9

Combler le foss avec lAdapter

141

class StringIOAdapter def initialize(string) @string = string @position = 0 end def getc if @position >= @string.length raise EOFError end ch = @string[@position] @position += 1 return ch end def eof? return @position >= @string.length end end

Notre StringIOAdapter comprend deux variables dinstance : la rfrence vers la chane de caractres et lindex de la position. chaque appel de getc, StringIOAdapter renvoie le caractre la position courante et incrmente lindex. Lorsquil ny a plus de caractres dans la chane, la mthode getc lance une exception. La mthode eof? retourne true sil ny a plus de caractres et false dans les autres cas. Pour utiliser Encrypter avec StringIOAdapter nous remplaons simplement le chier dentre par ladaptateur :
encrypter = Encrypter.new(XYZZY) reader = StringIOAdapter.new(We attack at dawn) writer = File.open(out.txt, w) encrypter.encrypt(reader, writer)

Comme vous lavez devin, la classe StringIOAdapter est un exemple dadaptateur. Un adaptateur est un objet qui fait le lien entre linterface existante et linterface souhaite.
Figure 9.1
Le pattern Adapter
Client Target

Adapter

Adaptee

Le diagramme de classes du pattern Adapter est prsent la Figure 9.1. Voici lide principale de ce diagramme : le client a connaissance dune classe cible en tant que client jai une rfrence vers mon objet cible. Le client sattend trouver une certaine

142

Patterns en Ruby

interface dans la classe cible. Il nest pas inform quen vrit lobjet cible est un adaptateur qui conserve une rfrence vers un autre objet fournisseur de la fonctionnalit. Dans un monde parfait toutes les interfaces seraient parfaitement compatibles et le client sadresserait directement lobjet adapt. Nanmoins, dans le monde rel nous sommes obligs de dvelopper des adaptateurs car linterface requise par le client ne correspond pas linterface propose par lobjet existant. Revenons notre exemple. Encrypter est notre objet client, il recherche une rfrence vers lobjet cible, qui dans ce cas est une instance dIO. En ralit, le client obtient une rfrence vers ladaptateur StringIOAdapter (voir Figure 9.2).
Figure 9.2
Le fonctionnement de lobjet StringIOAdapter
Encrypter (client) reader.getc

StringIOAdapter (adaptateur)

@string[@position]

String (la classe existante)

La classe StringIOAdapter ressemble de lextrieur un objet IO classique, mais elle rcupre secrtement ses caractres dans lobjet existant : une chane de caractres.

Les interfaces presque parfaites


Les situations probablement les plus frustrantes qui requirent un adaptateur sont celles o linterface existante correspond presque mais pas tout fait linterface requise. Par exemple, imaginez que nous crivions une classe pour afcher du texte lcran :
class Renderer def render(text_object) text = text_object.text size = text_object.size_inches color = text_object.color # afficher le texte ... end end

Chapitre 9

Combler le foss avec lAdapter

143

Il est clair que lobjet Renderer sattend afcher un objet qui ressemble ceci :
class TextObject attr_reader:text,:size_inches,:color def initialize(text, size_inches, color) @text = text @size_inches = size_inches @color = color end end

Malheureusement, on dcouvre quune partie du texte afcher est encapsule dans un objet qui ressemble plutt ceci :
class BritishTextObject attr_reader:string,:size_mm,:colour # ... end

La bonne nouvelle, cest que BritishTextObject contient tout ce qui est fondamentalement ncessaire pour afcher du texte. La mauvaise, cest que le champ qui encapsule le texte sappelle string au lieu de text, la taille du texte est en millimtre plutt quen pouce et, qui plus est, lattribut colour scrit chez nos amis grands-bretons avec la lettre "u". Pour rsoudre le problme, on peut certainement recourir au pattern Adapter :
class BritishTextObjectAdapter < TextObject def initialize(bto) @bto = bto end def text return @bto.string end def size_inches return @bto. size_itim / 25.4 end def color return @bto.colour end end

Cest une possibilit... Mais on pourrait tirer parti de la capacit qua Ruby modier des classes la vole.

144

Patterns en Ruby

Une alternative adaptative ?


Si vous tes dbutant en Ruby, vous pensez probablement que cest un langage plutt conventionnel avec son hritage simple ainsi que ses classes, ses mthodes et ses oprateurs if incorpors. Certes, les blocs de code semblent lgrement tranges mais, nalement, ils se transforment en objets Proc qui se comportent de faon assez familire. Si cest ce que vous pensez, accrochez-vous avant de lire ce qui suit : en Ruby, toute classe est modiable tout moment ! Pour comprendre les implications de ce principe, imaginez que lon dcide de ne pas faire le lien entre linstance de BritishTextObject existante et linterface requise de TextObject. Nous changerons plutt lobjet BritishTextObject an quil possde linterface ncessaire. Pour y parvenir, il faut tout dabord sassurer que la classe BritishTextObject est charge et, ensuite, nous ouvrons la classe et lui ajoutons quelques mthodes :
# Sassurer que la classe initiale est charge require british_text_object # Rajouter des mthodes la classe initiale class BritishTextObject def color return colour end def text return string end def size_inches return size_mm / 2 5.4 end end

Le fonctionnement de ce code est trs simple. La mthode require en dbut de chier charge la classe initiale BritishTextObject. Linstruction class BritishTextObject aprs lappel require ne cre pas une nouvelle classe, mais ouvre bel et bien la classe existante pour lui ajouter quelques mthodes. Vous tes toujours l ? Les modications des classes ne sont limites en aucune manire. Il est non seulement possible dajouter des mthodes, mais on peut aussi modier et mme compltement supprimer des mthodes existantes. Le plus surprenant vient peut-tre du fait que toutes ces oprations sont possibles aussi bien sur les classes standard de Ruby que sur vos propres classes. On pourrait, par exemple, vandaliser la mthode abs de Fixnum :
# Ne faites jamais ceci! class Fixnum

Chapitre 9

Combler le foss avec lAdapter

145

def abs return 42 end end

Selon la version "amliore" dabs, la valeur absolue de nimporte quel Fixnum est gale 42. Donc, les deux instructions
puts(79.abs) puts(-1234.abs)

afchent
42 42

La possibilit de modier des classes est un des secrets qui procurent Ruby sa exibilit et sa puissance. Toutefois, comme lillustre lexemple prcdent, une grande puissance implique de grandes responsabilits.

Modier une instance unique


Puisque modier une classe entire la vole parat une solution quelque peu extrme, Ruby propose une alternative moins invasive. Au lieu de modier une classe, on peut apporter des changements au comportement dune instance donne :
bto = BritishTextObject.new(hello, 50.8,:blue) class << bto def color colour end def text string end def size_inches return size_mm/25.4 end end

La ligne cl est ici :


class << bto

Ce code est essentiellement une instruction permettant de modier le comportement de lobjet bto indpendamment de sa classe. Une autre syntaxe est disponible : pour obtenir le mme effet, on peut simplement dnir des mthodes sur une instance :

146

Patterns en Ruby

def bto.color colour end def bto.text string end # ...

Ruby nomme les mthodes propres un objet donn "des mthodes singleton" 1. Il savre quune majorit des objets Ruby2 possdent une classe plus ou moins secrte en plus de leur classe normale. Comme le montre la Figure 9.3, cette seconde classe singleton est en fait le premier endroit que linterprteur Ruby inspecte lorsquune mthode est appele. Toute mthode dnie dans une classe singleton surcharge les mthodes de la classe habituelle3. Le code prcdent modie la classe singleton de lobjet bto. Les changements sont apports avec une discrtion extrme car mme aprs modication lobjet se considre toujours comme une instance de la classe dorigine 4.
Figure 9.3
Les mthodes Singleton de linstance de BritishTextObject
BritishTextObject colour() string() size_mm()

(classe singleton bto) color() text() size_inches()

bto (instance)

1. Le nom singleton est un terme regrettable. Ces mthodes nont rien en commun avec le pattern Singleton, que nous examinerons au Chapitre 12. 2. Les objets immuables par exemple des instances de Fixnum nacceptent pas lajout des mthodes singleton. 3. Les mthodes singleton surchargent galement les mthodes de tout module inclus dans la classe. 4. Pas si discret que cela, nalement : on peut se renseigner sur des mthodes singleton dun objet laide de la mthode singleton_methods.

Chapitre 9

Combler le foss avec lAdapter

147

Adapter ou modier ?
Il est indniable que le code gagne en simplicit lorsque le dveloppeur opte pour la modication de la classe ou de son instance plutt que pour la cration dun adaptateur. Si lon modie lobjet dorigine ou sa classe, il ny a nul besoin de crer une classe adaptateur supplmentaire ni de se soucier de la manire dencapsuler lobjet existant dans ladaptateur. Les choses fonctionnent. Pourtant, cette technique de modication implique des atteintes srieuses lencapsulation : vous plongez lintrieur dune classe ou dun objet et vous en modiez limplmentation. Dans quelle situation utiliser un adaptateur et quand est-il acceptable de remanier la tuyauterie interne dune classe ? Comme toujours, un peu de pragmatisme reste la meilleure recette. Prfrez la modication dune classe dans les circonstances suivantes :
m

Les modications sont simples et claires. Les alias de mthodes dans le code dexemple donn ci-dessus en sont un parfait exemple. Vous comprenez bien la classe modier ainsi que son usage. Une intervention chirurgicale sur une classe que vous navez pas tudie au pralable mnerait probablement des problmes.

Prfrez la solution adaptateur dans les situations suivantes :


m

Les discordances entre interfaces sont complexes et tendues. Par exemple, vous ne devriez probablement pas essayer de modier une chane de caractres an de lui affecter linterface de Fixnum. Vous ne comprenez pas le fonctionnement de la classe. Lignorance est une srieuse raison pour rester prudent.

Lingnierie est la science des compromis. Les adaptateurs prservent lencapsulation au prix dune certaine complexit. Vous gagnez en simplicit si vous modiez la classe, mais vous tes oblig de plonger dans les dtails de son implmentation.

User et abuser du pattern Adapter


Un des avantages du typage la canard de Ruby rside dans la possibilit de crer des adaptateurs pour une partie de linterface cible effectivement utilise par le client. Par exemple, les objets IO proposent un grand nombre de mthodes. Un vrai objet IO vous permet de lire des lignes, de faire des recherches dans un chier ainsi quun tas dautres

148

Patterns en Ruby

choses lies aux chiers. Mais notre objet StringIOAdapter implmente exactement deux mthodes : getc et eof?. Cest sufsant dans notre exemple car ce sont les deux mthodes de la classe IO rellement utilises par la classe Encrypter. Les adaptateurs avec des implmentations partielles sont des lames double tranchant : dune part, il est pratique dimplmenter le strict minimum mais, dautre part, votre programme ne fonctionnerait plus si le client dcidait soudainement dappeler une mthode que vous navez pas jug utile dimplmenter.

Le pattern Adapter dans le monde rel


On peut trouver un exemple classique du pattern Adapter dans ActiveRecord, le systme de mappage objet relationnel de Ruby on Rails. ActiveRecord doit grer tout un ventail de systmes de bases de donnes diffrentes : MySQL, Oracle et Postgres, sans parler de SQLServer. Tous ces systmes fournissent une API Ruby, ce qui est bien. Mais toutes les API sont diffrentes, ce qui est trs ennuyeux. Par exemple, lorsque vous tablissez une connexion vers une base de donnes MySQL et que vous devez excuter du code SQL, il faut appeler la mthode query :
results = mysql_connection.query(sql)

Mais, si vous utilisez Sybase, la mthode appeler est sql :


results = sybase_connection.sql(sql)

Et, si vous avez affaire Oracle, il faut appeler la mthode execute pour obtenir en retour un pointeur vers le rsultat et non le rsultat lui-mme. On dirait que les diteurs de bases de donnes ont complot pour sassurer que les systmes ne soient pas compatibles. ActiveRecord gre ces diffrences par une interface standardise, encapsule dans la classe AbstractAdapter. Cette classe dnit une interface vers la base de donnes. Cette interface unique est utilise partout dans ActiveRecord. Par exemple, AbstractAdapter expose une mthode standard select_all pour excuter une requte SQL select et retourner le rsultat. Une sous-classe de la classe AbstractAdapter est disponible pour chacune des bases de donnes : il existe MysqlAdapter, OracleAdapter ainsi que SybaseAdapter. Chaque adaptateur implmente la mthode select_all en se fondant sur lAPI du systme de base de donnes correspondant. Enn, notre exemple StringIOAdapter est inspir par la classe StringIO, qui est distribue avec Ruby.

Chapitre 9

Combler le foss avec lAdapter

149

En conclusion
Un adaptateur na rien de magique. Cest un objet qui absorbe la diffrence qui peut exister entre des interfaces requises et des objets existants. Un adaptateur expose au monde extrieur linterface ncessaire, mais cette interface est implmente en faisant des appels un objet cach lintrieur. Cet objet fournit toute la fonctionnalit ncessaire, mais laide dune interface inadapte. Ruby propose galement une deuxime manire plus limite de rsoudre le problme dinterface inadapte : nous pouvons simplement modier lobjet au moment de lexcution pour lui attribuer linterface requise. Formul autrement, nous pouvons forcer lobjet se soumettre. Le choix entre adaptateur ou modication dynamique dun objet se dtermine par votre comprhension de la classe concerne et les considrations lies lencapsulation. Si vous matrisez le fonctionnement de la classe et que les changements dinterface soient relativement mineurs, modier lobjet peut tre la bonne solution. Si lobjet est complexe et si vous ne comprenez pas compltement son fonctionnement, optez pour un adaptateur classique. Le pattern Adapter est le premier membre dune famille que nous allons tudier dans les chapitres qui suivent : la famille des patterns dans lesquels un objet en remplace un autre. Cette famille dimposteurs orients objet inclut galement des proxies (mandataires) et des dcorateurs. Dans les deux cas un objet agit en tant que porte-parole dun autre objet. Comme vous le verrez par la suite, le code de ces patterns se ressemble. Au risque de me rpter, noubliez pas quun pattern ne se rduit pas un bout de code : lintention est cruciale. Un adaptateur est seulement un adaptateur dans le cas o vous vous retrouvez avec des objets dont les interfaces sont inadaptes et que vous voulez isoler leffort de gestion de ces interfaces incompatibles dans votre systme.

10
Crer un intermdiaire pour votre objet avec Proxy
Le monde du gnie logiciel ne manque pas dironie. On peut travailler jour et nuit pendant des mois pour crer un objet BankAccount (un pur chef-duvre technique qui permet aux clients de grer leurs comptes bancaires) et passer quelques mois de plus pour restreindre laccs cet objet sauf pour quelques utilisateurs autoriss, pour nalement sentendre dire par le patron que les utilisateurs autoriss nont mme pas besoin de lobjet BankAccount. Ou, tout au moins, ils nen ont pas besoin sur leurs machines : ce serait bien sil pouvait utiliser la classe BankAccount distance partir dun serveur car les utilisateurs ne voudront jamais installer cet objet sur leur ordinateur. Et, bien entendu, cest au moment o on commence entendre dans la bouche du patron largument massue "il nous faut ce code tout de suite" quune toute nouvelle demande nous arrive : au moment de lexcution il faut retarder autant que possible la cration des objets BankAccount pour des raisons de performance. Aussi indpendants que ces problmes puissent paratre contrler laccs un objet, fournir un moyen daccs lobjet quel que soit son emplacement ou retarder sa cration , les trois ont une solution commune : le pattern Proxy. Dans ce chapitre, nous allons tudier le pattern Proxy, examiner la faon traditionnelle de limplmenter et essayer de lappliquer pour rsoudre nos trois problmes. Enn, nous allons sortir de notre chapeau magique une technique Ruby qui rend la tche de dveloppement dun proxy aussi facile crire quune simple mthode.

152

Patterns en Ruby

Figure 10.1
Le pattern Proxy
Service do_something()

RealService do_something()

ServiceProxy @subject do_something() def do_something @subject.do_something end

Les proxies la rescousse


Le pattern Proxy se fonde essentiellement sur un petit mensonge. Lorsquun client sollicite un objet par exemple lobjet BankAccount , il faut effectivement lui retourner un objet. Nanmoins, lobjet retourn nest pas tout fait lobjet que le client sattend recevoir. Nous retournons un objet imposteur qui fournit une interface identique celle de lobjet attendu. Les membres du GoF ont nomm cet objet contrefait un proxy (voir Figure 10.1). Il contient une rfrence vers le vritable objet, le sujet, qui est cach lintrieur. Lorsque le code client appelle une mthode sur le proxy, lappel est transfr vers le vrai objet. Pour concrtiser cette ide, faisons un peu de nance. Le code ci-aprs est une classe qui sert garder un suivi dun compte bancaire :
class BankAccount attr_reader:balance def initialize(starting_balance=0) @balance = starting_balance end def deposit(amount) @balance += amount end def withdraw(amount) @balance -= amount end end

Les instances de BankAccount seront nos vrais objets, ce que nous appelons nos sujets. Dnissons un proxy pour BankAccount :
class BankAccountProxy def initialize(real_object) @real_object = real_object end

Chapitre 10

Crer un intermdiaire pour votre objet avec Proxy

153

def balance @real_object.balance end def deposit(amount) @real_object.deposit(amount) end def withdraw(amount) @real_object.withdraw(amount) end end

Maintenant, on peut crer un objet BankAccount ainsi quun proxy et les utiliser comme objets interchangeables :
account = BankAccount.new(100) account.deposit(50) account.withdraw(10) proxy = BankAccountProxy.new(account) proxy.deposit(50) proxy.withdraw(10)

Rien dextraordinaire ne se passe dans BankAccountProxy. Cette classe prsente exactement la mme interface que son sujet, lobjet BankAccount. Mais le proxy ne matrise pas la nance. Lorsquune de ses mthodes est appele, lobjet BankAccountProxy dlgue lappel de mthode son sujet BankAccount. videmment, si notre proxy ne faisait quenvoyer btement un cho des appels de mthodes vers le sujet, le seul rsultat que nous aurions accompli, cest dimposer une charge supplmentaire au processeur. Mais avec un proxy nous avons un objet intermdiaire entre le client et le vrai objet. Si lon voulait contrler laccs un compte bancaire, le proxy serait lendroit parfait pour ce type de code.

Un proxy de contrle daccs


Transformons notre BankAccountProxy gnrique qui ne fait rien en un proxy de contrle daccs au sujet. Pour y arriver, il suft dajouter une vrication au dbut de chaque mthode :
require etc class AccountProtectionProxy def initialize(real_account, owner_name) @subject = real_account @owner_name = owner_name end

154

Patterns en Ruby

def deposit(amount) check_access return @subject.deposit(amount) end def withdraw(amount) check_access return @subject.withdraw(amount) end def balance check_access return @subject.balance end def check_access if Etc.getlogin!= @owner_name raise "Illegal access: #{Etc.getlogin} cannot access account." end end end

Chaque opration sur le compte est protge par un appel la mthode check_access. La mthode check_access porte bien son nom : elle vrie que lutilisateur courant est autoris accder au compte. La version de check_access utilise dans notre exemple se sert du module Etc1 pour rcuprer le nom de lutilisateur courant. Ce nom est ensuite compar avec le nom du propritaire du compte qui est pass dans le constructeur. videmment, on aurait pu inclure le code de vrication directement dans lobjet BankAccount. Mais lutilisation dun proxy de contrle daccs nous donne un avantage : une bonne sparation de responsabilits. Le proxy dcide qui a le droit deffectuer laction demande. La seule chose dont un objet BankAccount doit soccuper, cest de grer le compte bancaire. Limplmentation du contrle daccs dans un proxy nous permet de remplacer facilement lalgorithme de gestion de scurit (il suft dencapsuler le sujet dans un proxy diffrent) ou de supprimer ce niveau de scurit compltement (ne pas utiliser de proxy). Inversement, nous pouvons modier limplmentation de lobjet BankAccount sans affecter la gestion de scurit. Les proxies de contrle daccs prsentent aussi lavantage de sparer proprement la fonction de protection de la logique mtier de lobjet rel, minimisant ainsi le risque quune information importante transpire travers la couche de protection.
1. Le module Etc est plus ou moins standard dans Ruby. Il fait partie de la distribution Ruby pour des systmes UNIX et des systmes semblables. La version Windows est facultative, mais elle est largement disponible. La version Windows sinstalle en un clic laide de linstalleur Ruby, je vais donc partir du principe que vous lavez.

Chapitre 10

Crer un intermdiaire pour votre objet avec Proxy

155

Des proxies distants


Pour certaines applications il se peut que la scurit ne soit pas votre souci premier mais que le nud de laffaire soit relatif lemplacement o lapplication va sexcuter. Imaginons que vous ayez un programme sur une machine client et que vous vouliez utiliser lobjet BankAccount mais que cet objet se trouve sur un serveur trs loin sur le rseau. Lune des solutions consisterait faire travailler le programme client : il faudrait alors crer et envoyer des paquets et donc grer toute la complexit de la communication travers le rseau (potentiellement instable). Lalternative consiste cacher la complexit derrire un proxy, cest--dire un objet qui rside sur une machine client et prsente au code client exactement la mme interface quun objet BankAccount. Lorsquune requte arrive, le proxy prend en charge tout le travail dempaquetage de la requte, lenvoie sur le rseau, attend la rponse, la dcode et la retourne au client, qui ny voit que du feu. Du point de vue du client, il fait appel ce quil pense tre un vrai objet BankAccount, et plus tard (probablement beaucoup plus tard) il obtient la rponse. Quasiment tous les systmes dappels de procdures distants (RPC ou Remote Procedure Call en anglais) fonctionnent de cette faon. Voici un court exemple dun proxy distant. Le code suivant utilise le client SOAP livr avec Ruby pour crer un proxy distant dun service SOAP public qui prsente des informations mtorologiques1 :
require soap/wsdlDriver wsdl_url = http://www.webservicex.net/WeatherForecast.asmx?WSDL proxy = SOAP::WSDLDriverFactory.new( wsdl_url ).create_rpc_driver weather_info = proxy.GetWeatherByZipCode(ZipCode=>19128)

Une fois le proxy congur, le client ne se proccupe plus du fait que le service rside rellement ladresse www.webservicex.net. Il appelle simplement GetWeatherByZipCode et laisse le proxy grer les dtails de la communication rseau. Des proxies distants offrent en partie les mmes avantages que des proxies de contrle daccs. Plus particulirement, un proxy distant permet de sparer les responsabilits : le sujet peut se concentrer sur les prvisions mtorologiques ou toute autre tche mtier et laisser le soin un autre objet le proxy de se charger de la logistique de transfert des octets sur le rseau. Changer de protocole de communication (par exemple
1. Si vous dcidez de tester cet exemple, souvenez-vous que les services Web publics ont une esprance de vie extrmement courte, donc le service utilis dans lexemple peut ne pas tre accessible lorsque vous essaierez dy accder.

156

Patterns en Ruby

passer de SOAP XMLRPC) devient alors un jeu denfant puisquil suft de remplacer le proxy.

Des proxies virtuels vous rendre paresseux


Nous pouvons aussi recourir aux proxies pour retarder la cration coteuse de certains objets jusquau moment o ils deviennent absolument ncessaires. Cest prcisment la troisime demande que lon devait grer dans lexemple qui ouvre ce chapitre. Souvenezvous que la dernire exigence dans notre projet nancier tait de repousser la cration des instances de BankAccount aussi longtemps que possible. Il ne faut pas crer un vrai compte bancaire tant que lutilisateur nest pas prt lutiliser : par exemple pour dposer de largent. Mais contaminer le code client avec toute la complexit lie ce retard de cration nest pas une bonne ide non plus. La solution vient cette fois dun autre type de proxy : un proxy virtuel. Le proxy virtuel est en quelque sorte le plus grand imposteur de tous les proxies. Il se fait passer pour un vritable objet alors quil ne le connat mme pas et quil nen aura aucune connaissance. Ce nest quau moment o certaines mthodes seront sollicites que le proxy virtuel se prcipitera pour crer le vritable objet (o rcuprer laccs au vrai objet par un autre moyen). Implmenter un proxy virtuel est une chose trs simple :
class VirtualAccountProxy def initialize(starting_balance=0) @starting_balance = starting_balance end def deposit(amount) s = subject return s.deposit(amount) end def withdraw(amount) s = subject return s.withdraw(amount) end def balance s = subject return s.balance end def subject @subject ||(@subject = BankAccount.new(@starting_balance)) end end

Chapitre 10

Crer un intermdiaire pour votre objet avec Proxy

157

Le cur de notre VirtualAccountProxy est la mthode subject. Cette mthode vrie si lobjet BankAccount a dj t cr et le cre le cas chant. Pour y parvenir, la mthode subject utilise un idiome trs courant en Ruby mais la syntaxe lgrement trange :
@subject || (@subject = BankAccount.new(@starting_balance))

Cette ligne reprsente essentiellement une grande expression OR. Le premier lment de lexpression OR est @subject. Si la valeur de @subject nest pas nil, lexpression svalue cette valeur (dans notre cas, cest lobjet BankAccount). Si @subject est nil, Ruby value la partie droite de lexpression OR qui cre un nouvel objet BankAccount. Ce nouvel objet devient donc le rsultat de lexpression. Dans cette implmentation, VirtualAccountProxy est responsable de linstanciation dun nouvel objet BankAccount, ce qui est incontestablement un dfaut. Cette approche augmente srieusement le niveau de couplage entre le sujet et le proxy. La stratgie peut tre amliore en appliquant la magie des blocs Ruby :
class VirtualAccountProxy def initialize(&creation_block) @creation_block = creation_block end # Les autres mthodes sont omises... def subject @subject || (@subject = @creation_block.call) end end

Dans la nouvelle implmentation, le code de cration de proxy passe un bloc qui a pour mission de crer le compte bancaire au bon moment :
account = VirtualAccountProxy.new { BankAccount.new(10) }

Tout comme les deux autres types de proxies, les proxies virtuels permettent datteindre la sparation de responsabilits : le vrai objet BankAccount gre les transferts dargent, alors que VirtualAccountProxy soccupe de la cration de lobjet BankAccount.

liminer les mthodes ennuyeuses des proxies


Tous les proxies vus jusquici partagent une mme caractristique quelque peu embtante : la ncessit dcrire toutes les mthodes proxy de faon rptitive. Par exemple, tous les proxies dun compte bancaire sont obligs dimplmenter les mthodes deposit, withdraw et balance. Bien videmment, le nombre de mthodes peut tre bien plus grand que cela. La classe Array de Ruby, par exemple, compte 118 mthodes et

158

Patterns en Ruby

String, 142. crire 142 mthodes proxy nest pas seulement une corve, cest aussi extrmement propice aux erreurs. Est-il possible dviter dcrire toutes ces mthodes fort ennuyeuses ? Il savre que Ruby propose une solution. Cette solution est ancre dans les notions que vous avez apprises trs tt dans votre carrire oriente objet et que vous avez probablement oublies depuis longtemps. Les mthodes et le transfert des messages Si votre introduction la programmation oriente objet ressemblait la mienne, la premire journe de cours a d vous apprendre la notion de transfert des messages. On vous a probablement dit que
account.deposit(50)

signie que vous envoyez un message deposit lobjet account. videmment, si vous avez utilis par la suite un langage statiquement typ, vous avez rapidement compris que account.deposit (50) tait en fait un synonyme dappel de mthode deposit sur lobjet account. Le systme entier du typage statique tait l pour assurer que la mthode deposit tait bien prsente et quelle tait bien appele. Donc, la n de la premire journe de notre apprentissage de la programmation oriente objet, nous nous sommes tous arrts de parler de transfert de messages et avons commenc raisonner en termes dappels de mthodes. Le concept du transfert de messages aurait un sens si, en appelant account.deposit(50), la classe BankAccount tait libre de faire une action autre que simplement appeler la mthode deposit. Par exemple, la classe BankAccount devrait pouvoir utiliser une autre mthode plutt que deposit ou bien, au contraire, dcider de ne rien faire du tout. Il savre que Ruby rend toutes ces options possibles. La signication relle de account.deposit(50) en Ruby est plus proche du transfert de message que le modle dappel de mthode direct utilis dans la majorit des langages statiquement typs. Lorsquon invoque account.deposit(50), Ruby commence le traitement de manire classique : il recherche la mthode deposit dans la classe BankAccount, puis dans sa classe mre, etc. jusqu trouver la mthode ou jusqu atteindre le sommet de larbre hirarchique des classes. Si la mthode est trouve, on obtient le comportement habituel : la mthode deposit est appele et le compte est crdit de 50 euros. Mais quarrive-t-il sil ny a pas de mthode deposit ? Dans ce cas, Ruby suit un scnario un peu plus inhabituel : il appelle une autre mthode. Cette mthode du dernier

Chapitre 10

Crer un intermdiaire pour votre objet avec Proxy

159

recours est nomme method_missing. Une fois de plus, Ruby part la recherche de cette mthode dans la classe BankAccount. Si cette mthode nexiste pas dans BankAccount, Ruby remonte larbre dhritage des classes vers sa classe mre jusqu trouver la mthode ou bien jusqu atteindre la classe Object. La recherche se termine dans Objet, car cette classe est quipe de la mthode method_missing. Son implmentation lance simplement lexception NoMethodError. Cette approche implique que, si vous ne dnissez pas la mthode method_missing dans votre classe (ou dans une classe parent), le code fonctionne de manire classique : on appelle une mauvaise mthode et Ruby lance une exception. La beaut de la mthode method_missing tient au fait que lorsque vous limplmentez dans votre classe cette classe est capable de traiter nimporte quel appel de mthode ou plutt envoi de message et deffectuer une action adapte la situation1. Cest un rel transfert de messages. La mthode method_missing La mthode method_missing fait partie des mthodes liste darguments variable que nous avons brivement vues au Chapitre 2. Le premier argument est toujours un symbole : le nom de la mthode inexistante. Il est suivi par les arguments de lappel de mthode initial. Jetons un il sur un simple exemple, une classe qui lance sa propre exception lorsquune mthode inexistante est appele. Essayez dexcuter le code suivant :
class TestMethodMissing def hello puts("Hello from a real method") end def method_missing(name, *args) puts("Warning, warning, unknown method called: #{name}") puts("Arguments: #{args.join( )}") end end

Si lon envoie linstance de TestMethodMissing un message correspondant une vraie mthode,


tmm = TestMethodMissing.new tmm.hello

1. Le langage de programmation Smalltalk se comporte quasiment de la mme faon que Ruby lorsquun client appelle une mthode non existante. Mais le nom de la mthode du dernier recours en Smalltalk est beaucoup plus parlant : doesNotUnderstand.

160

Patterns en Ruby

son comportement est habituel :


Hello from a real method

Mais, lorsque lon appelle une mthode inexistante, lappel est transfr dans method_ missing :
tmm.goodbye(cruel, world) Warning, warning, unknown method called: goodbye Arguments: cruel world

Envoi des messages Lide de lenvoi de messages est incorpore trs profondment dans Ruby. On peut non seulement rcuprer des messages inattendus laide de method_missing, mais aussi envoyer des messages des objets explicitement avec la mthode send. Par exemple, lenvoi des messages
tmm.send(:hello) tmm.send(:goodbye, cruel, world)

provoque le mme rsultat que les appels normaux des mthodes hello et goodbye :
Hello from a real method Warning, warning, unknown method called: goodbye Arguments: cruel world

Les arguments passer sont identiques ceux de la mthode method_missing. Le premier argument est le nom du message. Il est suivi des autres paramtres. Vous vous demandez peut-tre quoi est due toute cette excitation ? Daccord, on peut grer des appels de mthodes inexistantes avec method_missing. Daccord, on peut passer des messages explicitement. Mais pourquoi choisir dutiliser lappel account.send(:deposit, 50) alors quaccount.deposit(50) est plus court et plus familier ? Eh bien, il se trouve que lappel par transfert de messages rend limplmentation des proxies ainsi que des nombreux autres patterns bien plus simple. Proxies sans peine Souvenez-vous quavant de partir dans la discussion sur le transfert des messages nous dplorions le fait que limplmentation des proxies impliquait une rptition pnible de toutes les mthodes offertes par les sujets. Et si lon navait plus besoin de les rpter ? Si lon dveloppait la classe proxy sans dnir toutes les mthodes du sujet ? Essayons :
class AccountProxy def initialize(real_account)

Chapitre 10

Crer un intermdiaire pour votre objet avec Proxy

161

@subject = real_account end end

Cette implmentation de notre AccountProxy est assez inutile. Si lon cre cette classe et quon lui lance des appels des mthodes dnies dans BankAccount,
ap = AccountProxy.new( BankAccount.new(100) ) ap.deposit(25)

on naura quun grand regard vide au retour :


proxy1.rb:32: undefined method deposit for #<AccountProxy:0x401bd408>(NoMethodError)

Grce notre discussion dans la section prcdente, on connat dsormais le mcanisme du fonctionnement de ce code. Tout dabord, Ruby recherche la mthode deposit et faute de la trouver il poursuit sa recherche par la mthode method_missing, jusqu la trouver dans la classe Object. Cette mthode de la classe Object dclenche lexception NoMethodError. Et voici lide matresse : si lon ajoute la mthode method_missing notre classe proxy, elle sera capable de traiter tout appel de mthode. Le proxy peut galement transfrer les messages quil est incapable de grer vers de vrais objets BankAccount laide de la mthode send. Voici la version amliore de la classe AccountProxy :
class AccountProxy def initialize(real_account) @subject = real_account end def method_missing(name, *args) puts("Delegating #{name} message to subject.") @subject.send(name, *args) end end

Nous sommes maintenant dans la position de passer nimporte quel message de lobjet BankAccount au proxy en toute srnit. Tous les messages que AccountProxy ne comprend pas seront transfrs la mthode method_missing, qui son tour les transmettra lobjet BankAccount. On peut maintenant crer et commencer utiliser le nouveau proxy :
ap = AccountProxy.new( BankAccount.new(100) ) ap.deposit(25) ap.withdraw(50) puts("account balance is now: #{ap.balance}")

162

Patterns en Ruby

Avec le rsultat suivant :


delegating deposit method to subject, delegating withdraw method to subject, delegating balance method to subject, account balance is now: 75

Cette technique offre une mthode de dlgation sans peine : exactement ce dont nous avons besoin pour faciliter le processus de dveloppement des proxies. On peut rcrire AccountProtectionProxy en utilisant lapproche method_missing :
class AccountProtectionProxy def initialize(real_account, owner_name) @subject = real_account @owner_name = owner_name end def method_missing(name, *args) check_access @subject.send(name, *args ) end def check_access if Etc.getlogin!= @owner_name raise "Illegal access: #{Etc.getlogin} cannot access account." end end end

Il faut remarquer deux points intressants dans la nouvelle version dAccountProtectionProxy, le premier est vident mais le deuxime est un peu plus subtil. Tout dabord, il ne vous a pas chapp quAccountProtectionProxy noccupe plus que quinze lignes et que la classe gardera cette taille quel que soit le nombre de mthodes de lobjet rel. Il est moins vident quAccountProtectionProxy ne comprenne plus aucun code spcique BankAccount. AccountProtectionProxy fonctionnera (et appliquerait la mme politique de scurit) sur tout objet que vous dcidez de lui passer. Pour vous donner un exemple simple, imaginez que nous voulions utiliser AccountProtectionProxy pour scuriser une chane de caractres. Si lon encapsule la chane dans notre proxy avec un utilisateur correct (moi !), le code fonctionne correctement :
s = AccountProtectionProxy.new( "a simple string", russ ) puts("The length of the string is #{s.length}")

Mais si le propritaire de la chane est Fred,


s = AccountProtectionProxy.new( "a simple string", fred ) puts("The length of the string is #{s.length}")

Chapitre 10

Crer un intermdiaire pour votre objet avec Proxy

163

on ne peut pas y accder :


string_permission.rb:17.in `check_access: Illegal access: russ cannot access account.

On peut tout aussi simplement dnir un proxy virtuel en utilisant method_missing :


class VirtualProxy def initialize(&creation_block) @creation_block = creation_block end def method_missing(name, *args) s = subject s.send(name, *args) end def subject @subject = @creation_block.call unless @subject @subject end end

Tout comme notre proxy de contrle daccs fond sur method_missing, le deuxime proxy virtuel est universel. On peut lutiliser par exemple pour retarder la cration dun tableau :
array array array array = VirtualProxy.new { Array.new } << hello << out << there

Comme vous le verrez par la suite, la mthode method_missing peut tre pratique dans de nombreuses situations qui font appel la dlgation.

User et abuser du pattern Proxy


Lorsquon construit un proxy, particulirement avec la mthode method_missing, il est facile de tomber dans le pige qui consiste oublier que tout objet est cr avec un nombre minimal de mthodes, celles hrites de la classe Object. Cest ainsi que tout objet hrite de la classe Object la mthode to_s. Lappel vers to_s sur la majorit des objets retourne une chane de caractres qui contient une description de cet objet. La mission dun proxy est de se faire passer pour son sujet mais, si lon appelle to_s sur un de nos proxies, lillusion disparat aussitt :
account = VirtualProxy.new { BankAccount.new } puts(account) #<VirtualProxy:0x40293b48>

164

Patterns en Ruby

Le problme, cest que nous appelons la mthode to_s de la classe VirtualProxy et non pas celle de BankAccount. Cest peut-tre le comportement souhait mais lorsque vous dveloppez des proxies il est important de se rappeler les mthodes dObject si souvent oublies. La technique method_missing glorie dans ce chapitre a galement quelques inconvnients. Par exemple, lutilisation de method_missing affecte le niveau de performance. Si lon compare une classe avec une mthode directe
class DirectCall def add(a, b) a+b end end

et une classe qui utilise la mthode method_missing


class MethodMissingCall def method_missing(name, a, b) a+b end end

la version avec method_missing sexcute lgrement plus lentement. Sur ma machine, la premire classe traditionnelle est environ 10 % plus rapide que si lon appelle add sur la deuxime version o lappel de la mthode inconnue transite par method_missing. Un point plus important : lusage excessif de method_missing, tout comme lusage excessif de lhritage, est un moyen trs sr de rendre votre code difcilement lisible. Lorsque vous employez la technique method_missing, vous crez des objets dont les messages sont traits plus ou moins par magie. Si quelquun lisait le code de notre systme bancaire, il commencerait par chercher les mthodes deposit et withdraw dans nos classes proxy. Une personne qui matrise bien Ruby arriverait assez rapidement la conclusion que vous utilisez la technique method_missing. Mais votre code ne devrait pas exiger plus de gymnastique mentale quil est ncessaire. Assurez-vous que vous avez une raison valable pour iniger cette difcult au dveloppeur qui arriverait aprs vous.

Proxies dans le monde rel


Lusage du pattern Proxy le plus populaire dans Ruby aujourdhui est le proxy distant. part le client SOAP de Ruby que jai mentionn ci-dessus, on trouve le package Distributed Ruby (drb). Il permet de dvelopper en Ruby des applications distribues,

Chapitre 10

Crer un intermdiaire pour votre objet avec Proxy

165

relies par un rseau TCP/IP. Le composant drb est trs facile dutilisation : quasiment tout objet typique Ruby peut agir en tant que service drb. Construisons un petit service tout bte :
class MathService def add(a, b) return a + b end end

Pour exposer cet objet en tant que service drb il suft dcrire quelques lignes de code :
require drb/drb math_service = MathService.new DRb.start_service("druby://localhost:3030", math_service) DRb.thread.join

Nous crons une instance de MathService et murmurons lincantation drb pour publier cet objet sur le port 3030. On peut maintenant dmarrer le programme de service mathmatique pour quil tourne en tche de fond en attendant des requtes entrantes. Pour gnrer une requte il nous faut un programme client. Ouvrez une autre fentre ou dplacez-vous sur une autre machine et composez le code suivant. Tout dabord, nous devons initialiser drb ct client :
require drb/drb DRb.start_service

Nous pouvons dsormais appeler le service mathmatique distant :


math_service = DRbObject.new_with_uri("druby://localhost:3030")

Il est clair que, si votre service est hberg sur une autre machine ou un autre port, vous devrez adapter lURL. Lexcution du programme client nous permet deffectuer une opration mathmatique incroyable :
sum = math_service.add(2,2)

Drb utilise effectivement le pattern Proxy car le service math_service du ct client est un proxy du service distant qui sexcute lintrieur de linterprteur Ruby du ct serveur. Si vous jetez un il dans le code de drb, vous trouverez la mme technique fonde sur method_missing que celle tudie dans ce chapitre.

En conclusion
Dans ce chapitre, nous avons examin trois problmes diffrents : protger un objet contre laccs non autoris, cacher le fait que lobjet rside ailleurs sur le rseau, et

166

Patterns en Ruby

retarder autant que possible la cration dun objet coteux. Il est remarquable que les trois problmes aient une solution commune : le pattern Proxy. Dans le monde de la programmation, les proxies sont des imitateurs : il se font passer pour les autres objets. Un proxy conserve en interne une rfrence vers un vrai objet : cet objet est nomm sujet par le GoF. Nanmoins, un proxy nest pas seulement une passerelle pour des appels des mthodes dun objet. Il sert de point intermdiaire entre le client et le sujet. "Cette opration estelle autorise ?" demande un proxy de contrle daccs. "Est-ce que cet objet rside rellement sur cette machine ?" demande un proxy distant. "Est-ce que jai dj cr le sujet ?" demande un proxy virtuel. Bref, un proxy contrle laccs au sujet ! Nous avons galement appris dans ce chapitre comment utiliser la technique method_ missing pour rduire de manire signicative leffort de codage lorsquon dveloppe des proxies. Le pattern Proxy est le deuxime pattern que nous avons tudi o un objet prend la place dun autre objet. Au Chapitre 9, nous avons vu le pattern Adapter, qui encapsule un objet dans un autre pour transformer linterface du premier. Au premier abord, le pattern Proxy est semblable Adapter : un objet se fait passer pour un autre. Mais Proxy ne change pas linterface : linterface de Proxy correspond exactement celle de son sujet. Au lieu de modier linterface de lobjet encapsul comme le fait un adaptateur, Proxy essaie de contrler laccs cet objet. Il savre que lapproche qui consiste placer "un objet dans un autre" une technique de conception la poupe russe , que nous avons rencontre dans les patterns Adapter et Proxy, est tellement pratique que nous risquons fort de la voir resurgir avant la n de ce livre. Pour tre prcis, elle rapparatra ds le chapitre suivant...

11
Amliorer vos objets avec Decorator
Lune des questions fondamentales du gnie logiciel est la suivante : comment ajouter des fonctionnalits un programme sans transformer lensemble en un bazar ingrable ? Jusqualors, nous avons appris comment diviser les dtails de limplmentation de vos objets en familles de classes laide du pattern Template Method ainsi qu sparer des parties dun algorithme avec le pattern Strategy. Nous savons galement comment des objets doivent ragir des requtes entrantes avec le pattern Command ou comment rester au courant des modications des autres objets avec le pattern Observer. Le pattern Composite et le pattern Itrateur nous aident chacun leur manire manipuler des collections dobjets. Mais comment faire pour ajuster le niveau de responsabilit dun objet pour quil soit capable de faire tantt un peu plus et tantt un peu moins ? Dans ce chapitre, nous allons tudier le pattern Decorator, qui permet dajouter des amliorations un objet existant de faon simple. Ce pattern permet galement dempiler des couches de fonctionnalits an de construire un objet muni des capacits optimales pour une situation donne. Comme toujours, nous examinerons une alternative Ruby au pattern Decorator. Enn, nous verrons pourquoi les objets qui portent beaucoup de dcorations ne sont pas toujours idals.

Dcorateurs : un remde contre le code laid


Imaginez que vous ayez du texte stock dans un chier. La tche va vous sembler assez primitive, mais supposons que votre systme ait parfois besoin dcrire du texte simple sans dcor et quil faille parfois numroter chaque ligne lorsquelle est crite.

168

Patterns en Ruby

Il peut aussi sagir dajouter une estampille chaque ligne qui scrit dans le chier mais aussi, pourquoi pas, de gnrer une somme de contrle (checksum) pour sassurer que le texte a t correctement sauvegard. Pour rpondre tous ces besoins, on pourrait partir dun objet qui encapsule la classe IO de Ruby en y ajoutant des mthodes pour chaque variation de traitement :
class EnhancedWriter attr_reader:check_sum def initialize(path) @file = File.open(path, "w") @check_sum = 0 @line_number = 1 end def write_line(line) @file.print(line) @file.print("\n") end def checksumming_write_line(data) data.each_byte {|byte| @check_sum = (@check_sum + byte) % 256 } @check_sum += "\n"[0] % 256 write_line(data) end def timestamping_write_line(data) write_line("#{Time.new}: #{data}") end def numbering_write_line(data) write_line("%{@line_number}: #{data}") @line_number += 1 end def close @file.close end end

On peut ensuite utiliser EnhancedWriter pour crire soit du texte simple :


writer = EnhancedWriter.new(out.txt) writer.write_line("A plain line")

soit une ligne avec la somme de contrle :


writer.checksumming_write_line(A line with checksum) puts("Checksum is #{writer.check_sum}")

soit une ligne avec une estampille ou avec un numro de ligne :


writer.timestamping_write_line(with time stamp) writer.numbering_write_line(with line number)

Chapitre 11

Amliorer vos objets avec Decorator

169

Il y a quelque chose qui ne va pas dans cette approche : tout ! Premirement, tout client qui utilise EnhancedWriter serait oblig de savoir quel texte il crit : numrot, estampill ou comprenant des sommes de contrle. Le client ne peut pas prendre cette dcision une fois linitialisation : cette information doit tre disponible constamment, lcriture de chaque ligne. Si le client se trompe une fois par exemple en utilisant timestamping_write_line au lieu de numbering_write_line ou en utilisant la mthode write_line basique alors quil lui faut appeler checksumming_write_line , le nom de la classe EnhancedIO paratrait plutt dplac. Un problme lgrement moins vident maintenant : tout est plac dans la mme classe. La classe dans laquelle on mlange le code de numrotation avec le code de la somme de contrle ainsi que la gestion de lestampille est particulirement sujet aux erreurs. On pourrait sparer les responsabilits dcriture en crant une classe parent et des sous-classes : autrement dit, recourir notre vieil ami lhritage selon le diagramme UML de la Figure 11.1. Mais que faire si lon veut gnrer une somme de contrle des lignes numrotes ? Ou si on veut numroter les lignes, mais pas avant dajouter une estampille ? Cela reste faisable, mais le nombre de classes devient ingrable, ce qui est clairement illustr la Figure 11.2.
Figure 11.1
Rsoudre le problme dEnhancedWriter avec lhritage
EnhancedWriter write_line(line)

NumberingWriter write_line(line)

TimestampingWriter write_line(line)

CheckSummingWriter write_line(line)

Il faut noter que, mme avec cette foule de classes (voir Figure 11.2), on ne peut toujours pas afcher une somme de contrle dans le texte estampill aprs avoir numrot les lignes. Le problme de lapproche fonde sur lhritage est que toutes les combinaisons des fonctionnalits doivent tre prises en compte ds la conception. Il est probable que vous ayez besoin non pas de toutes les combinaisons mais plutt de quelques combinaisons prcises.

170

Patterns en Ruby

Figure 11.2
Lhritage incontrl

EnhancedWriter write_line(line)

NumberingWriter write_line(line)

TimestampedWriter write_line(line)

CheckSummedWriter write_line(line)

NumberingCheckSummingWriter write_line(line)

CheckSummingLineNumberingWriter write_line(line)

TimestampingNumberingWriter write_line(line)

Une solution plus adapte consiste se donner les moyens dassembler les fonctionnalits ncessaires dynamiquement au moment de lexcution. Commenons avec un objet basique capable dcrire du texte simple et ajoutons quelques oprations de traitement supplmentaires :
class SimpleWriter def initialize(path) @file = File.open(path, w) end def write_line(line) @file.print(line) @file.print("\n") end def pos @file.pos end def rewind @file.rewind end def close @file.close end end

Si vous voulez ajouter des numros de lignes, insrez un objet (on peut le nommer par exemple NumberingWriter) entre la classe SimpleWriter et le client. Cet objet crirait un numro de ligne pour chacune des lignes et transfrerait le rsultat au SimpleWriter,

Chapitre 11

Amliorer vos objets avec Decorator

171

qui se chargerait de la sauvegarde sur le disque. NumberingWriter complte les capacits de SimpleWriter en le dcorant, do le nom du pattern. Nous avons lintention de dnir plusieurs objets dcorateurs, il faut donc factoriser le code gnrique dans une classe parent commune :
class WriterDecorator def initialize(real_writer) @real_writer = real_writer end def write_line(line) @real_writer.write_line(line) end def pos @real_writer.pos end def rewind @real_writer.rewind end def close @real_writer.close end end class NumberingWriter < WriterDecorator def initialize(real_writer) super(real_writer) @line_number = 1 end def write_line(line) @real_writer.write_line("#{@line_number}: #{line}") @line_number += 1 end end

La classe NumberingWriter expose la mme interface de base que SimpleWriter, or le client nest pas oblig de se proccuper du type de la classe laquelle il a affaire : le fonctionnement de base des deux classes est identique. Pour numroter les lignes, il suft dinsrer la classe NumberingWriter dans la chane du traitement aprs la classe SimpleWriter :
writer = NumberingWriter.new(SimpleWriter.new(final.txt)) writer.write_line(Hello out there)

Le mme patron sapplique pour dvelopper un dcorateur qui calcule les sommes de contrle : un objet supplmentaire sincruste entre le client et SimpleWriter, il calcule la somme des octets avant de transfrer le rsultat an que SimpleWriter lcrive dans un chier :

172

Patterns en Ruby

class CheckSummingWriter < WriterDecorator attr_reader:check_sum def initialize(real_writer) @real_writer = real_writer @check_sum = 0 end def write_line(line) line.each_byte {|byte| @check_sum = (@check_sum + byte) % 256 } @check_sum += "\n"[0] % 256 @real_writer.write_line(line) end end

La classe CheckSummingWriter diffre lgrement de notre premier dcorateur car son interface est plus complexe. Cette classe expose la mthode check_sum en plus des autres mthodes typiques1. Enn, nous pouvons dnir la classe destine estampiller les donnes :
class TimeStampingWriter < WriterDecorator def write_line(line) @real_writer.write_line("#{Time.new}: #{line}") end end

Cerise sur le gteau : puisque les dcorateurs partagent linterface de base avec la classe initiale, rien ne nous oblige fournir nos dcorateurs une instance de SimpleWriter. On a la libert de leur passer un de nos dcorateurs. Cela signie que nous sommes en position de construire des chanes de dcorateurs longueur illimite an que chacun deux puisse apporter sa contribution lensemble. Il nous est enn possible dafcher une somme de contrle du texte estampill aprs lavoir numrot :
writer = CheckSummingWriter.new(TimeStampingWriter.new( NumberingWriter.new(SimpleWriter.new(final.txt)))) writer.write_line(Hello out there)

La dcoration formelle
Tous les participants du pattern Decorator implmentent linterface de Component (voir Figure 11.3). La classe ConcreteComponent est lobjet "rel" qui implmente la fonctionnalit basique. Dans notre exemple, le rle de ConcreteComponent est jou par SimpleWriter. La classe Decorator possde une rfrence vers Component cest--dire le Component suivant dans la chane des dcorateurs et elle implmente toutes les mthodes de la
1. La mthode check_sum a t gnre par linstruction attr_reader, bien entendu.

Chapitre 11

Amliorer vos objets avec Decorator

173

classe Component. Et on trouve trois classes Decorator : une pour numroter des lignes, une pour calculer la somme de contrle et une pour gnrer une estampille. Chacun des dcorateurs ajoute une couche de fonctionnalits, en compltant au moins une mthode du composant de base avec ses propres capacits. Les dcorateurs peuvent galement introduire de nouvelles mthodes qui ne sont pas dnies dans linterface Component, mais ce comportement est facultatif. Dans notre exemple, le dcorateur qui calcule les sommes de contrle dnit effectivement une nouvelle mthode.
Figure 11.3
Le pattern Decorator
Component operation()

ConcreteComponent operation()

Decorator @component operation() new_operation()

Diminuer leffort de dlgation


Le pattern Decorator prend trs au srieux un des conseils du GoF : il sappuie abondamment sur la dlgation. Ce point est bien illustr par la classe WriterDecorator. Cette classe est constitue quasiment entirement de code gnrique qui ne fait que dlguer llment suivant de la chane. On aurait pu liminer tout ce code fastidieux avec une variation de la technique method_missing que nous avons apprise au Chapitre 10, mais le module forwardable serait probablement la solution la plus adapte. Ce module gnre automatiquement toutes les mthodes de dlgation quasiment sans effort. Voici notre classe WriterDecorator rcrite pour proter de forwardable :
require forwardable class WriterDecorator extend Forwardable def_delegators:@real_writer,:write_line,:rewind,:pos,:close def initialize(real_writer) @real_writer = real_writer end end

174

Patterns en Ruby

Le module forwardable fournit une mthode de classe def_delegators1 qui accepte deux paramtres ou plus. Le premier argument est le nom de lattribut dinstance 2. Il est suivi par les noms dune ou de plusieurs mthodes. La mthode def_delegators complte votre classe avec toutes les mthodes nommes. Chacune de ces nouvelles mthodes dlgue lobjet indiqu par lattribut. La classe WriterDecorator se retrouve donc avec les mthodes write_line, rewind, pos et close, qui dlguent le travail @real_writer. Le module forwardable apporte plus de prcision par rapport la technique method_ missing car il vous laisse le choix des mthodes que vous souhaitez dlguer. On peut certainement insrer la logique de dlgation de quelques mthodes dans method_ missing, mais la vraie force de cette technique rside dans sa capacit dlguer un grand nombre dappels.

Les alternatives dynamiques au pattern Decorator


La exibilit de Ruby au moment de lexcution prsente certaines alternatives intressantes au pattern Decorator du GoF. Il est possible de bncier de la plupart des avantages des dcorateurs soit laide de lencapsulation dynamique des mthodes soit en dcorant vos objets avec des modules. Les mthodes dencapsulation Nous avons dj appris que Ruby permet de modier le comportement dune instance ou dune classe entire quasiment tout instant. Fort de cette exibilit et du mot cl alias, on peut doter SimpleWriter de la fonctionnalit requise pour ajouter une estampille :
w = SimpleWriter.new(out) class << w alias old_write_line write_line def write_line(line) old_write_line("#{Time.new}: #{line}") end end

1. Vous avez probablement remarqu que dans WriterDecorator nous tendons le module Forwardable au lieu de linclure. La diffrence est subtile : le module Forwardable essaie dajouter des mthodes de classe et non pas des mthodes dinstance. 2. Accompagn de "@" pour une raison trange.

Chapitre 11

Amliorer vos objets avec Decorator

175

Le mot cl alias cre un nouveau nom pour une mthode existante. Dans le code ci-dessus on commence par dclarer un alias pour la mthode initiale write_line, an quelle puisse tre appele write_line aussi bien que old_write_line. Ensuite, on rednit la mthode write_line, mais la mthode old_write_line continue pointer vers la dnition initiale, ce qui est primordial. Le reste est facile : la nouvelle mthode estampille chacune des lignes et appelle la mthode initiale (maintenant nomme old_write_line), qui son tour crit le texte estampill dans un chier. Heureusement pour vous, futurs dcorateurs potentiels, la technique dencapsulation de mthode prsente certaines limites. La collision de noms, notamment, reprsente un des dangers. Par exemple, si lon voulait numroter nos lignes deux fois avec le code actuel, on perdrait la rfrence vers la mthode write_line initiale cause du double alias. On pourrait probablement inventer une convention ingnieuse pour viter la collision de noms mais, lorsque vos dcorations gagnent en complexit, la ncessit de les dplacer dans leurs propres classes devient de plus en plus une vidence. Toutefois, la technique dencapsulation de mthodes reste utile pour des cas simples. Elle mrite dtre matrise par tout dveloppeur Ruby. Dcorer laide de modules Un autre moyen dinjecter des fonctionnalits dans un objet Ruby consiste rajouter dynamiquement des modules laide de la mthode extend. Pour appliquer cette technique il faut refactoriser notre code et transformer nos classes dcorateurs en modules :
module TimeStampingWriter def write_line(line) super("#{Time.new}: #{line}") end end module NumberingWriter attr_reader:line_number def write_line(line) @line_number = 1 unless @line_number super("#{@line_number}: #{line}") @line_number += 1 end end class Writer def write_line(line) @f.write_line(line) end end

176

Patterns en Ruby

La mthode extend insre un module dans larbre de lhritage dun objet avant sa classe normale. On peut donc partir dun objet SimpleWriter et ensuite simplement ajouter la fonctionnalit requise :
w = SimpleWriter.new(out) w.extend(NumberingWriter) w.extend(TimeStampingWriter) w.write_line(hello)

Le dernier module ajout est le premier appel. Dans lexemple ci-dessus le ux de traitement commence par TimeStampingWriter, continue avec NumberingWriter et sachve par Writer. Les deux techniques dynamiques fonctionnent bien. En ralit, ce sont les solutions de prdilection dans le code Ruby existant. Mais il existe un dfaut inhrent aux deux approches : il est difcile dannuler la dcoration. En effet, extraire une mthode encapsule est assez fastidieux et il est tout simplement impossible dexclure un module dj inclus.

User et abuser du pattern Decorator


Le pattern Decorator classique est ador des dveloppeurs et moins apprci par leurs clients. Nous avons appris que le pattern Decorator aide les programmeurs sparer de manire propre les diffrentes responsabilits extraire la numrotation de lignes dans une classe, les sommes de contrle dans une deuxime et lestampille dans une troisime. Le moment de vrit survient lorsque quelquun tente de rassembler toutes les briques dans un ensemble fonctionnel. Le client est oblig de recoller toutes les pices seul au lieu de pouvoir instancier un objet unique, par exemple EnhancedWriter.new(path). videmment, il y a des solutions qui permettent de simplier la tche dassemblage. Sil existe des chanes de dcorateurs dont vos clients ont besoin frquemment, nhsitez pas leur fournir un utilitaire (peut-tre un Builder1?) dassemblage des chanes de traitement. Un autre point retenir lorsquon implmente le pattern Decorator, cest que linterface du Component doit rester simple. Il faut viter de rendre linterface excessivement complexe, car cette complexit sera un obstacle une implmentation correcte de chacun des dcorateurs.

1. Voir le Chapitre 14.

Chapitre 11

Amliorer vos objets avec Decorator

177

La baisse de performance lie une longue chane de dcorateurs est un autre inconvnient potentiel du pattern Decorator. Lorsque vous remplacez la classe monolithique ChecksummingNumberingTimestampingWriter par une chane de dcorateurs, vous gagnez normment en sparation de responsabilits et en clart de code. Le prix payer tient la multiplication des objets sollicits par votre programme. Ce nest pas bien grave lorsque, comme dans notre exemple, on manipule quelques chiers ouverts. Cela peut le devenir lorsquil faut grer chaque salari dune grande entreprise. Souvenez-vous que mis part le nombre des objets impliqus, les donnes que vous envoyez travers la chane doivent passer par n dcorateurs et changer n fois de propritaire : du premier dcorateur au deuxime, ensuite au troisime, etc. Pour nir, la technique dencapsulation de mthodes prsente linconvnient dtre difcile dboguer. Souvenez-vous que vos mthodes apparatront dans la pile dappels avec des noms qui ne correspondent pas leurs noms dans les chiers source. Ce nest pas rdhibitoire mais cest un point supplmentaire dont il faut tenir compte.

Les dcorateurs dans le monde rel


On trouve un bon exemple du style de dcoration fond sur lencapsulation de mthode dans ActiveSupport, le module dassistants (helpers) employ par Rails. ActiveSupport ajoute tous les objets une mthode nomme alias_method_chain. Cette mthode vous permet de dcorer vos mthodes avec un nombre de fonctionnalits illimit. Pour se servir dalias_method_chain, on commence par une mthode simple dans votre classe, par exemple write_line :
def write_line(line) puts(line) end

Ensuite, on ajoute une deuxime mthode qui insre une certaine dcoration la mthode initiale :
def write_line_with_timestamp(line) write_line_without_timestamp("#{Time.new}: #{line}") end

Enn, on appelle alias_method_chain :


alias_method_chain:write_line,:timestamp

La mthode alias_method_chain renomme la mthode initiale write_line en write_line_without_timestamp, alors que la mthode write_line_with_timestamp est renomme en write_line, ce qui cre une chane de mthodes. Lavantage

178

Patterns en Ruby

dalias_method_chain, comme lindique son nom, tient la possibilit de chaner un certain nombre de mthodes de dcoration. Par exemple, on peut aussi complter lensemble avec une mthode de numrotation de lignes :
def write_line_with_numbering(line) @number = 1 unless @number write_line_without_numbering("#{@number}: #{line}") @number += 1 end alias_method_chain:write_line,:numbering

En conclusion
Le pattern Decorator est une technique simple qui permet dassembler au moment de lexcution la fonctionnalit requise. Cette approche est une alternative la cration dun objet monolithique supportant toutes les fonctionnalits possibles avec dinnombrables classes et sous-classes couvrant toutes les combinaisons de fonctions possibles et imaginables. Lide du pattern Decorator consiste complter une classe dlivrant la fonctionnalit de base laide de plusieurs dcorateurs. Chacun des dcorateurs supporte la mme interface de base mais apporte sa contribution la fonctionnalit. Les dcorateurs acceptent un appel de mthode, excutent leur fonction unique et font suivre lappel au composant suivant. La cl de limplmentation du pattern Decorator est le fait que le composant suivant peut tre un autre dcorateur prt apporter sa contribution, mais ce peut tre galement lobjet rel qui nalise la requte. Le pattern Decorator vous permet de commencer par une fonctionnalit basique et dempiler des fonctions supplmentaires, un dcorateur aprs lautre. Puisque le pattern Decorator construit des couches de fonctionnalits au moment de lexcution, vous tes libre de choisir la combinaison adapte vos besoins. Le pattern Decorator est le dernier pattern du type "un objet en remplace un autre" que nous tudions dans ce livre. Le premier tait le pattern Adapter, qui encapsule un objet linterface inadapte dans un autre objet qui expose linterface requise. Le deuxime tait le pattern Proxy. Il encapsule galement un objet, mais sans changer son interface. Linterface dun proxy est identique celle du sujet. Un proxy existe pour contrler, non pas pour traduire. Les proxies sont pratiques pour contrler la scurit, cacher le fait que le vrai objet rside sur le rseau ou pour retarder autant que possible la cration dun vrai objet. Enn, nous avons vu dans ce chapitre le pattern Decorator, qui permet dajouter des fonctionnalits variables un objet basique.

12
Crer un objet unique avec Singleton
Pauvre pattern Singleton ! Mme les programmeurs qui ne savent rien sur les patterns connaissent le pattern Singleton. Gnralement, ils savent une chose : les singletons, cest Mal, avec un "M" majuscule. Et, pourtant, il est difcile de sen passer. Les singletons sont partout. Dans le monde Java on les trouve dans les logiciels les plus rpandus, par exemple Tomcat et JDOM. Pour vous donner encore quelques exemples, on rencontre des singletons Ruby dans WEBrick, rake et mme Rails. Quelles caractristiques du pattern Singleton le rendent si indispensable et pourtant si largement dtest ? Dans les pages qui suivent, nous verrons pourquoi les singletons sont si utiles et comment sy prendre pour les dvelopper correctement. Nous allons dcouvrir comment construire des singletons et des presque-singletons en Ruby, pourquoi les singletons peuvent devenir une source de problmes et quelles mesures prendre pour diminuer ces difcults.

Objet unique, accs global


La motivation sous-jacente du pattern Singleton est trs simple : certaines choses sont uniques. Par exemple, les programmes ont souvent un seul chier de conguration ou bien un seul chier de log. Les applications munies dune interface graphique comprennent frquemment une fentre principale et reoivent les donnes partir dexactement un clavier. Pour nir, on peut citer lexemple de nombreuses applications qui communiquent avec exactement une base de donnes. Si vous avez une seule instance dune classe et que beaucoup de code doive y accder, il parat injusti de passer lobjet dune mthode lautre. Dans cette situation, les membres du GoF suggrent de dvelopper un singleton, une classe qui propose un accs global une seule et unique instance.

180

Patterns en Ruby

Il existe plusieurs faons de reproduire le comportement singleton en Ruby et nous allons commencer par la mthode la plus proche de celle recommande par le GoF : laissons la classe de lobjet singleton grer la cration et laccs son instance unique. Pour y arriver, jetons un il sur les variables de classe et les mthodes de classe en Ruby.

Variables et mthodes de classe


Jusqualors, tout le code que nous avons crit se fondait sur des mthodes et variables dinstance, le code et les donnes taient attachs des instances particulires. Tout comme la majorit des langages orients objet, Ruby supporte galement des variables et mthodes attaches une classe1. Variables de classe Comme mentionn plus haut, une variable de classe est une variable attache une classe2 plutt qu une instance de la classe. Crer une variable de classe est trs simple : il suft dajouter au nom de la variable une arobase supplmentaire (@). Voici un exemple dune classe qui compte le nombre des appels de la mthode increment dans deux variables diffrentes : une variable dinstance et une variable de classe.
class ClassVariableTester @@class_count = 0 def initialize @instance_count = 0 end def increment @@class_count = @@class_count + 1 @instance_count = @instance_count + 1 end def to_s "class_count: #{@@class_count} instance_count: #{@instance_count}" end end

1. De nombreux autres langages, notamment C++ et Java, utilisent le nom "statique" pour dsigner les mthodes et variables de classe. Malgr la diffrence terminologique, le principe est le mme. 2. En ralit, des variables de classe en Ruby sont attaches la hirarchie entire de lhritage. Par consquent, une classe partage lensemble des variables de classe avec sa classe mre ainsi que toutes ses sous-classes. La plupart des programmeurs Ruby considrent ceci comme une caractristique trange du langage. Des discussions sont en cours pour modier cette fonctionnalit.

Chapitre 12

Crer un objet unique avec Singleton

181

On peut crer une instance ClassVariableTester et appeler sa mthode increment deux fois :
cl = ClassVariableTester.new cl.increment cl.increment puts("c1: #{c1}")

Il nest pas tonnant que les deux compteurs contiennent la valeur 2 :


cl: class_count: 2 instance_count: 2

Les choses deviennent plus intressantes lorsque vous crez une deuxime instance de cette classe :
c2 = ClassVariableTester.new puts("c2: #{c2}")

Le rsultat est
c2: class_count: 2 instance_count: 0

La diffrence sexplique par le fait que le compteur dinstance du deuxime objet ClassVariableTester a t rinitialis zro alors que le compteur de classe sest incrment normalement. Mthodes de classe Crer des mthodes de classe en Ruby est peine plus compliqu. Nous ne pouvons pas simplement crer une classe et dnir une mthode :
class SomeClass def a_method puts(hello from a method) end end

Nous savons dj que ce genre de code cre des mthodes dinstances :


SomeClass.a_method instance.rb:11: undefined method a_method for SomeClass:Class

Le secret de la dnition dune mthode de classe est de savoir quand on se trouve lintrieur de la dnition dune classe, mais lextrieur de la dnition dune mthode. La classe courante est rfrence par la variable self. Vous ntes pas oblig de me croire sur parole. Si lon excute le code suivant :
class SomeClass puts ("Inside a class def, self is #{self}") end

182

Patterns en Ruby

voici le rsultat obtenu :


Inside a class def, self is SomeClass

Cette information nous aidera dnir une mthode de classe :


class SomeClass def self.class_level_method puts(hello from the class method) end end

Nous pouvons dsormais appeler la mthode class_level_method au niveau de la classe, comme le suggre son nom :
SomeClass.class_level_method

Si la syntaxe self.method_name ne vous plat pas, Ruby propose une autre option. On peut dnir une mthode de classe en dclarant son nom explicitement :
class SomeClass def SomeClass.class_level_method puts(hello from the class method) end end

En ce qui concerne le choix syntaxique, les programmeurs Ruby semblent tre partags en deux groupes gaux : certains prfrent self, les autres prfrent le nom explicite de la classe. Personnellement, jaime le format self car il faut faire moins de modications si lon renomme la classe ou si le code est transplant dans une autre classe.

Premire tentative de cration dun singleton Ruby


Maintenant que nous savons crer des variables et mthodes de classes, nous avons tous les outils pour dnir un singleton. Partons dune classe ordinaire pour la transformer en un singleton. Supposons que vous ayez une classe de log charge de collecter les messages mis par votre programme. La version non singleton de votre classe de log pourrait ressembler ceci :
class SimpleLogger attr_accessor:level ERROR = 1 WARNING = 2 INFO = 3 def initialize @log = File.open("log.txt", "w") @level = WARNING end

Chapitre 12

Crer un objet unique avec Singleton

183

def error(msg) @log.puts(msg) @log.flush end def warning(msg) @log.puts(msg) if @level >= WARNING @log.flush end def info(msg) @log.puts(msg) if @level >= INFO @log.flush end end

On pourrait crer une instance de cette classe et la passer partout :


logger = SimpleLogger.new logger.level = SimpleLogger::INFO logger.info(Doing the first thing) # Effectuer la premire opration... logger.info(Now doing the second thing) # Effectuer la deuxime opration...

Gestion de linstance unique Le seul but du pattern Singleton est dviter de passer un objet tel que lobjet de log partout dans le programme. Il serait judicieux de coner la gestion de linstance unique la classe SimpleLogger. Mais comment transformer SimpleLogger en un singleton ? Tout dabord, on cre une variable de classe pour contenir la seule et unique instance de la classe. Une mthode de classe serait indispensable pour retourner linstance singleton :
class SimpleLogger # Beaucoup de code supprim... @@instance = SimpleLogger.new def self.instance return @@instance end end

Quel que soit le nombre dappels vers la mthode SimpleLogger, elle retournerait toujours le mme objet de log :
logger1 = SimpleLogger.instance logger2 = SimpleLogger.instance # Retourne lobjet de log # Retourne exactement le mme objet # de log

Il est trs pratique de pouvoir accder au singleton de log tout moment pour crire des messages :
SimpleLogger.instance.info(Computer wins chess game.) SimpleLogger.instance.warning(AE-35 hardware failure predicted.)

184

Patterns en Ruby

SimpleLogger.instance.error( HAL-9000 malfunction, take emergency action!)

Sassurer de lunicit Notre singleton est oprationnel, mais il nest pas complet. Souvenez-vous dune des exigences des singletons : il faut sassurer que lobjet singleton est linstance unique de sa classe. Jusqualors, nous avons ignor cette exigence. En ltat, tout programme peut appeler SimpleLogger.new pour crer une deuxime instance de notre "singleton". Comment faire pour protger la classe SimpleLogger contre des instanciations non sollicites ? Il suft de rendre prive la mthode new de SimpleLogger :
class SimpleLogger # Beaucoup de code supprim... @@instance = SimpleLogger.new def self.instance return @@instance end private_class_method:new end

Deux points sont noter dans le fragment de code prcdent : le premier nest quun dtail, et le deuxime est plus profond. En ajoutant linstruction private_class_ method nous avons rendu la mthode new de la classe prive et par consquent inaccessible. Aucune autre classe ne pourrait crer des instances de notre logger : cest un dtail. Il faut noter le point plus global : la mthode new nest quune mthode de classe classique. Elle excute effectivement des actions magiques et invisibles pour allouer lobjet, mais nalement cest une mthode de classe comme une autre.

Le module Singleton
Notre singleton est maintenant prt : nous avons rempli toutes les exigences dnies par le GoF pour limplmentation des singletons. Notre classe instancie un objet unique, tout code intress peut accder cette instance et nul ne peut crer une deuxime instance. Nanmoins, notre implmentation du pattern Singleton prsente un dfaut. Que se passet-il si lon veut crer une deuxime classe singleton, pour nos donnes de conguration, par exemple ? Nous allons devoir recommencer lexercice : crer une variable de classe pour linstance de singleton ainsi quune mthode de classe pour y accder. Et noubliez pas de rendre la nouvelle mthode new prive. Et lorsquune troisime instance devient ncessaire, il faudra recommencer nouveau. Finalement, beaucoup defforts rpts. Heureusement, il est possible dviter ce travail. Au lieu de prendre la peine de transformer nos classes en singletons la main, on peut simplement inclure le module Singleton :

Chapitre 12

Crer un objet unique avec Singleton

185

require singleton class SimpleLogger include Singleton # Beaucoup de code supprim... end

Le module Singleton ralise le plus gros du travail : il dnit une variable de classe et linitialise avec linstance unique, il injecte galement la mthode de classe pour retourner linstance et rend la mthode new prive. Il ne nous reste qu inclure le module. Vu de lextrieur, le nouvel objet de log fond sur le module Singleton est identique limplmentation faite la main : il suft dappeler SimpleLogger.instance pour rcuprer son instance et le tour est jou.

Singletons instanciation tardive ou immdiate


Il existe une diffrence signicative entre notre propre implmentation de singleton et celle fournie par le module Singleton. Souvenez-vous que notre implmentation cre linstance de singleton lorsque la classe est dnie :
class SimpleLogger # Beaucoup de code supprim... @@instance = SimpleLogger.new # Beaucoup de code supprim... end

Par consquent, notre instance de singleton est cre avant mme que le code client ait la possibilit dappeler SimpleLogger.instance. La cration de linstance du singleton avant quelle ne soit ncessaire sappelle linstanciation immdiate. Contrairement notre approche, le module Singleton attend un appel linstance avant de la crer rellement. Cette technique est connue sous le nom dinstanciation tardive.

Alternatives au singleton classique


La technique de dveloppement des singletons grs par une classe employe prcdemment rete dlement limplmentation recommande par le GoF. Mais il existe beaucoup dautres variantes pour implmenter le comportement au singleton. Voici quelques alternatives auxquelles on pourrait recourir pour atteindre le mme effet. Variables globales en tant que singletons Par exemple, on pourrait utiliser une variable globale en tant que singleton. Je fais une pause pour laisser les cris dhorreur se calmer... En Ruby, toute variable dont le nom commence par le caractre $ tel que $logger est globale. Laccs global vers un singleton est donc facilement ralisable laide dune variable globale : quel que soit le contexte classe, module ou mthode , $logger reste accessible et retourne toujours

186

Patterns en Ruby

le mme objet. Puisquil nexiste quune instance de la variable globale donne et compte tenu de son accs universel, une variable globale parat une bonne plate-forme pour implmenter des singletons. Hlas, il manque aux variables globales une des caractristiques fondamentales des singletons ! La variable $logger conserve une rfrence un objet unique, mais il est impossible de contrler sa valeur. On pourrait commencer par notre pseudo-singleton global :
$logger = SimpleLogger.new

Mais rien ne peut empcher la modication de la valeur par du code malveillant :


$logger = LoggerThatDoesSomethingBad.new

Si les modications posent un problme, nous devrions peut-tre nous tourner vers un autre type de variable Ruby. Les constantes sont des variables qui ont non seulement une porte globale, mais qui sont galement rsistantes aux changements. Souvenezvous quune constante en Ruby est une variable dont le nom commence par une lettre majuscule et dont la valeur est cense rester inchange :
Logger = SimpleLogger.new

Nous avons mentionn au Chapitre 2 que Ruby dclenche un avertissement lorsque quelquun change la valeur dune constante. Cest une amlioration par rapport lattitude "tout est possible" des variables globales. Mais est-ce une solution simple pour implmenter un singleton ? Pas vraiment. Les variables globales et les constantes partagent un certain nombre dinconvnients. Premirement, si lon choisit une variable globale ou une constante, il nexiste aucun moyen de retarder linstanciation de lobjet singleton jusquau moment o il devient ncessaire. Une variable globale ou une constante sont prsentes ds leur initialisation. Deuximement, rien dans ces techniques nempche la cration dune deuxime ou troisime instance de votre "singleton". On pourrait essayer de grer ce problme sparment. Par exemple, on pourrait crer une instance de singleton et ensuite modier la classe pour quelle refuse dinstancier de nouveaux objets, mais ce genre de solution ne semble pas tre trs propre. tant donn que les variables globales et les constantes ne font pas laffaire, quelles autres voies nous reste-t-il pour implmenter des singletons ? Des classes en tant que singletons Comme nous lavons appris, des mthodes ainsi que des variables peuvent tre dnies directement sur lobjet classe. En effet, notre implmentation initiale de ce pattern faisait appel des mthodes et des variables de classe pour grer linstance de lobjet

Chapitre 12

Crer un objet unique avec Singleton

187

singleton. Mais puisque nous pouvons avoir des mthodes et des variables attaches une classe pourquoi ne pas utiliser la classe mme en tant que conteneur de la fonctionnalit singleton ? Chaque classe est unique, il ne peut exister quun seul SimpleLogger charg linstant donn. On pourrait donc dnir notre fonctionnalit singleton laide des mthodes et des variables de lobjet classe :
class ClassBasedLogger ERROR = 1 WARNING = 2 INFO = 3 @@log = File.open(log.txt, w) @@level = WARNING def self.error(msg) @@log.puts(msg) @@log.flush end def self.warning(msg) @@log.puts(msg) if @@level >= WARNING @@log.flush end def self.info(msg) @@log.puts(msg) if @@level >= INFO @@log.flush end def self.level=(new_level) @@level = new_level end def self.level @@ievel end end

Lutilisation de lobjet singleton fond sur une classe nest pas difcile :
ClassBasedLogger.level = ClassBasedLogger::INFO ClassBasedLogger.info(Computer wins chess game.) ClassBasedLogger.warning(AE-35 hardware failure predicted.) ClassBasedLogger.error(HAL-9000 malfunction, take emergency action!)

La technique "classe en tant que singleton" offre un avantage cl par rapport aux variables globales et aux constantes : on est sr que la cration dune deuxime instance est impossible. Toutefois, avec cette technique linitialisation tardive prsente un problme. Votre classe est initialise lors du chargement (typiquement lorsque quelquun inclut le chier o rside la classe avec linstruction require). Donc, on ne contrle pas le moment de son initialisation. Un autre dfaut de cette technique tient au fait que programmer des variables et des mthodes de classe nest pas aussi facile que

188

Patterns en Ruby

coder des mthodes et des variables dinstance traditionnelles ; on a un sentiment trange quand on crit tous ces self.methods et ces @@variables. Des modules en tant que singletons Utiliser un module pour encapsuler le comportement singleton est une autre possibilit. On a dj not dans ce chapitre que les modules ont beaucoup de points communs avec les classes. En effet, les modules ressemblent tellement aux classes quon peut dnir des mthodes et variables au niveau des modules comme on le faisait avec des classes. Mise part la dclaration module, limplmentation fonde sur un module est identique celle fonde sur une classe :
module ModuleBasedLogger ERROR = 1 WARNING = 2 INFO = 3 @@log = File.open("log.txt", "w") @@level = WARNING def self.error(msg) @@log.puts(msg) @@log.flush end # Beaucoup de code identique # ClassBasedSingleton supprim... end

Tout comme les mthodes de classe, les mthodes de module sont universellement accessibles :
ModuleBasedLogger.info(Computer wins chess game.)

La technique "module en tant que singleton" possde un avantage considrable par rapport lapproche "classe en tant que singleton". Puisquun module ne peut pas tre instanci (ce qui est la diffrence cl entre une classe et un module), le but dun singleton fond sur un module se manifeste plus clairement la lecture du code : voici un ensemble de mthodes destines tre appeles mais dont on ne peut rien instancier.

Ceinture de scurit ou carcan ?


Le dbat sur les moyens alternatifs dimplmentation du pattern Singleton soulve certaines questions concernant les fonctions de scurit incorpores dans le langage et limpact quelles peuvent avoir dans un langage aussi exible que Ruby. Par exemple, nous avons vu que le fait dincorporer le module Singleton provoque le changement de modicateur daccs de la mthode new en private. Ceci empche la cration dune deuxime ou troisime instance de la classe singleton. Si notre classe est dnie de la faon suivante :

Chapitre 12

Crer un objet unique avec Singleton

189

require singleton class Manager include Singleton def manage_resources puts("I am managing my resources") end end

On ne peut pas crer une autre instance de Manager. Par exemple, si lon essaie
m = Manager.new

on obtient
private method new called for Manager:Class

En vrit, le module Singleton ne peut pas empcher une telle action. Pour contourner lavertissement, il suft de bien comprendre le fonctionnement de Singleton et davoir une ide de la mcanique de la mthode public_class_method (le frre ennemi de private_class_method) :
class Manager public_class_method:new end m = Manager.new

Nous avons not ci-dessus que lavantage des singletons fonds sur un module ou sur une classe rside dans limpossibilit de crer une deuxime instance. Si lappel est lanc par hasard, cest effectivement impossible. Mais peu importe quelle classe vous utilisez, que ce soit ClassBasedLogger ou son cousin ModuleBasedLogger, ce ne sont que des objets. Et tous les objets Ruby hritent de la mthode clone. Cette mthode est un moyen formidable pour contourner la protection des singletons que nous avons tablie avec tant deffort :
a_second_logger = ClassBasedLogger.clone a_second_logger.error(using a second logger)

On peut en effet surcharger la mthode clone dans ClassBasedLogger pour viter le clonage non sollicit, mais une personne dtermine pourrait aussi bien rouvrir la classe et supprimer la protection. Le but est de ne pas encourager ce type de code, mais dillustrer que dans un langage o quasiment tout ce qui est fait au moment de lexcution peut aussi tre dfait dynamiquement, trs peu de dcisions sont dnitives. La philosophie Ruby est telle que, si vous dcidez de contourner lintention trs claire de lauteur de la classe ClassBasedLogger et de la cloner, le langage vous donne les moyens de le faire. La dcision est alors prise sciemment par le dveloppeur et non pas par le langage. Ruby ouvre

190

Patterns en Ruby

quasiment tout la modication, vous tes donc libre de choisir vos propres pratiques et cest donc vous de faire les bons choix.

User et abuser du pattern Singleton


Maintenant que nous savons implmenter des singletons, essayons de comprendre pourquoi cest le pattern probablement le plus dtest. Ce sont simplement des variables globales, nest-ce pas ? Commenons par le problme le plus vident : un singleton ressemble fortement son cousin hors la loi, la variable globale. Peu importe le type de singleton que vous implmentez : un objet gr par une classe prconis par le GoF ou un ensemble de mthodes et de variables attaches une classe ou un module, vous crez un objet unique avec la porte globale. Un singleton ouvre la porte secrte par laquelle des parties compltement indpendantes de votre programme peuvent communiquer, ce qui augmente fortement le couplage entre des composants de votre systme. Les consquences horribles de ce couplage expliquent pourquoi les ingnieurs logiciel ont abandonn lusage des variables globales. Il nexiste quune seule solution au problme : ne faites pas cela. Lorsquils sont utiliss correctement, les singletons ne sont pas des variables globales. Leur but est de modliser des choses qui nont quune seule occurrence. Puisque loccurrence est unique, on peut utiliser un singleton en tant que canal unique de communication entre diffrentes parties de votre programme. Mais ne faites pas cela. Les singletons ne sont pas diffrents des autres patterns : si vous en abusez, vous pouvez provoquer beaucoup de dgts. Je ne peux que rpter cette recommandation : ne faites jamais cela. Vous en avez combien, des singletons ? La deuxime faon davoir des problmes avec le pattern Singleton peut paratre vidente mais elle est trs courante : dnir des singletons en grande quantit. Lorsque vous considrez lutilisation du pattern Singleton, posez-vous cette question : est-ce que je suis sr que cette chose est unique ? Le pattern Singleton nous fournit un moyen de crer un modle dune instance unique, mais ce modle est accompagn dune fonction trs pratique qui rend cette instance facilement accessible, il suft dappeler SimpleLogger.instance. La simplicit daccs peut avoir un effet hypnotique : "Mon code serait beaucoup plus simple si cette chose tait un singleton." Ncoutez pas ce chant des sirnes. Concentrez-vous sur la question ci-dessus et considrez laccs facile comme un bonus.

Chapitre 12

Crer un objet unique avec Singleton

191

Singletons pour les intimes Largement propager linformation sur le fait quun objet est un singleton est une autre erreur rpandue. On peut voir le fait quune classe est un singleton comme un simple dtail dimplmentation : une fois rcupr lobjet chier de conguration, peu vous importe de savoir comment cette opration est implmente. Souvenez-vous que lon peut retrouver lobjet singleton tout endroit du code et ensuite le passer comme un objet classique. Cette technique peut tre pratique lorsque votre application doit utiliser un singleton plusieurs endroits du code. Par exemple, votre application pourrait tre structure selon le diagramme de la Figure 12.1.
Figure 12.1
Une application utilisant des singletons dans des composants indpendants
Application

DataPersistence

PreferenceManager

PrefReader

PrefWriter

Imaginez que la classe PreferenceManager ainsi que les classes quelle utilise ncessitent un accs la base de donnes. Cest aussi le cas de la classe DataPersistence et de ses amis. Ensuite, imaginez que lapplication fasse appel une instance unique de la classe DatabaseConnectionManager pour grer toute connexion la base. Vous dcidez dimplmenter DatabaseConnectionManager comme un singleton :
require singleton class DatabaseConnectionManager include Singleton def get_connection # Retourner la connexion la base de donnes... end end

192

Patterns en Ruby

Question : quelles classes sont au courant que DatabaseConnectionManager est un singleton ? On pourrait rendre cette information largement disponible, accessible aux objets PrefReader et PrefWriter :
class PreferenceManager def initialize @reader = PrefReader.new @writer = PrefWriter.new @preferences = {:display_splash => false, :background_color =>:blue } end def save_preferences preferences = {} @writer.write(@preferences) end def get_preferences @preferences = @reader.read end end class PrefWriter def write(preferences) connection = DatabaseConnectionManager.instance.get_connection # crire les prfrences end end class PrefReader def read connection = DatabaseConnectionManager.instance.get_connection # Lire et retourner les prfrences... end end

Une approche amliore consiste concentrer linformation sur limplmentation de DatabaseConnectionManager dans la classe PreferenceManager et simplement passer linstance aux objets reader et writer :
class PreferenceManager def initialize @reader = PrefReader.new @writer = PrefWriter.new @preferences = {:display_splash => false, :background_color =>:blue } end def save_preferences preferences = {} @writer.write(DatabaseConnectionManager.instance, @preferences) end

Chapitre 12

Crer un objet unique avec Singleton

193

def get_preferences @preferences = @reader.read(DatabaseConnectionManager.instance) end end

Cette refactorisation diminue le volume de code qui est au courant du statut spcial de DatabaseConnectionManager. Cela prsente deux avantages. Premirement, il y aurait moins de code corriger, si vous dcouvrez que votre singleton nest nalement pas si unique que cela. Deuximement, le fait denlever le singleton des classes PrefReader et PrefWriter permet de les tester beaucoup plus facilement. Un remde contre les maux lis aux tests Le dernier point concerne les tests. Un inconvnient trs fcheux du pattern Singleton tient sa faon dinterfrer avec des tests unitaires. Un bon test unitaire doit dmarrer dans un tat bien dtermin. En effet, le rsultat de votre test ne serait pas trs able si vous ntiez pas certain des conditions initiales du test. Un bon test unitaire doit galement tre indpendant des autres tests : le test 3 est cens retourner exactement les mmes rsultats, peu importe sil est excut entre les tests 2 et 4, aprs le test 20 ou tout seul. Mais, si les tests de 1 20 vrient le fonctionnement dun singleton, chacun deux est susceptible de modier la seule instance du singleton de faon imprvisible. Dans ces conditions, lindpendance de tests nexiste plus. Une des solutions ce problme consiste crer deux classes : une classe ordinaire (non singleton) qui contient tout le code, et sa sous-classe qui est un singleton :
require singleton class SimpleLogger # Toute la fonctionnalit de logs est dans cette classe... end class SingletonLogger < SimpleLogger include Singleton end

Lapplication fait appel SingletonLogger, alors que les tests peuvent utiliser la classe normale Logger, qui nest pas un singleton.

Les singletons dans le monde rel


Un bon exemple du pattern Singleton se trouve dans ActiveSupport, la bibliothque des assistants fournis Rails. Rails sappuie normment sur le principe des conventions. Une des nombreuses conventions de Rails consiste passer de la forme singulier dun mot la forme pluriel et inversement. Pour y parvenir, ActiveSupport maintient une liste de rgles telles que "le pluriel du mot employee est employees, mais le pluriel du

194

Patterns en Ruby

mot criterion est criteria". Puisque ce sont des rgles, seul un exemplaire de ces rgles est ncessaire dans le systme. Voil pourquoi la classe Inflections est un singleton : cela conomise lespace et assure que les mmes rgles de pluralisation sont disponibles dans la totalit du systme. Lutilitaire dinstallation rake utilise galement un singleton. Tout comme les autres utilitaires dinstallation, rake lit lexcution les informations sur les tches effectuer : quels dossiers il faut crer et quels chiers copier, etc.1 Toute linformation doit rester disponible lensemble des composants de rake, cest pourquoi rake stocke les donnes dans un objet unique (lobjet Rake::Application pour tre prcis) qui est accessible la totalit du programme en tant que singleton.

En conclusion
Dans ce chapitre, nous avons pass en revue la carrire quelque peu chaotique du pattern Singleton. Ce pattern peut nous aider grer des cas o il nexiste quune occurrence dun objet. Un singleton prsente deux caractristiques fondamentales : la classe singleton a exactement une instance et cette instance est accessible globalement. Lutilisation des mthodes et des variables de classe nous permet dimplmenter facilement un singleton classique recommand par le GoF. Dautres mthodes sont disponibles pour dvelopper des singletons ou tout au moins des presque singletons. Par exemple, on pourrait atteindre en partie le comportement de singleton laide des variables globales et des constantes. Nanmoins, ces lments ne peuvent assurer la proprit indispensable dunicit dun singleton. Qui plus est, nous pouvons construire un singleton avec des mthodes et variables attaches une classe ou un module. Dans ce chapitre, nous avons consacr beaucoup dattention aux piges tendus par les singletons. Ce pattern prsente des dispositions particulires pour augmenter fortement le couplage de votre code. Nous avons appris quil est judicieux de limiter le volume de code qui est au courant du statut spcial dun objet singleton. Nous avons galement vu un moyen de lever les contraintes que ce pattern impose sur les tests unitaires. Le pattern Singleton me rappelle une scie de mon pre. Elle tait incroyablement efcace dans le dcoupage du bois, mais faute de dispositifs de scurit elle tait tout aussi bien capable de vous trancher la main.

1. Lutilitaire rake applique le pattern Internal DSL (voir Chapitre 16) pour la plupart de la lecture.

13
Choisir la bonne classe avec Factory
Mon professeur de physique lcole tait un de ces enseignants extraordinaires capables de rendre vivant et intressant le plus ennuyeux des sujets. Au bout de deux mois, tous les lves dans les cours de physique lmentaire semblaient avoir abandonn leur unique ambition davoir une bonne note pour un but plus noble : "faire vritablement de la physique". Cela impliquait bien des choses comme faire les expriences avec beaucoup de soin, rchir normment. Il y avait encore une chose quun bon lve de physique devait viter tout prix : lattitude dsinvolte. Les actions comme omettre un dtail cl, bidouiller une quation ou supposer quelque chose non conrm par lexprience illustrent bien cette notion. Je dois avouer quen crivant ce livre je me suis rendu coupable dune attitude dsinvolte. Jai en effet omis le mcanisme qui permet votre code de savoir comme par magie quelle classe choisir un moment critique. Dhabitude, slectionner la bonne classe nest pas difcile : lorsquon a besoin dun objet String ou Date ou mme PersonnelRecord, on appelle simplement la mthode new sur la classe correspondante. Mais, parfois, le choix de la classe est une dcision cruciale et se retrouver dans ce genre de situation est trs courant. Pensez au pattern Template Method, par exemple. Lorsque vous utilisez le pattern Template Method, vous devez slectionner une sousclasse, et ce choix dtermine la variante dalgorithme qui sera excute. Est-ce PlainReport ou HTMLReport que vous voulez utiliser ? De la mme manire, avec le pattern Strategy, il faut choisir la stratgie adapte pour passer votre objet de contexte : est-ce que vous avez besoin de FrenchTaxCalculator ou dItalianTaxCalculator ? Cest aussi valable pour le pattern Proxy : vous devez slectionner la classe proxy avec le comportement ncessaire.

196

Patterns en Ruby

Il existe plusieurs faons de dterminer la classe la mieux adapte aux circonstances, y compris deux patterns du GoF. Dans ce chapitre, nous examinerons les deux patterns : Factory Method et Abstract Factory. Nous apporterons quelques claircissements certaines techniques dynamiques du langage Ruby qui peuvent nous aider dvelopper ces fabriques (factories) de faon plus efcace.

Une autre sorte de typage la canard


Pour dbuter notre exploration des factories, commenons par un problme de programmation. Imaginez quon vous demande de dvelopper un simulateur de mare canard. Plus prcisment, on vous demande de crer un modle permettant de simuler la vie dun canard. Vous crivez donc une classe pour modliser un canard :
class Duck def initialize(name) @name = name end def eat puts("Duck #{@name} is eating.") end def speak puts("Duck #{@name} says Quack!") end def sleep puts("Duck #{@name} sleeps quietly.") end end

On peut voir dans ce code que les canards mangent, dorment et font du bruit, tout comme les autres animaux. Ils ont aussi besoin dun endroit pour vivre. Dveloppons donc la classe Pond (NDT : la mare canard en anglais) :
class Pond def initialize(number_ducks) @ducks = [] number_ducks.times do |i| duck = Duck.new("Duck#{i}") @ducks << duck end end def simulate_one_day @ducks.each {|duck| duck.speak} @ducks.each {|duck| duck.eat} @ducks.each {|duck| duck.sleep} end end

Chapitre 13

Choisir la bonne classe avec Factory

197

Excuter le simulateur ne prsente pas de difcults :


pond = Pond.new(3) pond.simulate_one_day

Le code prcdent est une simulation dune journe ordinaire dans la mare o vivent trois canards. Il afche le rsultat suivant :
Duck Duck Duck Duck Duck Duck Duck Duck Duck Duck0 Duck1 Duck2 Duck0 Duck1 Duck2 Duck0 Duck1 Duck2 says Quack! says Quack! says Quack! is eating. is eating. is eating. sleeps quietly. sleeps quietly. sleeps quietly.

Lidylle ltang continue jusqu ce que vous soyez amen crer un modle dun autre habitant : la grenouille. Crer une classe Frog (grenouille) est facile car elle prsente exactement la mme interface que la classe Duck :
class Frog def initialize(name) @name = name end def eat puts("Frog #{@name} is eating.") end def speak puts("Frog #{@name} says Crooooaaaak!") end def sleep puts("Frog #{@name} doesnt sleep; he croaks all night!") end end

Mais un problme surgit dans la classe Pond. On cre des canards explicitement dans sa mthode initialize :
def initialize(number_ducks) @ducks = [] number_ducks.times do |i| duck = Duck.new(nDuck#{i}n) @ducks << duck end end

Le problme, cest que nous avons besoin de sparer la partie variable les animaux qui habitent ltang (des canards ou des grenouilles) des parties statiques comme les

198

Patterns en Ruby

dtails de fonctionnement de la classe Pond. Sil y avait un moyen dextraire lappel Duck.new de la classe Pond, cette classe pourrait supporter des canards ainsi que des grenouilles. Ce dilemme nous amne la question principale de ce chapitre : quelle classe faut-il utiliser ?

Le retour du pattern Template Method


Une des faons de grer le problme consiste dlguer le choix de la classe une sous-classe. On commence par dvelopper une classe parent gnrique. Elle est gnrique dans le sens o elle ne slectionne pas la classe utiliser. Lorsque la classe parent a besoin dun nouvel objet, elle appelle une mthode dnie dans une sous-classe. Par exemple, on pourrait remanier la classe Pond an quelle se fonde sur la mthode new_animal pour instancier les habitants de ltang :
class Pond def initialize(number_animals) @animals = [] number_animals.times do |i| animal = new_animal("Animal#{i}") @animals << animal end end def simulate_one_day @animals.each { | animal | animal.speak} @animals.each { | animal | animal.eat} @animals.each {|animal| animal.sleep} end end

Ensuite, on peut dnir deux sous-classes de Pond, une pour un tang plein de canards et une autre pour un tang envahi de grenouilles :
class DuckPond < Pond def new_animal(name) Duck.new(name) end end class FrogPond < Pond def new_animal(name) Frog.new(name) end end

Dsormais, il suft de slectionner le type de ltang et il serait automatiquement rempli des habitants du type correspondant :
pond = FrogPond.new(3) pond.simulate_one_day

Chapitre 13

Choisir la bonne classe avec Factory

199

On obtient des rsultats qui sentent bon la vase et lherbe verte :


Frog Frog Frog Frog Frog ... Animal10 Animal11 Animal12 Animal10 Animal11 says Crooooaaaak! says Crooooaaaak! says Crooooaaaak! is eating. is eating.

Je ninclus pas cet exemple, mais on pourrait tout aussi bien crer une sous-classe de Pond dont la mthode new_animal produit un mlange de canards et de grenouilles. Les membres du GoF ont nomm Factory Method le pattern qui consiste dlguer le choix de la classe vers une sous-classe. Son diagramme de classe UML compte deux hirarchies de classes (voir Figure 13.1). Dune part, il y a des crateurs : la classe parent et des classes concrtes qui contiennent les mthodes de cration. Dautre part, nous avons des produits : des objets que lon instancie. Dans notre exemple avec ltang, la classe Pond est un crateur, alors que les types spciques dtangs (DuckPond et FrogPond) sont des crateurs concrets ; les classes Duck et Frog sont des produits.
Figure 13.1
Diagramme de classe du pattern Factory Method
Product Creator factory_method()

Product1

ConcreteCreator1 factory_method()

Product2

ConcreteCreator2 factory_method()

Malgr le fait quelles partagent la mme classe parent Product, les classes Duck et Frog ne sont pas apparentes (voir Figure 13.1). Elles sont du mme type car elles implmentent le mme ensemble de mthodes. Si vous scrutez la Figure 13.1 attentivement, vous dcouvrirez que le pattern Factory Method nest pas un nouveau pattern. Cest simplement le pattern Template Method (souvenez-vous du Chapitre 3) appliqu au problme dinstanciation dobjets. Dans les

200

Patterns en Ruby

deux patterns, Factory Method et Template Method, une partie gnrique de lalgorithme (dans notre exemple, cest lactivit journalire dun tang) est code dans la classe parent, et les sous-classes comblent les lacunes de cette classe. Dans le pattern Factory Method, les sous-classes dterminent les classes des objets qui peuplent ltang.

Des mthodes factory avec des paramtres


Un des problmes avec les programmes qui ont du succs est leur tendance attirer sans cesse de nouvelles demandes de fonctionnalits. Imaginez que votre simulateur dtang soit devenu populaire au point que vos utilisateurs vous demandent dajouter des modles de plantes. Vous levez votre baguette magique et dnissez quelques classes reprsentant des plantes :
class Algae def initialize(name) @name = name end def grow puts("The Algae #{@name} soaks up the sun and grows") end end class WaterLily def initialize(name) @name = name end def grow puts("The water lily #{@name} floats, soaks up the sun, and grows") end end

Vous modiez galement la classe Pond pour prendre en compte les plantes :
class Pond def initialize(number_animals, number_plants) @animals = [] number_animals.times do |i| animal = new_animal("Animal#{i}") @animals << animal end @plants = [] number_plants.times do |i| plant = new_plant("Plant#{i}") @plants << plant end end

Chapitre 13

Choisir la bonne classe avec Factory

201

def simulate_one_day @plants.each {|plant| plant.grow } @animals.each {|animal| animal.speak} @animals.each {|animal| animal.eat} @animals.each {|animal| animal.sleep} end end

Il faudrait apporter des modications aux sous-classes pour crer la vgtation :


class DuckWaterLilyPond < Pond def new_animal(name) Duck.new(name) end def new_plant(name) WaterLily.new(name) end end class FrogAlgaePond < Pond def new_animal(name) Frog.new(name) end def new_plant(name) Algae.new(name) end end

Notre code est quelque peu maladroit car nous avons une mthode spare pour chaque type dobjet produit : new_animal pour instancier des grenouilles et des canards et new_plant pour crer des nnuphars et des algues. Avoir des mthodes spares pour chaque type dobjet nest pas pnalisant sil nexiste que deux types comme dans notre exemple. Et si vous aviez cinq ou dix types diffrents ? Coder toutes ces mthodes deviendrait vite trs fastidieux. Une solution alternative, et probablement plus propre, sappuierait sur une mthode factory unique acceptant en paramtre le type dobjet qui doit tre cr. Le code suivant prsente la classe Pond une fois de plus. Cette fois, elle expose une mthode factory qui accepte des paramtres et qui est capable de produire une plante ou un animal en fonction du symbole pass en argument :
class Pond def initialize(number_animals, number plants) @animals = [] number_animals.times do |i| animal = new_organism(:animal, "Animal#{i}") @animals << animal end

202

Patterns en Ruby

@plants = [] number plants.times do |i| plant = new_organism(:plant, "Plant#{i}") @plants << plant end end # ... end class DuckWaterLilyPond < Pond def new_organism(type, name) if type ==:animal Duck.new(name) elsif type ==:plant WaterLily.new(name) else raise "Unknown organism type: #{type}" end end e end

Les mthodes factory paramtres amincissent considrablement votre code car les sous-classes ne dnissent quune seule mthode factory. Elles rendent galement lensemble du systme plus facilement extensible. Supposez quil faille dnir un nouvel habitant pour votre tang : par exemple le poisson. Dans ce cas, il suft de modier une seule mthode des sous-classes au lieu dajouter une nouvelle mthode : cest un autre exemple des avantages apports par la sparation des parties variables et statiques.

Les classes sont aussi des objets


La ncessit de crer une nouvelle sous-classe pour chaque type dobjet instancier constitue un srieux argument contre notre implmentation actuelle du pattern Factory Method. Les noms des sous-classes dans notre dernire version retent cet tat de fait : on a DuckWaterLilyPond et FrogAlgaePond, mais on pourrait aussi avoir besoin de DuckAlgaePond ou de FrogWaterLilyPond. Lorsque la collection de plantes et danimaux stoffe, le nombre de sous-classes possibles devient effrayant. Mais, en ralit, la seule diffrence entre ces tangs divers et varis se rduit la classe des objets instancis par la mthode factory : ce sont parfois des nnuphars et des canards et parfois des algues et des grenouilles. Il faut se rendre compte que les classes Frog, Duck, WaterLily et Algae sont des objets classiques qui passent leur vie crer dautres objets. En conservant les classes dobjets crer dans des variables dinstance, on pourrait se dbarrasser de la hirarchie toute entire des sous-classes de Pond :

Chapitre 13

Choisir la bonne classe avec Factory

203

class Pond def initialize(number_animals, animal_class, number plants, plant_class) @animal_class = animal_class @plant_class = plant_class @animals = [] number_animals.times do |i| animal = new_organism(:animal, "Animal# {i} " ) @animals << animal end @plants = [] number plants.times do |i| plant = new_organism(:plant, "Plant#{i}") @plants << plant end end def simulate_one_day @plants.each {|plant| plant.grow} @animals.each {|animal| animal.speak} @animals.each {|animal| animal.eat} @animals.each {|animal| animal.sleep} end def new_organism(type, name) if type ==:animal @animal class.new(name) elsif type ==:plant @plant_class.new(name) else raise "Unknown organism type: #{type}" end end end

La nouvelle classe Pond nest pas plus complexe dutilisation que la version prcdente. Nous passons les classes des plantes et des animaux dans le constructeur :
pond = Pond.new(3, Duck, 2, WaterLily) pond.simulate_one_day

Le fait de stocker les classes danimaux et de plantes dans Pond nous permet de rduire le nombre de classes ncessaire une seule. Et, de plus, nous avons obtenu ce rsultat sans rendre la classe Pond plus complexe.

Mauvaise nouvelle : votre programme a du succs


Supposons que votre simulateur dtangs gagne encore davantage en popularit et que de nouvelles exigences tombent sur votre bureau plus vite que jamais. La demande la plus urgente est le besoin de grer des environnements autres quun tang. En effet, un

204

Patterns en Ruby

commercial en grande forme vient de signer une commande pour un simulateur de jungle. Il est vident que cette modication ne serait gure possible sans une intervention majeure dans le code. On aurait certainement besoin de modles danimaux de la jungle (des tigres, peut-tre) ainsi que de plantes (principalement des arbres) :
class Tree def initialize(name) @name = name end def grow puts("The tree #{@name} grows tall") end end class Tiger def initialize(name) @name = name end def eat puts("Tiger #{@name} eats anything it wants.") end def speak puts("Tiger #{@name} Roars!") end def sleep puts("Tiger #{@name} sleeps anywhere it wants.") end end

Il faudrait galement transformer le nom de la classe Pond en un nom plus gnrique et adaptable un tang aussi bien qu une jungle. Habitat semble appropri :
jungle = Habitat.new(1, Tiger, 4, Tree) jungle.simulate_one_day pond = Habitat.new( 2, Duck, 4, WaterLily) pond.simulate_one_day

Mis part le changement de nom, notre classe Habitat est identique la dernire version de la classe Pond (celle qui avait des classes de plantes et danimaux). Des habitats se crent de la mme faon que des tangs.

Cration de lots dobjets


Lun des problmes de notre nouvelle classe Habitat tient la possibilit de crer des combinaisons de ore et de faune incohrentes dun point de vue cologique. Par

Chapitre 13

Choisir la bonne classe avec Factory

205

exemple, aucun mcanisme nest prvu dans notre implmentation actuelle pour nous prvenir que des tigres ne sont pas compatibles avec des nnuphars :
unstable = Habitat.new( 2, Tiger, 4, WaterLily)

Cela ne reprsente pas un problme majeur lorsque nous navons que deux types de choses grer (des plantes et des animaux dans ce cas). Mais il nen irait pas de mme si la simulation tait plus dtaille et quelle incorporait des insectes ainsi que des oiseaux, des mollusques et des champignons ? On voudrait certainement viter des champignons qui poussent sur des nnuphars et des bancs de poissons qui se dbattent dans les branches dun arbre tropical. Nous pouvons aborder ce problme en indiquant quel habitat appartiennent des organismes particuliers. Au lieu de passer Habitat des classes individuelles de plantes et danimaux, on pourrait lui passer un objet unique qui matrise la cration dune collection cohrente de produits. Une version de cet objet existerait pour des tangs : il crerait des instances de grenouilles et nnuphars. Une deuxime version serait propre lenvironnement de la jungle et instancierait des tigres ainsi que des arbres tropicaux. Un objet ddi la cration dun certain nombre dobjets compatibles sappelle une factory abstraite ou Abstract Factory, un pattern que le GoF a rendu clbre. Le code ci-aprs prsente deux factories abstraites de notre simulateur dhabitats. La premire correspond un tang et la deuxime, une jungle :
class PondOrganismFactory def new_animal(name) Frog.new(name) end def new_plant(name) Algae.new(name) end end class JungleOrganismFactory def new_animal(name) Tiger.new(name) end def new plant(name) Tree.new(name) end end

Aprs quelques modications simples, la mthode initialize de notre classe Habitat est prte utiliser la factory abstraite :
class Habitat def initialize(number_animals, number_plants, organism_factory)

206

Patterns en Ruby

@organism_factory = organism_factory @animals = [] number_animals.times do |i| animal = @organism_factory.new_animal("Animal#{i}") @animals << animal end @plants = [] number_plants.times do |i| plant = @organism_factory.new_plant("Plant#{i}") @plants << plant end end # Le reste de la classe...

Nous pouvons dsormais fournir la factory abstraite notre Habitat en tant sr de ne pas nous retrouver avec un trange mlange dhabitants des tangs et de la jungle :
jungle = Habitat.new(1, 4, JungleOrganismFactory.new) jungle.simulate_one_day pond = Habitat.new(2, 4, PondOrganismFactory.new) pond.simulate_one_day

La Figure 13.2 prsente le diagramme des classes UML du pattern Abstract Factory. On voit deux classes factory concrtes qui crent leurs groupes respectifs de produits compatibles.
Figure 13.2
Le pattern Abstract Factory
AbstractFactory create_product1() create_product2()

ConcreteFactoryA create_product1() create_product2()

ConcreteFactoryB create_product1() create_product2()

ProductA2

ProductB2

ProductA1

ProductB1

Lessentiel du pattern Abstract Factory rside dans lexpression du problme et de sa solution. Le problme est ici le besoin de crer un ensemble dobjets compatibles. La solution consiste crire une classe spare pour grer linstanciation. Le pattern

Chapitre 13

Choisir la bonne classe avec Factory

207

Factory Method est en ralit le pattern Template Method appliqu la cration dobjets. De la mme manire, le pattern Abstract Factory nest autre que le pattern Strategy appliqu au mme problme.

Des classes sont des objets (encore)


On peut voir la factory abstraite comme un objet dun niveau suprieur : elle est capable de crer plusieurs types dobjets (ou produits) alors que les classes normales ne savent crer quun seul type dobjet (des instances delles-mmes). Cette vision nous suggre un moyen de simplier notre implmentation du pattern Abstract Factory : elle peut se fonder sur des objets classe, une classe par produit. Cest exactement la mme approche adopte plus haut pour simplier le pattern Factory Method. Le code ci-aprs dmontre une factory abstraite fonde sur des classes. Au lieu davoir plusieurs factories abstraites dont chacune cre son propre groupe de produits, on peut dnir une classe qui stocke les objets classe des produits instancier :
class OrganismFactory def initialize(plant_class, animal_class) @plant_class = plant_class @animal_class = animal_class end def new_animal(name) @animal_class.new(name) end def new plant(name) @plant_class.new(name) end end

Cette approche nous permet de crer une nouvelle instance de factory pour chaque groupe dobjets compatibles :
jungle_organism_factory = OrganismFactory.new(Tree, Tiger) pond_organism_factory = OrganismFactory.new(WaterLily, Frog) jungle = Habitat.new(1, 4, jungle_organism_factory) jungle.simulate_one_day pond = Habitat.new( 2, 4, pond_organism_factory) pond.simulate_one_day

Cela peut ressembler un serpent qui se mord la queue : nous avons commenc par dnir une factory abstraite pour viter de spcier des classes individuelles et il semble que notre dernire implmentation laisse la porte ouverte la cration dtangs pleins de tigres ou dune jungle envahie par des algues. Ce nest pas le cas car une factory abstraite permet lencapsulation de linformation concernant la compatibilit

208

Patterns en Ruby

des types de produits. Cette encapsulation peut tre exprime par des classes et sousclasses ainsi que par des objets classe comme dans le code ci-dessus. Dans tous les cas, vous vous retrouvez avec un objet qui sait quelles choses sont lies ou interdpendantes.

Proter du nommage
Un autre moyen de simplier limplmentation dune factory abstraite consiste adopter une convention de nommage cohrente pour les classes de produits. Cette approche nest pas applicable dans notre exemple la classe Habitat, car cette classe contient des tigres et des grenouilles qui ont chacun un nom unique. Mais imaginez que vous deviez produire une factory abstraite pour des objets capables de lire et dcrire diffrents formats de chiers tels que PDF, HTML et PostScript. On pourrait certainement implmenter une classe IOFactory laide dune des techniques exposes dans ce chapitre. Mais, si les noms des classes de lecture et dcriture suivent une rgle prdnie, quelque chose comme HTMLReader / HTMLWriter pour du HTML et PDFReader / PDFWriter pour le format PDF, on pourrait dduire le nom de la classe partir du nom du format. Le code ci-dessous met cette dmarche en uvre :
class IOFactory def initialize(format) @reader_class = self.class.const_get("#{format}Reader") @writer_class = self.class.const_get("#{format}Writer") end def new_reader @reader_class.new end def new_writer @writer_class.new end end html_factory = IOFactory.new(HTML) html_reader = html_factory.new_reader pdf_factory = IOFactory.new(PDF) pdf_writer = pdf_factory.new_writer

La mthode const_get de la classe IOFactory accepte une chane de caractres (ou un symbole) qui contient le nom dune constante1 et retourne sa valeur. Par exemple, si vous passez dans la mthode const_get la chane "PDFWriter", vous obtiendrez en retour un objet classe avec ce nom : exactement ce que lon souhaite 2.
1. Souvenez-vous que les noms de classes en Ruby sont des constantes. 2. videmment, si la classe PDFWriter nexiste pas, la mthode const_get dclencherait une exception. Dans notre cas, cest aussi le comportement souhait.

Chapitre 13

Choisir la bonne classe avec Factory

209

User et abuser des patterns Factory


Le moyen le plus simple de se tromper dans lutilisation des techniques de cration dobjets dcrites dans ce chapitre est dy recourir sans en avoir besoin. Il ne faut pas instancier tous les objets avec des factories. En effet, dans la plupart des cas il serait judicieux de crer vos objets avec un simple appel MyClass.new. Servez-vous des factories lorsque vous devez faire un choix parmi des classes distinctes mais ayant un lien entre elles. Noubliez pas le principe YAGNI (You Aint Gonna Need It), qui dit que dans limmense majorit des cas "vous naurez pas besoin de a". Ce principe sapplique sans doute aux factories plus qu tous autres patterns. Pour le moment, je ne gre que des canards et des nnuphars. Il est possible que dans le futur jaie besoin de traiter des tigres et des arbres. Devrais-je dvelopper une factory pour my prparer ? Probablement non. Il faut trouver lquilibre entre le cot engendr par une factory supplmentaire, srement inutile au dbut, et la probabilit den avoir effectivement besoin ultrieurement. Noubliez pas dinclure le cot des ajustements futurs de la factory. La rponse dpend des dtails mais, en rgle gnrale, les ingnieurs ont tendance construire un cargo l o un cano est souvent sufsant. Si pour le moment vous navez quune classe slectionner, remettez plus tard limplmentation dune fabrique.

Les factories dans le monde rel


Il savre que les implmentations classiques des factories fondes sur lhritage sont assez difciles trouver dans la base de code Ruby. Les programmeurs Ruby semblent avoir vot pour des versions de factories plus dynamiques, qui se fondent sur des objets classe ou sur diffrentes conventions de nommage des classes. Ainsi, la bibliothque SOAP distribue avec linterprteur Ruby instancie des objets Ruby en fonction des chanes de caractres XML correspondant des noms des classes. De la mme manire, limplmentation de XMLRPC1 incluse dans la bibliothque standard Ruby supporte plusieurs options danalyse syntaxique de XML. Chacune de ces mthodes danalyse de XML a une classe danalyseur syntaxique associe. Il existe une classe pour convertir un chier XML en un ux et une autre pour le transformer en un arbre DOM. Mais il ny a pas de sous-classes qui correspondent chaque technique de traitement. Le code XMLRPC conserve la classe de lanalyseur XML slectionn et produit de nouvelles instances danalyseurs partir de lobjet classe si besoin.
1. Si vous ne lavez pas encore rencontr, XMLRPC est un mcanisme dappel de procdures distantes fond sur du XML, semblable SOAP. Contrairement SOAP, XMLRPC fait son possible pour simplier le protocole des communications.

210

Patterns en Ruby

Une version relativement exotique du pattern Factory Method se trouve dans ActiveRecord. Nous avons appris au Chapitre 9 quActiveRecord possde une classe adaptateur pour chaque type de base de donnes support. Cest ainsi quon trouve des adaptateurs pour MySQL, Oracle, DB2, etc. Lorsque vous demandez ActiveRecord dtablir une connexion avec la base de donnes, part le nom dutilisateur, le mot de passe et le port vous devez spcier une chane avec le nom de ladaptateur utiliser. Vous passez "mysql" si vous voulez quActiveRecord se connecte sur une base MySQL ou "oracle" si cest une base Oracle. Mais comment ActiveRecord arrive-t-il rcuprer une instance dadaptateur partir de cette chane de caractres ? Il savre quActiveRecord fait appel une technique assez intressante pour retrouver les instances dadaptateurs. Le cur de cette technique sappuie sur la classe Base1. Cette dernire est compltement indpendante de tout adaptateur spcique :
class Base # Beaucoup de code omis. Ce code nest pas li aux adaptateurs... end

Toutefois, chaque adaptateur contient du code apportant des modications spciques la classe Base. En dautres termes, chaque adaptateur ajoute la classe Base une mthode qui cre son propre type de connexion. Par exemple, ladaptateur MySQL contient du code qui ressemble ceci :
class def # # # end end Base self.mysql_connection(config) Crer et retourner une nouvelle connexion MySQL en utilisant le nom dutilisateur, le mot de passe, etc. qui sont dfinies dans le tableau associatif de configuration...

De la mme manire, ladaptateur Oracle contient le code suivant :


class Base def self.oracle_connection(config) # Crer une nouvelle connexion Oracle ... end end

1. En ralit, la classe Base est dnie hors du module ActiveRecord. Pour cette raison, son nom est habituellement crit comme ActiveRecord::Base. Dans un intrt de simplicit, jai supprim de notre code le nom du module, ainsi que de nombreux autres dtails qui ne sont pas pertinents dans cette section.

Chapitre 13

Choisir la bonne classe avec Factory

211

Une fois les modications effectues pour un adaptateur, la classe Base se retrouve avec une mthode appele <<db_type>>_connection pour chaque type de base de donnes quelle est capable de grer. Pour crer une vraie connexion partir dun nom dadaptateur, la classe Base construit une chane qui contient le nom de la mthode spcique la base de donnes. Le fonctionnement ressemble ceci :
adapter = "mysql" method_name = "#{adapter}_connection" Base.send(method_name, config)

La dernire ligne de cette mthode appelle la mthode spcique la base de donnes et passe en paramtre la conguration de la connexion : par exemple, le nom de la base, le nom dutilisateur et le mot de passe. La connexion est tablie !

En conclusion
Dans ce chapitre, nous avons tudi deux patterns Factory du GoF. Les deux techniques rpondent la question "Quelle classe choisir ?". Le pattern Factory Method nest rien dautre que lapplication du pattern Template Method la cration dobjets. Fidle ses racines Template Method, ce pattern laisse le soin une sous-classe de slectionner la bonne classe pour instancier lobjet qui nous intresse. Nous avons russi appliquer ce pattern pour dvelopper la classe Pond gnrique qui encapsule les dtails de la simulation environnementale mais dlgue le choix de plantes et danimaux sa sous-classe. Nous pouvons donc crer des sous-classes nommes DuckWaterLilyPond ou FrogAlgaePond qui compltent les implmentations des mthodes factory an de crer des objets appropris. Le pattern Abstract Factory entre en jeu lorsque nous devons crer des groupes dobjets compatibles. Si vous souhaitez viter que des grenouilles et des algues soient mlanges avec des tigres et des arbres, dnissez une factory abstraite pour chaque combinaison valide. Une des leons retenir de ce chapitre concerne la transformation que ces deux patterns ont subie une fois transposs dans lenvironnement Ruby : ils sont devenus bien plus simples. Alors que le GoF se concentre sur des implmentations de factories fondes sur lhritage, nous arrivons obtenir le mme rsultat avec beaucoup moins de code. On prote du fait que les classes Ruby sont des objets : on peut les rechercher par leur nom, les passer en paramtre et les stocker pour les rutiliser plus tard.

212

Patterns en Ruby

Le pattern que nous allons examiner au chapitre suivant se nomme Builder. Il produit galement de nouveaux objets, mais il se concentre davantage sur la construction des objets complexes que sur le choix de la bonne classe. Mais la question du choix des objets adapts une tche donne est loin dtre close. Au Chapitre 17, nous dcouvrirons la mta-programmation : une technique pour personnaliser vos classes et vos objets au moment de leur excution.

14
Simplier la cration dobjets avec Builder
Je me rappelle trs distinctement le jour o jai offert mon ls son premier vlo. La matine stait bien passe : on avait pris la voiture pour se rendre au magasin, trouv la bonne taille de vlo et pass beaucoup de temps sur le choix cornlien de la couleur. La phase intermdiaire consistait rapporter le vlo la maison et sortir toutes les pices de la bote car, bien sr, il fallait raliser soi-mme une partie non ngligeable de lassemblage. La troisime phase aprs notre heureux retour la maison a provoqu une frustration profonde et quelques blessures aux doigts. Jai pass des heures essayer dassembler la multitude de pices selon des instructions qui auraient pu dconcerter une quipe entire de cryptologues. Choisir le vlo tait facile, le vrai d consistait le monter. De telles situations surviennent galement avec les objets. Le Chapitre 13 a dcrit des factories qui permettent de rcuprer le bon type dobjet. Mais, parfois, rcuprer lobjet nest pas le problme principal. Parfois, le vrai souci est sa conguration. Dans ce chapitre, nous examinerons le pattern Builder, qui est conu pour vous aider paramtrer des objets complexes. Nous verrons que le pattern Builder a un certain nombre de points communs avec le pattern Factory, ce qui na rien dtonnant. Nous dcouvrirons galement des mthodes magiques : une technique Ruby pour faciliter lutilisation des objets builder. La question de la rutilisation de builders est un autre point voqu dans ce chapitre. Enn, nous allons apprendre la faon dont le pattern Builder peut vous aider viter des erreurs de paramtrage dobjets et mme vous aider crer des objets valides.

214

Patterns en Ruby

Construire des ordinateurs


Imaginez que vous dveloppiez un systme informatique pour une petite entreprise qui fabrique des ordinateurs. Chaque machine commande est faite sur mesure, il sera donc ncessaire de garder trace des composants installs sur chacune des machines. Pour faire simple, supposons quun ordinateur se compose dun cran, dune carte mre et de plusieurs lecteurs de mdia amovibles :
class Computer attr_accessor:display attr_accessor:motherboard attr_reader:drives def initialize(display=:crt, motherboard=Motherboard.new, drives=[]) @motherboard = motherboard @drives = drives @display = display end end

Slectionner un cran est simple, cest soit :crt soit :lcd. Lobjet Motherboard est plus complexe : il possde un certain volume de mmoire et contient soit un processeur normal, soit un processeur turbo trs rapide :
class CPU # Caractristiques gnrales dun processeur... end class BasicCPU < CPU # Caractristiques dun processeur pas trs rapide... end class TurboCPU < CPU # Caractristiques dun processeur trs rapide... end class Motherboard attr_accessor:cpu attr_accessor:memory_size def initialize(cpu=BasicCPU.new, memory_size=1000) @cpu = cpu @memory_size = memory_size end end

La classe Drive reprsente un lecteur qui est disponible en trois versions : disque dur, CD ou DVD :
class Drive attr_reader:type # peut tre:hard_disk,:cd ou:dvd attr_reader:size # en MO attr_reader:writable # true si le lecteur est disponible en criture

Chapitre 14

Simplier la cration dobjets avec Builder

215

def initialize(type, size, writable) @type = type @size = size @writable = writable end end

Mme ce modle simpli engendre des difcults lors de linstanciation dun objet Computer :
# Monter un ordinateur puissant avec beaucoup de mmoire... motherboard = Motherboard.new(TurboCPU.new, 4000) # ...avec un disque dur, un graveur CD et un lecteur DVD drives= [ ] drives<< Drive.new(:hard_drive, 200000, true) drives<< Drive.new(:cd, 760, true) drives<< Drive.new(:dvd, 4700, false) computer = Computer.new(:lcd, motherboard, drives)

Le principe simple du pattern Builder consiste encapsuler la logique de construction dans sa propre classe. La classe builder prend en charge lassemblage de tous les composants dun objet complexe. Chaque builder expose une interface permettant de spcier tape par tape la conguration de votre nouvel objet. On peut voir lobjet builder comme une mthode new plusieurs tapes. Les objets subissent un processus de construction tendu au lieu dtre crs instantanment. Le builder de nos ordinateurs pourrait ressembler ceci :
class ComputerBuilder attr_reader:computer def initialize @computer = Computer.new end def turbo(has_turbo_cpu=true) @computer.motherboard.cpu = TurboCPU.new end def display=(display) @computer.display=display end def memory_size=(size_in_mb) @computer.motherboard.memory_size = size_in_mb end def add_cd(writer=false) @computer.drives << Drive.new(:cd, 760, writer) end def add_dvd(writer=false) @computer.drives << Drive.new(:dvd, 4000, writer) end

216

Patterns en Ruby

def add_hard_disk(size_in_mb) @computer.drives << Drive.new(:hard_disk, size_in_mb, true) end end

La classe ComputerBuilder factorise tous les dtails de la cration dun objet Computer. Il suft de produire une nouvelle instance de builder et de spcier pas pas toutes les options requises pour votre ordinateur :
builder = ComputerBuilder.new builder.turbo builder.add_cd(true) builder.add_dvd builder.add_hard_disk(100000)

Enn, vous obtenez une instance de votre Computer ambant neuf :


computer = builder.computer

La Figure 14.1 prsente le diagramme de classes UML dun objet builder basique.
Figure 14.1
Le pattern Builder
Builder Director add_part1() add_part2() add_part3() result()

Product

Les membres du GoF ont nomm le client de lobjet builder director car il guide le builder dans la construction dun nouvel objet produit. Non seulement les builders facilitent la cration des objets complexes, mais ils cachent galement les dtails de limplmentation. Lobjet director nest pas oblig de matriser tous les points spciques de la cration dun nouvel objet. Lorsque lon utilise la classe ComputerBuilder, on peut ignorer compltement quelle classe reprsente un lecteur DVD ou un disque dur : on souhaite simplement obtenir un ordinateur correspondant la conguration requise.

Des objets builder polymorphes


Au dbut de ce chapitre, nous avons soulign la diffrence entre le pattern Builder et le pattern Factory : les builders sont moins concerns par le choix de la classe et se concentrent plus sur la conguration dobjets. Factoriser le code complexe li la conguration dobjets est la motivation principale des builders. Toutefois, puisquun builder gre la construction des objets, cest un endroit commode pour prendre une dcision sur le choix de la classe.

Chapitre 14

Simplier la cration dobjets avec Builder

217

Par exemple, imaginez que notre entreprise informatique grandisse et commence produire des ordinateurs portables en plus des ordinateurs de bureau. Nous avons dsormais deux types de produits : des ordinateurs de bureau et des portables.
class DesktopComputer < Computer # Beaucoup de dtails lis aux ordinateurs de bureau omis... end class LaptopComputer < Computer def initialize) motherboard=Motherboard.new, drives=[] ) super(:lcd, motherboard, drives) end # Beaucoup de dtails lis aux ordinateurs portables omis... end

Il est vident que les composants dun ordinateur portable ne sont pas les mmes que ceux installs sur des ordinateurs de bureau. Heureusement que nous pouvons refactoriser la classe builder en une classe parent et deux sous-classes pour grer ces diffrences. La classe abstraite prend en charge tous les dtails communs deux types dordinateurs :
class ComputerBuilder attr_reader:computer def turbo(has_turbo_cpu=true) @computer.motherboard.cpu = TurboCPU.new end def memory_size=(size_in_mb) @computer.motherboard.memory_size = size_in_mb end end

La classe DesktopBuilder encapsule linformation sur la construction des ordinateurs de bureau. Plus particulirement, elle est capable de crer des instances de la classe DesktopComputer et elle est au courant que lon installe des lecteurs de mdia classiques sur des ordinateurs de bureau :
class DesktopBuilder < ComputerBuilder def initialize @computer = DesktopComputer.new end def display=(display) @display = display end def add_cd(writer=false) @computer.drives << Drive.new(:cd, 760, writer) end def add_dvd(writer=false) @computer.drives << Drive.new(:dvd, 4000, writer) end

218

Patterns en Ruby

def add_hard_disk(size_in_mb) @computer.drives << Drive.new(:hard disk, size_in_mb, true) end end

De son ct, la classe LaptopBuilder cre des objets LaptopComputer et leur affecte des instances dun lecteur spcial LaptopDrive :
class LaptopBuilder < ComputerBuilder def initialize @computer = LaptopComputer.new end def display=(display) raise "Laptop display must be lcd" unless display ==:lcd end def add_cd(writer=false) @computer.drives << LaptopDrive.new(:cd, 760, writer) end def add_dvd(writer=false) @computer.drives << LaptopDrive.new(:dvd, 4000, writer) end def add_hard_disk(size_in_mb) @computer.drives << LaptopDrive.new(:hard disk, size_in_mb, true) end end

La Figure 14.2 prsente le diagramme de classes UML de notre nouveau builder polymorphe. Si lon compare la Figure 14.2 avec le diagramme de classes UML du pattern Abstract Factory (voir Figure 13.2), on remarque que ces patterns ont un certain air de famille.
Figure 14.2
Limplmentation dun builder polymorphe
Director ComputerBuilder add_dvd() add_cd() add_hard_drive() computer()

LaptopBuilder add_dvd() add_cd() add_hard_drive computer() DesktopComputer

DesktopBuilder add_dvd() add_cd() add_hard_drive computer() LaptopComputer

Chapitre 14

Simplier la cration dobjets avec Builder

219

On pourrait galement dnir une classe builder unique qui cre un ordinateur portable ou un ordinateur de bureau en fonction de la valeur passe en argument.

Les builders peuvent garantir la validit des objets


Les objets builders rendent la cration dobjets plus simple, mais ils peuvent aussi la rendre plus sre. La mthode nale "rends-moi mon objet" est un endroit parfait pour vrier que la conguration requise par le client est cohrente et compatible avec des rgles de gestion appropries. On pourrait par exemple amliorer notre mthode computer pour vrier que la conguration est raisonnable :
def computer raise "Not enough memory" if @computer.motherboard.memory_size < 250 raise "Too many drives" if @computer.drives.size > 4 hard_disk = @computer.drives.find {|drive| drive.type ==:hard_disk} raise "No hard disk." unless hard_disk @computer end

Si la conguration est incomplte, nous avons la possibilit deffectuer une correction au lieu de simplement dclencher une exception :
# ... if! hard_disk raise "No room to add hard disk." if @computer.drives.size >= 4 add_hard_disk(100000) end # ...

Le code prcdent ajoute un disque dur si le client ne la pas prcis et si lemplacement pour le disque est disponible.

Rutilisation de builders
Lorsque vous crivez et utilisez des builders, il faut vous poser une question importante : est-ce que votre instance de builder est capable de crer des objets multiples ? Par exemple, il est tout fait possible de demander votre LaptopBuilder deux instances dordinateurs identiques en une opration :
builder = LaptopBuilder.new builder.add_hard_disk(100000) builder.turbo computer1 = builder.computer computer2 = builder.computer

220

Patterns en Ruby

tant donn que la mthode computer retourne toujours la mme variable, computer1 et computer2 nissent par tre des rfrences vers le mme ordinateur. Ce nest probablement pas le comportement attendu. Une des solutions cette difcult consiste quiper votre builder de la mthode reset, responsable de la rinitialisation de lobjet en construction :
class LaptopBuilder # Beaucoup de code omis... def reset @computer = LaptopComputer.new end end

La mthode reset permet de rutiliser linstance de builder, mais elle vous oblige galement recommencer la conguration pour chaque ordinateur. Si vous voulez paramtrer lobjet une seule fois et demander lobjet builder de crer un nombre arbitraire dobjets suivant cette conguration, il faut conserver linformation sur la conguration dans des attributs dinstance et ne crer des produits rels que lorsquun client les demande.

Amliorer des objets builder avec des mthodes magiques


Employer notre builder dordinateurs est certainement une solution plus propre que rpandre dans lapplication le code qui gre la cration, la conguration et la validation de nos objets. Malheureusement, mme avec un builder, la procdure de conguration dun ordinateur nest pas trs lgante. Nous avons vu que pour congurer chaque nouvel ordinateur il faut instancier le builder et appeler un certain nombre de ses mthodes. Comment pourrait-on rendre le processus de conguration dun nouvel ordinateur plus concis et peut-tre lgrement plus lgant ? Une des faons dy parvenir repose sur la cration dune mthode magique. Lide sous-jacente cette mthode magique laisse le client dnir un nom de mthode selon un patron spcique. On pourrait par exemple congurer un nouveau portable avec
builder.add_dvd_and_harddisk

ou encore
builder.add_turbo_and_dvd_and_harddisk

Limplmentation des mthodes magique est trs simple si lon recourt la technique method_missing (nous lavons dj rencontre au Chapitre 10, lorsque nous faisions connaissance avec les proxies). Pour utiliser la mthode magique, il suft dattraper

Chapitre 14

Simplier la cration dobjets avec Builder

221

tous les appels inattendus laide de method_missing et de vrier que les noms de mthodes appeles correspondent aux patrons des noms de vos mthodes magiques :
def method_missing(name, *args) words = name.to_s.split("_") return super(name, *args) unless words.shift == add words.each do |word| next if word == and add_cd if word == cd add_dvd if word == dvd add_hard_disk(100000) if word == harddisk turbo if word == turbo end end

Le code ci-dessus divise le nom de mthode en supprimant les tirets bas et tche de traduire les fragments obtenus en requtes sur les diffrentes options dun ordinateur. La technique de la mthode magique ne se limite pas au pattern Builder. Elle est applicable dans toute situation o vous souhaitez que le code client puisse spcier des options multiples de faon concise.

User et abuser du pattern Builder


Le besoin pour le pattern Builder se fait souvent sentir lorsque votre application volue vers un niveau de complexit lev. Par exemple, supposons que tout au dbut votre classe Computer ne gre que les types de processeurs et le volume de la mmoire. Lutilisation dun builder ne serait pas justie dans ces conditions. Lorsque vous amliorez la classe Computer pour prendre en compte des lecteurs de mdia, le nombre doptions et leurs interdpendances augmentent nettement. Le besoin pour un builder devient plus prononc. Dhabitude, il est relativement simple didentier le code o un builder est ncessaire : la logique de la cration dun objet particulier est disperse partout dans le programme. Le fait que votre code commence produire des objets invalides et que vous vous disiez vous-mme "Jai vri le nombre de lecteurs lorsque jai cr un nouvel objet Computer un endroit, mais jai oubli la vrication un autre endroit dans le code" est un deuxime indicateur de la ncessit de mettre en place un builder. Tout comme dans le cas du pattern Factory, lerreur principale que vous pouvez tre amen faire consiste appliquer le pattern Builder lorsque ce nest pas ncessaire. Anticiper le besoin pour un builder nest pas une bonne ide mon avis. Par dfaut, optez pour MyClass.new lorsque vous crez des objets. Najoutez un builder que lorsquune liste interminable de nouvelles demandes vous oblige y recourir.

222

Patterns en Ruby

Des objets builder dans le monde rel


Lun des builders les plus intressants que lon trouve dans la base de code Ruby prtend ne pas en tre un. Malgr le nom, MailFactory1 est un builder sympathique qui vous aide crer des messages lectroniques. Alors que, fondamentalement, les messages lectroniques ne sont que des fragments de texte simple, tout dveloppeur qui a dj tent de construire des messages avec des pices jointes en format MIME multipart sait que mme le message le plus simple peut tre trs compliqu crer. MailFactory masque cette complexit laide dune agrable interface la builder pour crer votre message :
require rubygems require mailfactory mail_builder = MailFactory.new mail_builder.to =russ@russolsen.com mail_builder.from = russ@russolsen.com mail_builder.subject = The document mail_builder.text = Here is that document you wanted mail_builder.attach(book.doc)

Une fois que vous avez inform MailFactory (le builder !) des dtails de votre message, vous pouvez rcuprer le texte du message dans un format appropri pour un serveur SMTP laide de la mthode to_s :
puts mail_builder.to_s to: russ@russolsen.com from: russ@russolsen.com subject: Here is that document you wanted Date: Wed, 16 May 2007 14:02:32 -0400 MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=_NextPart_3rj.Kbd9.t9JpHIc663P_4mq6" Message-ID: <1179338750.3053.1000.-606657668@russolsen.com> This is a multi-part message in MIME format. ...

Les mthodes find dActiveRecord constituent un des exemples les plus remarquables de mthodes magiques. ActiveRecord nous permet dencoder des requtes vers la base de donnes avec exactement la mme technique que celle employe dans la dernire version de notre builder pour spcier les congurations laide du nommage des mthodes. On pourrait par exemple retrouver tous les salaris de la table Employee par leur numro de scurit sociale :
Employee.find_by_ssn(123-45-6789)

1. Lauteur du package MailFactory est David Powers.

Chapitre 14

Simplier la cration dobjets avec Builder

223

On peut galement rechercher par le nom et le prnom :


Employee.find_by_firstname_and_lastname(John, Smith)

En conclusion
Lorsque vos objets sont difciles construire et quil faut crire beaucoup de code pour les congurer, factorisez le code qui gre la cration dans sa propre classe : telle est lide principale du pattern Builder. Le pattern Builder stipule quil faut fournir un objet le builder qui accepte des spcications de votre nouvel objet tape par tape et qui gre tous les dtails ennuyeux et complexes de linstanciation. Puisque les objets builder gardent le contrle de la conguration, ils peuvent vous aider construire des objets valides. Un builder se trouve en position parfaite pour vrier les actions du client et dire : "Non, je trouve quune cinquime roue sur une voiture ne serait pas trs commode..." Avec un peu dingniosit, on peut crer des mthodes magiques pour faciliter le processus de construction. Pour y parvenir, on intercepte des appels de mthodes non dnies avec method_missing, on analyse les noms des mthodes, et on construit le bon objet en fonction de ce nom. Les mthodes magiques reprsentent un pas en avant pour la construction rapide des objets, car le client est autoris spcier des options de conguration multiples avec un appel de mthode unique. Lorsque vous crez un builder et surtout lorsque vous lutilisez, il faut se rendre compte du problme de sa rutilisation. Est-ce que vous pouvez utiliser une instance unique dun builder pour crer des produits multiples ? Il est plus simple de crer des builders non rutilisables ou rinitialiser lors dune utilisation rpte plutt que des builders compltement rutilisables. Il faut que vous vous posiez la question de savoir de quel type de Builder vous avez besoin. Le pattern Builder est le dernier dans la famille des patterns de cration dobjets 1 que nous examinerons dans cet ouvrage. Le chapitre suivant est consacr la cration de nouveaux objets, et nous allons retourner vers un autre sujet intressant : la cration dun interprteur.

1. Ou ceux qui empchent la cration, dans le cas du pattern Singleton.

15
Assembler votre systme avec Interpreter
la n des annes 1980, la version prcdente de lingnieur logiciel Russ Olsen probablement une version bta travaillait sur un systme dinformation gographique (GIS). Lun des objectifs cls de ce systme GIS tait sa capacit dadaptation. Les cartes des utilisateurs taient toutes diffrentes et chaque client avait sa propre vision de la prsentation idale dune carte. Chaque client souhaitait utiliser ses cartes sa guise et nous voulions naturellement que notre systme puisse sadapter tous leurs caprices. Malheureusement, le systme tait crit en langage C et, malgr les nombreux points forts de ce langage, la capacit dadaptation nen fait pas partie. crire en C est difcile : il faut faire trs attention larithmtique des pointeurs sous peine de voir votre programme exploser en vol. Pire encore : le langage C se trouvait un niveau conceptuel inadapt pour notre systme. Lorsque vous crivez un programme en C, vous grez des int, des float et des pointeurs vers des struct, alors que nous voulions rchir en termes dobjets qui constituent des cartes tels que des valles, des euves et des frontires politiques. Les architectes de ce systme GIS (un groupe dlite dont je ne faisais pas partie) avaient alors rsolu le problme de exibilit par une dcision radicale : le systme ne devait pas tre crit en C. Environ 80 % de lapplication tait code laide dun langage spcialis qui savait grer des notions gographiques telles que latitudes et longitudes. Ce langage proposait un langage de requtes sophistiqu permettant deffectuer simplement des oprations spciques, par exemple dplacer tous les arbres de taille moyenne 500 mtres plus au nord.

226

Patterns en Ruby

Ce langage de cartes ne permettait pas dexprimer tout ce quun cartographe peut tre amen dire, mais il tait nanmoins beaucoup plus adapt cette tche que nimporte quel programme en C. 80 % du systme utilisait donc ce langage spcique orient carte. Et le reste ? Le reste tait crit en C. Cette partie crite en C avait un objectif unique : fournir un interprteur pour les 80 % du code crit en langage spcialis. Bref, ce vieux systme GIS reprsentait une grande implmentation du pattern Interpreter. Dans ce chapitre, nous allons nous familiariser avec le pattern Interpreter, qui nous enseigne que parfois le meilleur moyen de rsoudre un problme est dinventer un langage spcique pour cette tche. Nous allons explorer les diffrentes faons de dvelopper un interprteur classique et verrons quelques approches de la tche fastidieuse danalyse syntaxique. Nous apprendrons que les interprteurs ne font pas partie des techniques de programmation les plus performantes, mais malgr le cot en terme de performances ils offrent beaucoup de exibilit et de capacit dextension.

Langage adapt la tche


Lide fondamentale du pattern Interpreter est trs simple : on peut rsoudre certains problmes de programmation en crant un langage spcique capable dexprimer la solution nos problmes. Quels problmes sont de bons candidats pour le pattern Interpreter ? En rgle gnrale, ces problmes sont indpendants du reste de lapplication et leur primtre peut tre clairement dlimit. Ainsi, vous pourriez crer un langage de requtes permettant de chercher des objets rpondant certaines spcications 1. Inversement, si votre problme ncessite la cration de congurations complexes, considrez lutilisation dun langage de conguration. Si vous crivez des fragments de code assez simples, mais que vous soyez oblig de les arranger en combinaisons interminables, cest un indicateur que le pattern Interpreter pourrait bien tre la solution. Tout le travail de combinaison des modules pourrait tre effectu par un interprteur simple.

Dvelopper un interprteur
Le fonctionnement des interprteurs se divise en deux phases. Premirement, lanalyseur syntaxique parcourt le texte du programme pour produire un arbre de syntaxe abstrait (AST). LAST reprsente la mme information que le programme initial en
1. Cet exploit a dj t ralis pour des bases de donnes par les auteurs de SQL.

Chapitre 15

Assembler votre systme avec Interpreter

227

forme dun arbre dobjets. Cela permet une excution relativement efcace contrairement au programme initial en format textuel.
Figure 15.1
Larbre syntaxique abstrait dune simple expression arithmtique
5.0 *

Deuximement, lAST svalue selon un ensemble de conditions externes, appel contexte, pour produire le calcul requis. On pourrait par exemple dvelopper un interprteur pour valuer une expression arithmtique simple, telle que :
5.0*(3+x)

Tout dabord, il faut analyser lexpression. Lanalyseur syntaxique commence par le premier caractre de lexpression : le chiffre 5. Il poursuit vers la virgule et le zro dcimal, qui indiquent que cest un nombre virgule ottante. Aprs avoir trait 5.0, lanalyseur continue le processus jusqu la n de lexpression pour obtenir une structure de donnes (voir Figure 15.1). La structure de donnes prsente la Figure 15.1 reprsente notre AST. Les feuilles de lAST cest--dire 5.0, 3 et x sont appeles des nuds terminaux. Ils reprsentent les briques les plus lmentaires du langage. Les nuds qui ne sont pas des feuilles dans cet exemple + et * sont (assez logiquement) nomms des nuds non terminaux. Ils reprsentent des notions de niveau suprieur dans le langage. Comme vous pouvez le voir sur le diagramme UML (voir Figure 15.2), les nuds non terminaux possdent une rfrence vers une ou plusieurs sous-expressions, ce qui nous permet de construire des arbres avec un nombre illimit de niveaux 1.

1. Oui, nous avons dj vu ce diagramme. Un AST est effectivement un exemple spcialis du pattern Composite dans lequel les nuds non terminaux ont le rle de composites.

228

Patterns en Ruby

Figure 15.2
Diagramme de classe du pattern Interpreter
Client Expression interpret(context)

Context

TerminalExpression interpret(context)

NonTerminalExpression @sub_expressions[] interpret(context)

Bien que les membres du GoF aient nomm la mthode principale du pattern Interpreter interpret, des noms comme evaluate ou execute sont aussi appropris et peuvent frquemment tre trouvs dans le code. Une fois lAST disponible, on pourrait valuer notre expression, si ce ntait pour un dtail mineur : quelle est la valeur de x ? Pour pouvoir valuer lexpression il faut affecter une valeur x. Est-ce 1, 167 ou 279 ? Les membres du GoF appellent contexte les valeurs ou les conditions fournies au moment de linterprtation de lAST. Retournons notre exemple. Si lon value notre AST avec x gal 1, le rsultat obtenu est 20.0 ; si lon value lexpression encore une fois avec la valeur de x 4, le rsultat serait gal 35.0. Quelle que soit la valeur du contexte, lAST excute lvaluation en parcourant larbre de manire rcursive. On demande au nud la racine de larbre de sauto-valuer (dans notre cas, cest le nud qui reprsente la multiplication). Ce nud tente dvaluer ses deux lments rcursivement. Llment 5.0 ne prsente pas de difcult, mais le deuxime lment, loprateur daddition, doit son tour valuer ses lments (3 et x). Enn, nous arrivons la n de larbre et les rsultats des valuations remontent comme une bulle dair. Ce simple exemple arithmtique nous apprend deux choses. Premirement, limplmentation du pattern Interpreter est assez complexe. Souvenez-vous de toutes les classes qui constituent lAST ainsi que lanalyseur syntaxique. Lenvergure mme de limplmentation limite lutilisation du pattern Interpreter des langages relativement simples : on essaie de rsoudre un problme rel et non pas de saventurer dans la recherche informatique. La deuxime conclusion que lon puisse tirer est la performance limite de ce pattern. Le besoin de parcourir lAST, sans mentionner lanalyse syntaxique, pnalise la vitesse dexcution.

Chapitre 15

Assembler votre systme avec Interpreter

229

En change de cette complexit et de cette moindre performance, un interprteur offre quelques avantages dont le principal est la exibilit. Ds que linterprteur est disponible, il est trs simple dy ajouter des oprations. Il est assez facile dimaginer quune fois notre petit interprteur dexpressions arithmtiques prt, il sera facile de rajouter des nuds de soustraction et de multiplication dans lAST. Un AST est une structure de donnes reprsentant un fragment spcique de logique de programmation. Notre AST tait crit initialement pour valuer cette logique et on peut le modier pour prendre en charge dautres tches. On pourrait par exemple demander lAST dafcher sa description :
Multiply 5.0 by the sum of 3 and x, where x is 1.

Un interprteur pour trouver des chiers


Assez de thorie, crons un interprteur Ruby. Redvelopper un nouvel interprteur de calcul arithmtique est probablement la dernire chose dont le monde a besoin et nous allons donc tenter quelque chose de diffrent. crivons un outil qui nous permet de grer de grandes quantits de chiers de formats et de tailles variables. Pour ce faire, nous serons frquemment amens faire des recherches sur lensemble des chiers selon des critres bien prcis : par exemple rechercher tous les chiers MP3 ou tous les chiers disponibles en criture. Qui plus est, nous souhaitons aussi retrouver les chiers qui rpondent une combinaison de critres, par exemple tous les chiers MP3 de grande taille ou tous les chiers JPEG protgs en criture. Retrouver tous les chiers Cela ressemble fort un problme quon pourrait rsoudre laide dun langage de requtes simple. Supposons que chaque expression de notre langage spcie le type de chier recherch. Commenons par les lments de lAST, lanalyseur syntaxique viendra plus tard. La recherche la plus basique retourne tout simplement la totalit des chiers. Dnissons la classe pour accomplir cette tche :
require find class Expression # Le code des expressions frquentes sera bientt inclus ici... end class All < Expression def evaluate(dir) results= []

230

Patterns en Ruby

Find.find(dir) do |p| next unless File.file?(p) results << p end results end end

Fonctionnellement parlant, ce code nest pas trs riche. La mthode cl de notre interprteur est nomme evaluate. La mthode evaluate de la classe All sappuie tout simplement sur la classe Find de la bibliothque standard de Ruby, qui permet de rcuprer tous les chiers rsidant dans un dossier donn. Si lon passe la mthode Find.find un nom du dossier et un bloc, le bloc en question est excut pour chaque lment du dossier. Jinsiste sur le mot "chaque". Vu que la mthode Find agit de manire rcursive, le bloc serait appel non seulement sur chacun des chiers du dossier mais aussi sur tous les sous-dossiers, tous les chiers des sous-dossiers et ainsi de suite. Dans notre cas, seuls les chiers nous intressent. Il faut donc mettre en place un mcanisme de ltrage. La ligne
next unless File.file?(p)

ignore tout lment qui nest pas un chier. Ne vous inquitez pas au sujet de la classe parent Expression vide. Nous allons y ajouter du code fort utile trs prochainement. Rechercher des chiers par nom Ltape suivante tombe sous le sens : nous allons crer une classe charge de retourner tout chier dont le nom correspond un patron donn :
class FileName < Expression def initialize(pattern) @pattern = pattern end def evaluate(dir) results= [] Find.find(dir) do |p| next unless File.file?(p) name = File.basename(p) results << p if File.fnmatch(@pattern, name) end results end end

Chapitre 15

Assembler votre systme avec Interpreter

231

La classe FileName est lgrement plus complique que la classe All. Elle fait appel quelques mthodes trs utiles de la classe File. La mthode File.basename retourne la partie du chemin qui correspond au nom dun chier : passez "/home/russ/ chapter1.doc" cette mthode et vous obtiendrez "chapter1.doc". La mthode File.fnmatch renvoie true uniquement si le patron du nom des chiers spci dans le premier paramtre (par exemple "*.doc") correspond au nom du chier pass dans le deuxime paramtre (par exemple "chapter1.doc"). Nos classes de recherche sont trs simples dutilisation. Si le dossier test_dir contient deux chiers MP3 et une image, nous pouvons rcuprer les trois chiers laide du code suivant :
expr_all = All.new files = expr_all.evaluate(test_dir)

Mais, si lon sintresse uniquement aux MP3, on peut faire ceci :


expr_mp3 = FileName.new(*.mp3) mp3s = expr_mp3.evaluate(test_dir)

Dans lexemple prcdent, le nom du dossier pass la mthode evaluate joue le rle du contexte, cest--dire les paramtres extrieurs qui servent lvaluation de lexpression. La mme expression peut tre value avec des contextes diffrents, ce qui engendre des rsultats diffrents. On pourrait, par exemple, rechercher tous les chiers MP3 dans le dossier music_dir :
other_mp3s = expr_mp3.evaluate(music_dir)

Des grands chiers et des chiers ouverts en criture Il est clair que la recherche de chiers correspondant un certain nom est loin dtre la seule option de recherche possible. On pourrait par exemple avoir besoin de retrouver tous les chiers dont la taille est suprieure une valeur donne :
class Bigger < Expression def initialize(size) @size = size end def evaluate(dir) results = [] Find.find(dir) do |p| next unless File.file?(p) results << p if( File.size(p) > @size) end results end end

232

Patterns en Ruby

Ou on pourrait rechercher les chiers disponibles en criture :


class Writable < Expression def evaluate(dir) results = [] Find.find(dir) do |p| next unless File.file?(p) results << p if( File.writable?(p) ) end results end end

Des recherches plus complexes laide des instructions Not, And et Or Dsormais, nous avons quelques classes de base pour rechercher des chiers et ces classes constituent les nuds terminaux de notre AST. Passons aux choses plus intressantes. Que faire si lon souhaite retrouver tous les chiers protgs en criture ? On pourrait videmment dnir encore une classe qui ressemble celles qui existent dj. Mais essayons une autre option : dnissons notre premier nud non terminal, en loccurrence le nud Not :
class Not < Expression def initialize(expression) @expression = expression end def evaluate(dir) All.new.evaluate(dir) - @expression.evaluate(dir) end end

Le constructeur de la classe Not accepte un argument qui reprsente lexpression que nous souhaitons valuer en ngatif. Lorsque la mthode evaluate est appele, elle commence par retrouver la totalit des chemins laide de la classe All et excute ensuite la mthode de soustraction de la classe Array pour supprimer les chemins des chiers retourns par lexpression. Nous nous retrouvons la n avec lensemble des chemins qui ne rpondent pas lexpression. Par consquent, pour retrouver tous les chiers non disponibles en criture on crirait :
expr_not_writable = Not.new( Writable.new ) readonly_files = expr_not_writable.evaluate(test_dir)

La classe Not est trs lgante car elle ne sapplique pas seulement Writable. On pourrait choisir demployer Not pour trouver tous les chiers dont la taille est infrieure 1 Ko :
small_expr = Not.new( Bigger.new(1024) ) small_files = small_expr.evaluate(test_dir)

Chapitre 15

Assembler votre systme avec Interpreter

233

ou rechercher tous les chiers dont le format nest pas MP3 :


not_mp3_expr = Not.new( FileName.new(*.mp3) ) not_mp3s = not_mp3_expr.evaluate(test_dir)

Il est aussi possible de dnir un nud non terminal pour combiner deux expressions de recherche :
class Or < Expression def initialize(expression1, expression2) @expression1 = expression1 @expression2 = expression2 end def evaluate(dir) result1 = @expression1.evaluate(dir) result2 = @expression2.evaluate(dir) (result1 + result2).sort.uniq end end

La classe Or nous permet de rcuprer en une seule requte tous les chiers au format MP3 ou ceux dont la taille est suprieure 1 Ko :
big_or_mp3_expr = Or.new( Bigger.new(1024), FileName.new(*.mp3) ) big_or_mp3s = big_or_mp3_expr.evaluate(test_dir)

La classe Or indique que And ne doit pas tre bien loin :


class And < Expression def initialize(expression1, expression2) @expression1 = expression1 @expression2 = expression2 end def evaluate(dir) result1 = @expression1.evaluate(dir) result2 = @expression2.evaluate(dir) (resultl & result2) end end

Dsormais, nous avons sous la main tous les outils pour spcier des recherches de chiers complexes. Tentons de rcuprer tous les MP3 de grande taille protgs en criture :
complex_expression = And.new( And.new(Bigger.new(1024), FileName.new(*.mp3)), Not.new(Writable.new))

234

Patterns en Ruby

Cette expression complexe nous rvle une autre proprit intressante du pattern Interpreter. Une fois que nous avons dni un AST complexe comme le prcdent, il peut tre rutilis dans des contextes diffrents :
complex_expression.evaluate(test_dir) complex_expression.evaluate(/tmp)

Conu correctement, un pattern Interpreter vous rcompensera gnreusement pour vos efforts. Dans notre exemple, seules sept classes auront t ncessaires pour obtenir un AST de recherche de chiers relativement exible.

Crer un AST
De faon assez surprenante, le pattern Interpreter dni par le GoF reste muet sur la cration de lAST lui-mme. Le pattern suppose que lAST est dj disponible et omet ltape de son dveloppement. Il est pourtant assez vident quun AST doit tre cr et, pour cela, nous disposons dune large palette de possibilits. Un analyseur syntaxique simple La manire probablement la plus vidente dobtenir un AST consiste dvelopper un analyseur syntaxique. Crer un analyseur syntaxique pour notre langage de recherche de chiers ne prsente pas de difcults particulires. Supposons que la syntaxe ressemble ceci :
and (and(bigger 1024)(filename *.mp3)) writable

Le code suivant (de 50 lignes environ) effectue correctement notre analyse syntaxique :
class Parser def initialize(text) @tokens = text.scan(/\(|\)|[\w\.\*]+/) end def next_token @tokens.shift end def expression token = next_token if token == nil return nil elsif token == ( result = expression raise Expected ) unless next_token == ) result elsif token == all return All.new

Chapitre 15

Assembler votre systme avec Interpreter

235

elsif token == writable return Writable.new elsif token == bigger return Bigger.new(next_token.to_i) elsif token == filename return FileName.new(next_token) elsif token == not return Not.new(expression) elsif token == and return And.new(expression, expression) elsif token == or return Or.new(expression, expression) else raise "Unexpected token: #{token}" end end end

Pour utiliser cet analyseur syntaxique il suft de passer les expressions de recherche de chiers au constructeur et dappeler la mthode parse. Cette mthode retourne lAST correspondant, prt tre utilis :
parser = Parser.new "and (and(bigger 1024)(filename *.mp3)) writable" ast = parser.expression

La classe Parser applique la mthode scan de la classe String pour diviser lexpression en fragments convenables :
@tokens = text.scan(/\(|\)|[\w\.\*]+/)

La chane est divise en un tableau de sous-chanes appeles tokens par la magie des expressions rgulires1. Chacun des tokens est soit une parenthse soit un fragment de texte contigu comme "lename" ou "*.mp3"2. La plus grande partie de lanalyseur syntaxique est occupe par la mthode expression. Elle parcourt les tokens un par un an de construire lAST.

1. Si vous ntes pas familier avec les expressions rgulires, jetez un il sur lAnnexe B, Aller plus loin. Les expressions rgulires valent bien la peine dtre tudies. 2. Si vous connaissez les expressions rgulires, vous avez sans doute remarqu que mon analyseur syntaxique ne gre pas les noms de chiers qui contiennent des espaces. Je me permets de rpter que les exemples doivent rester simples, sans parler du fait que la pratique dincorporer des espaces dans les noms des chiers est mon avis contraire lthique.

236

Patterns en Ruby

Et un interprteur sans analyseur ? Mme si le dveloppement de ce premier analyseur syntaxique na pas prsent de difcults particulires, un certain effort a cependant t ncessaire et on peut donc se poser la question de la ncessit dun tel analyseur. Les classes de recherche de chiers que nous avons ralises peuvent elles-mmes constituer une bonne API interne oriente programmeur. Si le but est de pouvoir spcier un bon moyen de dclencher des recherches de chiers partir de notre code, on pourrait probablement crer dans le code un AST de recherche de chiers de la mme faon que dans les exemples de la section prcdente. Cela nous permettrait de proter de toute la exibilit et de lextensibilit du pattern Interpreter sans devoir grer lanalyse syntaxique. Si vous dcidez dopter pour un interprteur sans analyseur, pensez rajouter quelques noms de mthodes raccourcis pour simplier la vie de vos utilisateurs. Nous pouvons par exemple tendre notre interprteur de recherche de chiers en dnissant dans la classe Expression quelques oprateurs pour crer des expressions And et Or avec une syntaxe plus compacte1 :
class Expression def |(other) Or.new(self, other) end def &(other) And.new(self, other) end end

On se doutait un peu que la classe Expression nirait bien par tre utilise ! Grce aux oprateurs nous pouvons lancer des recherches de chiers complexes sans taper des longues expressions au clavier. Au lieu de
Or.new( And.new(Bigger.new(2000), Not.new(Writable.new)), FileName.new(*.mp3))

nous pouvons crire


(Bigger.new(2000) & Not.new(Writable.new)) | FileName.new("*.mp3")

1. Malgr le fait quil ny a pas de contre-indications des oprateurs, cette pratique doit tre utilise avec modration, car un nombre doprateurs excessif a tendance rendre le code difcilement lisible.

Chapitre 15

Assembler votre systme avec Interpreter

237

Nous pouvons aller plus loin encore dans la simplication de la syntaxe. Il suft de dnir des mthodes raccourcies pour crer des nuds terminaux :
def all All.new end def bigger(size) Bigger.new(size) end def name(pattern) FileName.new(pattern) end def except(expression) Not.new(expression) end def writable Writable.new end

Ces nouvelles mthodes rduisent encore davantage lexpression de recherche prcdente :


(bigger(2000) & except(writable) ) | file_name(*.mp3)

Il ne faut pas oublier que nous ne pouvons pas utiliser le nom not pour raccourcir lappel Not.new cause du conit de nom avec loprateur not de Ruby. Dlguer lanalyse XML ou YAML ? Si toutefois vous jugez quun analyseur syntaxique est ncessaire, il existe une alternative assez intressante au dveloppement de votre propre analyseur qui consiste sappuyer sur XML ou sur YAML1. Cette solution vous permet de vous er aux bibliothques danalyse syntaxique de XML ou de YAML qui sont livres avec votre distribution de Ruby. Au premier abord, lide parat formidable : vous protez ainsi de toute la exibilit et des possibilits dextension dun interprteur complet sans vous soucier des dtails danalyse syntaxique. Rien dire, nest-ce pas ? Malheureusement, cette ide pourrait bien provoquer quelques plaintes de la part de vos utilisateurs. Alors que XML et YAML sont trs bien adapts pour reprsenter des donnes, ils ne sont pas idals pour exprimer des programmes. Gardez lesprit que le
1. YAML ou "YAML Aint Markup Language" est un format en texte simple. Tout comme XML il est utilis pour stocker des donnes hirarchiques. Contrairement XML, YAML est trs convivial et il est trs populaire dans la communaut Ruby.

238

Patterns en Ruby

dveloppement dun interprteur est motiv principalement par la volont de proposer vos utilisateurs un moyen naturel pour exprimer leurs besoins de traitement. Si les ides encapsules dans votre interprteur peuvent tre exprimes naturellement en XML ou YAML, nhsitez pas opter pour ce genre de formats an de proter pleinement des analyseurs syntaxiques fournis. Mais, si votre langage ne correspond pas vraiment ces formats et je me permets dafrmer que cest le cas de la majorit des langages du pattern Interpreter , nessayez pas de forcer votre langage entrer dans un format inadapt par pure paresse. Racc pour des analyseurs plus complexes Si votre langage est relativement complexe et que ni XML ni YAML ne semblent appropris, vous pouvez opter pour lutilisation dun gnrateur danalyseurs syntaxiques tel que Racc. Racc hrite son modle (et son nom) du vnrable utilitaire UNIX : YACC. Racc prend en entre la description de la grammaire propre votre langage et gnre un analyseur syntaxique Ruby pour ce langage. Racc est un outil formidable mais mes sensibles sabstenir : apprendre se servir dun gnrateur danalyseurs syntaxiques est long et la courbe dapprentissage est raide. Dlguer lanalyse Ruby ? Il existe une autre rponse au dilemme de lanalyseur syntaxique. Vous pourriez dcider dimplmenter votre pattern Interpreter de faon permettre aux utilisateurs dcrire du code Ruby classique. Il est peut-tre possible de concevoir lAPI de votre AST de faon que le code sinsre naturellement dans le reste du code Ruby. Ainsi, vos utilisateurs ne sauraient mme pas quils crivent du code Ruby. Cette ide est tellement curieuse que le chapitre suivant lui est entirement consacr.

User et abuser du pattern Interpreter


Le pattern Interpreter a tendance tre sous-utilis : mon avis, cette caractristique le distingue des autres patterns du GoF exposs dans ce livre. Pendant ma carrire jai rencontr un certain nombre de systmes qui auraient pu proter du pattern Interpreter. Ces systmes investissaient beaucoup deffort dans des solutions fondes sur des conceptions pas compltement adquates. Par exemple, lge de pierre des bases de donnes une requte se prsentait comme un programme cod laborieusement par un expert des bases de donnes. Cette approche a perdur pendant une longue priode jusqu lapparition des langages (en majorit interprts) tels que SQL. De la mme manire, pendant des annes la construction dune interface graphique mme la plus

Chapitre 15

Assembler votre systme avec Interpreter

239

simple ncessitait lintervention dun ingnieur logiciel. Il devait passer des jours et des semaines crire du code page aprs page. Aujourdhui, tout collgien qui a accs un clavier peut dvelopper des interfaces graphiques relativement complexes laide dun langage interprt que nous appelons HTML. Pourquoi le pattern Interpreter est-il nglig ? De nombreux ingnieurs logiciels qui passent leurs journes dvelopper des solutions mtier sont souvent des experts en conception de bases de donnes ou en dveloppement dapplications Web, mais la dernire fois o ils ont vu des AST ou des analyseurs syntaxiques remonte probablement leur deuxime anne dcole dingnieurs. Cest regrettable. Comme nous lavons vu, une application correcte du pattern Interpreter donne votre systme une exibilit redoutable. Nous avons dj not quelques inconvnients majeurs des interprteurs. Tout dabord, ils sont complexes. Lorsque vous considrez lusage du pattern Interpreter, surtout si vous planiez de construire un analyseur syntaxique, essayez destimer la complexit de votre langage. Cherchez le rendre le plus simple possible. Rchissez au prol des futurs utilisateurs de votre langage. Seront-ils des ingnieurs logiciel expriments capables davancer avec le minimum de messages derreurs ? Ou sagira-t-il dutilisateurs moins techniques qui auront besoin dun diagnostic dtaill en cas de problme ? Par ailleurs, le problme de lefcacit du programme se pose aussi. Noubliez pas que la vitesse dexcution de lexpression
Add.new(Constant.new(2), Constant.new(2)).interpret

ne sera jamais comparable celle de


2 + 2

Mme avec toute sa exibilit et sa puissance, le pattern Interpreter ne sera jamais un bon choix pour les 2 % de votre code dont le temps dexcution est critique. Mais pourquoi ne pas lappliquer aux 98 % du code restant ?

Des interprteurs dans le monde rel


Il est facile de trouver des exemples dinterprteurs dans le monde Ruby. Le langage Ruby lui-mme est videmment un langage interprt, quoique lgrement plus complexe que celui prvu par le pattern Interpreter. De la mme manire, les expressions rgulires des outils formidables pour comparer du texte des patrons, qui ont t si utiles pour notre implmentation dun analyseur syntaxique sont elles-mmes implmentes sous forme dun langage interprt.

240

Patterns en Ruby

Lorsque vous crivez lexpression rgulire /[rR]uss/, elle est traduite en un AST de faon transparente pour rechercher des occurrences des deux variantes de mon prnom. Il existe galement Runt1 : une bibliothque qui fournit un langage simple pour exprimer des intervalles de temps et de dates ainsi que des calendriers. Avec Runt on peut crire des expressions temporelles qui ne correspondront qu certains jours de la semaine :
require rubygems require runt mondays = Runt::DIWeek.new(Runt::Monday) wednesdays = Runt::DIWeek.new(Runt::Wednesday) fridays = Runt::DIWeek.new(Runt::Friday)

Les trois objets ci-dessus nous permettent de dcouvrir que Nol en 2015 tombera un vendredi, car le code
fridays.include?(Date.new(2015,12,25))

retourne true, alors que


mondays.include?(Date.new(2015,12, 25)) wednesdays.include?(Date.new(2015,12,25))

retournent false. Comme dhabitude avec le pattern Interpreter, vous utilisez pleinement la puissance de Runt lorsque vous commencez combiner des expressions. Voici une expression Runt qui exprime lemploi du temps horrible que jai subi luniversit :
nine_to_twelve = Runt::REDay.new(9,0,12,0) class_times = (mondays | wednesdays | fridays) & nine_to_twelve

Runt est un bon exemple dinterprteur sans analyseur syntaxique : il est conu comme une simple bibliothque de classes pour des programmeurs Ruby.

En conclusion
Dans ce chapitre, nous avons dcouvert le pattern Interpreter. Ce pattern suggre que parfois un interprteur simple est le meilleur moyen de rsoudre un problme. Le pattern Interpreter est bien adapt aux problmes clairement dlimits tels que des langages de requte ou de conguration. Cest galement une bonne approche pour combiner des blocs de fonctionnalits.
1. La bibliothque Runt a t crite par Matthew Lipper. Elle repose sur la notion dexpressions temporelles de Martin Fowler.

Chapitre 15

Assembler votre systme avec Interpreter

241

Larbre de syntaxe abstraite est le cur du pattern Interpreter. Vous traitez votre langage spcialis comme une srie dexpressions que vous dcomposez en une structure arborescente. vous de choisir comment effectuer cette dcomposition : on peut fournir aux clients une API pour construire larbre dans le code ou on peut implmenter un analyseur syntaxique qui accepte des chanes de caractres pour les convertir en un AST. Dans les deux cas, une fois lAST disponible, il peut sautovaluer pour retourner un rsultat. La exibilit et lextensibilit sont les principaux atouts du pattern Interpreter. Les mmes classes de linterprteur peuvent accomplir des tches diffrentes en fonction des AST. tendre votre langage pour ajouter de nouveaux nuds dans lAST est gnralement assez simple. Toutefois, ces avantages entranent un cot en termes de performance et de complexit. Les interprteurs ont tendance tre lents et il est difcile den acclrer lexcution. Il est donc recommand de limiter leur usage aux modules qui ne ncessitent pas une grande rapidit dexcution. Cette complexit est la consquence de linfrastructure ncessaire la mise en uvre du pattern Interpreter : il faut implmenter toutes les classes pour constituer un AST et, possiblement, lanalyseur. Dans le chapitre suivant, nous allons tudier les langages spciques dun domaine (DSL). Cest un pattern qui est troitement li au pattern Interpreter. Nous allons nous concentrer particulirement sur des DSL internes : une alternative lgante au travail (parfois pnible) dimplmentation d un analyseur syntaxique pour votre interprteur.

Partie III
Les patterns Ruby

16
Ouvrir votre systme avec des langages spciques dun domaine
Au Chapitre 15, nous avons examin comment rsoudre certains types de problmes laide des interprteurs. Larbre de syntaxe abstrait (AST) qui est employ pour obtenir un rsultat ou effectuer une action constitue llment cl du pattern Interpreter. Nous avons dcouvert au chapitre prcdent que le pattern Interpreter nest pas directement concern par la cration de lAST. On suppose que larbre est disponible et on se concentre sur sa faon de fonctionner. Dans ce chapitre, nous allons explorer le pattern Domain-Specic Language (DSL), qui voit le monde par lautre bout de la lorgnette. Le pattern DSL stipule quil faut diriger son attention vers le langage mme et non pas vers linterprteur. Il est parfois possible de faciliter la rsolution dun problme en fournissant aux utilisateurs une syntaxe adapte. Vous ne trouverez pas le pattern DSL dans le Design Patterns du GoF. Nanmoins, comme vous le verrez dans ce chapitre, lincroyable exibilit de Ruby permet dimplmenter trs simplement un style particulier de DSL.

Langages spciques dun domaine


Le pattern DSL nest pas diffrent de la majorit des autres patterns couverts dans ce livre : ici comme ailleurs, lide fondatrice nest pas trs complique. On comprend le principe des DSL lorsquon prend du recul et quon se demande ce quon cherche atteindre en crivant des programmes. La rponse est (je lespre) de rendre nos utilisateurs heureux. Un utilisateur veut se servir dun ordinateur pour accomplir une tche :

246

Les patterns Ruby

grer des comptes nanciers ou diriger une sonde spatiale vers Mars. Bref, lutilisateur souhaite que lordinateur satisfasse une demande. On pourrait alors se poser une question nave : pourquoi lutilisateur a-t-il besoin de nous ? Pourquoi ne pas lui passer linterprteur Ruby en lui souhaitant bonne chance ? Cest une ide ridicule car, en rgle gnrale, les utilisateurs ne comprennent pas la programmation et les ordinateurs. Ils matrisent des bits et des octets aussi bien que nous matrisons la comptabilit et la mcanique cleste. Lutilisateur connat sa matire, son domaine, mais pas le domaine de la programmation. Et si lon pouvait crer un langage de programmation qui permettrait un utilisateur dexprimer certaines des rgles mtier qui concernent son domaine spcique au lieu de rgles compliques intrinsquement lies aux ordinateurs ? Des comptables pourraient alors faire appel des notions de comptabilit et des chercheurs en arospatiale pourraient parler de sondes. Dans ce cas, lide de fournir un langage lutilisateur ne parat plus si absurde. Il est assurment possible de dvelopper ce type de langages en appliquant des techniques apprises au Chapitre 15. On pourrait retrousser nos manches et concevoir un analyseur syntaxique pour un langage de comptabilit ou recourir Racc pour crer un langage de navigation cleste. Martin Fowler nomme ces approches plus ou moins traditionnelles des DSL externes. Ces langages sont externes dans le sens o ils contiennent deux entits compltement distinctes. Dun ct, il y a lanalyseur syntaxique et linterprteur du langage, de lautre ct, il y a des programmes crits en ce langage. Si lon crait un DSL spcialis pour des comptables avec lanalyseur et linterprteur crits en Ruby, on se retrouverait avec deux choses compltement spares : le DSL de comptabilit dune part et le programme pour linterprter dautre part. tant donn lexistence des DSL externes, on pourrait se demander sil existe des langages internes et quelles sont les diffrences entre les deux. Selon Martin Fowler, un DSL interne consiste partir dun langage dimplmentation connu par exemple Ruby pour le transformer en DSL. Si lon utilise Ruby pour implmenter notre DSL (si vous avez vu le titre de ce livre, vous vous doutez que cest le cas), tout utilisateur qui crit un programme en utilisant notre petit langage produit en ralit et probablement sans le savoir un programme Ruby.

Un DSL pour des sauvegardes de chiers


Il savre que construire un DSL interne Ruby est assez simple. Imaginez que nous devions mettre en place un programme de sauvegarde : un systme qui sexcuterait

Chapitre 16

Ouvrir votre systme avec des langages spciques dun domaine

247

intervalles rguliers pour copier nos chiers importants dans un autre rpertoire (vraisemblablement mieux protg). On dcide de raliser cet exploit laide dun DSL, un langage nomm PackRat qui permettrait aux utilisateurs dexprimer quels chiers il faut sauvegarder et quel moment. On pourrait avoir la syntaxe suivante :
backup /home/russ/documents backup /home/russ/music, file_name(*.mp3) & file_name(*.wav) backup /home/russ/images, except(file_name(*.tmp)) to /external_drive/backups interval 60

Ce petit programme crit en PackRat dclare que nous avons trois dossiers pleins de donnes que nous voulons copier dans le rpertoire /external_drive/backups une fois par heure (toutes les 60 minutes). Il faudrait sauvegarder la totalit du dossier documents, ainsi que la totalit du dossier images lexception des chiers temporaires. En ce qui concerne le dossier music, nous voulons seulement copier des chiers audio. Comme nous avons horreur de rinventer des choses qui existent dj, PackRat fait appel des expressions de recherche de chiers que nous avons dj dveloppes au Chapitre 15.

Cest un chier de donnes, non, cest un programme !


Pour attaquer ce projet PackRat nous pourrions sortir nos expressions rgulires favorites ou un gnrateur danalyseurs syntaxiques pour produire un analyseur traditionnel : le premier mot lu doit tre "backup", puis on recherche un guillemet, etc. Mais il doit y avoir une solution plus simple. En regardant bien on se rend compte que les instructions backup pourraient presque tre des appels de mthodes Ruby. Stop ! Ils peuvent tre des appels de mthodes Ruby. Si backup, to et interval taient des noms de mthodes Ruby, ce code reprsenterait un programme Ruby parfaitement valide, cest--dire une srie dappels de backup, to et interval, chacun avec un ou deux arguments. Les parenthses autour des arguments sont omises, mais cest videmment parfaitement acceptable en Ruby. Pour commencer, essayons dcrire un petit programme Ruby qui ne fait rien dautre que lire le chier backup.pr file. Voici un petit programme qui sappelle packrat.rb, cest le dbut de notre interprteur DSL :
require finder def backup(dir, find_expression=All.new) puts "Backup called, source dir=#{dir} find expr=#{find_expression}" end

248

Les patterns Ruby

def to(backup_directory) puts "To called, backup dir=#{backup_directory}" end def interval(minutes) puts "Interval called, interval = #{minutes} minutes" end eval(File.read(backup.pr))

Ce nest pas un programme labor, mais ce code prsente la plupart des ides ncessaires la mise en uvre dun DSL interne en Ruby. Nous avons trois mthodes backup, to et interval. La partie cl du code de notre DSL est la dernire instruction :
eval(File.read(backup.pr))

Cette expression commence la lecture du contenu de backup.pr et excute ensuite ce contenu comme un programme Ruby1. Cela signie quinterval, to et toutes les expressions backup dans backup.pr autrement dit tout ce qui ressemble des mthodes Ruby seront aspirs dans notre programme et interprts comme des appels de mthodes Ruby. Lorsque lon excute packrat.rb, on obtient les messages de sortie de ces mthodes :
Backup called, source dir=/home/russ/documents find expr=#<All:0xb7d84c14> Backup called, source dir=/home/russ/music find expr=#<And:0xb7d84b74> Backup called, source dir=/home/russ/images find expr=#<Not:0xb7d84afc> To called, backup dir=/external_drive/backups Interval called, interval = 60 minutes

Le qualicatif "interne" dun DSL sexplique par cette technique daspiration et dinterprtation. Lexpression eval fait fusionner linterprteur et le programme PackRat. Cest de la science-ction sans peine.

Dvelopper PackRat
Dsormais, nos utilisateurs crivent sans mance des appels de mthodes Ruby. Mais que devons-nous faire lintrieur de ces mthodes exactement ? Quel travail doivent effectuer interval, to et backup ? Ils doivent retenir le fait dtre appels. Formul autrement, ils doivent congurer certaines structures de donnes. Pour commencer, dnissons la classe Backup, qui reprsente la totalit de la commande de sauvegarde :
1. Ruby fournit la mthode load, qui permet dvaluer le contenu dun chier en tant que code Ruby en une tape, mais le traitement en deux tapes avec read et eval permet de mieux illustrer un DSL.

Chapitre 16

Ouvrir votre systme avec des langages spciques dun domaine

249

class Backup include Singleton attr_accessor:backup_directory,:interval attr_reader:data_sources def initialize @data_sources = [] @backup_directory = /backup @interval = 60 end def backup_files this_backup_dir = Time.new.ctime.tr(:,_) this_backup_path = File.join(backup_directory, this_backup_dir) @data_sources.each { |source| source.backup(this backup path)} end def run while true backup_file sleep(@interval*60) end end end

La classe Backup nest quun conteneur pour linformation que contient le chier backup.pr. Il dclare des attributs pour lintervalle et le dossier cible de la sauvegarde ainsi quun tableau pour stocker les dossiers sauvegarder. Le seul aspect lgrement plus complexe de la classe gure dans la mthode run. Cette mthode excute des sauvegardes en copiant les donnes source dans le dossier de sauvegarde (qui est en ralit un sous-dossier estampill du dossier de sauvegarde). Ensuite, elle se met en veille jusqu la sauvegarde suivante. La classe Backup est dclare comme un singleton car notre utilitaire nen aura jamais plusieurs. Nous avons maintenant besoin dune classe pour reprsenter les dossiers sauvegarder :
class DataSource attr_reader:directory,:finder_expression def initialize(directory, finder_expression) @directory = directory @finder_expression = finder_expression end def backup(backup_directory) files=@finder_expression.evaluate(@directory) files.each do |file| backup_file( file, backup_directory) end end def backup_file(path, backup_directory) copy_path = File.join(backup_directory, path)

250

Les patterns Ruby

FileUtils.mkdir_p(File.dirname(copy_path)) FileUtils.cp(path, copy_path) end end

La classe DataSource est le conteneur dun chemin vers un dossier et de lAST des expressions de recherche de chiers. Elle contient galement la plupart de la logique ncessaire la copie des chiers.

Assembler notre DSL


Puisque la totalit du code utilitaire est disponible, faire fonctionner le DSL PackRat devient un jeu denfant. Nous allons rcrire les mthodes initiales backup, to et interval an quelles utilisent les classes que nous venons de crer :
def backup(dir, find_expression=All.new) Backup.instance.data_sources << DataSource.new(dir, find_expression) end def to(backup_directory) Backup.instance.backup_directory = backup_directory end def interval(minutes) Backup.instance.interval = minutes end eval(File.read(backup.pr)) Backup.instance.run

Nous allons examiner ce code mthode par mthode. La mthode backup ne fait que rcuprer linstance du singleton Backup et lui ajouter une source de donnes. De la mme manire, la mthode interval rcupre la valeur de lintervalle de sauvegarde et laffecte au champ correspondant du singleton Backup. La mthode to fait la mme action pour le chemin du dossier de sauvegarde. Enn, nous avons deux dernires lignes qui terminent notre interprteur PackRat :
eval(File.read(backup.pr)) Backup.instance.run

Lexpression eval nous est dj familire : elle dclenche la lecture du chier PackRat ainsi que son valuation en tant que code Ruby. La toute dernire ligne du programme dmarre le processus de sauvegarde. La structure de linterprteur PackRat est assez typique dun DSL interne. On commence par dnir les structures de donnes : dans notre cas, cest la classe Backup et compagnie. Ensuite, on dnit quelques mthodes de niveau suprieur pour supporter le

Chapitre 16

Ouvrir votre systme avec des langages spciques dun domaine

251

langage DSL mme : dans le cas de PackRat, ce sont les mthodes interval, to et backup. Puis on aspire le texte du DSL laide de linstruction eval(File.read (...)). Limportation du texte DSL initialise les structures de donnes. Dans notre cas, nous nous retrouvons avec une instance de Backup compltement congure. Enn, on fait ce que lutilisateur nous demande de faire : toutes les instructions sont disponibles dans nos structures de donnes initialises.

Rcolter les bnces de PackRat


Lapproche dun DSL interne offre quelques avantages : nous avons russi crer un vrai DSL de sauvegarde en moins de 70 lignes de code. Une grande partie de ce code est ddie linfrastructure Backup/Source, qui serait probablement ncessaire quel que soit le choix de limplmentation. Latout supplmentaire dun DSL interne fond sur Ruby rside dans laccs gratuit la totalit de linfrastructure du langage. Si vous aviez un nom de dossier comprenant un guillemet simple1, vous pourriez chapper ce caractre comme vous le faites habituellement dans Ruby :
backup /home/russ/bob\s_documents

Puisque cest Ruby, vous pourriez galement faire ceci :


backup "/home/russ/bobs_documents"

Si lon crivait une implmentation classique dun analyseur syntaxique, on serait oblig de grer ce guillemet imbriqu. Ce nest pas le cas ici, car on hrite cette fonctionnalit de Ruby. De la mme manire, nous obtenons gratuitement des commentaires :
# # Sauvegarder le dossier de Bob # backup "/home/russ/bobs_documents"

En cas de besoin, nos utilisateurs peuvent proter de toutes les possibilits de programmation proposes par Ruby :
# # Une expression de recherche de fichiers audio # music_files = file_name(*.mp3) | file_name(*.wav)

1. Pour moi, un tel nom tmoignerait que vous ntes pas raisonnable, mais plusieurs avis existent sur le sujet.

252

Les patterns Ruby

# # Sauvegarder mes deux dossiers de musique # backup /home/russ/oldies, music_files backup /home/russ/newies, music_files to /tmp/backup interval 60

Le code prcdent cre en amont une expression de recherche de chiers qui est ensuite utilise dans les deux instructions backup.

Amliorer PackRat
Malgr le fait que notre implmentation de PackRat est oprationnelle, elle est quelque peu limite, car on ne peut spcier quune seule conguration de sauvegarde la fois. Pas de chance, limplmentation actuelle ne permet pas dutiliser deux ou trois dossiers de sauvegarde ou de copier certains chiers avec un autre intervalle de temps. Lautre problme, cest que PackRat nest pas trs propre : il repose en effet sur les mthodes du niveau suprieur interval, to et backup. On peut rsoudre ce problme en remaniant la syntaxe du chier packrat.pr pour quun utilisateur cre et congure des instances multiples de Backup :
Backup.new do |b| b.backup /home/russ/oldies, file_name(*.mp3) | file_name(*.wav) b.to /tmp/backup b.interval 60 end Backup.new do |b| b.backup /home/russ/newies, file_name(*.mp3) | file_name(*.wav) b.to /tmp/backup b.interval 60 end

Commenons par la classe Backup et voyons comment raliser cette approche :


class Backup attr_accessor:backup_directory,:interval attr_reader:data_sources def initialize @data_sources = [] @backup_directory = /backup @interval = 60 yield(self) if block_given? PackRat.instance.register_backup(self) end def backup(dir, find_expression=All.new) @data_sources << DataSource.new(dir, find_expression) end

Chapitre 16

Ouvrir votre systme avec des langages spciques dun domaine

253

def to(backup_directory) @backup_directory = backup_directory end def interval(minutes) @interval = minutes end def run while true this_backup_dir = Time.new.ctime.tr(":","_") this_backup_path = File.join(backup_directory, this_backup_dir) @data_sources.each {|source| source.backup(this_backup_path)} sleep @interval*60 end end end

Puisque lutilisateur est autoris crer des multiples instances, la classe Backup ne peut plus tre un singleton. Nous avons dplac les mthodes backup, to et interval lintrieur de la classe Backup. Les deux autres modications apparaissent dans la mthode initialize. La mthode initialize de la classe Backup excute yield en se passant elle-mme comme paramtre unique. Cela permet aux utilisateurs de congurer linstance de Backup dans un bloc de code pass new :
Backup.new do |b| # Configurer une nouvelle instance de Backup end

La dernire modication porte sur la mthode initialize de Backup et permet la nouvelle version de senregistrer dsormais dans la classe PackRat :
class PackRat include Singleton def initialize @backups = [] end def register_backup(backup) @backups << backup end def run threads = [] @backups.each do |backup| threads << Thread.new {backup.run} end threads.each {|t| t.join} end end eval(File.read(backup.pr)) PackRat.instance.run

254

Les patterns Ruby

La classe PackRat maintient une liste dinstances de Backup et dmarre chacune delles lappel de run dans un thread spar.

User et abuser des DSL internes


Comme nous lavons dcouvert, les DSL internes offrent une occasion unique de maximiser notre efcacit face certains types de problmes. Mais, comme tous les outils, cette mthode a ses limites. Aussi uide que la syntaxe de Ruby puisse tre, la capacit danalyse syntaxique dun DSL interne fond sur Ruby nest pas innie. Par exemple, il est probablement impossible dcrire en Ruby un DSL interne capable danalyser la syntaxe dun fragment de code HTML. Les messages derreurs constituent un autre point non ngligeable. Si vous ntes pas extrmement prudent, les erreurs de programmes DSL peuvent produire des messages derreur assez tranges. Par exemple, que se passerait-il si un utilisateur malchanceux saisissait x au lieu de b dans le chier backup.pr :
Backup.new do |b| b.backup /home/russ/newies, name(*.mp3) | name(*.wav) b.to /tmp/backup x.interval 60 end

Il obtiendrait le message derreur suivant :


./ex6_multi_backup.rb:86: undefined local variable or method x ...

Ce message est compltement incomprhensible pour un utilisateur qui ne connat pas Ruby et qui essaie tout simplement de sauvegarder ses chiers. Ce problme peut tre attnu grce une gestion dexceptions soigneusement labore. Nanmoins, les messages derreurs "cryptiques" reprsentent un cueil frquent pour les DSL internes. Enn, si la scurit est un point critique, vitez les DSL internes. Au fond, lide principale dun DSL interne consiste absorber dans votre programme du code arbitraire provenant de lextrieur. Cette approche ncessite une conance absolue.

Les DSL internes dans le monde rel


Rake, la rponse de Ruby ant et make, constitue un des exemples les plus connus dun DSL interne crit en Ruby. La syntaxe de rake ressemble celle de la deuxime version de PackRat, qui autorise des sauvegardes multiples. Lutilitaire rake permet de spcier des tapes qui constituent des tches dun processus dinstallation. Les tches peuvent tre interdpendantes. Si la tche B dpend de la

Chapitre 16

Ouvrir votre systme avec des langages spciques dun domaine

255

tche A, rake excuterait A avant B. Voici un exemple simple, le chier rake suivant sert sauvegarder mes dossiers de musique :
# # Dossiers qui contiennent ma collection de musique # OldiesDir = /home/russ/oldies NewiesDir = /home/russ/newies # # Dossier de sauvegarde # BackupDir = /tmp/backup # # Nom du dossier unique pour cette sauvegarde # timestamp=Time.new.to_s.tr(":", "_") # # Les tches rake # task:default => [:backup_oldies,:backup_newies] task:backup_oldies do backup_dir = File.join(BackupDir, timestamp, OldiesDir) mkdir_p File.dirname(backup_dir) cp_r OldiesDir, backup_dir end task:backup_newies do backup_dir = File.join(BackupDir, timestamp, NewiesDir) mkdir_p File.dirname(backup_dir) cp_r NewiesDir, backup_dir end

Ce chier rake dnit trois tches. Les tches backup_oldies et backup_newies effectuent exactement ce que suggre leur nom. La troisime tche dpend des deux premires. Lorsque rake essaie dexcuter la tche default, il doit dabord effectuer backup_oldies et backup_newies. Rails est videmment un autre exemple de dnition de DSL. Contrairement rake, Rails nest pas un langage DSL interne pur, mais il est rempli de fonctionnalits inspires par ce pattern. Parfois, on peut compltement oublier que lon code en Ruby. Pour vous donner un exemple remarquable, ActiveRecord permet de spcier des relations de classe dune faon qui ressemble beaucoup un DSL :
class Manager < ActiveRecord::Base belongs_to:department has_one:office has_many:committees end

256

Les patterns Ruby

En conclusion
Le pattern Langage spcique dun domaine (ou Domain-Specic Language) est le premier pattern examin dans ce livre qui ne fait pas partie des patterns rpertoris par le GoF. Mais ne soyez pas dcourag car, lorsquon combine la syntaxe extrmement exible de Ruby avec la technique de DSL interne, on obtient un outil qui apporte normment de puissance et de exibilit sans pour autant ncessiter beaucoup de code. Lide sous-jacente de ce pattern est assez simple : vous dnissez votre DSL, qui sinscrit dans les rgles de syntaxe de Ruby ; ensuite, vous dnissez une infrastructure ncessaire pour permettre lexcution de programmes crits dans votre langage DSL. La cerise sur le gteau est apporte par le fait quun simple appel la mthode eval permet dexcuter votre programme DSL comme sil sagissait de code Ruby habituel.

17
Crer des objets personnaliss par mta-programmation
Au Chapitre 13, nous avons tudi deux patterns Factory proposs par le GoF. Ces deux patterns tentent de rsoudre un des problmes fondamentaux de la programmation oriente objet : comment obtenir un objet adapt un problme ? Comment choisir le bon analyseur syntaxique pour les donnes traiter ? Comment slectionner ladaptateur compatible avec la base de donnes disponible ? Quel objet de gestion de la scurit slectionner en fonction de la version des spcications fournies ? Les patterns Factory permettent justement de choisir la bonne classe qui se chargera de linstanciation de lobjet. Limportance de choisir une classe est parfaitement justie dans des langages typage statique. Lorsque le comportement dun objet est compltement dni par sa classe et que ses classes ne sont pas modiables au moment de lexcution, slectionner la bonne classe est le seul point critique. Mais nous avons dj appris que ces rgles statiques ne sont pas applicables en Ruby. Ruby permet de modier une classe existante, de changer le comportement dun objet indpendamment de sa classe et mme dvaluer des chanes de caractres comme du code Ruby au moment de lexcution. Dans ce chapitre, nous dcouvrirons le pattern Mta-programmation, qui propose de proter de ce comportement dynamique pour accder aux objets requis. Avec ce pattern, on part du principe que les classes, les mthodes et le code lintrieur des mthodes sont simplement des composants du langage Ruby. Un bon moyen dobtenir les objets requis serait donc de manipuler ces composants tout comme on manipule des entiers et des chanes de caractres. Cette approche ne doit pas vous effrayer. Certes, la mta-programmation adopte une approche moins traditionnelle pour crer lobjet dont vous avez besoin mais, en ralit, cest

258

Les patterns Ruby

un moyen de proter de la exibilit et de la nature dynamique de Ruby que jvoque sans cesse dans ce livre. En guise dintroduction la mta-programmation1, essayons encore une fois de crer des habitants dans notre simulateur environnemental.

Des objets sur mesure, mthode par mthode


Imaginez que nous sommes de retour dans la jungle du Chapitre 13 et que nous essayons de la peupler avec des plantes et des animaux. Lapproche adopte au Chapitre 13 consistait recourir une factory pour slectionner les classes de ore et de faune adaptes. Et si lon avait besoin une fois de plus de exibilit ? Par exemple, au lieu de slectionner un type dorganisme spcique dans une liste de choix prdnis, on aimerait spcier les proprits de lorganisme et obtenir un objet construit sur mesure ? On pourrait par exemple avoir une mthode de cration des plantes qui accepterait des paramtres dcrivant le type de plante requis. Cela nous permettrait de construire lobjet dynamiquement au lieu de choisir explicitement une classe adapte :
def new_plant(stem_type, leaf_type) plant = Object.new if stem_type ==:fleshy def plant.stem fleshy end else def plant.stem woody end end if leaf_type ==:broad def plant.leaf broad end else def plant.leaf needle end end plant end

1. Il faut se rendre compte que la communaut Ruby est assez partage en ce qui concerne la dnition exacte de ce quest la mta-programmation. Dans ce chapitre, jai tent denglober le maximum de concepts de la mta-programmation sans me proccuper de la dnition prcise du terme.

Chapitre 17

Crer des objets personnaliss par mta-programmation

259

Le code prcdent cre une instance classique dObject et modie ensuite cet objet selon les spcications fournies par le client. La mthode new_plant quipe lobjet des mthodes leaf et stem spciques en fonction des options reues. Lobjet nal est plus ou moins unique : la plupart de ses fonctionnalits proviennent non pas de sa classe (ce nest quun Object) mais plutt de ces mthodes singleton. Lobjet retourn par new_ plant est effectivement fait sur mesure. Lutilisation de la mthode new_plant est trs simple. Il suft de spcier quel type de plante il vous faut :
plant1 = new_plant(:fleshy,:broad) plant2 = new_plant(:woody,:needle) puts "Plant 1s stem: #{plant1.stem} leaf: #{plant1.leaf}" puts "Plant 2s stem: #{plant2.stem} leaf: #{plant2.leaf}"

Voici le rsultat :
Plant 1s stem: fleshy leaf: broad Plant 2s stem: woody leaf: needle

videmment, il nexiste pas de rgle qui vous oblige commencer votre personnalisation sur une instance simple dObject. Dans la ralit, on serait susceptible dinstancier une classe qui fournit un certain niveau de fonctionnalit et de modier les mthodes de cette instance. La technique de personnalisation est particulirement utile lorsque vous avez un ensemble de fonctionnalits orthogonales quil faut rassembler dans un objet unique. La construction des objets sur mesure permet dviter de dnir un tas de classes avec des noms comme WoodyStemmedNeedleLeafFloweringPlant et VinyStemmedBroadLeafNonfloweringPlant.

Des objets sur mesure, module par module


Si vous ne souhaitez pas crer vos objets mthode par mthode, vous pouvez toujours les personnaliser avec des modules. Supposons que vous ayez des modules spars pour des carnivores et des herbivores :
module Carnivore def diet meat end def teeth sharp end end

260

Les patterns Ruby

module Herbivore def diet plant end def teeth flat end end

On peut alors imaginer une autre collection de modules qui reprsentent des animaux actifs en journe, comme des gens (ou la plupart des gens), et ceux qui rdent pendant la nuit :
module Nocturnal def sleep_time day end def awake_time night end end module Diurnal def sleep_time night end def awake_time day end end

Puisque les mthodes sont organises dans des modules, le code ncessaire pour crer de nouveaux objets est plus concis :
def new_animal(diet, awake) animal = Object.new if diet ==:meat animal.extend(Carnivore) else animal.extend(Herbivore) end if awake ==:day animal.extend(Diurnal) else animal.extend(Nocturnal) end animal end

La mthode extend appele dans ce code a exactement le mme effet que linclusion dun module normal, extend est simplement plus commode pour modier des objets la vole.

Chapitre 17

Crer des objets personnaliss par mta-programmation

261

Quelle que soit la technique adopte, modication par ajout de mthodes ou de modules, le rsultat aboutit la cration dun objet personnalis, construit sur mesure selon vos spcications.

Ajouter de nouvelles mthodes


Imaginons que vous ayez reu une nouvelle demande dvolution pour votre simulateur dhabitats : les clients veulent modliser des populations diffrentes de plantes et danimaux. Ils aimeraient par exemple pouvoir grouper tous les tres vivants qui habitent une zone prcise, grouper tous les tigres et les arbres qui partagent la mme section de la jungle, ou grouper des jungles situes cte cte. Et ce serait bien de pouvoir ajouter un code pour suivre la classication biologique de tous ces tres vivants. Les clients souhaitent savoir quun tigre fait partie du genre Panthera, de la famille Felidae, et ainsi de suite jusquau rgne Animal. Au premier abord, nous sommes face deux problmes de programmation distincts : dune part organiser les organismes par situation gographique et dautre part les organiser par classication biologique. Les deux problmes semblent tre similaires et ressemblent fort au pattern Composite. Mais il faudrait sasseoir et crire du code pour grer les populations ainsi que du code pour grer la classication, nest-ce pas ? Peuttre pas. Il est peut-tre possible dextraire les aspects communs et dimplmenter une fonctionnalit qui rsout les deux problmes la fois. Parfois, le meilleur moyen de sattaquer une tche pareille consiste imaginer le rsultat nal et en dduire limplmentation. Idalement, on aimerait dclarer que linstance dune classe donne par exemple Frog ou Tiger1 fait partie dune population gographique, ou dune classication biologique, ou des deux, comme ceci :
class Tiger < CompositeBase member_of(:population) member_of(:classification) # Beaucoup de code omis... end class Tree < CompositeBase member_of(:population) member_of(:classification) # Beaucoup de code omis... end

1. Dans cette section, je retourne vers limplmentation traditionnelle de Tiger et Tree fonde sur des classes. Cela ne signie pas que les diffrentes techniques de mta-programmation soient incompatibles. Mais une tentative dexpliquer tout la fois ne serait pas compatible avec la notion dexplication comprhensible.

262

Les patterns Ruby

On essaie dexprimer par ce code le fait que les classes Tiger et Tree sont des feuilles de deux composites diffrents. Lun conserve un suivi des populations par rpartition gographique et lautre modlise les classications biologiques. On doit galement dclarer que les classes qui reprsentent les espces et les populations gographiques sont des composites :
class Jungle < CompositeBase composite_of(:population) # Beaucoup de code omis... end class Species < CompositeBase composite_of(:classification) # Beaucoup de code omis... end

Idalement, lutilisation de nos nouvelles classes Tiger, Tree, Jungle et Species doit tre extrmement simple. On aimerait par exemple pouvoir crer un tigre et ensuite lajouter une population gographique donne :
tony_tiger = Tiger.new(tony) se_jungle = Jungle.new(southeastern jungle tigers) se_jungle.add_sub_population(tony_tiger)

Une fois cette opration accomplie, on doit avoir la possibilit de retrouver la population parent de notre tigre :
tony_tiger.parent_population # Le rsultat doit tre southeastern # jungle

Enn, la mme approche doit fonctionner pour des classications biologiques :


species = Species.new(P. tigris) species.add_sub_classification(tony_tiger) tony_tiger.parent_classification # Le rsultat doit tre P. tigris

Ci-aprs, on trouve la classe CompositeBase, qui implmente toute cette magie :


class CompositeBase attr_reader:name def initialize(name) @name = name end def self.member_of(composite_name) code = %Q{ attr_accessor:parent_#{composite_name} } class_eval(code) end

Chapitre 17

Crer des objets personnaliss par mta-programmation

263

def self.composite_of(composite_name) member_of composite_name code = %Q{ def sub_# {composite_name}s @sub_#{composite_name}s = [] unless @sub_#{composite_name}s @sub_#{composite_name}s end def add_sub_#{composite_name}(child) return if sub_#{composite_name}s.include?(child) sub_#{composite_name}s << child child.parent_#{composite_name} = self end def delete_sub_#{composite_name}(child) return unless sub_#{composite_name}s.include?(child) sub_#{composite_name}s.delete(child) child.parent_#{composite_name} = nil end } class_eval(code) end end

Analysons cette classe tape par tape. Le dbut de la classe CompositeBase est assez conventionnel : on dnit une simple variable dinstance et la mthode initialize pour y affecter une valeur. Les choses deviennent intressantes dans la deuxime mthode, la mthode de classe member_of :
def self.member_of(composite_name) code = %Q{ attr_accessor:parent_#{composite_name} } class_eval(code) end

La mthode accepte le nom du composite correspondant et construit un fragment de code Ruby en se fondant sur ce nom. Si vous appelez member_of et passez :population en argument (comme la classe Tiger), la mthode member_of gnre la chane de caractres suivante :
attr_accessor:parent_population

Ensuite, la mthode member_of fait appel la mthode class_eval pour valuer la chane en tant que code Ruby. La mthode class_eval ressemble la mthode eval que nous connaissons dj, la seule diffrence que class_eval value une chane dans le contexte de la classe au lieu du contexte courant 1. Vous avez probablement
1. La mthode class_eval est galement connue sous le nom de module_eval.

264

Les patterns Ruby

devin que cela a pour effet dajouter la classe un accesseur et un mutateur de la variable dinstance parent_population. Cest prcisment la modication qui est ncessaire pour transformer la classe en un membre (une feuille, pour tre plus prcis) dun composite. La mthode suivante de la classe CompositeBase, nomme composite_of, suit le mme principe. Elle ajoute des mthodes qui correspondent un objet composite. Si vous appelez la mthode de classe composite_of dune de vos classes, elle acquiert trois nouvelles mthodes : une mthode pour ajouter un lment au composite, une mthode pour supprimer un lment et une mthode pour retourner un tableau qui contient la totalit des lments. Puisque nous gnrons toutes ces mthodes par la cration dune chane de caractres et son valuation avec class_eval, il est facile dinsrer dans les noms des mthodes le nom du composite. Les mthodes cres par lappel member_of (:population) sont donc add_sub_population, delete_sub_ population et sub_populations. Le point cl retenir propos de cet exemple est que les sous-classes de CompositeBase nhritent pas automatiquement du comportement du pattern Composite. Elles nhritent que des mthodes de classe member_of et composite_of, qui ajoutent les mthodes composites la sous-classe lorsquelles sont invoques.

Lobjet vu de lintrieur
La technique dajout de fonctionnalits telle que nous lavons utilise dans la classe CompositeBase soulve une question : comment peut-on savoir si un objet donn fait partie dun composite ? Plus gnralement, lorsquon injecte des fonctionnalits dans des classes la vole laide de la mta-programmation, comment peut-on savoir quelles fonctionnalits sont disponibles dans une instance donne ? Eh bien, il suft simplement de se renseigner auprs de cette instance ! Des objets Ruby fournissent une palette trs complte de mthodes dites "de rexion" qui renvoient diffrentes informations sur un objet, par exemple quelles sont ses mthodes ou ses variables dinstance. Pour dcouvrir si un objet fait partie dun composite comme CompositeBase, on peut inspecter la liste de ses mthodes publiques :
def member_of_composite?(object, composite_name) public_methods = object.public_methods public_methods.include?("parent_#{composite_name}") end

Chapitre 17

Crer des objets personnaliss par mta-programmation

265

On peut aussi avoir recours la mthode respond_to? :


def member_of_composite?(object, composite_name) object.respond_to?("parent_#{composite_name}") end

Des fonctions de rexion telles que public_methods et respond_to? sont effectivement trs pratiques, mais on peut mme les qualier de totalement indispensables lorsquon plonge dans le monde de la mta-programmation car vos objets dpendent alors beaucoup plus de lhistorique des modications subies lexcution que de leurs classes de base.

User et abuser de la mta-programmation


Chaque pattern doit tre employ lorsquil est adapt aux circonstances. Cest plus vrai que jamais pour la mta-programmation, qui est un outil extrmement puissant. Avec la mta-programmation, le code de lapplication lui-mme subit des changements au moment de lexcution. Plus vous utilisez la mta-programmation, moins le programme excut ressemble au programme qui se trouve dans les chiers source. Cest videmment le but, mais cest aussi le danger. Dboguer du code ordinaire est assez difcile, et il est encore plus compliqu de corriger des erreurs dans un programme phmre gnr par la mta-programmation. Alors que les tests unitaires sont importants pour des programmes classiques, ils deviennent absolument vitaux dans des systmes qui utilisent la mta-programmation intensivement. Le danger principal de ce pattern est linteraction inattendue entre plusieurs fonctions. Imaginez le chaos dans notre exemple des habitats si la mthode parent_classification tait dj dnie dans la classe Species lorsquelle appelle composite_ of(:classification) :
class Species < CompositeBase # Cette mthode est sur le point de disparatre! def parent_classification # ... end # Voici le code qui provoque sa destruction... composite_of(:classification) end

Parfois, il est possible de prvenir ce genre de carnage en ajoutant des garde-fous votre mta-code :
class CompositeBase # ...

266

Les patterns Ruby

def self.member_of(composite_name) attr_name = "parent_#{composite_name}" raise Method redefinition if instance_methods.include?(attr_name) code = %Q{ attr_accessor:#{attr_name} } class_eval(code) end # ... end

Cette version de CompositeBase dclenche une exception si la mthode parent_ <composite_name> existe dj. Lapproche nest pas idale, mais elle est probablement meilleure que passer sous silence lcrasement dune mthode existante.

La mta-programmation dans le monde rel


Chercher des exemples de mta-programmation dans la base de code Ruby est comme chercher des vtements sales dans la chambre de mon ls : il y en a partout. Regardez par exemple lomniprsent attr_accessor et ses amis attr_reader et attr_writer. Souvenez-vous du Chapitre 2, o nous avons dcouvert que toutes les variables dinstance Ruby sont prives et ncessitent des accesseurs et mutateurs pour ouvrir laccs au monde extrieur :
class BankAccount def initialize(opening_balance) @balance = opening_balance end def balance @balance end def balance=(new_balance) @balance = new_balance end end

Au Chapitre 2, nous avons galement appris la bonne nouvelle que nous ne sommes pas obligs dcrire toutes ces mthodes de base. Selon nos besoins, nous pouvons simplement insrer attr_reader, addr_writer ou la combinaison des deux attr_ accessor :
class BankAccount attr_accessor:balance def initialize(opening_balance) @balance = opening_balance end end

Chapitre 17

Crer des objets personnaliss par mta-programmation

267

Il savre quattr_accessor et ses copains reader et writer ne sont pas des mots cls rservs du langage Ruby. Ce sont juste des mthodes de classe ordinaires 1, tout comme les mthodes member_of et composite_of labores dans ce chapitre. Il est en effet assez simple dcrire notre propre version dattr_reader. tant donn que le nom "attr_reader" est dj occup, appelons notre mthode readable_attribute :
class Object def self.readable_attribute(name) code = %Q{ def #{name} @#{name} end } class_eval(code) end end

Une fois la mthode readable_attribute dnie, on peut lutiliser de la mme manire quattr_reader :
class BankAccount readable_attribute:balance def initialize(balance) @balance = balance end end

Le module Forwardable est un autre bon exemple auquel nous avons fait appel pour construire des dcorateurs. La cration fastidieuse des mthodes de dlgation devient automatique avec le module Forwardable. Par exemple, si lon avait une classe Car avec une classe Engine spare, on pourrait crire ceci :
class Engine def start_engine # Dmarrer le moteur end def stop_engine # Arrter le moteur end end

1. La mthode attr_accessor et ses amis rsident dans le module Module inclus dans la classe Object. Si vous partez la recherche des mthodes attr_accessor, attr_reader et attr_writer dans le code Ruby, vous risquez dtre du. Pour des raisons de performance et uniquement pour ces raisons , ces mthodes sont crites en C.

268

Les patterns Ruby

class Car extend Forwardable def_delegators:@engine,:start_engine,:stop_engine def initialize @engine = Engine.new end end

La ligne qui commence avec def_delegators cre deux mthodes : start_engine et stop_engine. Chacune delles dlgue lobjet rfrenc par @engine. Le module Forwardable cre ces mthodes laide de la mme technique fonde sur class_eval que celle tudie dans ce chapitre. Il ne faut pas oublier Rails. Le volume de mta-programmation dans Rails est si norme quil est difcile de choisir par o commencer. La faon de dnir les relations entre les tables dans ActiveRecord constitue probablement lexemple le plus remarquable. ActiveRecord fournit une classe pour chaque table de la base de donnes. Si lon modlise un habitat naturel avec ActiveRecord, on pourrait par exemple avoir une table et une classe pour les animaux. On pourrait galement modliser la description complte de chaque animal dans une table spare. Il est vident que les tables des animaux et des descriptions auront une relation un--un. ActiveRecord nous permet dexprimer cette relation de manire trs lgante :
class Animal < ActiveRecord::Base has_one:description end class Description < ActiveRecord::Base belongs_to:animal end

On peut exprimer de manire similaire toutes les relations courantes entre des tables dans une base de donnes. Par exemple, chaque espce pourrait inclure beaucoup danimaux :
class Species < ActiveRecord::Base has_many: animals end

Mais chaque animal appartient une espce unique :


class Animal < ActiveRecord::Base has_one:description belongs_to:species end

Chapitre 17

Crer des objets personnaliss par mta-programmation

269

Toutes ces dclarations ont pour consquence linjection de code dans les classes Animal, Description et Species. Ce code permet dtablir entre les classes les relations dnies dans la base de donnes. Une fois la relation spcie, on peut demander une instance dAnimal de retourner sa description correspondante avec un simple appel animal.description, ou bien nous pouvons rcuprer tous les animaux qui font partie dune espce donne laide de species.animals. Cest grce la mtaprogrammation quActiveRecord est capable de faire tout cela pour nous.

En conclusion
Dans ce chapitre, nous avons pass en revue quelques principes fondateurs de la mtaprogrammation et, notamment, quil est possible dinjecter le code ncessaire par programmation au moment de lexcution au lieu de le saisir au clavier. Les fonctionnalits dynamiques de Ruby nous permettent de partir dun objet simple et dy ajouter des mthodes ou mme des modules entiers remplis de mthodes. Des mthodes compltement nouvelles peuvent tre gnres au moment de lexcution laide de class_ eval. Nous avons aussi tir parti des capacits de rexion de Ruby, qui permettent un programme dexaminer sa propre structure et de dcouvrir ce quil est dj capable de faire avant de lui apporter des modications. Dans le monde rel, la mta-programmation est un des lments de base pour les DSL, les langages spciques dun domaine que nous avons dcouvert au Chapitre 16. Malgr le fait quil est possible de dvelopper un DSL avec peu ou pas de mtaprogrammation comme nous lavons fait au Chapitre 16 , la mta-programmation est souvent lingrdient primordial dans le dveloppement des DSL la fois puissants et faciles dutilisation. Au chapitre suivant, nous terminerons lanalyse des design patterns en Ruby avec un autre pattern qui sinscrit bien dans la philosophie de la mta-programmation. Il sagit du principe nomm "Convention plutt que conguration".

18
Convention plutt que conguration
Le dernier pattern que nous examinons dans ce livre se nomme "Convention plutt que conguration". Le livre du GoF Design Patterns nest pas lorigine de ce pattern, car il vient directement du framework Rails. On peut afrmer sans ambage que le pattern Convention plutt que conguration fait partie des cls du succs de Rails. Cest un pattern ambitieux dune trs grande porte, ce qui le distingue des autres patterns prsents dans ce livre. Alors que les autres patterns interviennent sur une plus petite chelle et sont principalement concerns par des problmes dorganisation dun certain nombre de classes interdpendantes, Convention plutt que conguration se concentre sur lorganisation des applications et des frameworks entiers. Comment structurer une application ou un framework de faon permettre aux autres ingnieurs dajouter facilement du code au fur et mesure de lvolution du programme ? Lorsque nous construisons des systmes de plus en plus ambitieux, le problme de leur conguration devient plus que jamais dactualit. La raction du monde du logiciel face au problme de lextensibilit me rappelle le dilemme que je rencontrais tous les soirs lorsque jtais au collge. Je ne pouvais jamais dcider quand il fallait faire mes devoirs. Il y avait des jours o je revenais de lcole, jouvrais mes livres et je faisais mes devoirs. Il ny a rien de mieux que le sentiment du travail accompli. Mais, par ailleurs, il ny a rien de mieux que revenir la maison, jeter ses livres sur la table, sauter sur son vlo et partir laventure. mon retour, les livres taient toujours l, bien entendu, et tt ou tard je devais faire mes devoirs quand mme. En n de compte, javais ni par trouver un compromis : je moccupais du franais tant dtest et des ennuyeuses sciences sociales directement aprs lcole et je remettais les mathmatiques, que jadorais, plus tard.

272

Les patterns Ruby

Lingnierie logicielle a suivi un parcours assez semblable en ce qui concerne les capacits dextension des systmes dinformation. Au l de leur carrire de nombreux programmeurs ont travaill avec des systmes, ers de leurs propres limitations. Ces programmes ne comprenaient quun protocole unique, ou bien exigeaient un schma bien prcis de la base de donnes, ou encore ils imposaient sur les malheureux utilisateurs une interface totalement rigide. La raction toutes les difcults provoques par cette rigidit fut de rendre les systmes extensibles lors de la phase de conguration. En effet, en dportant les dcisions importantes dans un chier de conguration on pourrait alors accder lUtopie totale : la dnition dun systme idal uniquement par leffet de la conguration. Malheureusement, on est pass ct de lUtopie pour arriver nouveau dans un dsert o le code est maintenant totalement dpendant de la conguration. Aujourdhui, nous sommes envahis par des frameworks sensibles la moindre modication de la conguration et des applications qui vivent avec la peur que toute approximation injustie devra faire lobjet dun patch en urgence. Les servlets Java sont un parfait exemple de ce genre de conguration excessive. Les servlets sont des composants critiques de quasiment toutes les applications Web crites en Java. Un servlet est une petite classe Java lgante capable de traiter des requtes HTTP qui arrivent partir dune ou de plusieurs URL. Mais, malheureusement, pour crire un servlet il ne suft pas dcrire une classe Java qui tend javax.servlet.HttpServlet. Vous devez galement congurer votre classe laide du chier de conguration web.xml. La forme la plus basique de web.xml permet dassocier la classe servlet avec un nom, puis dassocier ce nom avec une ou plusieurs URL. Pourtant, les applications relles ont rarement besoin de toute cette exibilit. Dans la plupart des cas pas toujours, mais trs souvent , on a tendance utiliser le nom de la classe en tant que nom de servlet. Dans la plupart des cas pas toujours, mais trs souvent , on associe ce nom une URL drive de ce nom et par consquent au nom de la classe. On pourrait choisir nimporte quel nom mais il est bien mieux dutiliser celui qui nous rappelle le nom de notre classe. De la mme manire, peu importe lURL qui sert de porte dentre notre servlet, mais cest tout de mme bien mieux si lui aussi utilise le mme nom que notre classe. Les programmeurs (les bons programmeurs, tout au moins) attachent beaucoup de valeur la simplicit. La solution simple dans cette situation serait dliminer compltement toute cette exibilit. Si vous navez aucune utilit de la exibilit qui vous est offerte, elle devient un danger : tous ces noms et associations reprsentent autant de moyens supplmentaires de se tirer une balle dans le pied.

Chapitre 18

Convention plutt que conguration

273

La motivation principale du pattern Convention plutt que conguration est prcisment dallger le fardeau de la conguration. Le but est de prserver lextensibilit essentielle de nos applications et de nos frameworks tout en supprimant la conguration excessive. Dans ce chapitre, nous commencerons par tudier les principes du pattern Convention plutt que conguration. Ensuite, nous dvelopperons un systme de messages hypothtiques pour comprendre comment concevoir des logiciels qui trouvent un compromis judicieux, comme javais su le faire pour mes devoirs scolaires.

Une bonne interface utilisateur... pour les dveloppeurs


Lcriture de logiciels la fois exibles et simples utiliser est un problme courant surtout si vous dveloppez des interfaces graphiques. Les programmeurs dinterfaces graphiques ont labor plusieurs rgles de conception pour crer des interfaces conviviales :
m

Essayez danticiper les besoins des utilisateurs. Dans une interface bien conue, les tches les plus courantes ne doivent requrir quasiment aucun effort, et le cas le plus courant doit tre le choix par dfaut. Les tches moins frquentes ou plus complexes doivent rester faisables avec un peu plus deffort. Nobligez pas les utilisateurs se rpter. Est-ce quil vous est arriv de vouloir donner un coup de pied dans lcran lorsque lapplication vous demande pour la troisime fois : "tes-vous sr de vouloir faire cette action ?" Fournissez des modles. Prsentez lutilisateur un modle suivre pour laider btir quelque chose. Nimposez pas votre utilisateur langoisse de la feuille blanche : sil souhaite crire un CV, fournissez un modle pour lui donner des ides.

Le pattern Convention plutt que conguration se concentre sur lapplication de ces mmes principes transposs dans la conception dapplications et des API de frameworks. Il ny a aucune raison pour que ces bonnes techniques ne protent qu lutilisateur nal. Les ingnieurs qui essaient de paramtrer votre application ou de faire appel votre API sont aussi des utilisateurs et ont besoin daide. Pourquoi ne pas offrir une interface conviviale tous vos utilisateurs ? Anticiper les besoins Que ce soit un client de courrier lectronique ou une API, il y a une caractristique qui aide distinguer les bonnes interfaces des mauvaises : si lutilisateur excute une action trs frquemment, cela doit tre loption par dfaut. Inversement, si laction nest effectue que rarement, il est tout fait acceptable quelle soit plus difcile daccs. Cest la

274

Les patterns Ruby

raison pour laquelle il suft dappuyer sur une touche pour passer dun message lectronique lautre, mais il faut naviguer dans plusieurs menus pour congurer un serveur de-mail. Trs souvent des API sont construites en partant du principe que la frquence dutilisation des diffrentes fonctions sera identique. Cest cette supposition qui a amen la conguration des servlets Java : peu importe si lon cre une servlet ordinaire qui ne rpond qu une seule URL ou une servlet complexe et multifonction lie plusieurs URL, le temps de dveloppement et de conguration de la tche la plus simple est le mme que celui de la tche la plus complexe ou la moins frquente. Une interface de programmation plus conviviale rendrait la tche courante la plus simple possible mais demanderait plus de travail pour accomplir la tche la plus complexe. Ne le dire quune seule fois Pour rendre fous vos utilisateurs, il suft de les obliger se rpter. Cest une leon bien connue dans le monde des interfaces graphiques, mais lvidence elle na pas t apprise dans le monde des programmeurs dAPI. Comment viter nos dveloppeurs des rptitions inutiles ? Il suft de leur donner le moyen dexprimer leurs souhaits une fois et de ne plus leur redemander. Des ingnieurs ont tendance naturellement adopter des conventions : ils suivent des rgles de nommage de chiers, ils regroupent des chiers source lis entre eux dans les mmes dossiers et ils nomment des mthodes selon des patrons rguliers. Le principe du pattern Convention plutt que conguration consiste prcisment dnir une convention probablement dj applique par des ingnieurs raisonnables : placer tous les adaptateurs dans un dossier prcis ou nommer toutes les mthodes de contrle daccs dune certaine faon. Pour tablir une bonne convention, tout comme pour concevoir une bonne interface graphique, il faut se mettre la place de ses utilisateurs. Essayez pour cela danticiper leur comportement : comment vont-ils appeler certaines fonctions et quelle serait pour eux la faon la plus naturelle de les organiser ; ensuite, mettez en place votre convention en vous fondant sur ces hypothses. Lorsque votre convention est prte, tchez de maximiser son utilit : lorsquun ingnieur nomme une classe et la place dans un rpertoire donn, il exprime quelque chose. Soyez attentif et ne lobligez pas se rpter. Fournir un modle Un autre moyen de faciliter lutilisation de votre systme consiste fournir vos utilisateurs un modle ou un exemple. Les diteurs de texte modernes, par exemple, ne vous

Chapitre 18

Convention plutt que conguration

275

laissent plus seul face la page blanche. Lorsque vous crez un document, le programme vous demande si cest un CV, une lettre ou un discours prsidentiel et, si votre document fait partie des dizaines de modles connus, lditeur de texte vous proposera un contenu et une mise en page adapts. Vous pouvez rendre le mme service aux programmeurs qui essaient dtendre votre systme : proposez des exemples, des modles et des fragments de code pour les aider bien dmarrer. Si un dessin vaut mieux quun long discours, alors, un ou deux bons exemples valent certainement au moins deux cents pages de documentation.

Une passerelle de messages


Voyons comment ces nobles idaux sappliquent au code rel. Imaginons quon nous ait demand de dvelopper une passerelle de messages. Notre code doit tre mme de recevoir des messages et de les transfrer vers leurs destinations nales. Les messages ressemblent ceci :
require uri class Message attr_accessor:from,:to,:body def initialize(from, to, body) @from = from @to = URI.parse(to) @body = body end end

Le champ from est une simple chane de caractres qui contient linformation sur lexpditeur du message, quelque chose comme russ.olsen. Le champ to est un URI qui indique la destination du message. Le champ body est une chane qui correspond au corps du message. La classe Message emploie la classe URI, qui fait partie de la distribution standard de Ruby, pour convertir des chanes en objets URI utilisables. Les URI entrants de notre passerelle sont de trois types : on peut tre amen envoyer un message soit par courrier lectronique :
smtp://fred@russolsen.com

Soit par une requte HTTP Post :


http://russolsen.com/some/place

Soit lcrire dans un chier :


file:///home/messages/message84.txt

276

Les patterns Ruby

Lexigence cl pour notre passerelle de messages est la possibilit dajouter de nouveaux protocoles facilement. Par exemple, si lon devait envoyer des messages par FTP, il faudrait pouvoir adapter la passerelle trs simplement pour grer les nouvelles destinations. Aprs avoir consult votre livre prfr sur les design patterns1, vous vous rendez compte que pour traiter les diffrentes destinations de ces messages il vous faut un adaptateur. Trois adaptateurs, pour tre plus prcis : un adaptateur par protocole. Dans ce cas, linterface de ladaptateur est trs simple, il ne consiste quen une seule mthode : send_message(message). Voici ladaptateur qui transfre des messages en tant que courrier lectronique :
require net/smtp class SmtpAdapter MailServerHost = localhost MailServerPort = 25 def send_message(message) from_address = message.from to_address = message.to.user + @ + message.to.host email_text = "From: #{from_address}\n" email_text += "To: #{to_address}\n" email_text += "Subject: Forwarded message\n" email_text += "\n" email_text += message.body Net::SMTP.start(MailServerHost, MailServerPort) do |smtp| smtp.send_message(email_text, from_address, to_address) end end end

Voici ladaptateur qui envoie des messages par HTTP :


require net/http class HttpAdapter def send_message(message) Net::HTTP.start(message.to.host, message.to.port) do |http| http.post(message.to.path, message.body) end end end

Enn, voici ladaptateur qui "envoie" des messages en les copiant dans un chier :
class FileAdapter def send_message(message) # # Rcuprer le chemin de lURL

1. Celui-ci, bien videmment !

Chapitre 18

Convention plutt que conguration

277

# et effacer le premier / # to_path = message.to.path to_path.slice!(0) File.open(to_path, w) do |f| f.write(message.body) end end end

Slectionner un adaptateur
Le problme suivant consiste dterminer la classe adaptateur approprie selon le message reu. Une des solutions consiste coder en dur la logique de slection dun adaptateur :
def adapter_for(message) protocol = message.to.scheme return FileAdapter.new if protocol == file return HttpAdapter.new if protocol == http return SmtpAdapter.new if protocol == smtp nil end

Toutefois, cette solution prsente un problme : la personne qui ajoutera un nouveau protocole de livraison, et par consquent un nouvel adaptateur, sera oblige de plonger dans le code de la mthode adapter_for pour y ajouter son nouvel adaptateur. Obliger quelquun modier le code existant ne rend assurment pas notre application "facilement extensible". On peut probablement mieux faire. On pourrait, par exemple, avoir un chier de conguration pour dnir la correspondance entre les protocoles et les noms des adaptateurs, comme ceci :
smtp: SmtpAdapter file: FileAdapter http: HttpAdapter

Cette solution pourrait fonctionner, mais ce chier de conguration signierait en ralit que nous avons opt pour un autre type de codage en dur. Dans les deux cas, la personne qui ajoute un nouvel adaptateur doit effectuer une manipulation supplmentaire an que le systme reconnaisse ce nouvel adaptateur. Cela nous amne la question de fond : pourquoi cela ne suft-il pas dcrire la nouvelle classe adaptateur pour quelle soit immdiatement prise en compte ? Si lon demande aux auteurs de nouveaux adaptateurs dadhrer une convention judicieuse, le rajout dun adaptateur peut se rduire la cration de la classe. Voici la convention magique : nommez votre classe adaptateur <protocol>Adapter.

278

Les patterns Ruby

Selon cette convention, un nouvel adaptateur capable denvoyer des chiers via FTP sappellerait FtpAdapter. Si tous les adaptateurs suivaient cette convention, le systme pourrait slectionner la classe adaptateur en fonction de son nom 1 :
def adapter_for(message) protocol = message.to.scheme.downcase adapter_name = "#{protocol.capitalize}Adapter" adapter_class = self.class.const_get(adapter_name) adapter_class.new end

La mthode adapter_for extrait le protocole cible du message, puis elle transforme le nom tel que "http" en "HttpAdapter" avec quelques manipulations de chane de caractres. Ensuite, il suft dappeler const_get pour rcuprer la classe qui porte le mme nom. Cette approche nous permet dviter lutilisation dun chier de conguration : pour ajouter un nouvel adaptateur il suft de crer la classe adaptateur en la baptisant correctement.

Charger des classes


Enn presque... Il nous reste tout de mme grer le chargement des adaptateurs dans linterprteur Ruby. Dans notre code nous devons inclure les chiers qui contiennent les classes adaptateurs laide de linstruction require :
require file_adapter require http_adapter require smtp_adapter

On pourrait placer toutes ces instructions require pour chacun des adaptateurs dans un chier et prvenir les auteurs des adaptateurs de complter ce chier lorsquun nouvel adaptateur arrive. Mais, une fois de plus, cela signie que lon oblige le programmeur se rpter : il doit non seulement crer ladaptateur, mais aussi nous le conrmer en ajoutant une ligne dans le chier des instructions require. On peut srement trouver une solution plus lgante. Commenons par nous concentrer sur la structure des dossiers. Dhabitude, on ne prte pas trop attention lemplacement physique des chiers et dossiers o rside notre code source, alors quil est possible dtablir des conventions trs efcaces en sappuyant sur lemplacement des chiers. Imaginez que lon dnisse pour notre passerelle de message une structure de dossiers prsente la Figure 18.1.
1. Souvenez-vous que nous avons employ la mme technique au Chapitre 13 pour simplier une factory abstraite.

Chapitre 18

Convention plutt que conguration

279

Figure 18.1
La structure des dossiers de la passerelle des messages
lib/ gateway/

README.txt

gateway.rb

adapter/

http_adapter.rb

smtp_adapter.rb

file_adapter.rb

Cette structure de dossiers nest pas particulirement originale, elle est souvent utilise dans des projets Ruby1. Une structure standard de rpertoires peut nous aider rsoudre le problme du chargement dadaptateurs, donc cette structure est particulirement pertinente dans notre cas :
def load_adapters lib_dir = File.dirname(__FILE__) full_pattern = File.join(lib_dir, adapter, *.rb) Dir.glob(full_pattern).each {|file| require file } end

La mthode load_adapters dduit le chemin du dossier des adaptateurs en partant de la constante __FILE__. Linterprteur Ruby affecte cette constante le chemin du chier source courant. Quelques manipulations des mthodes de la classe File nous permettent de trouver un patron de nom pour tous nos adaptateurs : "adapter/* . rb". Ensuite, la mthode utilise ce patron pour rechercher et inclure toutes les classes dadaptateurs. Cette approche fonctionne car require nest quun appel de mthode Ruby comme un autre, et cet appel peut tre effectu partir du code tout moment lorsque nous devons charger un chier source dans linterprteur Ruby. Nous devons toutefois complter notre convention : nommez votre classe <protocol>Adapter et placez-la dans le dossier adapter.

1. Un projet doit tre organis de cette faon pour tre empaquet en tant que gem, cest probablement la raison pour laquelle cette structure est si rpandue.

280

Les patterns Ruby

Tout ceci est assez remarquable car nous venons dcrire en trs peu de lignes tout ce qui est ncessaire au bon fonctionnement dune passerelle de messages. Voici la classe MessageGateway complte :
class MessageGateway def initialize load_adapters end def process_message(message) adapter = adapter_for(message) adapter.send_message(message) end def adapter_for(message) protocol = message.to.scheme adapter_class = protocol.capitalize + Adapter adapter_class = self.class.const_get(adapter_class) adapter_class.new end def load_adapters lib_dir = File.dirname(__FILE__) full_pattern = File. join(lib_dir, adapter, *.rb) Dir.glob(full_pattern).each {|file| require file } end end

Il suft maintenent dappeler la mthode process_message en lui passant un objet Message, et vous le verrez tout de suite partir joyeusement vers sa destination. Il faut noter deux caractristiques importantes dans la convention que nous venons dtablir. Premirement, le seul but de cette convention est de faciliter lajout dadaptateurs. Lobjectif ntait absolument pas de rendre tous les aspects de la passerelle de messages facilement extensibles. Pourquoi ? Parce que nous avons suppos que nos utilisateurs, des ingnieurs logiciel, auraient besoin dajouter de nouveaux adaptateurs assez frquemment. Si lon anticipe ce besoin futur, on peut faciliter la vie des fournisseurs dadaptateurs. Deuximement, malgr le fait que notre convention impose quelques contraintes lauteur dun adaptateur doit suivre une rgle pour nommer son adaptateur et il doit le placer dans un dossier prcis , ces contraintes ne sont pas gnantes, car tout ingnieur consciencieux applique dj ces rgles.

Chapitre 18

Convention plutt que conguration

281

Ajouter un niveau de scurit


Maintenant que la premire version de la passerelle de messages est oprationnelle, il faut penser ajouter un niveau de scurit. Pour tre prcis, on voudrait appliquer une politique gnrale pour contrler quels utilisateurs ont lautorisation denvoyer des messages un hte donn. Qui plus est, il faut pouvoir grer un certain nombre dutilisateurs particuliers, qui ne seront pas soumis la politique gnrale pour un hte donn. On peut commencer par la mise en place dune classe de contrle daccs pour chaque hte de destination. Cette contrainte parat acceptable si le nombre dhtes reste limit. La convention de nommage et lemplacement des chiers ont tellement bien fonctionn pour les adaptateurs que nous dcidons dadopter une approche similaire pour les classes de contrle daccs : nommez votre classe de contrle daccs <destination_ host>Authorizer et placez-la dans le dossier auth. La Figure 18.2 illustre notre structure de dossiers aprs la mise jour.
Figure 18.2
La structure de dossiers de la passerelle tendue avec le contrle daccs
lib/ gateway/

README.txt

gateway.rb

adapter/

auth/

Le nommage des classes de contrle daccs soulve un problme car, en rgle gnrale, les noms des htes ne correspondent pas des noms de classes valides en Ruby. On devra recourir la magie de la transformation de chanes. Traduisons un nom comme russolsen.com en classe de contrle daccs RussolsenDotComAuthorizer1 :
def camel_case(string) tokens = string.split(.) tokens.map! {|t| t.capitalize} tokens.join(Dot) end

1. Pour rester simple, la mthode authorizer_for ne gre pas correctement les noms dhte qui incorporent des tirets. videmment, il suft dajouter quelques expressions rgulires bien choisies pour traiter tous les noms dhte possibles.

282

Les patterns Ruby

def authorizer_for(message) to_host = message.to.host || default authorizer_class = camel_case(to_host) + "Authorizer" authorizer_class = self.class.const_get(authorizer_class) authorizer_class.new end

Mais quoi doit ressembler linterface des classes de contrle daccs ? Souvenez-vous que pour chaque hte donn il doit exister un certain nombre de rgles applicables presque tous les utilisateurs. Je dis "presque" car il peut y avoir plusieurs exceptions. On pourrait imaginer, par exemple, que tous les utilisateurs sont autoriss envoyer des messages courts ladresse russolsen.com, mais que seul "russ.olsen" a le droit dy envoyer des messages longs. On pourrait adopter une convention qui stipule que si la classe de contrle daccs possde une mthode nomme <user name>_authorized?, cette mthode sera utilise pour autoriser le message. On serait l aussi oblig de transformer le nom de lutilisateur pour quil respecte les rgles de nommage de mthodes. Dans le cas o cette mthode nexisterait pas, on appellerait alors la mthode gnrique authorized?. Voil quoi pourrait ressembler une classe de contrle daccs typique :
class RussolsenDotComAuthorizer def russ_dot_olsen_authorized?(message) true end def authorized?(message) message.body.size < 2048 end end

Le code qui met en uvre cette convention est trs simple. Tout dabord, on obtient une instance de la classe de contrle daccs pour le message en cours de traitement. Ensuite, on rcupre le nom de la mthode qui gre la politique spciale concernant lexpditeur du message. Enn, on vrie que lobjet de contrle daccs rpond cette mthode. Si cest le cas, alors, on utilise cette mthode, sinon on appelle la mthode standard authorized? :
def worm_case(string) tokens = string.split(.) tokens.map! {|t| t.downcase} tokens.join(_dot_) end def authorized?(message) authorizer = authorizer_for(message) user_method = worm_case(message.from) + _authorized?

Chapitre 18

Convention plutt que conguration

283

if authorizer.respond_to?(user_method) return authorizer.send(user_method, message) end authorizer.authorized?(message) end

Et voici maintenant la version nale de notre convention concernant le contrle daccs : nommez votre classe de contrle daccs <destination_host>Authorizer et placezla dans le dossier auth. Implmentez la politique gnrale pour lhte dans la mthode authorize. Si une politique spciale existe pour un utilisateur donn, implmentez-la dans la mthode <user>_authorized?.

Aider un utilisateur dans ses premiers pas


Il existe un autre moyen daider un ingnieur qui tend notre passerelle : on peut lui mettre le pied ltrier ses tout dbuts. Nous avons not plus tt dans ce chapitre que lun des principes de la conception efcace dinterfaces consiste fournir lutilisateur des modles et des exemples. Dune part, on pourrait leur proposer quelques exemples qui illustrent la cration dun adaptateur pour un protocole ou la cration dune classe de contrle daccs. Dautre part, on pourrait fournir un utilitaire pour gnrer un squelette de ce genre de classe. Pourquoi ne pas offrir nos utilisateurs un gnrateur dadaptateurs ? Voici un script Ruby qui prpare les bases dun adaptateur :
protocol_name = ARGV[0] class_name = protocol_name.capitalize + Adapter file_name = File.join(adapter, protocol_name + .rb) scaffolding = %Q{ class #{class_name} def send_message(message) # Le code denvoi dun message end end File.open(file_name, w) do |f| f.write(scaffolding) end

Si lon place ce code dans un chier nomm adapter_scaffold.rb, on peut lappeler laide de la ligne de commande suivante pour crer le squelette dun adaptateur FTP :
ruby adapter_scaffold.rb ftp

Nous nous retrouvons avec une classe nomme FtpAdapter dans un chier ftp.rb plac dans le dossier adapter.

284

Les patterns Ruby

On sous-estime souvent la valeur de ce type de gnrateurs. Pourtant, ces utilitaires sont prcieux pour les nouveaux utilisateurs souvent surchargs dinformation lorsquils abordent un environnement inconnu et qui peinent dmarrer.

Rcolter les bnces de la passerelle de messages


On pourrait continuer lextension de notre passerelle de messages en ajoutant une tape de transformation du format des messages ou en crant un outil daudit exible qui puisse garder un suivi de certains messages. Mais arrtons-nous et voyons ce que nous avons accompli. On a construit une passerelle de messages extensible sur deux points : lajout de nouveaux protocoles et la prise en compte de rgles de contrle daccs y compris des rgles spciques certains utilisateurs. Aucun chier de conguration nest ncessaire pour notre systme. Le programmeur qui souhaite tendre notre systme doit simplement crire la bonne classe et la placer dans le dossier appropri. Un effet de bord intressant et inattendu de lutilisation de ces conventions rside dans la simplication du code principal de la passerelle. Si lon avait opt pour lusage de chiers de conguration, il aurait fallu les rechercher, les lire et probablement corriger des erreurs avant de pouvoir paramtrer nos adaptateurs et nos classes de contrle daccs. Dans notre cas, rien ne retarde lusage de nos adaptateurs et de nos classes de contrle daccs.

User et abuser du pattern Convention plutt que conguration


Un des dangers des systmes fonds sur des conventions consiste dnir des conventions incompltes, ce qui limite la exibilit de votre systme. Par exemple, la transformation des noms dhte en noms de classes Ruby nest pas effectue avec normment de soin dans notre passerelle de messages. Le code dans ce chapitre fonctionne correctement avec des noms dhte simples tels que russolsen.com, qui est correctement converti en RussOlsenDotCom. Mais, si lon utilise dans notre systme actuel un nom comme icl-gis.com, le programme va tenter de trouver la classe portant le nom illgal Icl-gisDotComAuthorizer. Dhabitude, ce type de problme peut tre rsolu lgamment si lon autorise nos classes surcharger les conventions en cas de besoin. Dans notre exemple, on pourrait permettre aux classes de contrle daccs de surcharger la correspondance par dfaut entre le nom dhte et la classe et spcier les htes pour lesquels cette nouvelle rgle sapplique.

Chapitre 18

Convention plutt que conguration

285

Lorsquun systme sappuie fortement sur ce genre de conventions, son fonctionnement peut sembler magique aux nouveaux utilisateurs. Cest une autre source de soucis potentiels. Il est peut-tre pnible dcrire et de maintenir des chiers de conguration, mais ils fournissent une sorte de plan de limplmentation du systme un plan compliqu et difcile interprter, certes, mais un plan nanmoins. Inversement, un systme fond sur des conventions et bien conu doit prsenter ces dtails oprationnels sous forme de documentation ! Noubliez pas quau fur et mesure de lvolution de la magie des conventions vous aurez besoin de tests unitaires de plus en plus dtaills pour vous assurer que le comportement de vos conventions demeure bien... conventionnel. Il est extrmement droutant pour un utilisateur de travailler sur un systme pilot par des conventions incohrentes ou dfaillantes.

Convention plutt que conguration dans le monde rel


Rails reste le meilleur exemple dun systme fru de conventions. En pratique, notre passerelle de messages sest fortement inspire des conventions mises en uvre dans Rails. Llgance de Rails tient en grande partie son application cohrente des conventions. En voici quelques exemples :
m

Si votre application Rails est dploye sur http://russolsen.com, alors, la requte http://russolsen.com/employees/delete/1234 appelle par dfaut la mthode delete de la classe EmployeesController. La valeur 1234 est passe dans la mthode en paramtre. Les rsultats de cet appel au contrleur sont traits par la vue dnie dans le chier views/employees/delete. html.erb. Les applications Rails utilisent typiquement ActiveRecord pour communiquer avec la base de donnes. Par dfaut, la table nomme proposals (au pluriel) est gre par la classe Proposal (singulier), qui rside dans un chier nomm proposal.rb (en minuscules) dans le dossier models. Le champ comment de la table proposals devient comme par magie lattribut comment dun objet Proposal. Rails fournit tout un ventail de gnrateurs qui aident les utilisateurs crer leurs premiers modles, vues et contrleurs.

Une application Rails classique est littralement ptrie de conventions.

286

Les patterns Ruby

Mais Rails nest pas le seul exemple de lutilisation judicieuse des conventions dans le monde Ruby. RubyGems est un utilitaire de paquetage standard pour des applications Ruby. Il est relativement simple dutilisation, surtout lorsque lon suit ses conventions dorganisation des dossiers, comme nous lavons fait dans lexemple avec la passerelle de messages.

En conclusion
Dans ce chapitre, nous avons tudi le pattern Convention plutt que conguration. Ce pattern stipule quon peut parfois rendre son systme plus convivial si le code est conu autour de conventions fondes sur le nommage de classes, de chiers, de mthodes et lorganisation standardise des dossiers. Cette technique rend vos programmes facilement extensibles : pour tendre le systme il suft dajouter un chier, une classe ou une mthode correctement nomm. Le pattern Convention plutt que conguration tire parti des mmes qualits de dynamique et de exibilit de Ruby qui rendent possibles les deux autres patterns spciques Ruby dcrits dans ce livre. Tout comme le pattern Domain-Specic Language, Convention plutt que conguration sappuie principalement sur lvaluation de code au moment de lexcution. Tout comme le pattern Mta-programmation, pour bien fonctionner il requiert un niveau dintrospection relativement lev de la part des programmes. Ces trois patterns partagent une autre caractristique : leur faon daborder certains problmes de programmation. Leur philosophie commune prescrit quil ne faut pas simplement utiliser un langage de programmation, mais plutt le remanier en un outil plus adapt la rsolution de vos problmes.

Conclusion

Nous avons parcouru un chemin considrable dans cet ouvrage de nos dbuts avec le pattern Template Mthode jusquau chargement dynamique de classes en respectant des conventions. En cours de route, nous avons dcouvert que la nature dynamique et le typage la canard de Ruby modient la faon dapprocher de nombreux problmes de programmation. Lorsquil faut faire varier le comportement dun algorithme profondment encapsul dans une classe, on peut dvelopper un objet Strategy, mais on a galement la possibilit de passer simplement un bloc de code. Limplmentation de patterns comme Proxy et Decorator, qui sappuient fortement sur le principe de la dlgation, nest plus un exercice pnible dcriture de code comme avec dautres langages. Les fonctions dynamiques et rexives de Ruby nous permettent dimplmenter lide de fabrique de classes tout en sortant du cadre des limitations imposes par les patterns classiques Abstract Factory et Factory Method, fonds sur lhritage. Les adaptateurs ne prsentent plus de problme dans un langage qui permet dajuster linterface dun objet la vole. Des itrateurs externes sont toujours possibles et on peut en trouver dans le code Ruby, mais les itrateurs internes les remplacent avantageusement et ils sont, de fait, omniprsents. La technique des langages spciques dun domaine, dans sa variante DSL interne, permet de se servir de linterprteur Ruby en tant quanalyseur syntaxique lorsquon crit son propre interprteur. Tout ceci ne devrait pas vous surprendre. Bien que le livre Design Patterns du GoF reprsente un pas gant dans lart dcrire des programmes, plus de quinze annes se sont coules depuis sa parution. Ce ne serait pas trs atteur pour notre profession si tant dannes plus tard on cherchait encore rsoudre exactement les mmes problmes avec exactement les mmes techniques. De nombreux patterns originaux du GoF sont des solutions prennes qui nous accompagneront encore trs longtemps. Mais la programmation a une caractristique commune avec la littrature : la traduction de Romo et Juliette de langlais vers le franais change les phrases, les formulations et latmosphre gnrale de luvre. Juliette serait toujours jeune et belle, mais elle serait lgrement diffrente dans la version franaise. Un design pattern traduit vers un autre langage Ruby resterait le mme pattern, mais il serait diffrent.

288

Les design patterns en Ruby

Lorsque lon tudie les traductions en Ruby des motifs de conception rpertoris dans Design Patterns, on dcouvre que la plupart des diffrences rsultent de la exibilit quasiment illimite de ce langage. En Ruby, lorsque le comportement ou linterface dune classe ne vous semble pas adapte la tche, vous avez beaucoup doptions. Vous pouvez certainement encapsuler des instances de cette classe dans un adaptateur. Vous pouvez doter cette classe dun dcorateur ou dun proxy ou bien crer une fabrique pour crer des instances encapsules. Cette fabrique peut tre implmente comme un singleton. Si vous avez sous la main un objet complexe, probablement dvelopp par une autre quipe dans votre grande organisation, tous ces choix peuvent tre judicieux. Toutefois, lorsque vous grez un objet simple que vous connaissez bien, vous avez la possibilit de le modier directement et de lui apporter le comportement requis. Avec Ruby, nous ne sommes plus obligs de recourir des design patterns trs labors pour rsoudre des problmes locaux. Ruby nous fournit des outils pour faire simplement les choses simples. Sil est une chose qui na pas chang depuis la publication de Design Patterns, cest bien le besoin de lexprience collective. Bruce Tate aime rappeler 1 que, lorsquune nouvelle technique de programmation ou un langage apparat, on observe souvent un retard dans ladoption des bonnes pratiques dutilisation. Lindustrie a besoin de temps pour sapproprier la technique et laborer les meilleurs moyens de lexploiter. Pensez aux annes qui se sont coules entre la prise de conscience que la programmation oriente objet tait la voie suivre et le moment o lon a effectivement commenc appliquer cette technologie efcacement. Cet intervalle, cest le fameux "temps dexprience" ncessaire pour accumuler les bonnes techniques orientes objet. La reconnaissance de plus en plus large des avantages offerts par les langages dynamiques tels que Ruby nous plonge dans cette nouvelle phase dacquisition dexprience. Les fonctions puissantes de Ruby suggrent de nouvelles approches des problmes de programmation avec lesquels on lutte depuis des annes. Ruby nous fournit galement les moyens de raliser des choses qui taient impossibles ou trs difciles jusqualors. Mais que devons-nous faire ? Quels raccourcis peut-on prendre sans danger ? Quels piges faut-il viter ? Ruby met notre disposition toute cette puissance, mais nous avons besoin de conseils et dexprience pour laccompagner. Dans ce livre, jai tent dapporter quelques claircissements sur la faon de canaliser la puissance de Ruby. Au fur et mesure que nous traversons cette phase dexprimentation, de nouvelles solutions et de nouveaux patterns vont surgir qui sinscriront mieux dans le monde dynamique et exible de Ruby. Je ne sais pas quoi ressembleront ces patterns, mais jattends leur apparition avec impatience. Je sais aussi que cest une priode formidable pour tre un dveloppeur.
1. Voici un exemple de ces propos : http://weblogs.java.net/blog/batate/archive/2004/10/time_wisdom_ and.html.

Annexes

A
Installer Ruby
Lune des caractristiques dun langage populaire est quil nest pas difcile trouver. Le bon endroit pour commencer vos recherches se trouve sur la page daccueil du site Web ddi au langage Ruby, situe sur http://www.ruby-lang.org. La suite dpend du systme dexploitation que vous utilisez.

Installer Ruby sous Microsoft Windows


Si vous utilisez Microsoft Windows, optez pour linstalleur "en-un-clic" de Ruby, qui se trouve cette adresse : http://rubyforge.org/projects/rubyinstaller. Ce programme installe sur votre machine lenvironnement de base Ruby ainsi que tout un tas dutilitaires pratiques. La procdure ne demande que quelques clics de souris. Noubliez pas dactiver loption RubyGems pour installer le gestionnaire de paquetages logiciels de Ruby. Si vous tes plus orient UNIX, mais que vous utilisiez nanmoins un poste de travail Windows, jetez un il sur Cygwin (http://www.cygwin.com), un environnement la UNIX pour Windows, qui inclut Ruby dans sa distribution.

Installer Ruby sous Linux ou un autre systme de type UNIX


Si vous utilisez un systme de type UNIX tel que Linux, vous avez plusieurs options :
m

Installer une distribution toute faite. Il est trs probable quune distribution de Ruby existe pour votre systme. Noubliez pas dinstaller galement RubyGems pour rcuprer le gestionnaire de paquets Ruby. Si votre systme est sous Debian Linux ou un de ses drivs (ce qui inclut Ubuntu Linux, aujourdhui trs populaire), sachez que RubyGems nest pas disponible comme un paquetage Debian prconstruit

292

Annexes

cause de diffrences philosophiques sur la faon dempaqueter les logiciels. Dans ce cas, passez la construction de RubyGems partir du code source.
m

Construire Ruby partir du code source. Construire votre environnement Ruby partir du code source nest pas trs difcile. Il suft de tlcharger le logiciel et de suivre les instructions prsentes dans le chier README. Lorsque linstallation de Ruby est termine, rcuprez et construisez de la mme faon le code source de RubyGems.

Mac OS X
Bonne nouvelle : OS X Tiger est livr avec Ruby dj prinstall. La mauvaise nouvelle, cest quil sagit dune vieille version de Ruby. Beaucoup dutilisateurs de Ruby sous OS X probablement la majorit prfrent construire Ruby partir du code source (voir la section prcdente) ou bien rcuprer Ruby sur le site MacPorts (http://www.macports.org/). La version Leopard de Mac OS X qui vient de sortir a considrablement amlior le support de Rubyet il y a plus de bonnes nouvelles que de mauvaises.

B
Aller plus loin
Une littrature norme portant sur les design patterns est apparue au cours des quinze dernires annes, et la littrature sur Ruby augmente de jour en jour. Cette annexe indique certaines ressources qui peuvent tre utiles au programmeur qui sintresse la fois Ruby et aux design patterns.

Design patterns
videmment : Gamma E., Helm R., Johnson R. et Vlissides, J., Design Patterns: Elements of Reusable Object-Oriented Software, Reading, MA: Addison-Wesley, 1995. Japprcie toujours les uvres originales, et si vous tes intress par les design patterns il ny a rien de mieux que le Design Patterns du Gang of Four. Un choix, moins vident, mais qui vaut le dtour : Alpert S., Brown K. et Woolf B., The Design Patterns Smalltalk Companion, Reading, MA: Addison-Wesley, 1998. Mes collgues qui travaillent avec Smalltalk me rappellent sans cesse que ce langage de programmation possde tous les points positifs que nous dcouvrons aujourdhui dans Ruby. Et ces points existent depuis des dcennies. Le fait que Smalltalk nait pas gagn une large popularit est probablement d sa syntaxe trange plutt qu un manque de puissance ou dlgance. Mais ne parlons plus des diffrences des langages, The Design Patterns Smalltalk Companion vaut la peine dtre lu car cest une tude soigneuse de lapplication des design patterns dans un langage tout aussi dynamique et exible que Ruby.

294

Annexes

Pour un regard plus rcent sur les mmes thmes on se portera sur : Sweat J., php|architects Guide to PHP Design Patterns, Toronto, ON: Marco Tabini and Associates, 2005. Il existe normment de littrature concernant les design patterns dans des langages diffrents, notamment en Java. Voici deux livres intressants qui se concentrent sur Java : Freeman E., Freeman E., Bates et Sierra K., Head First Design Patterns, Sebastopol, CA: OReilly Media, 2004. Stelting S. et Maassen O., Applied Java Patterns, Palo Alto, CA: Sun Microsystems Press, 2002. Ces deux livres sont trs diffrents : Applied Java Patterns est un ouvrage trs dtaill et plus traditionnel, alors que Head First Design Patterns contient moins de dtails mais il est bien plus ludique.

Ruby
Le meilleur ouvrage dintroduction Ruby est celui crit par Dave Thomas, Chad Fowler et Andy Hunt : Thomas D., Fowler C. et Hunt A., Programming Ruby: The Pragmatic Programmers Guide, seconde dition, Raleigh, NC: The Pragmatic Bookshelf, 2005. Programming Ruby est une prsentation complte de Ruby, son environnement et ses bibliothques. Nanmoins, je dois admettre que lorsque je cherche une analyse vraiment profonde des questions lies Ruby jai tendance ouvrir le livre suivant : Black D., Ruby for Rails, Greenwich, CT: Manning Publications, 2006. Soyez conscient que, malgr son nom, Ruby for Rails se concentre 85 % sur Ruby et seulement 15 % sur Rails. Ruby est en grande partie un langage idiomatique dans lequel il existe une vritable faon de sexprimer en langage Ruby. Dans ce livre, jai essay dindiquer la faon daborder des problmes selon la philosophie Ruby. Si vous souhaitez mieux comprendre cette philosophie, lisez : Fulton H., The Ruby Way, seconde dition, Boston: Addison-Wesley, 2006.

Annexe B

Aller plus loin

295

The Ruby Way est en partie un rpertoire de solutions techniques et en partie un texte dintroduction. Si vous voulez savoir quelles techniques appliquer pour accomplir certaines tches en Ruby et si vous souhaitez avoir un peu dinformation sur le pourquoi de ces techniques, ce livre est fait pour vous. Un ouvrage du mme genre : Carlson L. et Richardson L., Ruby Cookbook, Sebastopol, CA: OReilly Media, 2006. Malgr mon penchant pour les livres, je reconnais que lorsquon apprend un nouveau langage de programmation il existe une ressource tout aussi importante que la littrature : des bons programmes crits dans ce langage. Si vous souhaitez srieusement apprendre Ruby, vous devez passer du temps tudier les codes source suivants :
m

La bibliothque standard de Ruby. Cest la totalit du code livr avec votre distribution de Ruby. Vous tes curieux propos de la classe Complex ? Vous avez des questions sur Webrick ? Vous voulez tout savoir sur URI ? Il suft de jeter un il, car tout ce code est sur votre disque dur. Ruby on Rails. Nhsitez pas tudier le code source de lapplication vedette. Toutefois, la plus grande partie de Rails est crite en Ruby trs avanc. Ne soyez pas intimid mais attendez-vous vous poser frquemment la question : "Comment estce que ce truc fonctionne ?" Le site Web de Rails se trouve http://www.rubyonrails.org. Ruby Facets est une collection norme dutilitaires Ruby. Ces utilitaires sont en ralit des extensions des classes standard de Ruby, ce qui prsente un intrt particulier pour les dbutants en Ruby. Cette ressource est bien intressante et trs utile. Le site Web de Facets se trouve ladresse http://facets.rubyforge.org.

Expressions rgulires
Les expressions rgulires ont t mentionnes plusieurs fois dans ce livre. Si vous navez pas encore trouv le temps dapprendre cet outil terriblement pratique, je vous recommande de le faire en priorit. Commencez par : Friedl J., Mastering Regular Expressions, Sebastopol, CA: OReilly Media, 2006.

296

Annexes

Blogs et sites Web


Le site Web principal de Ruby est http://www.ruby-lang.org. La plupart des gens qui sintressent Ruby seront aussi curieux de Rails, qui se trouve ladresse http:// www.rubyonrails.org. Le portail francophone de rfrence, Railsfrance, se trouve quant lui ladresse http://www.railsfrance.org. Il existe un certain nombre de bons blogs sur Ruby et Rails. En voici quelques-uns que je trouve particulirement utiles et intressants :
m m m

Jamis Bucks blog http://weblog.jamisbuck.org. Jay Fieldss blog http://blog.jayelds.com. Ruby Inside http://www.rubyinside.com (o Peter Cooper nous livre un condens de sa grande sagesse).

Le site Web associ ce livre est http://designpatternsinruby.com. Enn, si vous voulez avoir de mes nouvelles, visitez http://www.russolsen.com ou envoyez-moi un message russ@russolsen.com.

propos des traducteurs


Laurent Julliard est actuellement directeur associ de la socit Nuxos, SSLL spcialise dans le conseil, le dveloppement et la formation Ruby & Rails. Prcdemment chef de projet et architecte logiciel dans les divisions R&D de grands groupes comme HP et Xerox, il est aussi pionnier de lOpen Source en France, fondateur du premier groupe dutilisateur Linux en France (GUILDE) ds 1995 et membre actif de la communaut Ruby depuis dbut 2000. Laurent Julliard a particip plusieurs projets Ruby dimportance (dont lenvironnment de dveloppment intgr FreeRIDE). En coopration avec Richard Piacentini, il est aussi le traducteur douvrages de rfrence et lauteur de plusieurs articles sur Ruby et Rails. Il intervient rgulirement en tant que confrencier sur Ruby et Rails et notamment lors de la confrence annuelle "Paris on Rails" dont il est co-organisateur. Mikhail Kachakhidze est traducteur technique multilangues depuis 1996. Il compte son actif de nombreux projets de localisation de logiciels, sites web et documentation. Il a contribu entre autres aux versions russes dOracle 8i, Windows XP, MS SQL Server 2005. En 2004, Mikhail Kachakhidze passe du ct obscur de la force et devient dveloppeur. Aprs quelques expriences en Java, il adopte Ruby et Rails. Aujourdhui, il fait partie de lquipe de dveloppement de la socit Eyeka. Richard Piacentini ctoie les technologies du libre depuis le dbut de sa carrire dans des domaines allant de la gestion dinformation ux tendu (TF1/LCI, Tempost-La Poste) la modlisation comportementale (eGoPrism) en passant par les communauts de pratiques (Alphanim, Libration) ou la gestion de systmes de production infographique en rseaux (INA, France Animation). Initiateur et organisateur, avec Laurent Julliard, de la confrence "Paris on Rails", il est le fondateur de Nuxos Group, SSLL spcialiste de Ruby et Rails, ainsi que le crateur du portail Railsfrance. Il a traduit plusieurs ouvrages de rfrence et dispense rgulirement des formations en Europe et en Afrique du Nord. Richard Piacentini est un fervent adepte des bonnes pratiques du dveloppement logiciel et il traumatise rgulirement les dveloppeurs et stagiaires qui lentourent en les persuadant dcrire du code efcace et lgant ainsi que dinnombrables tests unitaires et fonctionnels.

Index

Symboles !, oprateur 28 !~, oprateur 38 &, oprateur 28 &&, oprateur 28 +, oprateur 32 <, oprateur 27 <<, oprateur 36 <=, oprateur 27 <=>, oprateur 82 ==, oprateur 27 =~, oprateur 37 >, oprateur 27 >=, oprateur 27 |, oprateur 28 ||, oprateur 28 A abs, mthode 26, 144 Abstract Factory 16 Abstract Factory, pattern 196, 205 lots dobjets 205 nommage 208 Accesseur 40 attr_accessor 41 attr_reader 42 pattern Mta-programmation 266 ActiveRecord mthodes magiques 222 migration 134 pattern Adapter 148

pattern Command 134 pattern DSL 255 pattern Factory Method 210 pattern Mta-programmation 268 pattern Observer 94 relation 268 ActiveSupport pattern Decorator 177 pattern Singleton 193 Adapter, pattern 15, 139 ActiveRecord 148 adapter ou modier 147 alternatives 144 chier 276 FTP 283 HTTP 276 modier une instance unique 145 slectionner 277 SMTP 276 Addition 24 alias, mot cl 175 alias_method_chain, mthode 177 all?, mthode 116 and, oprateur 28 any?, mthode 116 Arbre de syntaxe abstrait (AST) pour analyseurs syntaxiques complexe 238 dvelopper 226 interprteur de recherche de chiers 229 interprteur sans analyseur 236 simple 234 XML et YAML 237

300

Les design patterns en Ruby

Argument 43 astrisque 44 nombre arbitraire 44 valeurs par dfaut 44 Array, classe 35 AST Voir Arbre de syntaxe abstrait attr_accessor 41 attr_reader 42 attr_writer 42 B Base, classe 210 begin, instruction 47 Bignum, classe 24 Bloc Voir Proc, classe Boucle 30 break 31 each 31 for 31 next 31 until 30 while 30 break, oprateur 31 Builder, pattern 16, 215 director 216 MailFactory 222 mthodes magiques 220 polymorphisme 216 produit 216 validation 219 C call, mthode 75 Chane de caractres 32 + 32 plusieurs lignes 33 concatnation 32 downcase 32 each_byte, scan 119 immuable 34 interpolation 33 length 32

modiable 34 String 119 upcase 32 class, mthode 26 Classe 38 dnition 38 Command, pattern 15, 125 ActiveRecord 134 annulation, rollback 130 bloc 126 les de commandes 132 FXRuby 134 Madeleine 135 Comparaison 28 < 27 <= 27 <=> 82 == 27 > 27 >= 27 Composite, pattern 15, 97, 100 AST 227 composant 100 composite 99, 101, 105 cration 100 feuille 100, 105 FXRuby 108 oprateurs 104 pointeurs 106 tableau 104 Composition 7 Console interactive irb 20 Constante 24 Constructeur 39 Convention plutt que conguration, pattern 17, 271 chargement de classes 278 exemple 275 historique 272 pattern Adapter 277 principes 273 Rails 285 RubyGems 286

Index

301

D Decorator, pattern 16, 167 ActiveSupport 177 dlgation 173 forwardable 174 hritage 169 mthodes dencapsulation 174 module 175 def, mot cl 39 Dlgation 11, 69 Design pattern 14, 293 Distributed Ruby (drb), Proxy 164 Division 24 Domain-Specic Language (DSL) Voir DSL, pattern downcase, oprateur 32 DSL, pattern 17, 245 ActiveRecord 255 analyseur syntaxique 246 avantages 251 externe 246 interne 246 interprteur 246 rake 254 Duck typing 61 E each, mthode 119 each, oprateur 31 each_byte, mthode 119, 120 each_entry, mthode 121 each_lename, mthode 120 each_key, mthode 119 each_line, mthode 120 each_object, mthode 121 each_value, mthode 119 else, oprateur 29 elsif, oprateur 29 end, oprateur 29, 30 Entier 24

Enumerable, module 116 Etc, module 154 Exception 47 begin, rescue 47 NoMethodError 159 raise 48 Expression rgulire 37, 295 extend, mthode. 175 F Factory Method, pattern 16, 196, 199 ActiveRecord 210 paramtres 201 false 28 FalseClass, classe 28 Fichier source 49 chargement 49 require 49 File.basename, mthode 231 Fixnum, classe 24, 144 oat 25 for, oprateur 31 Forwardable, module 267 forwardable, module 174 FXRuby 108, 134 pattern Command 134 pattern Composite 108 G Gang of Four 4 GenericServer, classe 67 H Hash, classe 119 Hello World 20 Hritage 7, 42 pattern Decorator 169 super 43 Template Methode 69

302

Les design patterns en Ruby

I if, oprateur 29 include, instruction 45 Inections, classe 194 initialize, mthode 39 Installer Ruby Linux, Unix 291 Mac OS X 292 Microsoft Windows 291 Interface 6 Interpolation 33 Interpreter, pattern 16, 226 analyseur syntaxique 226 AST 226 contexte 228 noeuds non terminaux 227 noeuds terminaux 227 Runt 240 IO, classe 120, 147 each_byte 120 each_line 120 pattern Itrateur 120 irb 20 Itrateur, pattern 15, 111 Enumerable 116 externe 112 interne 113 IO 120 Java 112 ObjectSpace 121 tableau 119 J join, mthode 49 L lambda, mthode 75 Langage spcique dun domaine Voir DSL, pattern length, mthode 32

M Madeleine, pattern Command 135 MailFactory 222 Mta-design patterns 5 Mta-programmation, pattern 17, 257 ActiveRecord 268 attr_accessor 266 attr_reader 266 attr_writer 266 Forwardable 267 pattern Composite 261 pattern DSL 269 public_method 265 rexion 264 respond_to? 265 method_missing, mthode 159, 162 Mixin 47 Module 45 dnition 45 Enumerable 116 Etc 154 extend 175 Forwardable 267 forwardable 174 include 45 mixin 47 Module 267 ObjectSpace 121 pattern Mta-programmation 259 Singleton 184, 189 Module, module 267 Multiplication 24 Mutateur 40 attr_accessor 41 attr_writer 42 pattern Mta-programmation 266 N new, mthode 39 next, oprateur 31

Index

303

nil 27 nil?, mthode 26 NilClass, classe 27 Nombre virgule ottante 25 not, oprateur 28 O Object, classe 26 ObjectSpace, each_object 121 ObjectSpace, module 121 Objet sur mesure mthode 258 module 259 pattern Mta-programmation 258 Observable, module 90 Observer, pattern 15, 87 bloc 91 couplage 86 Java 88 Observable 90 observateur 87 pull 92 push 92 sujet 87 Oprateurs boolens 27 Oprations arithmtiques 24 or, oprateur 28 P Pathname, classe 120 each_entry 121 each_lename 120 pattern 16 Philosophie Ruby 22 Prevayler 135 private, mot cl 188 Proc, classe 75, 122 accolades 76 call 75

convention 76 cration 75 do/end 76 lambda 75 paramtres 76 pattern Commande 126 pattern Itrateur 122 yield 77 Proxy, pattern 15, 151 contrle daccs 153 Distributed Ruby 164 method_missing 159 proxy distant 155 proxy virtuel 156 sujet 152 transfert des messages 158 public_methods, mthode 265 R Racc 238 Rails ObjectSpace 122 pattern Convention plutt que conguration 271, 285 raise, instruction 48 rake pattern DSL 254 pattern Singleton 194 rdoc RIGenerator 81 Strategy 80 require, instruction 49 rescue, instruction 47 respond_to?, mthode 265 reverse, mthode 36 reverse!, mthode 36 reverse_each, mthode 119 REXML,Observer 94 round, mthode 26 RPC 155

304

Les design patterns en Ruby

Ruby 16, 294 RubyGems 50, 291 gem 279 pattern Convention plutt que conguration 286 run, mthode 67 Runt, Interpreter 240 S scan, mthode 119 Scurit de type 61 self, variable 42, 181 send, mthode 160 Singleton, module 184 Singleton, pattern 179 ActiveSupport 193 classes 187 instanciation immdiate 185 instanciation tardive 185 Java 179 module 184 modules en tant que singletons 188 test 193 variables globales 186 size, oprateur 35 SOAP 155, 209 sort, mthode 36 sort!, mthode 36 Soustraction 24 Strategy, pattern 15, 71 composition, dlgation 72 contexte 71 rdoc 80 vs. Template Method 82 String, classe Voir Chanes de caractres succ, mthode 26 super, mthode 43 super(), mthode 90 Symbole 34

T Tableau 35 Array 35 each 119 length 35 pattern Composite 104 pattern Itrateur 119 reverse 36 reverse! 36 reverse_each 119 size 35 sort 36 sort! 36 trie 81 Tableau associatif 36 cration 37 each_key 119 each_value 119 Hash 119 initialisation 37 symbole 37 Template Method, pattern 15, 58, 199 dfauts, hritage 69 hook 60 mthodes daccrochage 59 WEBrick 67 Temps dexprience 288 Test 64 JUnit, NUnit, XUnit 64 pattern Singleton 193 setup, teardown, assert_equal, assert_not_ nil 65 Thread cration, excution 48 join 49 Thread, classe 48 Tk 134 to_s, mthode 26, 163 true 28 TrueClass, classe 28 truncate, mthode 26 Typage la canard 61

Index

305

Typage dynamique 61, 147 Strategy 74 vs. typage statique 63 Type primitif 26 U unless, oprateur 30 until, oprateur 30 upcase, oprateur 32 V Variable 22 $ 185 @ 39 accesseur 40 affectation 23 constante 24 dinstance 39 de classe 180 dclaration 23 globale 185 mutateur 40

nommage 23 ordinaire 22 self 181 Vous naurez pas besoin de a 12, 209 W WEBrick GenericServer 67 Template Method 67 while, oprateur 30 X XML 237 XMLRPC 209 XOR 140 Y YAGNI Voir Vous naurez pas besoin de a YAML 237 yield, mot cl 77

Les design patterns en Ruby


Abordez les design patterns sous langle Ruby !
La plupart des livres consacrs aux design patterns sont bass sur C++ et Java. Mais le langage Ruby est diffrent et les qualits uniques de ce langage rendent limplmentation et lutilisation des patterns plus simples. Russ Olsen dmontre dans ce livre comment combiner la puissance et llgance des design patterns pour produire des logiciels plus sophistiqus et efcaces avec beaucoup moins de lignes de code. Il passe en revue du point de vue Ruby quatorze des vingt - trois patterns classiques du livre de rfrence produit par le fameux Gang of Four (problmes rsolus par ces patterns, analyse des implmentations traditionnelles, compatibilit avec lenvironnement Ruby et amliorations spciques apportes par ce langage). Et vous apprendrez comment implmenter des patterns en une ou deux lignes de code l o dinterminables lignes de code sans intrt sont ncessaires avec dautres langages plus conventionnels. Vous y dcouvrirez galement de nouveaux patterns labors par la communaut Ruby, en particulier la mtaprogrammation qui permet de crer des objets sur mesure ou le trs ambitieux pattern Convention plutt que conguration popularis par Rails, le clbre framework de dveloppement dapplications web crit en Ruby. Passionnant, pratique et accessible, le livre Les design patterns en Ruby vous aidera dvelopper des logiciels de meilleure qualit tout en rendant votre exprience de la programmation en Ruby bien plus gratiante. Niveau : Intermdiaire / Avanc Programmation Conguration : Multiplate-forme
TABLE DES MATIRES Amliorer vos programmes avec les patterns Dmarrer avec Ruby Varier un algorithme avec le pattern Template Method Remplacer un algorithme avec le pattern Strategy Rester inform avec le pattern Observer Assembler le tout partir des composants avec Composite Accder une collection avec l'Itrateur Effectuer des actions avec Command Combler le foss avec l'Adapter Crer un intermdiaire pour votre objet avec Proxy Amliorer vos objets avec Decorator Crer un objet unique avec Singleton Choisir la bonne classe avec Factory Simplier la cration d'objets avec Builder Assembler votre systme avec Interpreter Ouvrir votre systme avec des langages spciques d'un domaine Crer des objets personnaliss par mta-programmation Convention plutt que conguration

Rfrence

propos de lauteur
Russ Olsen est dveloppeur de logiciels depuis plus de vingt-cinq ans. Il a gr des projets de dveloppement travers plusieurs gnrations de technologies de programmation : de FORTRAN Ruby en passant par C, C++ et Java. Il utilise et enseigne Ruby depuis 2002 et est lauteur dun blog technique trs lu, Technology As If People Mattered (www.russolsen.com).

Image de couverture iStockphoto 4573371

Pearson Education France 47 bis, rue des Vinaigriers 75010 Paris Tl. : 01 72 74 90 00 Fax : 01 42 05 22 17 www.pearson.fr

ISBN : 978-2-7440-4018-4

Vous aimerez peut-être aussi