Approche impérative vs. approche objet

A) L'exemple traité

Il est tiré du partiel : il s'agit essentiellement, étant donnée une grille rectangulaire (semblable à un grillage) placée parallèlement aux axes d'un repère orthonormé (la position dans la grille est donnée les coordonnées de son coin inférieur gauche), de pouvoir obtenir, pour tout point du plan des informations relativement à son appartenance à la grille et, s'il lui appartient, le nombre de ses voisins.
Dans tout ce qui suit, le langage utilisé sera Java. De plus une grille sera modélisée par la donnée des coordonnées x0 et y0 de son coin inférieur gauche, sa longueur (les abscisses des points de la grille seront tous les entiers compris entre x0 et x0+longueur) et sa hauteur (les ordonnées des points de la grille seront tous les entiers compris entre y0 et y0+hauteur).

B) Approche impérative pure

La solution est constituée d'une classe unique Grille intégrant la définition d'un ensemble de fonctions (méthodes static, chacune de ces fonctions ayant en paramètres l'ensemble des informations nécessaires, c'est-à-dire définition de la grille (les coordonnées de son coin inférieur gauche et ses longueur et hauteur) et celle du point considéré (ses deux coordonnées x et y). La classe Grille contient par ailleurs la définition de la méthode main. Cette méthode, après avoir lu toutes les informations nécessaires (définition de la grille et d'un point), réalise un appel à la fonction déterminant le nombre de voisins du point sur la grille. Le fichier Grille.java une fois compilé constitue une application directement exécutable par une machine java (commande java Grille).

C) Approche objet du problème : une grille simple

1. Introduction
Dans cette approche une grille apparaît comme une entité possédant un certain nombre d'attributs et à laquelle on peut s'adresser au travers d'une interface constituée d'un ensemble de méthodes (le qualificatif static va disparaître de la définition de ce que nous avons appelé jusque là des fonctions). La définition d'une classe correspondra à celle d'un «patron» (au sens des couturières d'antan) à partir duquel on pourra créer (instancier) des objets de la classe (de la même manière que les couturières façonnaient une jupe ou chemise à partir d'un patron). L'objet encapsule donc des données et des traitements.
La définition d'une classe correspond ainsi, pour partie, à celle de nouveaux types et plus précisément de structures (struct en langage C) ou d'enregistrements (record en Pascal). Elle comportera ainsi la définition d'un certain nombre d'attributs qui caractérisent les objets construits sur ce modèle (variables d'instance ou champs) d'un tel objet. Toute nouvelle instance d'un objet de la classe possèdera son propre exemplaire de ces champs. Pour un objet donné d'une classe, la valeur de ces différents champs définit son état.
Mais la définition d'une classe va plus loin en ce sens qu'elle définit ce que l'on est en droit de faire sur les objets construits sur ce modèle. Un objet de la classe a un comportement défini par une interface constituée par un ensemble de méthodes. Par l'intermédiaire d'un appel d'une méthode de cette interface, on pourra interroger un objet (ce qui pourra par exemple entaîner une simple réponse ou une modification de l'objet lui-même, c'est-à-dire de certains de ses champs).
2. Les variables d'instance
En ce qui concerne notre exemple, on voit bien que ce qui caractérise une grille, c'est son positionnement dans le plan, sa longueur et sa hauteur. Les champs d'une grille seront donc, pour calquer ce que nous avons fait dans le style purement impératif, quatre entiers correspondant à ces caractéristiques. Ainsi, la définition du modèle de grille, la classe GrilleObjet, fait apparaître l'existence de ces attributs :


class GrilleObjet {
   int x0, y0, longueur, hauteur; 
        .........

ou de manière plus en adéquation avec l'approche objet


class GrilleObjet {
   private int x0, y0, longueur, hauteur; 
        .........

Le qualificatif private cache aux utilisateurs de la classe la manière dont la grille est représentée et leur interdit d'y accéder directement.
Cet ensemble de fonctionnalités définit le principe d'encapsulation et de protection des données.

3. Les méthodes d'instance
La définition de la classe contient par ailleurs la définition de qu'on peut demander à un objet de la classe, c'est-à-dire la définition des méthodes de son interface. Ainsi, on pourra demander à une grille instanciant la classe GrilleObjet si un point donné par ses coordonnées x et y est un de ses coins. La définition de la classe GrilleObjet doit donc contenir, en particulier, une méthode estUnCoin permettant à une grille donnée de déterminer si un point particulier est un des coins. La définition de cette méthode est, pour ce qui concerne son corps proprement dit, semblable à celle que nous en avons donnée dans la version impérative. Là où elle en diffère, c'est dans son en-tête. On s'adresse à une grille particulière qui connaît ses attributs. La méthode n'est plus qualifiée static et ne reçoît comme seuls paramètres que les caractéristiques du point par rapport auquel elle doit fournir une réponse sur la base de ses propres caractéristiques :


             ...................
   boolean estUnCoin(int x, int y) {
     if ((x == x0) || (x == x0 + longueur))
        return  (y == y0) || (y == y0 + hauteur);
     else
        return false;
    // return ((x==x0)||(x==x0+longueur)) && ((y==y0)||(y == y0 + hauteur)); 
    }
             ...................

4. Les constructeurs
Les classes ne demandent évidemment qu'à être instanciées, c'est-à-dire à servir de modèles à des objets effectifs. Nous verrons qu'ils le sont, comme les tableaux, au travers de l'opérateur new qui a essentiellement comme effet d'allouer en mémoire l'espace nécessaire aux constituants de l'objets. Le problème qui reste en suspens est celui de la valeur attribuée aux différents champs de l'objet créé.
L'opérateur new a besoin d'un argument qui est un constructeur. Un constructeur réalise un certain nombre d'opérations sur l'objet créé, comme par exemple initialiser à des valeurs paticulières les champs de l'objet. Il porte le même nom que la classe qui lui est associée. Une classe peut posséder plusieurs constructeur : chacun d'eux se distingue par sa signature, en l'occurrence la liste et le type de ses paramètres, puisqu'ils ont tous le même nom). Il en existe un par défaut qui ne fait rien. De fait, lorsqu'un objet est créé, tous les champs sont initialisés à une valeur par défaut : 0 pour les champs numériques, false pour les champs booléens et la valeur null pour les variables de type non primitifs (dites variables de référence tels que les tableaux par exemple). Ce constructeur par défaut n'est plus utilisable explicitement dès qu'un constructeur spécifique est défini dans une classse.

Un constructeur, du point de vue de son utilisation, apparaît donc comme une fonction : il peut posséder des paramètres (le constructeur par défaut n'en a pas). Par contre, il s'en distingue, dans sa définition, par le fait que son en-tête ne spécifie pas de type de retour (même pas void). Un exemple de constructeur associé à la classe GrilleObjet, intégré dans la définition de cette classe est donné ci-après :


             ...................
    GrilleObjet(int a, int b, int lg, int ha) {
       x0 = a; y0 = b; longueur = lg; hauteur = ha;  
    }
             ...................


Une demande d'instanciation de la forme new GrilleObjet(3, 5, 15, 25) aura pour effet de créer une grille dont les champs x0, y0, longueur et hauteur ont respectivement commes valeur 3, 5, 15 et 25.

On pourra consulter le code complet de la classe GrilleObjet.

5. Références et variables de référence
Généralement, une application instanciant une classe, et créant donc un objet, l'utilisera ultérieurement. Cela suppose qu'il lui soit possible d'y accéder. Comme c'est le cas pour les instanciations des tableaux, l'opérateur new renvoie une référence sur l'objet créé. Cette valeur peut être utilisable en tant que référence et en particulier mémorisée dans une variable référence sur la classe correspondante. La séquence suivante permettrait à une application utilisant une grille de la classe GrilleObjet (dans laquelle le constructeur spécifique à 4 paramètres entiers serait défini) de créer une grille et d'en mémoriser l'adresse dans une variable :


             ...................
    GrilleObjet refGrilleObjet; // ne référence rien. Pas d'objet créé  
    refGrilleObjet = new GrilleObjet(3, 5, 15, 25); 
    // GrilleObjet refGrilleObjet = new GrilleObjet(3, 5, 15, 25);
             ...................

Notons au passage l'existence de la reférence symbolique this qui désigne dans tout objet l'objet lui-même (principe d'auto-référençage). Elle est souvent omise, mais son usage s'avère cependant indispensable dans certaines situtations décrites un peu plus loin.

6. Accès aux composants d'un objet
Dans la mesure où les qualifications des attributs (champs ou méthodes le permettent et en paticulier ne sont pas qualifiés private), il est possible d'accéder à un champ ou d'invoquer une méthode sur un objet par l'intermédieire de sa référence, suivie du caractère ., suivie du nom du champ ou de l'appel de méthode souhaitée.
Ainsi, pour la classe GrilleObjet, après création d'un objet de cette classe


    refGrilleObjet = new GrilleObjet(3, 5, 15, 25); 

refGrilleObjet.longueur désigne le champ longueur de l'objet référencé par refGrilleObjet. Cette expression produira une erreur de compilation si le champ longueur est qualifié private dans la définition de la classe.
refGrilleObjet.estUnCoin(6,8) correspond à une invocation de la méthode estUnCoin sur ce même objet. Elle renverra une valeur de type boolean indiquant si le point (6,8) est un coin de la grille ou non. Là encore, si la méthode est qualifiée private, une erreur de compilation sera rencontrée.

Remarque : nous avons mentionné l'existence de la référence this. Il est le plus souvent omis (la référence à l'objet courant est implicite dans la définition des méthodes d'une classe) mais son usage s'avère nécessaire pour lever l'ambiguïté provoquée par l'utilisation comme nom de paramètre le nom d'un champ de la classe. Ainsi, dans la définition du constructeur suivant:


             ...................
    GrilleObjet(int x0, int b, int lg, int ha) {
       x0 = this.x0; y0 = b; longueur = lg; hauteur = ha;  
    }
             ...................

l'utilisation de la référence this permet de distinguer les deux interprétations de l'identificateur x0.

7. Un exemple d'application
On pourra consulter le code complet d'une application utilisant la classe GrilleObjet.

D) Aller plus loin : composer ou dériver des classes

1. Objectif
Nous allons illustrer au travers du même exemple quelques concepts fondateurs de l'approche objet. Nous nous proposons de raffiner la notion de grille telle que nous l'avons définie précédemment. Une grille correspondant à ce nouveau modèle aura tout d'abord tous les attributs d'une grille du modèle GrilleObjet précédent, c'est-à-dire qu'on pourra invoquer sur elle les méthodes de cette classe. Mais elle possèdera des attributs supplémentaires (champs et/ou méthodes) que ne possède pas la classe GrilleObjet qui permettront d'en avoir une représentation graphique telle que celle présentée dans le sujet du partiel et de visualiser des points de la grille et leurs voisins.
Ainsi la grille correspondant à l'objet instancié par l'appel du constructeur GrilleObjet(2,4,12,6), le point de coordonnées (6,7) et ses voisins pourront être visualisés sous une forme assez semblable à la figure suivante:

Nous allons présenter deux approches possibles pour définir cette nouvelle classe. On utilisera, au passage, des fonctions simples de la classe Deug, telles que fillRect et fillCircle qui permettent de visualiser des rectangles et des cercles pleins.
2. Composition et délégation
2.1. Lors de la définition d'une nouvelle classe, il est, heureusement, tout d'abord possible d'encapsuler des variables de type non primitif, c'est-à-dire de définir des champs de type référence. Le développement d'une nouvelle application peut conduire à la définition de différentes nouvelles classes ou faire appel aux nombreuses classe fournies par les paquetages (packages) standard. Il est ainsi possible de composer des classes.

En partant de cette possibilité, on va définir une nouvelle classe GrilleObjetGraphique qui encapsulera une variable de référence sur la classe GrilleObjet et on ajoutera quelques champs utiles pour le traitement graphique des grilles. On a choisi ici de permettre la visualisation d'un point d'une grille sous la forme d'un carré coloré dont le côté correspond au champ cote et ses voisins par des cercles dont le rayon correspond au champ rayon. Par ailleurs, l'affichage se faisant par pixel, un facteur d'échelle correspondant au champ ech a été ajouté. Un dernier champ bord a par ailleurs été ajouté pour éloigner la figure du bord de la fenêtre graphique. Dans l'amorce de définition de la classe GrilleObjetGraphique donnée ci-après, les valeurs de ces quatre nouveaux champs ont été définis «en dur» : on imagine que leur initialisation pourrait être réalisée par un constructeur.


class GrilleObjetGraphique{
   private GrilleObjet grille;  // la grille encapsulée
   private int cote = 7;        // côté (# pixels) du carré pour un point 
   private int rayon = 7;       // rayon du cercle (# pixels) pour un voisin 
   private int ech = 15;        // facteur d'échelle
   private int bord = 5;        // taille (# pixels) du bord inutilisé
        ..........

2.2. La définition de la classe inclut ensuite celle d'un constructeur qui ne fait ici rien d'autre qu'instancier l'objet de la classe GrilleObjet sous-jacente de l'objet de la classe GrilleObjetGraphique (comme nous l'avons dit, on pourrait concevoir d'y réaliser l'initialisation des champs spécifiques) :


        ..........
   GrilleObjetGraphique(int x0, int y0, int lg, int ha) { 
      grille= new GrilleObjet(x0, y0, lg, ha);
        ..........
   }

2.3. En ce qui concerne les méthodes invocables sur une grille de la classe GrilleObjetGraphique, il faut distinguer celles qui font qu'un tel objet est également un objet de la classe GrilleObjet de celles qui font qu'il en diffère.
2.3.1. Tout d'abord, les méthodes invocables sur les objets de la classe GrilleObjet doivent l'être sur ceux de la classe GrilleObjetGraphique. Pour cela on procède par délégation. Chacune de ces méthodes est explicitement définie dans la classe GrilleObjetGraphique et se contente d'invoquer la méthode correspondante sur l'objet de la classe GrilleObjet qui y est encapsulé. Cela donne :


        ..........
   boolean estSurLaGrille(int x, int y) {
       return grille.estSurLaGrille(x,y); 
   }
   boolean estAuBord(int x, int y)
   {
     return grille.estAuBord(x, y);
   }
   boolean estUnCoin(int x, int y)
   {
     return grille.estUnCoin(x, y);
   }
   int nombreDeVoisins(int x, int y) {
       return grille.nombreDeVoisins(x,y); 
   }
        ..........

2.3.2. La définition de la classe GrilleObjetGraphique offre la possibilité d'invoquer sur les objets l'instanciant de nouvelles méthodes qui enrichissent l'interface de la classe GrilleObjet.

- La première chose que l'on souhaite pouvoir faire, c'est évidemment visualiser graphiquement une grille. Cela est réalisée par la méthode dessinerGrille :


        ..........
   void dessinerGrille(){
       Deug.startDrawings(ech*(grille.x0 + grille.longueur + bord), 
                     ech*(grille.y0 + grille.hauteur + bord));
       Deug.setGray(0); // dessin en noir
       for(int x = ech * grille.x0; 
                 x < ech * (grille.x0 + grille.longueur); x += ech)
           for(int y = ech * grille.y0;
                 y < ech *(grille.y0 + grille.hauteur); y += ech)
              Deug.drawRect(x, y, ech, ech);
   }
        ..........

- La méthode suivante, visualiserPoint visualise un point (dont les coordonnées sont données en paramètres), s'il est sur la grille, sous la forme d'un carré plein (la couleur est un nombre entier compris entre 0 et 255 correspondant à un niveau de gris, 0 correspondant au noir et 255 au blanc et est transmis en troisième paramètre) :


        ..........
   void visualiserPoint(int x, int y, int couleur){
      if(!estSurLaGrille(x,y)) return;
      Deug.setGray(couleur);
      Deug.fillRect(ech*x-cote/2, ech*y-cote/2, cote, cote);
   }
        ..........

- Enfin, la dernière méthode, montrerVoisins permet la visualisation des voisins d'un point de la grille. Le calcul des coordonnées des voisins d'un point est résumé dans la figure suivante :

Le code de la fonction correspondante est le suivant :

        ..........
   void montrerVoisins(int x, int y, int coul) {
      int a, b;
      Deug.setGray(coul);
      switch(nombreDeVoisins(x,y)) {
        case 0 : return;
        case 3 : a = (x == grille.x0) ? 1 : -1;
                 b = (y == grille.y0) ? 1 : -1;
                 Deug.fillCircle(ech*(x+a), ech*y, rayon);
                 Deug.fillCircle(ech*(x+a), ech*(y+b), rayon);
                 Deug.fillCircle(ech*x, ech*(y+b), rayon);
                 return;
        case 5 : if (x == grille.x0 || x == grille.x0 + grille.longueur) { 
                    Deug.fillCircle(ech*x, ech*(y+1), rayon);
                    Deug.fillCircle(ech*x, ech*(y-1), rayon);
                    a = (x == grille.x0) ? 1 : -1;
                    Deug.fillCircle(ech*(x+a), ech*(y-1), rayon);
                    Deug.fillCircle(ech*(x+a), ech*y, rayon);
                    Deug.fillCircle(ech*(x+a), ech*(y+1), rayon);
                    return; }
                 if (y == grille.y0 || y == grille.y0 + grille.hauteur) {
                    Deug.fillCircle(ech*(x-1), ech*y, rayon);
                    Deug.fillCircle(ech*(x+1), ech*y, rayon);
                    b = (y == grille.y0) ? 1 : -1;
                    Deug.fillCircle(ech*x, ech*(y+b), rayon);
                    Deug.fillCircle(ech*(x-1), ech*(y+b), rayon);
                    Deug.fillCircle(ech*(x+1), ech*(y+b), rayon);
                    return; }
                 // return
        case 8 : Deug.fillCircle(ech*(x-1), ech*(y-1), rayon);
                 Deug.fillCircle(ech*(x-1), ech*y, rayon);
                 Deug.fillCircle(ech*(x-1), ech*(y+1), rayon);
                 Deug.fillCircle(ech*(x+1), ech*(y-1), rayon);
                 Deug.fillCircle(ech*(x+1), ech*y, rayon);
                 Deug.fillCircle(ech*(x+1), ech*(y+1), rayon);
                 Deug.fillCircle(ech*x, ech*(y-1), rayon);
                 Deug.fillCircle(ech*x, ech*(y+1), rayon);
                 return;
       }
   }
        ..........

2.4. On pourra consulter le code complet de la classe GrilleObjetGraphique ainsi qu'un exemple d'application utilisant cette classe.

3. Sous-classe et héritage
3.1. Dans la solution précédente on a fait beaucoup de choses explicitement. On a, tout d'abord, fait apparaître explicitement l'existence d'un objet de la classe GrilleObjet sous-jacent à un objet de la classe GrilleObjetGraphique. Puis, afin de pouvoir utilser un objet de la classe GrilleObjetGraphique comme un objet de la classe GrilleObjet, on a défini dans la nouvelle classe les méthodes qui étaient définies dans la classe GrilleObjet. Même si l'écriture de ces fonctions est simple, le travail étant délégué à l'objet de la classe GrilleObjet sous-jacent, il peut néanmoins paraître fastidieux.
Les langages orientés objets, et Java en particulier, permettent de réaliser de manière implicite ce type de définition de classes au travers du concept de sous-classe et du mécanisme d'héritage qui lui est attaché. Cette facilité, si elle simplifie la définition de nouvelles classes, ne manque cependant pas de soulever de délicates questions, auxquelles tous les langages ne répondent pas de la même manière. Nous n'aborderons pas dans cette première approche ces problèmes et nous contenterons, sur notre exemple d'en voir les avantages.
3.2. Le concept de sous-classe
3.2.1. L'objectif visé est simple : il s'agit de rendre implicite le fait qu'une classe est raffinement d'une autre (certains langages permettent celui de plusieurs autres, mais pas Java). En d'autres termes, un objet instanciant une classe définie comme sous-classe SC d'une autre classe C pourra être vu, de manière implicite comme intanciant la classe C. Cela signifie que la définition de la classe SC ne nécessite pas l'encapsulation explicite d'une référence sur l'objet de la classe C, ni la définition (par délégation) des méthodes invocables sur les objets de cette même classe.
Nous allons illustrer ce mécanisme par la définition d'une classe GrilleObjetGraphiqueHeritage, sous-classe de la classe GrilleObjet (on dit aussi classe dérivée).
3.2.2. D'un point de vue syntaxique, cela est particulièrement simple. En Java, on exprime le fait que GrilleObjetGraphiqueHeritage est une sous-classse de la classe GrilleObjet de la manière suivante dans sa définition :


class GrilleObjetGraphiqueHeritage extends GrilleObjet { 
        ..........

3.2.3. La définition de la sous-classe contient la spécification des champs propres à la nouvelle classe, c'est-à-dire sur notre exemple :


   private int rayon = 7; 
   private int cote = 7;
   private int ech = 15;
   private int bord = 5;

3.2.4. Un point particulier à remarquer sur la définition du constructeur donné ci-après concerne l'utilisation de la méthode notée symboliquement super. Elle correspond à un appel de constructeur de la classe dont la classe courante dérive : sur notre exemple elle permet d'instancier l'objet de la classe GrilleObjet sous-jacent à l'objet de la classe GrilleObjetGraphiqueHeritage au moyen du constructeur de signature GrilleObjet(int, int, int, int).


   GrilleObjetGraphiqueHeritage(int x0, int y0, int lg, int ha) { 
      super(x0, y0, lg, ha);
   }

3.2.5. Finalement, les seules méthodes qui sont à définir (sur notre exemple tout du moins) sont les méthodes non définies dans la classe GrilleObjet et qu'on souhaite ajouter à l'interface de la classe GrilleObjetGraphiqueHeritage (c'est une hautre histoire, mais disons que rein n'interdit de redéfinir dans la classe GrilleObjetGraphiqueHeritage certaines des méthodes définies dans la classe mère GrilleObjet pour en adapter le comportement aux caractéristiques de la nouvelle classe). La définition de ces fonctions est exactement la même que celle donnée pour la classe GrilleObjetGraphique
3.3. On pourra consulter le code complet de la classe GrilleObjetGraphiqueHeritage ainsi qu'un exemple d'application utilisant cette classe.