Aller au contenu

Programmation D/Programmation orientée objet

Un livre de Wikilivres.

Le langage D permet de faire de la programmation orientée objet. Pour cela, il faut passer par l'utilisation des classes. Il est important de distinguer la classe de l'objet. Par exemple, "Humain" est une classe et "Roger" est un objet. Ainsi, la classe est une définition générale ("Un humain est un mammifère avec 2 jambes, 2 bras, un nom et un âge.") alors que l'objet est le pendant concret de cette définition. On parle alors d'instance ("Roger" est une instance de la classe "Humain").

Exprimons cela en langage D

class Humain{
// Contenu de la classe
}

Par convention le nom d'une classe s'écrit avec la 1ère lettre en majuscule.

Si on reprend l'exemple précédent, les attributs de la classe humain sont :

  • le nombre de jambes
  • le nombre de bras
  • le nom
  • l'âge

Soit :

class Humain
{
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;

}

Vous remarquerez l'utilisation du mot private qui signifie que l'on ne peut accéder à tout ce qui suit depuis l'extérieur de l'objet, par opposition à public. Observez l'exemple qui suit :

class Humain
{
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
 public:
     getNbJambes(){
         return _nbJambes;
     }

}

Humain robert = new Humain(); // création d'un objet robert de type Humain
uint nbJambes = robert._nbJambes; // Erreur: ceci ne peut pas fonctionner car _nbJambes est un attribut privé. On ne peut pas accéder à cette valeur.
uint nbJambes = robert.getNbJambes(); // renvoie le nombre de jambes de l'objet robert

Par convention, on préfixe les attributs par un tiret bas (underscore) : "_".

Les méthodes d'une classe correspondent aux actions. Par exemple, on peut implémenter la méthode "avancer". Pour cela, il faut stocker la position de l'humain, soit :

class Humain
{
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
     int _x;
     int _y;
 public:
     getNbJambes(){
         return _nbJambes;
     }
     avancer(int x, int y){
         _x += x;
         _y += y;
     }

}

Humain robert = new Humain(); // création d'un objet robert de type Humain
robert.avancer(2,3);

Dans cet exemple, il y a 2 méthodes :

  1. getNbJambes
  2. avancer

On fait avancer robert de +2 et +3 sur la grille (abstrait).

Constructeur et Destructeur

[modifier | modifier le wikicode]

Les constructeurs et les destructeurs sont des méthodes particulières.

Le constructeur permet de construire un objet. La méthode utilisée s'appelle "this". Si l'on reprend l'exemple précédent :

class Humain
{
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
     int _x;
     int _y;
 public:
     getNbJambes()
     {
         return _nbJambes;
     }
     avancer(int x, int y)
     {
         _x += x;
         _y += y;
     }
     this(nbJambes, nbBras, nom, age, x, y)
     {
         _nbJambes = nbJambes;
         _nbBras = nbBras;
         _nom = nom;
         _age = age;
         _x = x;
         _y = y;
     }

}

Humain robert = new Humain(2, 2, "robert", 28, 3, 5); // création d'un objet robert de type Humain

Dans l'exemple précédent, on créé un objet robert de type Humain en précisant qu'il a 2 jambes, 2 bras, que son nom est robert, qu'il a 28 ans et se trouve en x:3 y:5.

Le destructeur est appelé par le ramasse-miette (« garbage collector ») pour libérer la mémoire et supprimer l'objet. On peut explicitement demander la destruction de l'objet avec le mot clé delete. Le nom de la méthode destructeur est ~this. Soit :

class Humain
{
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
     int _x;
     int _y;
 public:
     getNbJambes()
     {
         return _nbJambes;
     }
     avancer(int x, int y){
         _x += x;
         _y += y;
     }
     this(nbJambes, nbBras, nom, age, x, y)
     {
         _nbJambes = nbJambes;
         _nbBras = nbBras;
         _nom = nom;
         _age = age;
         _x = x;
         _y = y;
     }
     ~this(){
     }

}

Humain robert = new Humain(2, 2, "robert", 28, 3, 5); // création d'un objet robert de type Humain
delete robert;

Si on ne supprime pas l'objet robert explicitement avec le mot clé delete le ramasse-miette (garbage collector) le supprimera tout seul quand l'objet ne sera plus utilisé. On peut donc choisir de gérer ou de ne pas gérer la mémoire.

