Concepts de base de la programmation orientée objet

Temps de lecture20 min

En bref

Résumé de l’article

Dans cet article, nous présentons les concepts de base liés à la Programmation Orientée Objet (POO). En particulier, nous étudions les concepts de classes et d’objets et la manière dont ils sont liés les uns aux autres, puis nous abordons la notion d’héritage.

Points clés

  • La programmation orientée objet (POO) est un paradigme de programmation basé sur le concept d’objets, qui peuvent contenir des données et du code : des données sous forme de champs (appelés attributs ou propriétés), et du code sous forme de fonctions/procédures (appelées méthodes).

  • Les programmes informatiques orientés objets sont conçus comme des objets qui interagissent les uns avec les autres.

  • Un objet est une abstraction (logicielle) d’une entité du monde réel impliquée dans l’exécution d’un programme. Il est composé d’une identité, d’un état et d’un comportement.

  • Une classe est une entité qui définit la structure (état ou attributs) et le comportement (méthodes) d’une famille d’objets. Elle se compose d’un nom, d’attributs, de méthodes et d’un ou plusieurs constructeurs. Un constructeur est une méthode particulière dont le but est de créer un objet.

  • L’encapsulation est le principe qui consiste à cacher ou à protéger (une partie) de l’état d’un objet de manière à ce qu’il ne soit pas directement accessible depuis l’extérieur de la classe.

  • L’héritage est une façon de structurer le code qui définit des relations hiérarchiques de factorisation entre les classes. Les attributs et les méthodes d’une classe mère deviennent automatiquement des attributs et des méthodes de ses sous-classes. Les sous-classes peuvent modifier les méthodes de la classe mère (les méthodes de la classe mère peuvent être redéfinies) et en ajouter de nouvelles pour mieux caractériser la sous-classe.

Contenu de l’article

Comme indiqué dans le cours consacré aux paradigmes de programmation, la POO a été introduite pour faciliter la création et la maintenance de programmes informatiques qui résolvent des problèmes complexes. Le principe est très simple : regrouper dans une même entité les données et les actions qui les manipulent, et limiter (voire interdire) l’accès direct aux données. Ces entités sont appelées objets en POO. Le but de ce cours est d’introduire la notion d’objet en POO et certains des concepts associés.

Definition

La programmation orientée objet (POO) est un paradigme de programmation basé sur le concept d’objets, qui peuvent contenir des données et du code : des données sous forme de champs (souvent appelés attributs ou propriétés), et du code sous forme de fonctions (souvent appelées méthodes). Dans la POO, les programmes informatiques sont conçus comme des objets qui interagissent les uns avec les autres.

1 - Objets, attributs et méthodes

Un logiciel orienté objet est constitué d’un ensemble d’objets qui interagissent entre eux pour fournir les fonctionnalités attendues.

Definition

Un objet est une abstraction (logicielle) d’une entité du monde réel qui participe à l’exécution d’un programme. Concrètement, un objet est composé de :

  • un état, représenté par des variables appelées attributs ou propriétés
  • un comportement, qui peut modifier l’état de l’objet et qui est représenté par des fonctions qu’on appelle méthodes.

Par exemple, pour le jeu présenté dans le cours sur les paradigmes de programmation, un objet character pourrait être une abstraction d’un personnage du jeu, il représenterait le personnage dans le logiciel.

La figure ci-après illustre la différence fondamentale entre la structure d’un programme orienté objet et celle d’un programme procédural dans le cas du jeu où on ne tient pas compte des gains d’expérience des personnages. Dans la version procédurale, le programme est décomposé en plusieurs fonctions qui partagent des données communes (dans notre cas, les personnages et leurs données). Dans la version orientée objet, les concepts de personnage et de jeu sont implémentés en tant qu’objets.

Figure 1. Jeu mage-guerrier-voleur : version objet vs procédurale.

