Paradigmes de programmation : du procédural à l'objet

Temps de lecture20 min

En bref

Résumé de l’article

Dans cet article, nous présentons le concept de paradigme de programmation comme la manière d’organiser les constituants d’un programme (données et fonctions). Nous nous concentrerons sur la programmation procédurale et ses limites lorsqu’il s’agit de développer et de maintenir des programmes pour justifier le paradigme de programmation orienté objet. Nous abordons ces notions en utilisant l’exemple de développement d’un jeu.

Points clés

  • Un paradigme de programmation est une méthode ou un style de programmation qui définit un ensemble de principes, de techniques et de modèles pour structurer le code (données et fonctions) afin de résoudre des problèmes sur un ordinateur.

  • Il existe différents types de paradigmes de programmation. Un langage de programmation peut prendre en charge un ou plusieurs paradigmes de programmation. En Python et Java, par exemple, il est possible d’écrire des programmes dans les paradigmes procédural, orienté objet et fonctionnel.

  • Le paradigme orienté objet a été introduit pour surmonter les problèmes de la programmation procédurale, notamment la difficulté de créer et de maintenir des programmes pour résoudre des problèmes de plus en plus complexes.

Contenu de l’article

Important

Dans cette session, nous utilisons le terme pseudo-code. Il désigne une représentation simplifiée d’un programme, sans se soucier de la syntaxe d’un langage de programmation particulier. Il permet de se concentrer sur la logique du programme sans s’imposer de contraintes liées à un langage de programmation. Il est toujours possible, ensuite, de traduire ce pseudo-code dans le langage de programmation de notre choix.

Et bien que nous ayons choisi un pseudo-code dont la syntaxe s’inspire de Python, il ne s’agit pas d’un code pensé pour être exécutable. L’objectif est de se concentrer sur la logique du programme plutôt que sur les détails de la syntaxe.

Du problème à une solution classique

Supposons que l’on souhaite programmer, en pseudo-code, un jeu de rôle de combat. Dans le jeu on a trois types de personnages : le mage, le guerrier et le voleur. Ils ont tous des points de vie au départ (hp), causent des dégâts à ses opposants (damage) et ont un personnage qui leur cause 2 fois plus de dégâts (weakness). Plus précisément :

  • Mage : hp = 50, damage = 10, weakness = Guerrier
  • Guerrier : hp = 40, damage = 12, weakness = Voleur
  • Voleur : hp = 45, damage = 8, weakness = Mage

Une partie se déroule de la manière suivante : à chaque tour, l’attaquant et la cible sont choisis de manière aléatoire. Après l’attaque, les dégâts infligés à la cible sont calculés : la puissance d’attaque de l’attaquant ou 2 fois cette puissance si l’attaquant est le point faible de la cible. La partie est finie quand il ne reste qu’un seul survivant.

Important

L’objectif de l’exercice est bien de programmer (en pseudo-code) la mécanique du jeu et pas l’aspect d’incarnation d’un personnage par un humain.

De manière classique (telle que vous l’avez certainement fait jusqu’à présent), pour réaliser un programme vous :

  • Identifiez les données manipulées par le programme.
  • Identifiez les fonctions, l’algorithmique associée au logiciel que vous voulez programmer.
  • Regroupez ces fonctions et données dans des fichiers (ou modules), c.-à-d. vous trouvez une structure pour le logiciel.

Dans notre jeu, le pseudo-code du programme qui réalise le déroulement d’une partie serait constitué :

  • d’une structure de données pour contenir les personnages de la partie et leurs propriétés,
  • d’une fonction pour implémenter le déroulement de la partie,
  • d’instructions pour le lancement du jeu.

Dans le pseudo-code ci-dessous, la structure de données utilisée est un dictionnaire, la fonction combat() réalise la logique du jeu et l’appel à cette fonction lance une partie.

