Patron Visiteur Gestion de clients Denis Conan et Christian Bac Revision : 1985 CSC4102 Télécom SudParis Mardi 25 janvier 2011
Avertissement : cet exemple de spécification, de conception et mise en œuvre de patron de conception correspond à un sujet donné en contrôle final, première session, du module CSC4002 il y a quelques années. Par conséquent, le programme ayant changé depuis, il y manque des éléments, comme la préparation des tests d intégration. Par ailleurs, la réalisation est très incomplète. Enfin, notez que le code de cet exemple est disponible dans un module Maven associé. 1 Cahier des charges Nous proposons l analyse d un cas d étude que nous présentons ci-après et qui démontre l utilisation d un patron de conception : le patron de conception «Visiteur». Étude de cas. Le patron de conception «Visiteur» propose une manière de séparer lors de la conception et de la programmation d un côté la gestion de données organisées dans des collections imbriquées et de l autre côté les algorithmes qui s appliquent sur les données référencées dans ces collections. Dans notre cas d étude dont le diagramme de classes est présenté dans la figure 1, les classes CustomerGroup, Customer, Order et Item sont organisées dans une hiérarchie de compositions. Nous souhaitons tout d abord afficher les informations d un groupe de clients (classes CustomerGroup et Customer) passant des commandes (classe Order) comprenant plusieurs articles (Item). Le traitement spécifique effectué dans cette première visite est spécifié dans la classe ContentVisitor. Ensuite, nous souhaitons parcourir de la même manière les collections imbriquées pour calculer le chiffre d affaire total, ce chiffre d affaire étant calculé à partir du prix de chaque article (attribut price de la classe Item) et des remises appliquées selon le client (attribut reductionrate de la classe Customer). Cette seconde visite est spécifiée dans la classe BusinessVisitor. Le fonctionnement du patron de conception «Visiteur» est le suivant. Le patron de conception «Visiteur» appelle la méthode accept sur l objet racine des collections imbriquées. La méthode accept de l objet racine appelle une méthode de même nom (accept) sur les références des objets de la collection qu il gère. La méthode accept des objets de cette collection appelle à son tour une méthode accept sur les objets des collections qu ils possèdent, etc. Ainsi, toutes les collections sont parcourues pour construire un graphe d appels de méthodes accept. C est la première fonctionnalité du patron de conception «Visiteur» : parcourir les éléments des collections imbriquées. La seconde fonctionnalité est la suivante. Lorsqu un nœud de l arbre est visité, en plus des appels accept sur les objets enfants accessibles à partir de ce nœud, la méthode accept effectue un traitement local. Ce traitement est effectué en appelant une méthode visit d une classe appelée «visiteur» qui réalise les traitements. Par conséquent, la classe jouant le rôle d un visiteur contient des méthodes visit, une méthode par type d objets visités. Présentation de la modélisation disponible pour l implantation. Voici quelques détails sur le diagramme de classes : interface Visitor : déclare les méthodes appelables lors des visites (des parcours d une collection), une méthode pour chaque type d objets de collection visités ; classe ContentReport et BusinessReport : exemples de visiteurs des collections. Chaque méthode visit définit ce qu il faut faire lorsqu un objet est parcouru lors de la visite. Par exemple, en ce qui concerne la classe ContentReport, le type et le nom de l objet sont affichés à l écran puis un compteur par classe d objets est incrémenté (customernb, ordersnb et itemsnb pour respectivement Customer, Order et Item). La dernière méthode displayresults affiche les compteurs. Cette méthode est généralement appelée après les visites. Le contenu de cette classe est donné dans la figure 2. interface Visitable : déclare la méthode accept utilisée lors des visites. Dans les collections feuilles de l arborescence (par exemple, la classe Item), cette méthode appelle la méthode visit du visiteur. Dans les autres collections (par exemple, la classe Order), elle parcourt en plus les «sous-collections» et appelle la méthode accept sur chacun des objets des sous-collections ; classes CustomerGroup, Customer, Order et Item : exemples de classes «visitables» dont la méthode accept est appelée lorsqu un objet de la classe est visité lors du parcours de la collection dans laquelle il est inséré. Nous donnons le corps de la méthode accept ci-dessous pour la classe Customer. Remarquez que la référence sur le visiteur est un argument de la méthode. Cela permet d appeler la méthode visit correspondante du visiteur pour le traitement à effectuer lors de la visite, par exemple de mettre à jour les statistiques dans le visiteur ContentReport. Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 2
public void accept(visitor visitor) { visitor.visit(this); for (Iterator<Order> it = orders.iterator(); it.hasnext();) { ((Visitable) it.next()).accept(visitor); classe CustomerGroup : contient la racine de l arborescence des collections. Dans notre exemple, ce sont tous les objets de classe Customer. La classe CustomerGroup contient aussi une méthode accept dont le corps de la méthode est le suivant : public void accept(visitor visitor) { for (Iterator<Customer> it = customers.iterator(); it.hasnext();) { ((Customer) it.next()).accept(visitor); Enfin, la figure 3 présente le diagramme de communications d une méthode main d une classe Main montrant un cas d utilisation du système. Ce diagramme de communications n est pas complet : le message «21» donne lieu a des traitements imbriqués. La figure 4 présente le début du traitement de ce message. Ce n est que le début dans le sens où la figure présente la visite jusqu au premier objet feuille (de l objet cg:customergroup à l objet ia:item en passant par les objets c2:customer et o2:order), sans la visite des autres objets de types Customer, Order et Item. Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 3
visitor BusinessReport totalamount:double customervisited:customer +visit(customer):void +visit(order):void +visit(item):void +gettotalamount():double <<interface>> Visitor +visit(customer):void +visit(order):void +visit(item):void ContentReport customersnb:int ordersnb:int itemsnb:int +visit(customer):void +visit(order):void +visit(item):void +displayresults():void CustomerGroup +constructeur() +addcustomer(customer):void +accept(visitor):void <<interface>> Visitable +accept(visitor):void Item name:string price:double +constructeur(string,double) +getname():string +getprice():double +accept(visitor):void * * Customer Order name:string discountrate:double +constructeur(string,double) +getname():string +getdiscountrate():double +addorder(order):void +accept(visitor):void * name:string +constructeur(string) +constructeur(string,string,double) +getname():string +additem(item):void +accept(visitor):void Figure 1 Diagramme de classes Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 4
p u b l i c c l a s s ContentReport implements V i s i t o r { p r i v a t e i n t customersno ; p r i v a t e i n t ordersno ; p r i v a t e i n t itemsno ; p u b l i c void v i s i t ( Customer customer ) { System. out. p r i n t l n ( customer. getname ( ) ) ; customersno++; p u b l i c void v i s i t ( Order order ) { System. out. p r i n t l n ( order. getname ( ) ) ; ordersno++; p u b l i c void v i s i t ( Item item ) { System. out. p r i n t l n ( item. getname ( ) ) ; itemsno++; p u b l i c void d i s p l a y R e s u l t s ( ) { System. out. p r i n t l n ( "Nb o f customers : " + customersno ) ; System. out. p r i n t l n ( "Nb o f o r d e r s : " + ordersno ) ; System. out. p r i n t l n ( "Nb o f i t e m s s : " + itemsno ) ; Figure 2 Code Java de la classe ContentReport Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 5
20 create() 22 displayresults() 17 create() 18 addcustomer(c1) 19 addcustomer(c2) 21 cr:contentreport cg:customergroup 15 create("customer2",0.2) 16 addorder(o4) 11 create("order4") 12 additem(ia) 13 additem(ib) 14 additem(ic) c2:customer o4:order 10 create("itema",4) ia:item 9 create("itemb",5) ib:item 8 create("itemc",6) ic:item Main 5 addorder(o1) 6 addorder(o2) 7 addorder(o3) 1 create("customer1",0.1) c1:customer 2 create("order1","item1",1) o1:order 2.1 create("item1",1) i1:item 3 create("order2","item2",2) o2:order 3.1 create("item2",2) i2:item 4 create("order3","item3",3) o3:order 4.1 create("item3",3) i3:item Figure 3 Diagramme de communications de la méthode main de la classe Main Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 6
cr:contentreport 21.1.2.2.1 visit(ia) 21.1.2.1 visit(o4) 21.1.1 visit(c2) o4:order ia:item 21 cg:customergroup c2:customer 21.1.2 21.1 21.1.1.1 getname() 21.1.2.2 21.1.2.1.1 getname() 21.1.2.2.1.1 getname() ib:item ic:item 21.1.2.3 21.1.2.4 c1:customer 21.2 o1:order i1:item o2:order i2:item o3:order i3:item Figure 4 Début de la fin du diagramme de communications de la figure 3 à partir du message «21». Ce n est que le début du traitement car la figure présente la visite jusqu au premier objet feuille (de l objet cg:customergroup à l objet ia:item en passant par les objets c2:customer et o2:order) sans la visite des autres objets de types Customer, Order et Item. Les flèches en pointillées montre la suite des interactions à ajouter pour terminer le diagramme. C est clairement une entorse à la notation UML, mais nous désirons uniquement montrer le principe de la visite et souhaitons que le diagramme ne soit pas trop chargé. Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 7
2 Programmation Voici le code de l interface Visitable. p u b l i c i n t e r f a c e V i s i t a b l e { p u b l i c void accept ( V i s i t o r v i s i t o r ) ; Voici le code de la classe Order. import java. s q l. R esultset ; import java. s q l. SQLException ; import java. s q l. Statement ; import java. u t i l. I t e r a t o r ; import java. u t i l. Vector ; p u b l i c c l a s s Order implements V i s i t a b l e { p r i v a t e S t r i n g name ; p r i v a t e Vector<Item> items = new Vector<Item >(); p u b l i c Order ( S t r i n g n ) { name = n ; items = new Vector<Item >(); p u b l i c Order ( S t r i n g n, S t r i n g itemname, double itemprice ) { name = n ; items = new Vector<Item >(); additem ( new Item ( itemname, itemprice ) ) ; p u b l i c S t r i n g getname ( ) { r e t u r n name ; p u b l i c void accept ( V i s i t o r v i s i t o r ) { v i s i t o r. v i s i t ( t h i s ) ; f o r ( I t e r a t o r <Item> i t = items. i t e r a t o r ( ) ; i t. hasnext ( ) ; ) { ( ( Item ) i t. next ( ) ). accept ( v i s i t o r ) ; p u b l i c void additem ( Item item ) { items. add ( item ) ; // code pour l a reponse a l a q u e s t i o n 9 p u b l i c void initfromdb ( Statement stmt ) throws OperationImpossible { S t r i n g query = " s e l e c t from order where name = " + name + " " ; t r y { ResultSet r s e t = stmt. executequery ( query ) ; i n t order_idx ; i f ( r s e t. f i r s t ( ) ) { order_idx = r s e t. g e t I n t ( " idx " ) ; e l s e { throw new OperationImpossible ( " Order not found i n DB " + query ) query = " s e l e c t from item where order_id = " + order_idx + " " ; r s e t = stmt. executequery ( query ) ; while ( r s e t. next ( ) ) { S t r i n g n = r s e t. g e t S t r i n g ( " name " ) ; double p = r s e t. getdouble ( " p r i c e " ) ; additem ( new Item (n, p ) ) ; catch ( SQLException se ) { throw new OperationImpossible ( " Echec a c c e s Items " ) ; Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 8
Voici la méthode main de la classe Main, y compris son prototype. package eu. t e l e c o m s u d p a r i s. csc4102. d e s i g n p a t t e r n. v i s i t o r ; import eu. t e l e c o m s u d p a r i s. csc4102. d e s i g n p a t t e r n. v i s i t o r. g e s t i o n d e c l i e n t s. ContentReport ; import eu. t e l e c o m s u d p a r i s. csc4102. d e s i g n p a t t e r n. v i s i t o r. g e s t i o n d e c l i e n t s. Customer ; import eu. t e l e c o m s u d p a r i s. csc4102. d e s i g n p a t t e r n. v i s i t o r. g e s t i o n d e c l i e n t s. CustomerGroup ; import eu. t e l e c o m s u d p a r i s. csc4102. d e s i g n p a t t e r n. v i s i t o r. g e s t i o n d e c l i e n t s. Item ; import eu. t e l e c o m s u d p a r i s. csc4102. d e s i g n p a t t e r n. v i s i t o r. g e s t i o n d e c l i e n t s. Order ; p u b l i c c l a s s Main { p u b l i c s t a t i c void main ( S t r i n g [ ] a r g s ) { Customer c = new Customer ( " customer1 ", 0. 1 ) ; c. addorder ( new Order ( " order1 ", " item1 ", 1 ) ) ; c. addorder ( new Order ( " order2 ", " item2 ", 2 ) ) ; c. addorder ( new Order ( " order3 ", " item3 ", 3 ) ) ; Customer c2 = new Customer ( " customer2 ", 0. 2 ) ; Order o = new Order ( " order4 " ) ; o. additem ( new Item ( " itema ", 4 ) ) ; o. additem ( new Item ( " itemb ", 5 ) ) ; o. additem ( new Item ( " itemc ", 6 ) ) ; c2. addorder ( o ) ; CustomerGroup customers = new CustomerGroup ( ) ; customers. addcustomer ( c ) ; customers. addcustomer ( c2 ) ; ContentReport v i s i t o r = new ContentReport ( ) ; customers. accept ( v i s i t o r ) ; v i s i t o r. d i s p l a y R e s u l t s ( ) ; Nous écrivons maintenant le constructeur de la classe Item pour qu il lève une exception lorsque l un des attributs n est pas renseigné. Nous supposons que nous avons à notre disposition la classe d exception OperationImpossible (telle qu elle est implantée dans le cas d étude «Médiathèque»). p u b l i c c l a s s ItemWithException { p r i v a t e S t r i n g name ; p r i v a t e double p r i c e ; p u b l i c ItemWithException ( S t r i n g n, double p ) throws OperationImpossible { i f ( ( name == n u l l ) ( p r i c e == 0 ) ) { throw new OperationImpossible ( "Un des arguments n e s t pas r e n s e i g n e " ) ; name = n ; p r i c e = p ; Nous écrivons maintenant un test permettant de tester que le constructeur de la classe Item lève une exception lorsque l un des attributs n est pas renseigné. import org. j u n i t. Test ; p u b l i c c l a s s JUnit_ItemWithExceptionTest { @Test ( expected=operationimpossible. c l a s s ) p u b l i c void testnotnullname ( ) throws OperationImpossible { new ItemWithException ( n u ll, 1. 0 ) ; Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 9
Le second visiteur, c est-à-dire la classe BusinessReport, sert à calculer le chiffre d affaire total selon la formule qui suit. Nous écrivons tout le code Java de la classe BusinessReport. ( ( )) totalamount = (i.getp rice() (1 c.getdiscountrate())) c Customer o Order i Item p u b l i c c l a s s BusinessReport implements V i s i t o r { p r i v a t e double totalamount ; p r i v a t e Customer c u s t o m e r V i s i ted ; p u b l i c BusinessReport ( ) { totalamount = 0 ; c u s t o m e r V i s i t e d = n u l l ; p u b l i c void v i s i t ( Customer customer ) { c u s t o m e r V i s i t e d = customer ; p u b l i c void v i s i t ( Order order ) { p u b l i c void v i s i t ( Item item ) { totalamount += item. g e t P r i c e ( ) (1 customervisited. getdiscountrate ( ) ) ; p u b l i c double gettotalamount ( ) { r e t u r n totalamount ; Télécom SudParis Denis Conan et Christian Bac Mardi 25 janvier 2011 CSC4102 10