Les objets interagissent entre eux en envoyant des messages, auxquels il est possible de répondre, de sorte qu’ils délèguent certaines tâches à leurs collaborateurs. On dit que les objets interagissent par appels de méthode. Un message :

  • spécifie une méthode (m) de l’objet cible,
  • peut contenir des arguments (args),
  • est envoyé par un objet source (s) à un objet cible (t) : t.m(args) invoqué depuis le code de la classe décrivant s,
  • provoque l’exécution de m(args) de l’objet t,
  • finalement t renvoie le résultat de m à s,
  • s peut alors continuer son exécution.

En reprenant l’exemple de la figure 1, lorsque dans le jeu jeu il est nécessaire de savoir si le joueur mage est vivant, le code de jeu contiendra mage.is_alive(). L’exécution de cette instruction implique l’envoi du message is_alive() à l’objet mage. L’objet de type jeu récupère ensuite le retour de l’appel de la méthode is_alive(), c.-à-d. un booléen.

2 - Classes, instanciation et constructeurs

Dans les langages orientés objet, un objet est créé à partir d’une classe. Le terme formel pour la création d’objets est instanciation, les objets étant également appelés instances d’une classe. Une classe est une entité qui définit la structure (état ou attributs/propriétés) et le comportement (méthodes) d’une famille d’objets. Elle se compose :

  • d’un nom,
  • des attributs/propriétés (leur nom et, le cas échéant, leur type),
  • des méthodes (leur nom, leur type de retour, le nom et, le cas échéant, le type de chaque argument, et leur code),
  • un (ou plusieurs) constructeur, une méthode particulière appelée lors de la création d’un objet et dont le rôle est d’initialiser l’état de l’objet (c’est-à-dire ses attributs/propriétés).
Definition

Une classe est une entité logicielle qui définit la structure (état ou attributs/propriétés) et le comportement (méthodes) d’une famille d’objets.

Par exemple, le code du jeu Mage-Guerrier-Voleur, peut être structuré en deux classes : Game et Character. Dans la figure 2, il y a trois objets qui sont des instances de la classe Character et une instance de la classe Game. On voit aussi que chaque objet est instance d’une seule classe, par exemple guerrier est une instance de la classe Character. Ça veut dire que pour créer l’objet guerrier, le constructeur de la classe Character a été appelé et a initialisé l’état de l’objet (donc son nom, ses points de vie de départ, etc). Et ça veut dire aussi que le type de guerrier sera Character.

Figure 2. Objets et classes pour le jeu mage-guerrier-voleur.

Construire un programme orienté objet revient à identifier les classes qui le constituent, écrire ces classes (attributs/propriétés et méthodes), créer des instances (objets) et les appels de méthode entre objets.

2 - Liens entre les objets

Pour qu’un objet puisse faire un appel de méthode sur un autre objet, il doit connaître (l’adresse en mémoire de) l’objet cible. Par exemple, pour qu’un objet jeu puisse appeler la méthode is_alive() de l’objet guerrier, (faire guerrier.is_alive()), jeu doit connaître (l’adresse de) guerrier. C’est la notion de lien entre objets.

On peut créer (programmer) différents types de liens entre objets mais on s’intéresse ici aux associations. Ce type de liens est permanent : L’adresse de l’objet est mémorisée dans un attribut/propriété de la classe (dont l’objet est une instance). Dans notre exemple, ça veut dire qu’un des attributs de la classe Game sera guerrier et le type de cet attribut sera Character. Et si on a programmé une association entre deux classes, l’association sera présente dans tous les objets de ces classes.

3 - Un peu de pseudo-code

Avant d’avancer dans les concepts, on va voir un peu de pseudo-code associé à ceux qu’on a vu jusqu’à présent. Le code ci-après définit une classe Game avec un attribut qui est une liste de personnages (ligne 4), un constructeur (lignes 7-16) et une méthode (combat()) (lignes 19-37). Le constructeur initialise la liste avec trois personnages, un par type.