# Characters as dictionaries
VARIABLE characters = [
{"name": "mage", "type": "Wizard", "hp": 50, "damage": 10, "weakness": "Fighter"},
{"name": "guerrier", "type": "Fighter", "hp": 40, "damage": 12, "weakness": "Thief"},
{"name": "voleur", "type": "Thief", "hp": 45, "damage": 8, "weakness": "Wizard"}
]

FUNC combat():
    # All characters are alive at the beginning of the game
    alive_characters = characters

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

		# The striker attacks the target

		# If target defeated, remove it from alive characters

    # Display the winner name


# Launch the game
combat()

Si on détaille maintenant la méthode combat(), on retrouve deux fonctions secondaires, une qui indique pour un personnage s’il est vivant (is_alive()) et une deuxième pour implémenter une attaque (le calcul des dégâts) entre deux personnages. Et le tout, comme il s’agit d’un problème simple on pourrait le regrouper ensemble dans un fichier game.py.

# Characters as dictionaries
VARIABLE characters = [
{"name": "mage", "type": "Wizard", "hp": 50, "damage": 10, "weakness": "Fighter"},
{"name": "guerrier", "type": "Fighter", "hp": 40, "damage": 12, "weakness": "Thief"},
{"name": "voleur", "type": "Thief", "hp": 45, "damage": 8, "weakness": "Wizard"}
]

# Helper function to check if a character is alive
FUNC is_alive(character: dict) -> Boolean:
    return character["hp"] > 0

# Helper function to implement attack logic
FUNC attack(striker: dict, target: dict):
    damage = striker["damage"]
	if target["weakness"] == striker["type"]:
		damage *= 2
	target["hp"] -= damage

FUNC combat():
    # All characters are alive at the beginning of the game
    alive_characters = characters

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

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

		# If target defeated, remove it from alive characters
        if not is_alive(target):
            alive_characters = alive_characters.remove(target)

    # Display the winner
    print(alive_characters[0]['name'] WIN!)

# Launch the game
combat()

Le pseudo-code ci-dessus répond au problème posé (au cahier de charges initial) et il est bien structuré :

  • la structure de données permet d’avoir les trois types de personnages,
  • les fonctions, is_alive() et attack() accèdent aux propriétés des personnages
  • la fonction combat() implémente la logique d’une partie

On modifie les règles du jeu

Le client est satisfait du livrable et souhaite maintement avoir une version du jeu un peu plus intéressante. Dans la nouvelle version les personnages évoluent avec leurs victoires. A chaque victoire, un personnage gagne en expérience et ces gains sont différents selon le personnage :

  • Mage : +5 hp ; +1 damage
  • Guerrier : +10 hp; +2 damage
  • Voleur : +8 hp; +1 damage

Dans la nouvelle version, ce qui change est la logique de la fonction attack() : une fois les dégâts calculés, il faut déterminer si la cible a été vaincue et, si c’est le cas, calculer les gainss d’expérience de l’attaquant selon le type de personnage :

Pseudo code du jeu mage-guerrier-voleur avec gains d’expérience.

Pour cette extension du jeu, une nouvelle fonction secondaire (gain_experience()) permet de calculer les gains d’expérience des attaquants. Elle est appelée lors d’une attaque avec les bonnes valeurs de paramètres en fonction du type du personnage.

Nouvelle modification du jeu

Dans l’énoncé initial, le client avait oublié de mentionner que le mage n’est vaincu que lorsque ses points de vie sont <= -5. Pour tenir compte de cette nouvelle information, il faut modifier la fonction is_alive() pour tenir compte du type de personnage :

# Characters as dictionaries
VARIABLE characters = [
{"name": "mage", "type": "Wizard", "hp": 50, "damage": 10, "weakness": "Fighter"},
{"name": "guerrier", "type": "Fighter", "hp": 40, "damage": 12, "weakness": "Thief"},
{"name": "voleur", "type": "Thief", "hp": 45, "damage": 8, "weakness": "Wizard"}
]

# Helper function to check if a character is alive
FUNC is_alive(character: dict) -> Boolean:
    if character["type"] == "Wizard":
        return character["hp"] > -5
    return character["hp"] > 0

