Une remarque pertinente ?
Une critique impertinente ?
Un lynchage en règle ?
Une invitation sous les tropiques ?

Ecrivez-moi !
Conçu et enseigné tel qu'en lui même, avec pertes, fracas et humour de qualité supérieure            
 par Christophe Darmangeat dans le M2 PISE du Master MECI (Université Paris 7)            

 
 
 
  
 
 
 
Partie 16
Plus loin avec les objets
 
1. Retour sur l'analyse orientée objet

1.1 Quelques généralités
Jusqu'à maintenant, même si cela malmène notre amour-propre, il faut bien l'avouer : nous n'avons été que des petits joueurs. Nous nous sommes certes dépatouillés de bien des situations difficiles avec VB.Net, à commencer par la première d'entre elles : se repérer dans toutes ces notions de la programmation objet que sont les classes, les propriétés, les méthodes et les événements. Mais malgré les heures d'intense émotion que nous ont valu ces épreuves, il faut bien le dire, nous sommes restés au seuil de la véritable et ultime révélation.
En effet, jusqu'à présent, nous n'avons fait que manipuler des classes qui avaient été déjà créées par les développeurs de VB, ou à la grande rigueur par d'autres (celles que nous sommes allés pêcher sur le net). Ce n'est déjà pas si mal, me direz-vous. Certes, répondrai-je. Mais le véritable programmeur objet, c'est celui qui est capable non seulement de faire de l'assemblage, mais aussi de la conception ; qui est capable non seulement d'utiliser les classes déjà programmées par d'autres que lui, mais aussi de concevoir et de réaliser ses propres classes.
Maîtriser ce savoir-faire, cela suppose en réalité deux types de compétences distinctes, même si une seule personne peut tout à fait cumuler les deux.
  • il faut être capable, face à un problème donné (une application à réaliser) de formaliser ce problème, d'imaginer quelles classes seront nécessaires, quelles seront les caractéristiques de ces différentes classes (leurs propriétés, leurs méthodes, les événements qu'elles devront gérer), l'articulation éventuelle qu'elles auront entre elles, bref : il faut produire une analyse orientée objet du problème.
  • et il faut savoir traduire ces spécifications dans un langage (ici, le VB.Net) afin de réaliser concrètement les classes en question, pour qu'elles soient utilisables dans une ou plusieurs applications. Il s'agit là d'un savoir purement technique.
Des deux aspects, vous vous doutez que c'est le second, même s'il apparaît comme le plus rebutant, qui est en réalité le plus facile à maîtriser. Et c'est donc celui-là que je vais développer dans les lignes qui suivent. Car pour acquérir de bons réflexes et une bonne intuition sur l'analyse orientée objet, il faut du temps et de la pratique sur des cas concrets, toutes choses bien difficiles à transmettre via un site web, fut-il aussi extraordinaire que celui-ci. Cela ne nous empêchera pas de partir d'un exemple précis, ne serait-ce que pour que l'exposé technique qui va suivre ne soit pas totalement désincarné. Et puis, l'analyse objet, c'est comme le reste, il faut bien commencer par en faire si on veut pouvoir s'améliorer.
Pour la suite, nous allons donc partir d'un exemple que tout le monde connaît : le jeu du Monopoly (qu'on s'entende bien, il ne s'agit pas de le programmer intégralement, mais simplement d'y puiser nos petits bouts d'exemples).