Vous avez remarqué le destructeur est vide, en effet aucune variable a été créé par le mot clé new par conséquent le garbage collecteur pourra faire son travail correctement quand il sera appelé.

L'héritage consiste à spécialiser une classe : par exemple, un magicien est un humain. Afin d'éviter de réécrire le code de la classe humain, on réutilise le code et on effectue un héritage, de la manière suivante :

class Magicien : Humain
{
private:
    uint _mana;
public:
    this(uint mana,char[] nom, uint age, x, y)
    {
        super(2, 2, nom, age, x, y); // construction de l'humain
        _mana = mana;
    }
    ~this()
    {
    }
}

Magicien gandalf = new(200, "gandalf", 7000, 156, -54);

Vous remarquerez l'utilisation du mot-clé super celui-ci permet d'appeler le constructeur de la classe mère (c'est à dire ici le constructeur Humain). Pour construire un magicien on doit lui donner une quantité de mana (pouvoirs magiques), un nom, un age et sa position dans l'espace.

Les Classes Abstraites

[modifier | modifier le wikicode]

Une classe abstraite est une classe que l'on ne peut pas instancier (c'est à dire dont on ne peut créer un objet). Par exemple, on peut décider que la classe mammifère est abstraite. En effet, un mammifère en tant que tel n'existe pas (par exemple, l'Homme est un mammifère et n'existe qu'en tant qu'homme).

La mise en place d'une classe abstraite se fait de la manière suivante :

abstract class Mammifère
{
    private:
        uint _temperatureCorporelle;
        uint _taille;
        char[] _espece;
    public:
        this(uint temperatureCorporelle, uint taille, char[] espece)
        {
            _temperatureCorporelle = temperatureCorporelle;
            _taille = taille;
            _espece = espece;
        }
        ~this()
        {
        }
        getTemperatureCorporelle()
        {
            return _temperatureCorporelle;
        }
        getTaille()
        {
            return _taille;
        }
        getEspece()
        {
            return _espece;
        }
}

Ainsi, on ne pourra pas écrire :

Mammifere homme = new Mammifere(37, 170, "Homo sapiens");

Mais on devra créer une classe Homme qui héritera de la classe abstraite Mammifère.

Les interfaces

[modifier | modifier le wikicode]

Le langage D ne supporte pas l'héritage multiple. Pour contourner cela, on utilise les interfaces. Une interface liste des fonctions que doit obligatoirement utiliser une classe. Reprenons la classe Humain et demandons à celle ci d'implémenter les méthodes suivantes:

  • seReposer
  • esquiver
  • marcher
  • courir

Vous en conviendrez, ces méthodes peuvent-être utilisées par d'autres classes que les Humains mais par l'utilisation d'une interface, on standardise le nom de ces méthodes et on évitera ainsi de se retrouver avec du code comme :

  • repos se_reposer regenere
  • esquiver evite
  • marcher marche
  • courir seDepecher

On déclare une interface comme cela :

interface actions{
    void seReposer();
    void esquiver();
    void marcher();
    void courir();
}

Et on l'implémente à une classe ainsi :

class Humain : actions, Mammifere
{
 private:
     uint _nbJambes;
     uint _nbBras;
     char[] _nom;
     uint _age;
     int _x;
     int _y;
 public:
     getNbJambes()
     {
         return _nbJambes;
     }
     avancer(int x, int y)
     {
         _x += x;
         _y += y;
     }
     this(nbJambes, nbBras, nom, age, x, y)
     {
         super(37, 170, "Homo sapiens");
         _nbJambes = nbJambes;
         _nbBras = nbBras;
         _nom = nom;
         _age = age;
         _x = x;
         _y = y;
     }
     ~this(){
     }
    void seReposer(){
    
    }
    void esquiver(){
    
    }
    void marcher(){
    
    }
    void courir(){
    
    }

}

Je vous invite également à lire le tutoriel traitant de la notion d'interface sur le site du zero (bien que le tutoriel en question soit rédigé pour java, la syntaxe est proche et l'utilisation des interfaces est identique).

Les exceptions

[modifier | modifier le wikicode]

