EPOOL - Environnement de Programmation Orientée Objet en Lingo par Irv Kalb

Chapitre précédent

Table des matières

Chapitre suivant

 

Un objet simple

Un exemple basique

Pour essayer d'expliquer ce qu'est un objet, nous allons commencer avec une portion de code qui n'est pas orienté objet, puis nous regarderons comment faire la même chose à partir d'un script parent orienté objet. Ainsi nous pourrons comparer les deux approches et trouver les points communs et les différences.

Supposons que nous voulons écrire du code pour simuler un compte en banque. Le code manipulera deux variables: gMotDePasse qui sera utilisé pour stocker le mot de passe du compte, et gSolde qui contient le montant d'argent actuel du compte. Pour le fonctionnement du compte en banque, nous voulons pouvoir créer un compte, déposer de l'argent sur ce compte, retirer de l'argent et connaître le solde du compte. Voici un moyen simple de remplir cette tâche:

-- script de compte en banque avec globales
global gMotDePasse
global gSolde


on NouveauCompte motDePasse, soldeInitial
  
gMotDePasse = motDePasse
  
gSolde = soldeInitial
end

on Depot montantADeposer
  
gSolde = gSolde + montantADeposer
end

on Retrait montantARetirer,
motDePasse
  if
motDePasse <> gMotDePasse then
    alert("Mauvais mot de passe!")
    return
  end
if
  if
montantARetirer > gSolde then
    alert("Vous n'avez pas assez d'argent!")
    return
  end
if
  
gSolde = gSolde - montantARetirer
end


on GetSolde motDePasse
  if motDePasse <> gMotDePasse then
    alert("Mauvais mot de passe!")
    return
  end if
  return gSolde
end


Ce code devrait vous paraître assez simple. Depot permet d'ajouter de l'argent sur le compte. Ce gestionnaire ajoute simplement le montant à ajouter au solde courant. Retrait permet de retirer de l'argent du compte. Cependant il a besoin de faire quelques tests. D'abord il regarde si le mot de passe est celui du compte. Il vérifie aussi que le compte dispose d'assez d'argent pour le retrait. Si ces deux conditions sont réunies, alors montantARetirer est soustrait à gSolde. GetSolde vérifie le mot de passe puis retourne le solde gSolde.

Maintenant essayons d'étendre ce programme. Supposons que nous voulons gérer deux comptes en banque - un pour Paul et un pour Sophie. Une première idée peut être d'ajouter des variables globales. Pour être précis, nous pourrions remplacer gMotDePasse par gMotDePassePaul et gMotDePasseSophie. De même nous changerions gSolde en gSoldePaul et gSoldeSophie. Ensuite nous n'avons plus qu'à modifier nos routines pour passer en paramètre un nom ou un symbole pour identifier le compte manipulé. Alors chaque routine devrait contenir une déclaration case pour tester le mot de passe par rapport au compte indiqué, et modifier le solde du compte demandé. Par exemple le gestionnaire Retrait ressemblerait désormais à :

global gMotDePassePaul
global gSoldeSophie
global
gMotDePasseSophie
global gSoldePaul

on Retrait montantARetirer, motDePasse, qui
  case
qui of
    "Paul":
      if
motDePasse <> gMotDePassePaul then
        alert("Mauvais mot de passe!")
        return
      end
if
      if
montantARetirer > gSoldePaul then
        alert("Vous n'avez pas assez d'argent!")
        return
      end
if
      
gSoldePaul = gSoldePaul - montantARetirer
      
    "Sophie":
      if
motDePasse <> gMotDePasseSophie then
        alert("Mauvais mot de passe!")
        return
      end
if
      if
montantARetirer > gSoldeSophie then
        alert("Vous n'avez pas assez d'argent!")
        return
      end
if
      
gSoldeSophie = gSoldeSophie - montantARetirer
  end
case
end

Maintenant supposons que nous voulons gérer un nombre quelconque de comptes en banque. Pour chaque nouveau compte nous devrions ajouter deux variables globales et modifier le code pour manipuler les bonnes variables. Vous vous apercevez que cela deviendrez vite ingérable. De plus cette approche n'est pas dynamique puisqu'elle ne nous permet pas d'ajouter un nouveau compte à la volée - nous devons connaître à l'avance les noms des gens qui veulent créer un compte.