# Helper function to implement attack logic
FUNC attack(striker: dict, target: dict):
    ...

FUNC combat():
    ...

    # Display the winner
    print(alive_characters[0]['name'] WIN!)

# Launch the game
combat()

Dans le jeu maintenant on trouve :

  • la fonction propre au déroulement d’une partie (combat())
  • des fonctions qui manipulent les propriétés des personnages (attack(), is_alive() et gain_experience())

et ces fonctions partagent la structure de données avec les personnages et, pour chacun, ses propriétés (voir figure ci-après).

Version classique du jeu mage-guerrier-voleur.

Cette manière de structurer les programmes est très bien lorsqu’il s’agit de résoudre des problèmes simples. Mais, dès que les besoins sont un peu plus élaborés avec de la diversité dans les données et leurs propriétés et dans le comportement, elle mène à des programmes complexes qui seront difficiles à modifier pour tenir compte de nouveaux besoins et/ou simplement pour les déboguer. C’est pour cette raison que nous introduisons aujourd’hui une autre manière de structurer les programmes : La programmation orientée objet. Mais d’abord, on va formaliser ce qu’on a illustré dans cette partie avec un exemple.

Les paradigmes de programmation

Qu’est-ce qu’un paradigme de programmation?

Depuis leur création, les programmes informatiques combinent des algorithmes (c’est-à-dire les instructions utilisées pour résoudre un problème) et des données (c’est-à-dire les informations manipulées par les algorithmes). C’est la manière dont ces deux aspects sont structurés et organisés qui a évolué au fil du temps. Cette structuration et cette organisation du code informatique constituent ce que nous appelons un paradigme de programmation.

Definition

Un paradigme de programmation est une méthode ou un style de programmation qui définit un ensemble de principes, de techniques et de modèles pour structurer le code afin de résoudre des problèmes sur un ordinateur.

Types de paradigmes de programmation

Les paradigmes de programmation peuvent être classés en différentes catégories, notamment les paradigmes de programmation impérative, fonctionnelle ou logique. Jusqu’à présent dans votre formation, vos programmes ont (très probablement) suivi le paradigme impératif : L’exécution du programme est donnée par l’ordre des instructions de contrôle présentes dans le code. Plus précisément, vous avez utilisé la programmation procédurale, dans laquelle un programme est décomposé en une série de fonctions/procédures dont le but est d’exécuter une tâche particulière et qui partagent un ensemble de données.

Comme nous l’avons vu dans la partie précédente, ce paradigme est adapté pour créer des logiciels de petite taille qui évoluent peu. Mais dès que le logiciel est un peu plus compliqué, il présente un certain nombre d’inconvénients liés à la nécessité d’utiliser des données globales, c’est-à-dire partagée par différentes fonctions. Lorsque le problème devient un peu plus complexe :

  • Il est plus difficile de comprendre un programme, de le déboguer et de le modifier ultérieurement.

  • Lors du débogage d’un programme, si une variable globale contient des informations erronées, il est plus difficile de trouver la source de l’erreur, car la variable peut avoir été modifiée dans n’importe quelle partie du programme.

  • Lors de la modification d’un programme, il est complexe de changer la façon dont une variable globale est gérée, car il faut comprendre l’ensemble du programme, étant donné que la variable peut être modifiée à partir de n’importe quelle fonction.

Au fil du temps, les problèmes à résoudre par les programmes informatiques ont gagné en complexité : comptabilité, jeux automatiques, compréhension et traduction des langues naturelles, aide à la décision, programmes graphiques, etc. L’architecture et le fonctionnement d’un programme procédural destiné à résoudre un problème donné sont devenus difficiles à produire, à comprendre et à maintenir. Il est donc devenu vital d’inventer des mécanismes informatiques simples à mettre en œuvre qui réduiraient cette complexité et rapprocheraient l’écriture des programmes de la façon dont les humains posent et résolvent les problèmes. Et c’est là que le paradigme de programmation orienté objet entre en jeu.

Pour aller plus loin