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.
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 :
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 :
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 :
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.
|