1.2 Le Monopoly : une brève analyse procédurale
Pour programmer un Monopoly, on pourrait tout à fait adopter une démarche traditionnelle, c'est-à-dire procédurale. Dans ce cas, il faudrait s'interroger sur les informations à manipuler, et sur le type de codage le plus approprié. Pour figurer le plateau de jeu, on sent que le mieux serait que l'affaire tourne autour d'un tableau à une dimension, où quand on arrive à la case du bout (la rue de la Paix), on repart à la case zéro. On pourrait ainsi avoir par exemple un tableau de chaînes de caractères qui stocke uniquement la position des pions (codés A pour le premier joueur, B pour le deuxième, etc. et "" s'il n'y a pas de pions sur la case ; s'il y a plusieurs pions sur la même case, une simple concaténation "AC" pourrait facilement l'indiquer). A ce tableau codant le plateau de jeu pourrait également correspondre un autre tableau codant les noms des différentes cases, un troisième codant leur couleur, un quatrième codant le prix de la construction d'une maison, un autre codant le prix du loyer sur terrain nu, etc.
Pour coder la totalité des informations nécessaires, on aurait ainsi, à vue de nez, une dizaine de tableaux fonctionnant un quelque sorte en parallèle : lorsque le joueur tombe sur la case numéro i du plateau de jeu, on sait que les informations utiles se trouvent dans les cases numéro i des différents tableaux. C'est une manière de faire, peut-être pas la plus élégante, mais qui permettra sans doute de parvenir à un résultat qui fonctionne.
Une alternative à cette démarche, toujours en restant dans le cadre de la programmation procédurale, serait de nous donner, plutôt que plusieurs tableaux côte à côte, un seul tableau, où chaque case regrouperait l'ensemble des informations correspondant à ladite case sur le plateau de jeu. Autrement dit, où chaque case ne serait plus une information de type simple, mais un ensemble d'informations de type simple... c'est-à-dire une structure. Et l'ensemble du plateau de jeu serait donc un tableau de structures.
Nous aurions donc une structure (la case) regroupant toutes les informations nécessaires : une chaîne de caractères pour coder les pions présents, un numérique ou un caractère pour en coder la couleur, une chaîne pour en coder le nom, un numérique pour en coder le prix d'achat, un autre pour le prix de la maison, etc. Et notre plateau de jeu serait un tableau (à une dimension) de cette structure.
Cette deuxième solution, tout en restant dans les limites de la programmation procédurale, est déjà beaucoup plus proche, dans son esprit, de l'approche objet que ne l'était la première (celle avec les différents tableaux). Cela ne doit pas nous étonner ! Rappelons-nous de la manière dont nous avions défini les classes (ou les objets) au tout début de ce cours : une classe, c'est une structure plus du code.
En regroupant nos informations en structures, nous avons donc fait la moitié du chemin qui nous mène à l'approche objet. Reste la deuxième moitié, celle du code.

1.3 Le Monopoly : une brève analyse objet
Partons de notre structure, c'est-à-dire de notre case du plateau de Monopoly.
En définissant l'ensemble des informations devant être stockées pour chacune des cases, nous avons donné les éléments de cette structure. Cependant, si nous considérons dorénavant chaque case non plus comme une structure, mais comme un objet formé à partir de la classe "case", alors nous pouvons tout aussi bien dire que nous venons de définir les propriétés de cette classe.
Normalement, si vous avez digéré tout ce que nous avons fait jusque là, cette affirmation ne doit vous poser aucun problème. Si c'est le cas, vous n'avez plus qu'à tout relire depuis le début (mais non, je plaisante ! Enfin... allez savoir).
La nouveauté, maintenant que chaque case est un objet, c'est que nous allons pouvoir réfléchir à toutes les actions du jeu, à tout ce qui peut se passer, et incorporer ces actions, sous forme de code, dans la classe case. Pour cela, il va nous falloir distinguer :
  • les méthodes : ce sont les actions de jeu qui modifient uniquement les caractéristiques de la case concernée. Le code correspondant pourra être donc intégré à la classe case, donc aux objets qui seront créés d'après cette classe.
  • les événements : ce sont les actions de jeu qui auront des répercussions sur d'autres cases, ou sur la banque, ou sur les autres joueurs, etc. Dans ce cas, on ne pourra bien sûr pas intégrer le code dans la classe, mais ils devra figurer en dehors, dans l'application proprement dite.