Une autre approche serait de créer deux listes "parallèles", une pour les mots de passe et l'autre pour les soldes. Pour chaque nouvelle création de compte, vous devriez ajouter un nouveau mot de passe à la table des mots de passe, et un nouveau solde à la liste des soldes. Voici le code qui implémenterait ce modèle :

-- Code de Compte en Banque avec des listes globales
global glMotsDePasse -- liste globale des mots de passe
global glSoldes -- liste globale des soldes


on
startMovie
  
glMotsDePasse = []
  
glSoldes = []
end

on NouveauCompte motDePasse, soldeInitial
  add(
glMotsDePasse, motDePasse)
  add(
glSoldes, soldeInitial)
  return
count(glMotsDePasse)
end

on Depot unNumeroDeCompte, montantADeposer
  
glSoldes[unNumeroDeCompte] = glSoldes[unNumeroDeCompte] + montantADeposer
end

on Retrait
unNumeroDeCompte, montantARetirer, motDePasse
  if
motDePasse <> glMotsDePasse[unNumeroDeCompte] then
    alert("Mot de passe erroné!")
    return
  end
if
  if
montantARetirer > glSoldes[unNumeroDeCompte] then
    alert("Vous ne disposez pas d'assez d'argent sur votre compte!")
    return
  end
if
  
glSoldes[unNumeroDeCompte] = glSoldes[unNumeroDeCompte] - montantARetirer
end

on GetSolde
unNumeroDeCompte, motDePasse
  if
motDePasse <> glMotsDePasse[unNumeroDeCompte] then
    alert("Mot de passe erroné!")
    return
  end
if
  return
glSoldes[unNumeroDeCompte]
end


Lors de la création d'un nouveau compte, un nouvel enregistrement est ajouté dans chacune des deux listes globales glMotsDePasses et glSoldes. Cette approche marche mais est un peu curieuse. Comme les mots de passe sont regroupés dans une liste, et tous les soldes regroupés dans une autre, lorsque nous voulons les informations d'un compte, nous devons utiliser une variable commune pour préciser l'indice des deux listes : glMotsDePasse[i] et glSoldes[i].

Si nous nous représentons ces informations dans des tableaux, nous voyons que tous les mots de passe sont réunis verticalement, ainsi que les soldes.

Cependant, si on suit la conception réelle d'un compte, chaque compte en banque correspond à une ligne de ce même tableau.

Dans l'idéal il nous faudrait trouver un moyen de réunir les informations d'un compte pour les voir comme une chose liée. La question est donc "comment pouvons-nous organiser les données pour que le mot de passe et le solde d'un compte soit groupés ensemble?". Vous vous y attendez : la réponse est apportée avec un objet.

 

Un objet simple

Rappelez-vous notre définition d'un objet : des données, et du code qui agit sur ces données au cours du temps. En gardant cette définition à l'esprit, nous nous apercevons que notre compte en banque a deux éléments de données - un mot de passe et un solde - et a des actions qui agissent sur ces données - nous permettons à l'utilisateur de faire des dépôts, des retraits et de consulter son solde. Voici de bonnes conditions pour faire un script parent. Voici donc les même fonctionnalités que précédemment écrite dans un script parent. Vous pourrez remarquer que ce code ressemble fort à notre première version non orientée objet.

-- script CompteEnBanque
property pMotDePasse
property pSolde


on
new me, motDePasse, soldeInitial
  
pMotDePasse = motDePasse
  
pSolde = soldeInitial
  return
me
end

on mDepot me, montantADeposer
  
pSolde = pSolde + montantADeposer
end

on mRetrait me, montantARetirer,
motDePasse
  if
motDePasse <> pMotDePasse then
    alert("Mot de passe erroné!")
    return
  end
if
  if
montantARetirer > pBalance then
    alert("You don't have that much cash in your account!")
    return
  end
if
  
pSolde = pSolde - montantARetirer
end

on mGetSolde me,
motDePasse
  if motDePasse <> pMotDePasse then
    alert("Mot de passe erroné!")
    return
  end if
  return pSolde
end