La méthode combat() est très similaire à la version procédurale. La différence fondamentale vient des fonctions attack() et is_alive() :

  • pour qu’un personnage attaque un autre, la méthode attack(Character) de l’objet striker est appelée avec la cible comme paramètre (ligne 153);
  • pour savoir si un personnage a survecu à une attaque, on appelle la méthode is_alive() de l’objet cible (ligne 156).
Information

Le pseudo-code des méthodes attack(Character) et is_alive() est donné plus loin dans l’article.

Information

Ne vous attardez pas pour l’instant sur le mot clé self dans le pseudo-code de la classe Game. On va expliquer son rôle un peu plus tard dans l’article. Pour l’instant, considérez que lorsqu’on utilise un attribut dans le code de la classe, il faut précéder son nom par ce mot clé.

Enfin, lancer une partie correspond à créer une instance de la classe Game et d’appeler la méthode combat() de l’objet créé.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
CLASS Game:

    # Characters of the game
    ATTRIBUTE characters: Character[]

    # Define the constructor of the class: initialize class attributes
    CONSTRUCT():
        # Character data (instances of Character)
        mage = Character("Mage", 50, 10, guerrier)
        guerrier = Character("Guerrier", 40, 12, voleur)
        voleur = Character("Voleur", 45, 8, mage)

        # Add characters to the game
        self.characters.append(mage)
        self.characters.append(guerrier)
        self.characters.append(voleur)

    # Main game loop
    METHOD combat():
        # All characters are alive at the beginning of the game
        alive_characters = self.characters

        while len(alive_characters) > 1:
            # Select striker and target randomly
            striker = alive_characters.random()
		        target = alive_characters.random()
            while (target == striker):
              target = alive_characters.random()

		        # The striker attacks the target
            striker.attack(target)

            # If target defeated (call method is_alive of object target)
            if not target.is_alive():
                alive_characters.remove(target)

        print(alive_characters[0].name WIN!)

# Create an instance of the game
game = Game()

# Launch the game
game.combat()

4 - Héritage

L’héritage est une façon de structurer le code qui définit des relations hiérarchiques entre les classes. Ce concept très simple s’inspire de notre propre façon cognitive de conceptualiser le monde. Par exemple, un ordinateur portable peut être décrit à différents niveaux de précision : une machine, un ordinateur, un notebook, un Lenovo ThinkPad. De même, dans une application orientée objet, on peut créer une classe de base (appelée classe mère) et des sous-classes plus spécifiques (ou classes enfants). Cette technique nous aide à organiser et à réutiliser le code de manière efficace.

Lors de l’utilisation de l’héritage, le code d’une sous-classe peut être :

  • des attributs et des méthodes caractérisant la classe mère, car ils deviennent automatiquement des attributs et des méthodes de la sous-classe, sans qu’il soit nécessaire de les spécifier davantage, c’est-à-dire que les sous-classes héritent des attributs et des méthodes définies dans la classe mère,
  • de nouveaux attributs et méthodes, ajoutés pour mieux caractériser la sous-classe,
  • des méthodes de la classe mère dont le comportement a été modifié, c’est-à-dire que le comportement des méthodes de la classe mère peut être remplacé dans la sous-classe.

Par exemple, pour le jeu Mage-Guerrier-Voleur, on pourrait créer les classes Wizard, Fighter et Thief qui seraient des sous-classes de Character (voir le code ci-après). Dans ce cas, la classe Character a 4 attributs (lignes 2-5), un constructeur et les méthodes is_alive, gain_experience et attack. Les classes filles (lignes 34-52) modifient les méthodes attack() et is_alive() selon la classe :

  • les trois classes héritent toutes les méthodes et les attributs de la classe Character. Elles peuvent donc manipuler ces attributs et appeler ces méthodes,

  • la classe Wizard (lignes 34-40) modifie le comportement de la méthode is_alive(). Ainsi, voleur.is_alive() (dans la figure 3) retourne vrai si les points de vie de l’objet voleur sont >0 alors que mage.is_alive() retourne vrai si les points de vie de l’objet mage sont >= -5,

  • les trois classes modifient le comportement de la méthode gain_experience() pour implémenter les gains d’expérience lors d’une victoire.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