Cette distinction entre méthodes et événements, elle aussi, ne doit normalement pas vous poser de problèmes particuliers, si vous réfléchissez bien à tout ce que nous avons déjà fait avec des classes déjà préfabriquées. Les méthodes des classes, pré-écrites dans les classes elles-mêmes, étaient toujours un moyen de modifier l'objet concerné, et uniquement lui. Les événements constituaient quant à eux une "porte ouverte", nous permettant de taper le code de notre choix, qui pouvait du coup concerner n'importe quel autre objet que celui ayant déclenché l'événement.
Cela dit, quand on  regarde de plus près, la distinction n'est pas toujours évidente. Ainsi par exemple, prenons deux actions du jeu et tâchons de savoir si nous devons en faire des méthodes ou des événements :
  • acheter un terrain : il faudra noter que le terrain est dorénavant attribué au joueur Duchemol (si nous en faisons une méthode, celle-ci devra donc exiger le nom, ou son numéro en argument). Problème, le compte en banque du joueur devra également être délesté de la somme adéquate. A partir de là, une chose est sûre : nous ne pouvons nous contenter d'une méthode. Pour le reste, cela nous laisse deux possibilités. La première est de n'en faire qu'un événement, lequel gèrera les deux aspects (modification des caractéristiques de la case et du compte en banque de l'acheteur). La seconde consiste à décomposer l'affaire en deux parties : une méthode pour gérer ce qu'il advient du terrain, et un événement pour gérer la modification du compte en banque. En l'occurrence, les deux choix sont possibles, et ont chacun leur pertinence.
  • arrivée d'un pion sur la case : ceci peut en revanche être traité dans une simple méthode, puisqu'il suffira de modifier pour la case concernée la propriété adéquate. Cela dit, l'arrivée d'un pion sur une case pouvant donner lieu à toute une série de conséquences (paiement éventuel d'un loyer, achat éventuel, etc.) on peut également choisir, comme précédemment, d'en faire un événement.
Comme on le voit, la frontière entre méthodes et événements est beaucoup plus floue que celle qui sépare les propriétés d'une part, les méthodes et les événements de l'autre. Bien souvent, on est amené à faire des choix sinon arbitraires, du moins stylistiques. Et bien souvent, en ce qui concerne au moins les classes fournies par Visual Studio, nous avons pu constater que les programmeurs n'avaient pas reculé devant la redondance, nous proposant pour une même notion à la fois une propriété, une méthode pour la gérer et un événement déclenché par sa modification (pensez à la sélection d'un item dans une liste). Mais abondance de biens nuit rarement, et après tout, une manière de voir les choses est de se dire que plus on a prévu de "branchements" lors de la création d'une classe, plus celle-ci sera facile à employer en toute circonstance.
2. Le code

2.1 Déclarer une classe
Il y a pour cela plusieurs possibilités, mais nous opterons pour la plus générale : celle d'une classe créée pour être utilisée par les différentes Form d'un projet donné, par exemple le Monopoly. Nous commencerons donc par créer une nouvelle classe, en utilisant la commande Projet - Ajouter une classe.
Nous constatons qu'apparaît alors un nouveau fichier, d'extension *.vb, portant le nom de la classe. Ce fichier possède toutes les caractéristiques des fichiers Form, dont nous sommes familiers, à cette différence que pour sa part, il ne contient que du code (on parle parfois à ce propos de module de classe).
Le code de chaque classe que nous créerons sera ainsi stocké dans un fichier particulier. Lequel aura pour particularité d'être encadré par les deux instructions suivantes :
Public Class NomdelaClasse

End Class
Le mot clé Public signifiant ici que cette classe sera accessible depuis n'importe quelle Form (ou module) du projet.
A partir du moment où une classe est créée, nous pouvons instancier cette classe dans notre code, c'est-à-dire créer des objets à partir de cette classe, en tapant, comme nous l'avons toujours fait :
Dim Toto As New NomdelaClasse
Nous vérifions au passage que NomdelaClasse apparaît dorénavant dans la liste déroulante de l'éditeur de code après le mot New, preuve que la classe est bien reconnue et disponible. Comme quoi, on n' a pas bossé pour rien.