La première partie de notre définition d'un objet est: des données. Si vous comparez ce script avec la première version non orientée objet, la première différence que vous noterez est la différence des variables: là où nous utilisions deux variables globales nous codons désormais deux propriétés pMotDePasse et pSolde pour contenir les informations de cet objet. Dans le script original, puisque gMotdePasse et gSolde étaient globales, elles pouvaient être modifiées non seulement par le script, mais aussi de n'importe où dans le programme. Nous verrons que cela peut représenter un danger potentiel. Dans notre présentation sur la portée des variables, nous avons vu que les propriétés n'étaient accessibles que par le script qui les déclarait, ni plus, ni moins. En clair cela signifie que les données stockées dans pMotDePasse et pSolde peuvent être manipulées par les méthodes du script CompteEnBanque, mais rien ne peut les modifier de l'extérieur de ce script.

La deuxième partie de notre définition d'un objet est: du code qui modifie les données. Ce script contient quatre méthodes qui agissent sur ses données : new, mDepot, mRetrait et mGetSolde.

La méthode "new" est une méthode spéciale que tous les scripts parents doivent avoir. Nous regarderons plus tard plus en détail ce que fait cette méthode, pour l'instant il vous suffit de savoir que la méthode "new" est toujours la première méthode d'un script parent qui est exécutée. En fait, l'appel à la méthode "new" d'un script parent créé - on dit instancie - un objet. Dans ce cas, lorsque nous faisons appel à la méthode "new" du script parent CompteEnBanque, nous créons un objet CompteEnBanque.

La méthode "new" peut accepter des paramètres, comme n'importe quel autre gestionnaire. Cela nous donne la possibilité de spécifier les valeurs initiales des paramètres de l'objet. Dans la méthode "new" de notre script parent CompteEnBanque, nous avons prévu deux paramètres: une valeur initiale pour définir le mot de passe et un solde initial pour le compte. Le code de notre méthode "new" copie simplement ces valeurs dans les propriétés de l'objet pour pouvoir les conserver.

mDepot agit de la même façon que la routine Depot de notre premier script, mais ici nous agissons sur la propriété pSolde alors que nous manipulions précédemment la variable globale gSolde. De même, mRetrait et mGetSolde vérifient le mot de passe stocké dans la propriété pMotDePasse au lieu d'interroger une globale comme gMotDePasse dans notre premier script.

 

Instanciation (création) d'un objet

La troisième partie de notre définition concerne le temps. Un objet a une durée de vie. Comme je l'ai dit, un script parent définit un objet. Cependant le script parent ne crée pas d'objet de lui-même. Le programmeur décide quand créer un objet, et quand il veut l'éliminer (le "libérer").

Si vous placez le script parent CompteEnBanque dans une animation Director et lancez l'animation, vous ne créerez aucun objet. Un objet ne commence à vivre qu'à partir du moment où le programmeur appelle explicitement la méthode "new" d'un script parent. La méthode "new" d'un script parent peut être appelée de n'importe où hors de son propre code. Chaque objet que vous créez à partir d'un script parent est souvent appelé une instance de cet objet.

Une bonne analogie est de se représenter un script parent comme un emporte-pièces "high-tech". Il vous fournit la forme et toutes les caractéristiques de la pièce que vous voulez découper, mais celle-ci n'est découpée que quand vous l'utilisez pour découper vous-même une pièce. Pour prolonger cette analogie, notre emporte-pièces high-tech peut aussi recevoir des indications pour modifier légèrement la pièce à sa découpe. Chaque pièce est donc bien une instance de la même matière première... mais chacune a ses caractéristiques propres définies par l'emporte-pièce.

Lorsque vous voulez créer un nouvel objet CompteEnBanque vous devez taper une ligne du style :

oCompteEnBaque = new(script "CompteEnBanque", "xyzzy", 400.00)

Cette ligne appelle la méthode "new" du script nommé "CompteEnBanque", et lui passe deux paramètres : un mot de passe "xyzzy" et un solde initial de 400. Lorsqu'il exécute cette ligne, Director crée un objet pour nous et appelle ensuite la méthode "new" du script parent spécifié. Dans le méthode "new" de notre objet CompteEnBanque nous nous contentons d'assigner les valeurs des propriétés de l'objet.

La dernière ligne de code dans la méthode "new" du script parent est:

return me