La gestion des erreurs en D est syntaxiquement identique à celle de java. Par conséquent, vous pouvez jeter un œil sur le site du zero pour en apprendre plus.

Le langage D propose une classe Exception que vous pourrez proposer en héritage à vos propres classes de gestion d'erreur. Vous trouverez la définition de cette classe dans /usr/include/d/object.di . Cette classe étant définie dans object.d, elle n'a pas besoin d'être importé.

Pour commencer, nous allons voir un cas plus simple, celui du dépassement de la taille d' un tableau :

import tango.core.Exception;
import tango.io.Stdout;

void main()
{
    try 
    {
        int[5] array;
        for (uint i = 0; i <= 10; i++)
        {
            array[i] = 5;
        }
    }
    catch (ArrayBoundsException e)
    {
        Stdout("Vous avez dépassé la taille du tableau !").nl;
        Stdout.formatln("Erreur : {}", e.toString);
    }
}

On définit un tableau de 5 éléments et on essaye d'aller sur le 6ème (cela déclenche l'erreur) :

  • Lorsque le programme atteint une valeur de 6 pour i, une exception de type ArrayBoundsException est levée.
  • Le bloc catch contient justement un objet de type ArrayBoundsException en paramètre. Nous l'avons appelé e (par convention).
  • L'exception étant capturée, l'instruction du bloc catch s'exécute !
  • Le message d'erreur personnalisé s'affiche alors à l'écran.

Note : la définition de l'exception ArrayBoundsException se trouve dans le module tango.core.Exception (c'est pour cela qu'on l'importe ici).

Voyons maintenant le cas de la division par zero, avec une classe Exception personnalisée :

import tango.io.Stdout;

class DivideByZeroException : Exception 
{
    this( char[] file, long line ){
        super("Division par zero interdite !", file, line );
    }
}

void main()
{
    float a = 3;
    float b = 0;
    float c;
    try
    {
        if (b ==0)
            throw new DivideByZeroException(__FILE__,__LINE__);
        c = a/b;
    }
    catch (DivideByZeroException e)
    {
        Stdout("Division par zero impossible !").nl;
        Stdout.formatln("Erreur: {}", e.toString);
    }
}

Un template permet d'écrire une fonction ou une classe qui peut accepter n'importe quel type de paramètre et qui retourne également n'importe quel type de variable. Prenons en exemple une fonction pour additionner deux nombres. Traditionnellement, vous feriez :

int addition (int a, int b)
{
 return a + b;
}
float addition(float a, float b)
{
return a + b;
}
float addition(int a, float b)
{
return a + b;
}

et ainsi de suite. On remarquera que c'est fastidieux et pour pas grand chose !

En D, avec la notion de template, on peut simplifier cela de la manière suivante :

typeof(T + U) Addition(T = typeof(element1), U = typeof(element2))(T element1, U element2)
{
    return element1 + element2;
}

Explication :

  1. on met en type de retour le type retourné par element1 + element2, par exemple si on additionne un entier avec un flottant le résultat sera un flottant
  2. le nom du template
  3. les types que prend le template. Ici il peut prendre 2 types différents, T ou U (T et U peuvent-être de même type). Par défaut, les types T et U sont respectivement du type de element1 et element2
  4. on retourne le résultat de l'addition

Exemple :

import tango.io.Stdout;

typeof(T + U) Addition(T = typeof(element1), U = typeof(element2))(T element1, U element2)
{
    return element1 + element2;
}

void main()
{
    int a   = 3;
    int b   = 5;
    float c = 3.14;
    Stdout.formatln("{} + {} = {}", a, b, Addition !()(a,b));
    Stdout.formatln("{} + {} = {}", a, c, Addition !()(a,c));
}

Note: un template se remarque par identifiant!(type)(parametres)

Ici, nous n'avons pas précisé de type, le template prendra donc les types par défaut de T et U. On pourrait demander, lorsque l'on additionne un entier par un flottant de considérer le dernier comme un entier.

Pour cela, il nous suffit de changer la dernière ligne :

Stdout.formatln("{} + {} = {}", a, c, Addition!()(a,b));
Stdout.formatln("{} + {} = {}", a, c, Addition!(uint, uint)(a,c));

Et d'observer le résultat !