Ce cours est une introduction à la modélisation de programmes et à la programmation orientée objet. Il utilise UML 2.5 pour la première, et donne des exemples en Python, Java et C++ pour la seconde.
La dernière version de ce cours, ainsi que des exercices et les diapositives présentées en cours sont disponibles sur https://nodet.github.io.
1. Pré-requis
Pour étudier ce cours, vous devriez connaître quelques bases de la programmation : notions de variables (locales et globales), les structures de contôles, et les fonctions. Par exemples, les chapitres 1 et 4 de https://www.rocq.inria.fr/secret/Anne.Canteaut/COURS_C/. Mais n’importe quel autre langage fera l’affaire…
2. Introduction
Le but de ce cours est d’offrir une introduction aux notions utilisées dans la modélisation de programme et la programmation orientée objet. Il ne fera pas du lecteur un analyste, ni un programmeur. Mais il permettra d’être familier des concepts utilisés par ces derniers, et ainsi l’établissement d’une communication plus efficace avec eux.
Le cours est divisé en trois parties qui s’appuient chacune sur les résultats des parties précédentes. L'étude fonctionnelle permet d’établir les fonctions que doit remplir le système étudié. Ces fonctions seront décrites sous forme de textes et de diagrammes, en considérant le système de l’extérieur pour décrire ce qu’il doit faire. La modélisation statique décrit les entités qui composent le système et son entourage, ainsi que les relations entre ces entités. La programmation orientée objet traduit dans un langage informatique les éléments explicités dans les deux premières phases.
3. Étude fonctionnelle
L’étude fonctionnelle d’un système a pour but de présenter les fonctions que doit remplir ce système. Elle doit le faire de façon suffisamment détaillée pour permettre ensuite la modélisation de ce système, et enfin son implémentation dans un langage informatique. Elle utilise en entrée les éléments fournis par le client, et fournit en sortie différents textes et diagrammes qui forment une description précise et complète du système, vu de l’extérieur à travers les intéractions qu’il a avec son environnement.
Très souvent, les éléments fournis en entrée sont complètement insuffisants pour l’établissement d’une étude fonctionnelle. L’analyste doit donc discuter avec le client, avec les futurs utilisateurs du système, avec les experts métier, pour obtenir les détails dont il a besoin. En particulier, il devra rendre explicites toutes les conditions nécessaires au bon déroulement des opérations. Il doit faire préciser les différents cas d’erreurs, les performances attendues du système, et tous autres éléments qui permettront de valider que le système répond bien aux besoins du client.
Les livrables de l’étude fonctionnelle sont la liste des acteurs et les cas d’utilisation. Ces derniers peuvent être regroupés dans des diagrammes de cas, et complétés de diagrammes d’activité ou de séquence.
3.1. Acteur
Un acteur représente un rôle spécifique joué par une entité qui interagit directement avec le système considéré. Il ne fait donc pas partie du système. Un acteur peut être humain ou non.
-
L’utilisateur d’une carte de paiement lors d’une transaction sur Internet.
-
Le système de gestion des stocks, dans l’étude d’une caisse de supermarché.
3.1.1. Représentation
Les acteurs humains sont généralement représentés par un stick man, tandis que les acteurs non-humains sont représentés par un rectangle muni d’un tag <<actor>>.
3.1.2. Spécialisation d’un acteur
Lorsqu’un acteur A
peut realiser tout ce qu’un autre acteur B
peut faire, et ajoute d’autres rôles, on dit que A
est une spécialisation de B
.
L’acteur A
est un B
puisqu’il peut faire tout ce que B
peut faire. Mais comme il possède également un ou plusieurs rôles propres (i.e. il intervient dans des cas d’utilisation qui ne concernent pas le B
), c’est un acteur distinct.
-
Le superviseur d’un employé peut remplacer l’employé dans tout ce que fait ce dernier. Donc le superviseur est un employé. Mais comme il peut aussi effectuer des tâches que l’employé ne peut pas effectuer, les deux acteurs ne sont pas confondus. Donc le superviseur est une spécialisation de l’employé.
3.2. Exemple de cas d’utilisation
3.3. Cas d’utilisation
Un cas d’utilisation décrit une séquence d’évènements pendant laquelle un acteur, qualifié alors d’acteur principal, intéragit avec le système pour obtenir un résultat qui l’intéresse. Chaque cas d’utilisation décrit le comportement attendu du système lors de cette interaction.
Un cas d’utilisation ne décrit pas comment le système réagit aux actions des acteurs, mais quel est le résultat visible de ces actions. Le système est considéré comme une boite noire, et c’est seulement par les interactions avec les acteurs que son comportement est décrit.
3.3.1. Scénario nominal
Un cas d’utilisation comprend un et un seul scénario, dit nominal, qui décrit le déroulement attendu pour ce cas, sans erreur. Par exemple, le porteur de carte de retrait ne fait pas d’erreur sur son code, demande un montant qui n’est pas supérieur à la limite, prend un ticket, et retire sa carte et l’argent.
3.3.2. Enchaînements alternatifs
En plus du scénario nominal, un cas d’usage peut contenir des enchaînements alternatifs. Le choix d’un enchainement alternatif est fait par l’acteur principal. En effet, le choix fait par un acteur secondaire devrait soit être transparent pour l’acteur principal, soit entrainer une erreur. Nous ne serions donc pas dans le cas d’un enchaînement alternatif.
-
Le porteur de carte fait une ou deux (mais pas trois) erreurs de code.
-
Le client présente sa carte de fidélité à la caisse
3.3.3. Enchaînements d’erreur
Un cas d’usage contiendra souvent un ou plusieurs enchaînements d’erreur décrivant la réaction du système aux erreurs possibles. Par définition, les enchaînements d’erreur se finissent sur une situation dans laquelle l’acteur principal n’obtient pas ce qu’il cherchait. Dans le cas contraire, il s’agit d’un scénario alternatif.
3.3.4. Préconditions, post-conditions
Un cas d’utilisation peut n’être valide que sous certaines conditions particulières. Par exemple, le cas d’utilisation le client paye ses articles avant de sortir du magasin peut nécessiter que la caisse est ouverte. Ces pré-conditions font partie du cas d’utilisation, et doivent donc être incluse dans ce dernier.
D’autre part, un cas d’utilisation peut inclure des assertions qui doivent être vraies lorsque le cas d’utilisation se déroule sans erreur, nommées post-conditions. Par exemple, le cash disponible dans le distributeur est égal à ce qu’il était au début du cas d’utilisation, moins ce que le client a demandé.
3.3.5. Exigences non-fonctionnelles
Un cas d’utilisation peut aussi lister des contraintes qui doivent être respectées par le système. Ces contraintes ne se traduisent pas par des actions ou des évènements qui feraient partie d’un scénario, mais par des éléments techniques à prendre en compte pendant l’étude du système pour garantir que ce dernier satisfera le donneur d’ordre.
-
Toute action sur l’écran du système entraine une réaction visible par l’utilisateur en moins de 500 ms.
-
Le programme sera exécuté sur un ordinateur de type Raspberry Pi 4 muni de 2 GO de mémoire.
-
Le serveur supportera au moins 250 requêtes par secondes provenant de 100 utilisateurs différents avec 99% des requêtes servies en moins de 300 ms.
3.3.6. Relations entre cas d’utilisation
Il peut exister des relations entre les différents cas d’utilisation. Par exemple, un cas d’utilisation peut en utiliser un autre ("use" ou "include"). Un cas d’utilisation peut également étendre un autre cas ("extend").
-
Le cas d’utilisation "passage en caisse" utilise le cas "payer les achats".
-
Le paiement à la caisse d’un magasin peut être fait en liquide ou par carte de paiement. Ce sont deux cas d’utilisation séparés, qui étendent le cas général "payer les achats".
3.3.7. Diagrammes de cas
Les cas d’utilisations peuvent être groupés dans un ou plusieurs diagrammes qui permettent de représenter les acteurs participant à chaque cas.
3.4. Diagramme d’activité
Un diagramme d’activité est (avec le diagramme de séquence, voir plus loin) une des représentation graphique possible pour les actions décrites dans un cas d’utilisation.
Un diagramme d’activité est similaire à un organigramme traditionnel. Les éléments suivants peuvent y être représentés :
-
Début (unique, rond noir plein)
-
Fin (unique, rond noir à bordure)
-
Actions (rectangles arrondis)
-
Tests (losanges)
-
Flot de contrôle (flèches)
-
Signal reçus ou émis (drapeau)
-
Commentaire (étiquettes)
-
Début de traitements en parallèle (aka fork, barre épaisse noire)
-
Fin de traitements en parallèle (aka join, barre épaisse noire)
Voici un exemple de diagramme d’activité.
3.5. Diagramme de séquence
Un diagramme de séquence montre les actions réalisées ou les messages échangés entre différents acteurs lors de la réalisation d’un cas d’utilisation (ou d’un scénario d’un cas d’utilisation).
Chaque acteur est représenté par une ligne verticale. L’acteur principal du cas d’utilisation est placé à gauche, avec le système étudié au milieu, et les autres acteurs vers la droite. Le temps défile du haut vers le bas, et les messages sont donc ordonnés dans le temps par leur position relative.
Les messages sont de deux types :
-
Synchrones : les messages synchrones sont envoyés, sont reçus par leur destinataire, et reviennent avec une réponse en une seule action. L’emetteur ne peut rien faire d’autre en attendant la réponse. Ils sont représentés avec une flèche pleine.
-
Asynchrones : l’envoi du message et sa réception par le destinataire sont décorrellés. Le second peut avoir lieu significativement après le premier. L’emetteur peut continuer ses traitements sans attendre de réponse du destinataire. S’il doit y avoir une réponse, elle sera sous la forme d’un nouveau message qui sera émis par le destinataire du premier message. Ils sont représentés par une flèche en pointillé.
Les messages asynchrones sont les plus courants lorsqu’ils sont émis entre systèmes distants. D’autre part, les messages sont nécessairement émis de façon asynchrone si l’emetteur ne doit pas être bloqué en attendant la réponse.
Un rectangle au dessus de la ligne de vie symbolise les traitements effectués par l’acteur.
-
Le guichetier peut envoyer une demande d’approbation de crédit au responsable. Il doit alors attendre la réponse avant de pouvoir servir le client.
-
Un client peut faire une demande de crédit et il peut recevoir des informations bancaires par téléphone ou retirer de l’argent à un GAB, tout en attendant toujours une réponse à sa demande de crédit.
4. Modélisation statique
La modélisation statique consiste à transformer les besoins exprimés lors de l’étude fonctionnelle en entités, attributs et relations, qui seront ensuite implémentées dans un langage de programmation.
Idéalement, l’étude fonctionnelle est suffisamment détaillée pour que l’analyste n’ait plus besoin d’intéragir avec le client. Il peut maintenant utiliser ces spécifications pour définir l’organisation interne du système.
4.1. Décomposition
Lorsque le système étudié est complexe, il sera décomposé en sous-systèmes. Chaque sous-système fera l’objet d’une étude séparée, dans laquelle le sous-système étudié est considéré dans son ensemble, et dans laquelle les autres sous-systèmes seront des acteurs intéragissant avec le sous-système étudié. Ce processus récursif peut être répété jusqu’à obtenir des systèmes suffisamment simples pour être considérés par eux-même.
4.2. Classes et objets
Chaque type d’acteur est représenté par une classe.
Les éléments manipulés dans le système (e.g. les livres d’une bibliothèque) sont également représentés par une classe.
Une classe définit un modèle, un patron, à partir duquel on pourra construire des instances, appelées objets. Un objet ne peut pas changer de classe.
Ainsi, chaque objet est construit à partir d’une et une seule classe.
Réciproquement, une classe peut être instanciée plusieurs fois pour construire autant d’objets.
Par exemple, si on gère le stock d’un vendeur de voitures d’occasion, il y aura probablement autant d’instances de la classe Voiture
que de véhicules à gérer.
Le fait qu’une classe ne soit instanciée qu’une fois dans un programme ne témoigne cependant pas forcément d’un problème. C’est même assez courant pour représenter, par exemple, l’IHM de l’appareil étudié, ou le système de gestion des stocks avec lequel une caisse enregistreuse devra communiquer.
4.3. Attributs
Un attribut est une propriété d’une classe qui associe une information à chacun des objets instanciés à partir de cette classe. Un attribut peut avoir un type spécifié. Le type d’un attribut, s’il est spécifié, doit être simple: entier, chaîne de caractère, date, etc. Ce ne sera donc pas, par exemple, une référence à un autre instance : les liens entre objets ne sont pas des attributs, mais des associations (voir ci-après).
-
Auteur, titre, éditeur seront des attributs de la classe Livre dans le système d’une bibliothèque.
Un attribut peut être dit dérivé. C’est un attribut dont la valeur peut être calculée d’après les autres données. Son nom est préfixé d’un "/".
4.4. Opérations
Une opération est un service que peuvent rendre les instances de cette classe. On peut distinguer trois types d’opérations:
-
demande d’information
-
enregistrement d’information
-
traitements sans échange d’informations
-
nombre_emprunts_en_cours peut être une opération de la classe Utilisateur pour un système de gestion de bibliothèque, qui retourne le nombre d’articles que l’utilisateur a emprunté et pas encore rendu.
-
rendre(livre) peut être une opération de la classe Utilisateur pour un système de gestion de bibliothèque, qui enregistre le fait, pour cet utilisateur, de rendre un livre.
-
envoyer_rappel(utilisateur, livre) peut être une opération de la classe Bibliothèque qui envoie à un utilisateur un rappel concernant un livre qu’il a emprunté depuis trop longtemps.
4.5. Représentation d’une classe
4.6. Associations
Une association est une relation sémantique durable entre deux classes. Elle est généralement nommée par un verbe.
-
Une bibliothèque possède des livres. La relation possède est une association entre la classe Bibliothèque et la classe Livre.
-
Un utilisateur emprunte des livres. La relation emprunte est une association entre la classe Utilisateur et la classe Livre.
Par défaut, une association est bidirectionnelle. Mais nous verrons plus loin un type d’association qui ne l’est pas. On peut ajouter un symbole '>' ou '<' pour indiquer le sens de lecture donné par le nom de l’association. On peut préciser le rôle joué par chaque classe dans l’association, par un nom donné à chaque entité.
4.7. Multiplicités
Pour donner une indication sur le nombre d’objets qui prennent part à chaque sens de l’association, on utilise des multiplicités.
Une multiplicité est déterminée en considérant, pour un instant donné et une instance de la première classe, combien d’instances de la deuxième classe peuvent être impliquées dans la relation.
Une multiplicité indique soit l’intervalle des valeurs possibles (noté n..m
), soit l’unique valeur possible.
Un nombre quelconque est représenté par une *
.
-
Un utilisateur peut emprunter entre 0 et un nombre quelconque de livres :
0..*
. -
Un livre peut être emprunté par 0 ou 1 utilisateur :
0..1
. -
Une bibliothèque possède entre 0 et un nombre quelconque de livres :
0..*
. -
Un livre est possédé par une et une seule bibliothèque :
1
Les multiplicités sont placées du côté qui rend la lecture naturelle : une bibliothèque → possède → 0 ou plusieurs → livres.
4.8. Agrégation et composition
Un cas particulier d’association uni-directionnelle est l'agrégation. Les deux (ou plus) classes n’ont plus un rôle symétrique.
4.8.1. Agrégation
L’agrégation peut être vue comme une relation d’appartenance faible. Elle n’implique pas qu’il n’y a qu’un seul contenant.
-
Une page web contient des images
-
Un ordinateur comporte (ou pas) un écran, un clavier, un ou plusieurs disques durs.
4.8.2. Composition
Si l’agrégat gère le cycle de vie des sous-parties, qui ne peuvent appartenir qu’à un seul agrégat, on parle de composition.
-
le chassis est une partie constitutive de la voiture : il n’a pas d’existence en dehors de la voiture.
4.9. Généralisation
Deux classes peuvent avoir une relation de généralisation. Dans l’autre sens, on parlera de spécialisation.
Les instances d’une classe spécialisée sont aussi des instances de la classe de générale (ou classe de base, ou super-classe) : elles peuvent être utilisées partout où la classe générale peut être utilisée.
La classe spécialisée hérite de toutes les propriétés (attributs, opérations et relations) de la classe générale. Elle peut rajouter ses propres propriétés.
Elle respecte le contrat de la classe de base :
-
les invariants de la classe de base sont vérifiés par la classe spécialisée ;
-
les pré-conditions associées aux opérations de la classe spécialisées ne sont pas plus restrictives que les pré-conditions associées aux mêmes opérations de la classe de base ;
-
de même, toutes les post-conditions des opérations de la classe de base sont vérifiées par les mêmes opérations appliquées à la classe spécialisée.
4.10. Relation n-aire
Une relation peut metre en jeu plus de deux classes.
-
Les docteurs prescrivent des médicaments à leurs patients.
-
Les cours sont donné par des enseignants, pour enseigner des matière à des groupes d’élèves.
-
Les clients passent des commandes pour des objets proposés par des vendeurs.
Ces relations sont représentées par un losange lié aux différentes classes.
Chaque multiplicité s’entend comme le nombre d’instance quand les autres éléments de la relation sont fixés.
Ainsi, pour un client et un vendeur donné, il peut y avoir un ou plusieurs objets commandés (puisqu’une commande porte sur au moins un objet). La multiplicité de la classe Objet
est donc 1..*
.
De même, si un objet a été commandé à un vendeur, il y a nécessairement au moins un client et la multiplicité correspondant à la classe Client
est donc 1..*
également.
Par contre, si on suppose qu’un objet n’apparait que chez un seul vendeur, alors la multiplicité de Vendeur
ne pourra être que 1
: si une commande a été enregistrée pour un client et un objet, il y a nécessairement un vendeur, mais il n’y en a pas plus car seul ce vendeur propose cet objet.
De façon similaire, pour les deux autres exemples que sont les prescriptions d’un médecin et l’affectation d’un enseignant à une matière, on trouve des multiplicités 1
ou 1..*
.
4.11. Classe-association
Il est souvent nécessaire de stocker des informations à propos d’une relation. Par exemple, on peut vouloir stocker le niveau de compétence d’un employé pour une compétence donnée. Ou le nombre d’actions d’une société détenues par une personne dans une banque donnée. L’association est alors définie par une classe-association, qui portera les attributs et les opérations.
Graphiquement, la classe en question est liée par un trait pointillé au trait plein qui représente la relation. Ou au losange, dans le cas d’une relation n-aire.
Note: quand on utilise une classe-association, il ne peut exister qu’au plus une seule instance de la classe pour une paire donnée d’instances des classes liées (ou pour un n-uplet d’instances, dans le cas d’une relation n-aire). Ainsi, il n’existe nécessairement qu’un seul niveau de compétence pour un individu donné et une compétence donnée.
Si l’on souhaite pouvoir s’affranchir de cette limitation, il faut utiliser une classe 'normale' et la lier par deux relations aux deux autres classes. Dans ce cas, on aura probablement des multiplicités 1 vers ces classes. Ainsi, si plusieurs contrats de travail peuvent lier successivement la même société et le même employé (et donc on ne peut pas utiliser une classe-association), un contrat de travail donné est nécessairement lié à exactement une entreprise, et un employé.
5. Programmation Orientée Objet
Une fois le modèle statique définit, on peut l’exprimer dans n’importe quel langage de programmation.
En C, on pourrait utiliser des struct
pour grouper les attributs d’un objet, et nommer les fonctions qui représentent les messages en les prefixant du nom de la classe dans laquelle elle sont définies.
L’héritage ne s’exprimerait pas directement, cependant…
Les langages orientés objet permettent d’exprimer directement dans le code source les concepts introduits par la modélisation statique. En particulier, les notions de classes, objets, attributs, opérations et généralisations apparaissent explicitement dans ces langages.
5.1. POO en Python
5.1.1. Définition d’une classe
class MyClass: (1)
def __init__(self, arg1, arg2): (2)
self._val = None (3)
self._val1 = arg1 (4)
self._val2 = arg2
self._update_val() (5)
def _update_val(self): (6)
self._val = self._val1 + self._val2
def display_val(self): (7)
print(f'My value is {self._val}')
the_instance = MyClass(1, 2) (8)
the_instance.display_val() (9)
-
Début de la définition de la classe.
Comme toujours en Python, la fin de la définition est marquée par le retour à l’indentation d’origine. -
Définition d’un constructeur pour cette classe.
Un constructeur, en Python, est une méthode appelée__init__
.
Elle sera appelée lors de la création d’une instance de cette classe.
Elle recevra les arguments passés lors de cette instanciation. -
Toutes les instances de cette classe ont un attribut
_val
. -
Définition d’attributs supplémentaires.
Leurs noms commence par un_
et donc, par convention, ils sont privés. -
Envoi d’un message à soi-même, i.e. appel d’une méthode de la classe elle-même.
-
Définition d’une méthode de la classe.
Cette méthode doit être considérée comme privée puisque son nom commence par_
. -
Une autre méthode, qui doit être appelée sans argument.
-
Création d’une instance de la classe, stockée dans une variable nommée
the_instance
. -
Envoi d’un message à cette instance. I.e. appel d’une méthode de cette instance.
Le résultat de ce programme est:
My value is 3
5.1.2. Héritage
from datetime import datetime
class Auteur:
def __init__(self, nom, prenom):
self.nom = nom
self.prenom = prenom
class Oeuvre:
def __init__(self, auteur, titre, annee_creation):
self.auteur = auteur
self.titre = titre
self.annee_creation = annee_creation
def describe(self):
print(f'Auteur: {self.auteur.prenom} {self.auteur.nom}')
print(f'Titre: {self.titre}')
def age(self):
return datetime.now().year - self.annee_creation
class Livre(Oeuvre): (1)
def __init__(self, auteur, titre, annee_creation, nb_mots): (2)
super().__init__(auteur, titre, annee_creation) (3)
self.nb_mots = nb_mots
def describe(self):
super().describe()
print(f'Nombre de mots: {self.nb_mots}')
jules = Auteur('Verne', 'Jules')
vingtk_lieues = Livre(jules, 'Vingt mille lieues sous la mer', 1869, 142172)
vingtk_lieues.describe()
age = vingtk_lieues.age() (4)
print(f'{vingtk_lieues.titre} a été écrit il y a {age} ans.')
-
Chaque instance de la classe
Livre
est uneOeuvre
. -
Une classe, en Python, n’a qu’un seul constructeur. Puisqu’on définit un constructeur pour
Livre
, il remplace celui qui est défini dans la classeOeuvre
et dontLivre
aurait hérité par défaut. -
On peut appeler une méthode de la classe de base sans nécessairement spécifier le nom de cette classe grâce à
super()
. Cette ligne est équivalente àOeuvre.__init__(auteur, titre, annee_creation)
. -
On peut demander son âge à un
Livre
, puisque c’est uneOeuvre
.
Le résultat de ce programme est:
Auteur: Jules Verne Titre: Vingt mille lieues sous la mer Nombre de mots: 142172 Vingt mille lieues sous la mer a été écrit il y a 154 ans.
5.2. POO en Java
5.2.1. Définition d’une classe
class MyClass {
int val; (1)
int val1;
int val2;
public MyClass(int arg1, int arg2) { (2)
val1 = arg1;
val2 = arg2;
update_val();
}
public void update_val() { (3)
val = val1 + val2;
}
public void display_val() {
System.out.println("My value is " + val);
}
public static void main(String args[]) { (4)
MyClass the_instance = new MyClass(1, 2); (5)
the_instance.display_val(); (6)
}
}
-
En java, les attributs sont publics: ils peuvent être directement référencés de l’extérieur de la classe.
-
En Java, un constructeur est une méthode qui porte le nom de la classe, et n’a pas de type de retour.
Un constructeur est généralementpublic
, afin qu’il puisse être utilisé de l’extérieur de la classe. -
La visibilité d’une méthode est indiquée dans sa déclaration
-
En Java, la fonction
Main
doit faire partie de la classe. C’est elle qui sera automatiquement appelée au démarrage du programme. -
Création d’une instance de la classe, initialisée par l’appel du constructeur avec les arguments spécifiés.
-
Envoi d’un message à cette instance, i.e. appel d’une méthode de cette instance.
Le résultat de ce programme est:
My value is 3
5.2.2. Héritage
class Test {
public static void main(String args[]) {
Auteur jules = new Auteur("Verne", "Jules");
Oeuvre vingtk_lieues = new Livre(jules, (5)
"Vingt mille lieues sous la mer",
1869, 142172);
vingtk_lieues.describe();
int age = vingtk_lieues.age(); (4)
System.out.println(vingtk_lieues.titre_ + " a été écrit il y a "
+ age + " ans.");
}
}
class Auteur {
String nom_;
String prenom_;
public Auteur(String nom, String prenom) {
nom_ = nom;
prenom_ = prenom;
}
}
class Oeuvre {
Auteur auteur_;
String titre_;
int annee_creation_;
public Oeuvre(Auteur auteur, String titre, int annee_creation) {
auteur_ = auteur;
titre_ = titre;
annee_creation_ = annee_creation;
}
public void describe() {
System.out.println("Auteur: " + auteur_.prenom_ + " " + auteur_.nom_);
System.out.println("Titre: " + titre_);
}
public int age() {
return 2021 - annee_creation_;
}
}
class Livre extends Oeuvre { (1)
int nb_mots_;
public Livre(Auteur auteur, String titre, int annee_creation, int nb_mots) {
super(auteur, titre, annee_creation); (2)
nb_mots_ = nb_mots;
}
public void describe() {
super.describe(); (3)
System.out.println("Nombre de mots: " + nb_mots_);
}
}
-
Chaque instance de la classe
Livre
est uneOeuvre
. -
On peut initialiser la classe de base (ici,
Oeuvre
) grâce au mot-clésuper
. -
On peut appeler une méthode de la classe de base sans nécessairement spécifier le nom de cette classe grâce à
super
. -
On peut demander son âge à un
Livre
, puisque c’est uneOeuvre
. -
Puisqu’une instance de
Livre
est aussi une instance deOeuvre
, on peut stocker l’instance nouvellement créée dans une variable de typeOeuvre
.
Le résultat de ce programme est:
Auteur: Jules Verne Titre: Vingt mille lieues sous la mer Nombre de mots: 142172 Vingt mille lieues sous la mer a ?t? ?crit il y a 152 ans.
5.3. POO en C++
5.3.1. Définition d’une classe
#include <iostream>
class MyClass {
private: (1)
int val_;
int val1_;
int val2_;
public:
MyClass(int arg1, int arg2) (2)
: val1_(arg1)
, val2_(arg2)
{
update_val();
}
void update_val() {
val_ = val1_ + val2_;
}
void display_val() const { (3)
std::cout << "My value is " << val_ << std::endl;
}
};
int main() {
MyClass* the_instance = new MyClass(1, 2); (4)
the_instance->display_val(); (5)
delete the_instance; (6)
return 0;
}
-
En C++, les règles d’accès sont explicites, et enforcées par le compilateur.
En général, les attributs sont privés pour minimiser le couplage entre les clients et l’implémentation de la classe.
Une convention usuelle est de faire suivre les noms de variables membres d’un _ pour les distinguer des variables. -
Toute fonction membre qui porte le nom de la classe est un constructeur.
Un constructeur peut comporter une liste d’initialisation. -
le mot-clé
const
indique que la fonction ne modifie pas l’objet sur lequel elle s’applique. -
L’opérateur
new
crée une instance de la classe en utilisant le constructeur qui correspond aux arguments fournis, et renvoie un pointeur sur cette instance. -
o→f()
est équivalent à(*o).f()
. C’est à dire appel de la fonction membref
de l’objet pointé par le pointeuro
. -
Détruit l’objet pointé.
Le résultat de ce programme est:
My value is 3
5.3.2. Héritage
#include <iostream>
#include <string>
class Auteur {
private:
std::string nom_;
std::string prenom_;
public:
Auteur(std::string nom, std::string prenom)
: nom_(nom)
, prenom_(prenom)
{}
std::string nom() const {return nom_;}
std::string prenom() const {return prenom_;}
};
class Oeuvre {
private:
Auteur* auteur_;
std::string titre_;
int annee_creation_;
public:
Oeuvre(Auteur* auteur, std::string titre, int annee_creation)
: auteur_(auteur)
, titre_(titre)
, annee_creation_(annee_creation)
{}
virtual void describe() const { (5)
std::cout << "Auteur: " << auteur_->prenom() << " "
<< auteur_->nom() << std::endl;
std::cout << "Titre: " << titre_ << std::endl;
}
int age() const {
return 2021 - annee_creation_;
}
std::string titre() const {
return titre_;
}
};
class Livre : public Oeuvre { (1)
private:
int nb_mots_;
public:
Livre(Auteur* auteur, std::string titre, int annee_creation, int nb_mots)
: Oeuvre(auteur, titre, annee_creation) (2)
, nb_mots_(nb_mots)
{}
void describe() const {
Oeuvre::describe(); (3)
std::cout << "Nombre de mots: " << nb_mots_ << std::endl;
}
};
int main() {
Auteur* jules = new Auteur("Verne", "Jules");
Oeuvre* vingtk_lieues = new Livre(jules, (7)
"Vingt mille lieues sous la mer",
1869, 142172);
vingtk_lieues->describe(); (6)
int age = vingtk_lieues->age(); (4)
std::cout << vingtk_lieues->titre() << " a été écrit il y a "
<< age << " ans." << std::endl;
return 0;
}
-
Chaque instance de la classe
Livre
est uneOeuvre
. En C++, l’héritage peut être privé ou public. -
Le premier élément de la liste d’initialisation du constructeur est l’appel d’un constructeur de la classe de base.
-
La syntaxe
[nom de classe]::methode
permet de se référer (et d’appeler) la méthode définie dans cette classe, par opposition à la méthode définie dans la classe courante. -
On peut demander son âge à un
Livre
, puisque c’est uneOeuvre
. -
Notez le mot-clé
virtual
… -
Parce que la méthode
describe
a été déclarée avec le mot-clévirtual
, c’est bien la méthode de la classeLivre
qui est appelée, carvingtk_lieues
est bien une instance deLivre
.
En Java et Python, c’était le comportement par défaut. -
Puisqu’une instance de
Livre
est aussi une instance deOeuvre
, on peut toujours stocker unLivre*
dans une variable de typeOeuvre*
.
Le contraire n’est pas vrai.
Le résultat de ce programme est:
Auteur: Jules Verne Titre: Vingt mille lieues sous la mer Nombre de mots: 142172 Vingt mille lieues sous la mer a été écrit il y a 152 ans.
6. Un peu plus de Python…
6.1. Liste
l = [] (1)
assert(len(l) == 0)
l.append(3) (2)
l.append(5)
assert(l == [3, 5]) (3)
n = l.pop() (4)
assert(n == 5)
assert(l == [3])
print('<aucune erreur>')
-
Création d’une liste vide, définie par les crochets
-
Ajout d’un élément à la fin de la liste
-
On crée une liste non vide en séparant les éléments par des virgules
-
Retrait d’un élément de la fin de la liste
Le résultat de ce programme est:
<aucune erreur>
6.2. Structures de controle
n = 3
while n > 2: (1)
n -= 1 (2)
if 1 == 2:
print('That would be a surprise!')
elif [2, 3].pop() == 3:
print('This one looks good.')
else:
print('How on earth would we get there?!?')
l = [n,3,5,7] (3)
for i in l: (4)
print(i)
-
Comme partout ailleurs en Python, c’est l’indentation qui définit où commence et où termine un bloc de code, une définition de fonction ou de classe, etc.
-
Équivalent à
n = n - 1
-
On peut utiliser des variables pour construire des listes, ou toute autre structure de données
-
Itération sur les éléments d’une liste
Le résultat de ce programme est:
This one looks good. 2 3 5 7
6.3. Dictionnaires
cle = 'cle'
valeur = 3
d = { (1)
'a': 1, (2)
cle: valeur (3)
}
d['b'] = 2 (4)
assert(d['a'] == 1) (5)
assert('cle' in d) (6)
assert('c' not in d)
print(d) (7)
for key, value in d.items(): (8)
print(f'Key: {key}, value: {value}')
-
Définition d’un dictionnaire (classe
dict
)… -
La clé (ici,
'a'
) peut être n’importe quel type immutable (nombres, chaînes de caractères, tuples). La valeur peut être quelconque. -
On peut bien sûr utiliser des variables à la place de constantes pour construire un dict, comme pour toute structure de donnée en Python
-
Ajout d’une clé avec sa valeur ou modification d’une valeur
-
Accès à la valeur pour une clé donnée. Erreur si la clé n’existe pas dans le dictionnaire.
-
Tests de la présence d’une clé dans un dictionnaire
-
Impression d’un dictionnaire
-
Itération sur les paires clé/valeur d’un dictionnaire
Le résultat de ce programme est:
{'a': 1, 'cle': 3, 'b': 2} Key: a, value: 1 Key: cle, value: 3 Key: b, value: 2
6.4. Tuples
t = ('a', 1) (1)
assert(t[0] == 'a' and t[1] == 1) (2)
s, n = t (3)
assert(s == 'a' and n == 1)
t = ('a', 1, True) (4)
print(t) (5)
l = [t]
for s, n, b in l: (6)
print(f'({s}, {n}, {b})')
-
Construction d’un tuple, ici une paire. Un tuple est immutable, et peut contenir autant d’élément qu’on le souhaite, de types quelconques.
-
Accès aux membres d’un tuple un par un
-
Accès à tous les membres d’un tuple à la fois ('unpacking')
-
Un triplet, au lieu d’une paire
-
Impression d’un tuple
-
Itération sur les éléments de la liste (ici, un unique tuple) et 'unpacking' en même temps.
Le résultat de ce programme est:
('a', 1, True) (a, 1, True)