CLASS Character:
    ATTRIBUTE name: str
    ATTRIBUTE hp: int
    ATTRIBUTE damage: int
    ATTRIBUTE weakness: Character

    # Constructor to initialize character attributes
    CONSTRUCT(name: str, hp: int, damage: int, weakness: Character):
        self.name = name
        self.hp = hp
        self.damage = damage
        self.weakness = weakness

    METHOD is_alive() -> Boolean:
        return self.hp > 0

    METHOD gain_experience():
        pass

    # Attack logic
    METHOD attack(target):
        damage = self.damage
        if type(target.weakness) == type(self):
            damage *= 2

        target.hp -= damage

        # Check if the target is defeated after the attack
        if not target.is_alive():
            # Gain experience for the striker
            self.gain_experience()


CLASS Wizard INHERITS Character:
    METHOD is_alive() -> Boolean:
        return self.hp > -5

    METHOD gain_experience():
        self.hp += 5
        self.damage += 1


CLASS Fighter INHERITS Character:
    METHOD gain_experience():
        self.hp += 10
        self.damage += 2


CLASS Thief INHERITS Character:
    METHOD gain_experience():
        self.hp += 8
        self.damage += 1

Maintenant on va parler du mot clé self. Lorsque vous créez un objet, en l’occurrence un personnage, les valeurs de ses attributs lui sont propres (la valeur de l’attribut name de l’objet mage n’est pas la même que celle de l’objet guerrier). C’est logique, car chaque personnage a son propre nom. Ainsi, lorsque vous appelez une méthode (ou que vous accédez à un attribut) d’un objet, il est important de savoir sur quel objet elle doit être exécutée (accédée). Dans les méthodes d’une classe, on utilise un mot clé spécifique (dans notre pseudo-code, self, mais cela dépendra du langage de programmation utilisé) pour faire référence à l’instance courante de l’objet. Il permet ainsi d’accéder aux attributs et méthodes de l’objet depuis ses propres méthodes. Par exemple, dans la méthode attack() (lignes 21 à 31 de la class Character), l’appel à self.gain_experience() signifie que la méthode gain_experience() sera exécutée sur l’objet courant. Si on appelle mage.attack(voleur) (comme dans la figure 5), self correspondra donc à mage. Inversement, dans guerrier.attack(mage), self désignera l’objet guerrier.

Figure 3. Utilisation des classes filles de Character.

5 - Encapsulation

L’encapsulation est le principe qui consiste à masquer ou à protéger (une partie de) l’état d’un objet afin qu’il ne soit pas accessible depuis l’extérieur de la classe. Il ne peut donc être directement accessible (et modifié) qu’à partir des méthodes de l’objet. Cette sécurité de l’état d’un objet :

  • Protège les données internes de l’objet. Par exemple, elle empêche la modification d’un attribut ou garantit qu’un attribut ne soit mis à jour que lorsqu’un autre est modifié.
  • Simplifie l’utilisation de l’objet. Elle ne peut être effectuée qu’en appelant les méthodes de l’objet, et non en manipulant directement ses attributs.
  • Dissocier l’utilisation d’un objet de sa structure interne. Comme les attributs ne sont pas directement accessibles depuis l’extérieur de la classe, il est possible de modifier l’implémentation de l’état d’un objet sans affecter le code des autres objets.

En conséquence, l’ensemble du processus d’exécution d’un programme orienté objet repose sur un principe simple de répartition des responsabilités : chaque objet doit s’occuper de ses propres attributs.

Dans notre exemple de jeu Mage-Guerrier-Voleur, la classe Game n’utilise pas directement les attributs de la classe Character (voir figure 3). Par exemple, la ligne if not target.is_alive() pourrait être remplacée par if not target.hp > 0 mais 1. il faut que le programmeur de la classe Game sache que les vies des personnages sont codées dans un attribut hp et 2. si l’attribut change, alors le code de la classe Game doit changer aussi.

Pour aller plus loin