"me" est très important dans cette ligne. Lorsqu'avec "new" vous demandez à Director de créer un objet, Director alloue de la mémoire pour cet objet, et crée une variable de type référence d'objet. La référence d'objet peut être vue comme un pointeur vers cet objet. Quand vous créez un objet, celui-ci doit retourner dans sa méthode "new" une valeur qui permet de pointer vers lui et nous permettra d'y faire référence ultérieurement. C'est à ça que sert le "me". En effet il s'agit de l'indication de l'adresse mémoire où a été créé l'objet. Lorsque "new" retourne son résultat, celui-ci est stocké dans une variable, ici oCompteEnBanque. Il est important de saisir la différence entre le pointeur vers l'objet et l'objet en lui même. Le pointeur n'est qu'une variable qui contient l'adresse mémoire où est stocké l'objet. L'objet est lui la portion de mémoire allouée pour conserver les données de l'objet.

 

Chaque fois que vous voulez faire appel à une méthode de l'objet, vous devez passer par le pointeur qui a été retourné à la fin de la méthode "new". Par exemple, si vous avez déjà créé un objet CompteEnBanque et mémorisé sa référence dans la variable oCompteEnBanque, et que vous voulez maintenant faire un dépôt sur ce compte, vous allez écrire :

mDepot(oCompteEnBanque, 250.00)

(ou la même chose en notation pointée)

oCompteEnBanque.mDepot(250.00)

Ceci précise l'objet CompteEnBanque pointé par oCompteEnBanque, ainsi que le paramètre 250.00 . De l'autre côté, à la réception de cette appel de méthode par l'objet, et plus particulièrement dans la méthode mDepot, oCompteEnBanque correspond au paramètre "me", et 250.00 renseigne le paramètre montantADeposer. A l'intérieur de cette méthode, "me" renvoie vers une instance de l'objet CompteEnBanque. Remarquez que le premier paramètre de toutes les méthodes d'un objet est toujours "me". Nous reviendrons plus tard en détail sur "me".

 

Instancier plusieurs copies d'un objet

Précédemment nous avons vu comment utiliser des globales ou des listes globales pour gérer un nombre quelconque de comptes en banque. Maintenant que nous disposons du script parent CompteEnBanque voyons comment l'utiliser pour gérer plusieurs comptes. Comme dans nos exemples précédent supposons que nous voulons créer deux comptes en banque, un pour Paul et un pour Sophie. Voici comment nous pouvons faire à partir de notre script parent :

oComteEnBanquePaul = new(script "CompeEnBanque", "gateau", 1000.00)

oComteEnBanqueSophie = new(script "CompeEnBanque", "asperge", 1200.00)

 

A l'exécution de ces deux lignes, Lingo crée deux instances différentes à partir du même script parent. Le premier objet correspond au compte en banque de Paul, avec son mot de passe "gateau" et un solde initial de 1000. Le second objet correspond au compte de Sophie, avec "asperge" comme mot de passe et un solde de départ de "1200". Voici deux copies d'un objet, chacune ayant ses valeurs propres pour les propriétés pMotdePasse et pSolde. Nous avons réussi à créer une entité unique, un objet, qui regroupe un mot de passe et un solde pour un compte. Si nous reprenons la visualisation des données en tableau, chaque objet que nous avons défini correspond à une ligne de notre tableau.

Maintenant si nous voulons pouvoir gérer un nombre quelconque de comptes (chacun étant un objet ComteEnBanque)voici quelques lignes. Nous commençons par déclarer une liste globale de références d'objet CompteEnBanque. Ensuite nous devons définir un gestionnaire pour créer de nouvelles instances de notre objet CompteEnBanque et ajouter les références de ces instances dans la liste globale de références d'objets :

-- script de Compte en Banque avec une liste globale
global gloComptesEnBanque -- global list of BankAccount object references

on startMovie
  gloComptesEnBanque = []
end

on NouveauCompte motDePasse, soldeInitial
  oNouveauCompte = new(script "CompteEnBanque", motDePasse, soldeInitial)
  add(gloComptesEnBanque, oNouveauCompte)
  nComptes = count(gloComptesEnBanque)
  return nComptes
end

 

Libérer un objet

La durée de vie d'un objet dépend totalement de ce que décide le programmeur en fonction du programme. Suivant ce à quoi sert l'objet, sa durée de vie peut varier d'une milliseconde à la durée d'exécution de tout le programme. Un programmeur peut créer un objet pour faire du stockage temporaire et des opérations basiques, puis libérer l'objet quelques instants plus tard. Un programmeur peut aussi vouloir créer un objet au démarrage d'une animation, et y faire des appels fréquents tout au long du programme. Souvent un développeur crée un objet en réponse à une action de l'utilisateur. De plus des objets différents peuvent être instancier pour gérer les données d'un programme - le nombre d'objets et donc liés aux données traitées.