2. Les propriétés
Une propriété est tout simplement une variable de la classe qui a été déclarée comme publique. Une classe gère donc deux sortes de variables : les privées, celles qui lui servent pour ses propres opérations, et qui sont invisibles depuis l'extérieur (depuis le programme qui utilise cette classe), et les variables publiques, qui sont donc les propriétés.
Il suffit donc de taper dans le code de la classe une ligne comme :
Public Pions As String
Public Prix As Long
...pour que Pions et Prix soient considérés comme les deux propriétés de cette classe, et que je puisse entrer dans ma Form, à la suite de la ligne ayant créé Toto :
Toto.Pions = ""
Toto.Prix = 12000
Et là encore, je vérifie que sitôt entré le point qui suit Toto, s'affiche une superbe liste déroulante qui comporte les deux propriétés Pions et Prix. Elle est pas belle, la vie ?
Bon, là, je n'ai fait qu'employer le minimum du minimum (ce qui s'avère toutefois suffisant dans bien des cas). Mais parfois, on peut souhaiter vouloir introduire un certain nombre de limitations ou de vérifications.
Une première solution consistera à avoir recours à une énumération. Nous n'avons jusque là guère utilisé cet outil, mais il est temps de découvrir, ou plutôt de redécouvrir, son existence. Redécouvrir ? En effet. Rappelez-vous de ce que j'avais dit sur les constantes VB : ce sont des mots-clés, associés par le langage à des entiers. Eh bien, en réalité, ces constantes VB sont les membres d'une énumération. Voyez plutôt.
Pour nos cases de Monopoly, la couleur ne peut prendre qu'un certain nombre de valeurs bien définies. Mettons, pour faire simple, bleu marine, bleu clair, violet, orange et rouge. Cela signifie que la propriété couleur, que je vais créer dans un instant, n'est censée accepter que l'une de ces cinq valeurs, et rien d'autre.
Commençons par créer cette énumération, dans la classe. Avant même les déclarations de variables (de propriétés), tapons :
Public Enum MesCouleurs As Integer
   bleu marine = 0
   bleu clair = 1
   violet = 2
   orange = 3
   rouge = 4
End Enum
Nous venons de créer un nouveau type, qui associe un texte à cinq nombres, et qui ne peut admettre comme valeur que l'un de ces cinq textes, ou l'un de ces cinq nombres. Exactement comme pour les vbOK et autres vbYesNoCancel que nous avons longuement fréquentés.
Il ne reste plus qu'à créer la propriété Couleur, en nous basant sur ce type MesCouleurs :
Public Couleur As MesCouleurs
A partir de là, dans le code de la Form, lorsque je tape une ligne comme :
Toto.Couleur = ...
Je vois apparaître, après le signe égal, une liste déroulante comportant les cinq valeurs autorisés par mon énumération MesCouleurs. Magique, non ?
Pour terminer sur ce point, il existe des moyens encore plus puissants d'introduire des vérifications et des traitements divers au niveau des propriétés. Pour cela, il est indispensable de sortir l'artillerie lourde, à savoir de ne plus déclarer la propriété comme une simple variable, mais comme une procédure Property. Et c'est dans cette procédure qu'on pourra introduire le code voulu.
Pour mémoire car je m'arrêterai là, le bloc d'instructions associé à la lecture d'une propriété s'intitulera Get ... End Get, et celui associé à son écriture sera Set ... End Set. Mais pour une description plus détaillée du maniement de ces choses-là, je vous renvoie sur l'aide, ou sur des manuels plus complets que ce malheureux site.

3. Les méthodes
Si les propriétés ne sont finalement que les variables publiques d'une classe, les méthodes ne sont, elles, rien d'autre que les procédures et les fonctions publiques de cette classe. La boucle est bouclée, et comme le disait le sage, tout est dans tout et réciproquement.
Voyons par exemple la méthode permettant d'ajouter un nouveau pion à une case. Nous passerons en paramètre de cette méthode le nom du joueur ("A", "B", "C", etc.) et la procédure s'occupera d'ajouter la lettre correspondante à la propriété Pions. Écrivons le code dans notre classe :
Public Sub PionArrive(ByVal P As String)
   Pions = Pions & P
End Sub
De là, l'arrivée d'un pion dans la case Toto pourra aisément être programmée dans la Form par la ligne :
Toto.PionArrive ("A")
Et voilà le travail. Et comme d'hab, dès qu'on tape le point qui suit Toto, on voit apparaître PionArrive dans les choix possibles. Et dès qu'on ouvre la parenthèse, on nous signale qu'il faut entrer un paramètre de type String. Ça a beau être la troisième fois que ça nous arrive en peu de temps, il n'y a pas à dire, ça fait encore quelque chose.
Les mauvais esprits, ceux qui d'habitude roupillent dans le fond près du radiateur mais qui viennent de se réveiller juste pour dénigrer mes propos, feront sans doute remarquer que c'était bien la peine de créer une méthode exprès, alors qu'un simple :
Toto.Pions = Toto.Pions & "A"
...écrit dans la Form aurait eu exactement le même résultat. A ceux-là, je répondrai tout d'abord qu'il faut bien commencer par des choses simples, voire un peu niaises, avant de passer à celles qui sont plus compliquées. Ensuite, que les classes de Visual Studio sont truffées de méthodes redondantes par rapport au maniement immédiat des propriétés. Enfin, que c'est moi le chef, et que c'est moi qui décide ce qui est intelligent ou non.
Il suffit d'ailleurs de se poser le problème inverse pour voir l'intérêt d'une méthode : enlever un pion d'une case n'est pas aussi facile que d'en ajouter un (car s'il y a plusieurs pions sur la même case, donc plusieurs lettres dans la chaîne Pions, on ne sait pas en quel position se trouve celui qu'on doit supprimer). Dans ce cas, la méthode pourrait être écrite comme suit :
Public Sub EnleverPion(ByVal P As String)
   Dim x As String
   Dim i As Integer
   x = ""
   For i = 1 To Pions.Length
      If Mid(Pions, i, 1) <> P Then
         x = x & Mid(Pions, i, 1)
      End If
   Next i
   Pions = x
End Sub
De sorte que l'enlèvement du pion "C", dans la Form, s'écrira :
Toto.EnleverPion ("C")
Même les mauvais esprits commenceront à comprendre que plus le code de la classe est complet et comprend de traitements, plus le code de la Form en sera clair, allégé et lisible. En fait, on peut dire que si le découpage d'un traitement en sous-procédures et fonctions représentait un pas vers l'allègement et la rationalisation du code, sa structuration en classes, avec leurs propriétés et leurs méthodes, représente de ce point de vue une avancée supplémentaire. Et je conclurai cette envolée par l'aphorisme suivant, que je viens d'inventer et dont je ne suis pas peu fier : la programmation objet, c'est le règne de la sous-traitance, où le donneur d'ordre (les procédures) délèguent au maximum aux classes tant le stockage des données que leur traitement.

4. Les événements
Là, l'affaire se corse un tantinet. Mais on peut tout de même faire au mieux pour rester dans les limites du raisonnable.
Pour qu'un objet puisse générer un événement, il faut tout d'abord que cet événement, tout comme les propriétés et les méthodes, soit déclaré dans la classe. Ceci s'effectue par le mot-clé Event, suivi des paramètres nécessaires.
Par exemple, admettons qu'outre une méthode AcheterRue, que nous avons créée pour signifier par exemple que la rue Toto vient d'être attribuée au joueur B, nous souhaitions créer un événement DébitCompte afin d'aller débiter le montant du compte en banque du joueur concerné du prix de la rue qu'il vient d'acquérir.
Cet événement étant généré par une méthode de la classe CasesMonopoly (c'est l'exécution de la méthode AcheterRue qui doit provoquer l'événement DébitCompte, il convient de déclarer l'événement dans la classe CasesMonopoly. Les paramètres nécessaires pour effectuer le traitement étant le nom du joueur et le montant de la transaction, ceci s'effectuera ainsi :
Event DebitCompte (ByVal Joueur As String, ByVal Montant As Long)
Et c'est tout. Cependant, il va falloir à présent déclencher l'événement lorsqu'un achat est effectué. En admettant que l'appartenance d'une rue est stockée dans une propriété appelée Possesseur et que son prix d'achat le soit dans une propriété appelée Prix, la méthode AcheterRue ressemblera donc à quelque chose du genre :
Public Sub AcheterRue (J As String)
   Possesseur = J
   RaiseEvent DebitCompte(J, Prix)
End Sub
Il ne reste plus, pour finir, qu'à écrire la procédure DebitCompte proprement dite. Où ça ? Eh bien, vu qu'il s'agit d'une procédure qui ne se rattache à aucune classe particulière, dans la Form, ou mieux, sur un module (un module est un emplacement destiné à stocker du code, mais qui, à la différence de la Form, ne comporte aucun aspect visuel ; si l'on veut, il stocke des procédures et des fonctions, mais pas de contrôles).
On écrira donc, en supposant que les comptes en banque des joueurs sont stockés sous la forme d'un tableau Comptes() de numériques, la case 0 correspondant au joueur A, la 1 au joueur B, etc. :
Public Sub DebitCompte(ByVal X As String, ByVal Y As Long)
   Dim Num As Integer
   num = Instr("ABCDEF", X) - 1
   Comptes(num) = Comptes(num) - Y
End Sub
Ainsi, si l'on considère le trajet effectué par le nom du joueur, celui-ci aura finalement accompli un joli périple. En effet, le nom du joueur a été transmis depuis la procédure située dans la Form jusqu'à la méthode AcheterRue de l'objet Toto. Là, rejoint par le montant de l'achat, il a été transmis via l'événement DebitCompte, à la procédure correspondante, et traité à cet endroit... ouf !
Cela dit, l'avantage de cette architecture, c'est de découper au maximum le code, et de procéder par emboîtage successifs d'éléments (dont les classes) pouvant être testés et débogués au fur et à mesure de la construction.
On remarquera évidemment que dans le Monopoly, les cases ne sont pas du tout les seuls éléments à pouvoir être traités comme des objets. On aurait très bien pu faire de même avec les joueurs  : auquel cas le compte en banque et le nom auraient été des propriétés de cette classe Joueurs, et auquel cas il aurait également fallu écrire le code précédent d'une manière assez différente. Mais bon, hein, le but de la manoeuvre, c'était de donner les clés. Après, c'est à vous d'apprendre à ouvrir de plus en plus de portes (tiens, c'est beau, ça, comme image, je la garde).
3. Créer ses propres contrôles
Nous pouvons maintenant ajouter quelques mots au sujet d'un problème que nous avions laissé de côté au chapitre précédent : créer nos propres contrôles.
Les contrôles n'étant jamais qu'un type particulier d'objets, tout ce que nous venons de voir à l'instant à propos des classes reste évidemment valide à propos de contrôles. Cependant, les contrôles présentant certaines particularités, leur création peut s'avérer selon les cas plus facile, ou plus difficile, que la création des classes.
Plus facile, car on peut partir d'un contrôle existant, on se contente d'apporter quelques modifications. Plus difficile, car le contrôle est le plus souvent une classe dont les objets doivent avoir une existence à l'écran, et réagir à une pléthore d'événements.
On peut ainsi définir trois cas de figure, classés par difficulté croissante :
  • le contrôle est une variante d'un contrôle existant : on dit alors qu'on procède par héritage de ce contrôle. La classe définissant le contrôle devra alors hériter de la classe définissant le contrôle existant.
  • le contrôle est un "mix" de plusieurs contrôles existants  : on parle alors d'assemblage. Cette fois, la classe de départ sera UserControl.
  • le contrôle est entièrement conçu à partir de rien, et la classe de base à utiliser sera Control.
Ces techniques dépassent assez largement l'ambition limitée de ce cours - et du temps qui nous est imparti. Ceux qui souhaiteront les mettre en oeuvre devront potasser des ouvrages spécialisés... et beaucoup plus onéreux que ce modeste site.