Comme nous l'avons vu, vous devez appeler explicitement la méthode "new" pour créer un objet. Director le crée alors et vous donne la référence de l'objet. Lorsque vous créez un objet, celui-ci occupe de la mémoire. A partir du moment où vous n'avez plus besoin de l'objet, c'est une bonne idée de libérer l'objet pour que Director puisse réutiliser la mémoire occupée par l'objet. Nettoyer un objet (et donc libérer la mémoire qu'il occupe) est très simple. Director retient le nombre de variables qui pointent vers un objet. C'est ce qu'on appelle le "comptage de références". La règle veut que Director libère la mémoire occupée par un objet dès que celui-ci n'est plus pointé par aucune variable.

Vous comprendrez mieux avec un exemple. Commençons par créer un objet Widget à partir du script parent Widget (peu importe ce que peut faire cet objet).

oWidget = new(script "Widget")

Maintenant nous avons une référence d'objet Widget stockée dans une variable oWidget. Director assigne le compteur de référence vers l'objet Widget à un. (en effet il y a une variable qui contient une référence vers cet objet) Ensuite nous faisons appel à diverses méthodes de l'objet pendant son cycle de vie. Enfin nous voulons détruire l'objet Widget et libérer sa mémoire. Nous pouvons indiquer à Director qu'il peut détruire l'objet en faisant simplement :

oWidget = VOID

Nous avons juste changer la valeur de oWidget pour qu'il ne pointe plus vers l'objet Widget. VOID est une valeur intéressante à assigner à une variable, puisqu'elle force la variable à se "remettre à zéro" et ne rien contenir du tout. En ne faisant plus pointer oWidget vers l'objet Widget, Director décrémente le compteur de références pour l'objet précédemment pointé par oWidget. Le nombre total de référence vers cet objet arrive donc à zéro. Director réalise alors qu'il n'y a plus aucun pointeur vers cette instance en mémoire de l'objet, et libère la mémoire qui lui était allouée.

Maintenant compliquons un peu les choses. Nous allons créer un objet Widget, et faire une copie de la référence:

oWidget = new(script "Widget")

oAutreWidget = oWidget

La première ligne demande à Director d'allouer de la mémoire pour un objet Widget. Le compteur de références pour l'objet Widget arrive à un. Director exécute ensuite la deuxième ligne, et incrémente encore le compteur de références pour arriver à deux. En effet nous avons deux variables qui pointent vers la même instance en mémoire. Maintenant pour détruire l'objet Widget, nous devons nous assurer de nettoyer toutes les variables qui contiennent une référence vers l'objet Widget. Pour libérer la mémoire nous devons donc écrire :

oWidget = VOID

oAutreWidget = VOID

Director exécute la première ligne, et le compteur de références descend à un. Avec ce compteur, Director "sait" qu'il reste quelque part une variable qui fait référence vers cet objet Widget. Quand Director exécute ensuite la seconde ligne, le compteur de références tombe à zéro et Director s'aperçoit qu'il peut libérer sans risque la mémoire occupée par l'objet Widget.

Ce système de copiage de références peut paraître délicat, mais dans la pratique cela devient une routine simple. Souvenez-vous qu'à chaque fois que vous stockez une référence vers un objet, vous devrez la "nettoyer". Il y a quelque cas spécial à discerner. Si vous stockez une référence dans une variable locale, le compteur sera incrémenté. A la fin du gestionnaire courant la variable locale disparaît et le compteur est automatiquement décrémenté. Si vous stockez une référence dans une liste, cela "compte" comme une variable pointant vers l'objet. Si la liste est une variable locale, le compteur de référence sera décrémenté automatiquement à la fin du gestionnaire. Cependant si vous stockez une référence dans une variable globale, ou dans un liste globale, lorsque vous voulez libérer la mémoire occupée par cette instance vous devez vous assurer d'avoir pris en compte toute variable contenant une référence vers cet objet, sans quoi la mémoire occupée par l'objet ne sera pas libérée.

 

Chapitre précédent

Table des matières

Chapitre suivant