Dart en 10 chapitres : Chap 7 Programmation Orientée Objet (POO).
Salut, j’espère que ça roule bien 🤗
Merci encore d’être là. Si vous venez d’arriver vous pouvez revenir sur les notions précédentes en cliquant ici pour ne pas être perdu.
0. Vue d’ensemble
Sous ce chapitre, nous verrons la notion de la Programmation Orientée Objet (POO). Une des notions les plus indispensables du développement dans son général.
Plan :
a. La POO en programmation
b. Classe & Objet en Dart
c. Constructeurs en Dart
d. La notion sur le Getter & Setter en Dart
e. La notion sur l’héritage en Dart
f. La notion sur le remplacement de méthode
g. La notion sur les “Abstract Class”
h. La notion sur l’Interface
i. La notion sur les variables et méthodes statiques
j. Conclusion
1. Qu’est-ce que la Programmation Orientée Objet (POO)
La POO est un dogme de programmation qui utilise des objets pour représenter des concepts et des données.
Un objet est une entité qui regroupe des données et des méthodes qui opèrent sur ces données.
Et Dart lui est un langage Orienté-Objet et supporte tous les concepts de la POO, tels que :
- Classe : Un plan qui définit la structure et le comportement d’un objet.
- Objet : Une instance d’une classe.
- Encapsulation : Le fait de cacher les détails internes d’un objet.
- Héritage : La capacité d’une classe à hériter des propriétés et des méthodes d’une autre classe.
- Polymorphisme : La capacité d’une classe à se comporter différemment selon le contexte.
Citons quelques avantages de la POO en Dart pour Flutter, elle :
- Permet de créer des applications plus robustes et plus faciles à maintenir.
- Facilite la création d’interfaces utilisateur intuitives et réactives.
- Améliore la performance du code en optimisant l’utilisation des ressources.
2. Classe et Objet en Dart
La Classe, dit constructeur d’un objet, peut contenir plusieurs éléments (données), comme : les variables, les fonctions, les constructeurs, …
Elle regroupe ou encapsule ces éléments (données) qui sont lus par l’objet qui est créé pour son instance ou sa référence. Chaque objet qui sera déclaré comme son instance aura toutes ses caractéristiques (éléments).
A l’exemple d’un Ordinateur qui comprend les éléments comme prix, marque, date de création, type de processeur, … Ces caractéristiques d’un ordinateur peuvent être comprises dans une Classe Ordinateur (avec grand O) et chaque fois on peut construire un objet ordinateur (avec petit o) avec ces caractéristique ainsi nous n’aurons qu’à appeler la classe Ordinateur pour chaque objet ordinateur 1, 2, 3, … d’où les avantages comme ceux d’une fonction : la réutilisabilité du code, la modularité, la maintenabilité, la lisibilité, …
Une Classe en Dart est définie en utilisant le mot-clé class
, comme vu dans le chapitre précédent, suivi du nom de la Classe (qui commence par une lettre majuscule) + les accolades { }. Entre ceux-ci se placent toutes les instructions (caractéristiques) qu’encapsulent la dite Classe.
Qu’en est-il alors d’un Objet ?
Un Objet quant à lui est une variable ou une instance d’une Classe, déclaré pour accéder aux propriétés (caractéristiques) de celle-ci.
Il comporte deux grandes caractéristiques :
- l’état / propriété
- le comportement
On dirait un homme qui a lui un état ou propriété comme nom, âge, taille, santé, … et un comportement comme courir, manger, dormir, travailler, …
Les objets eux aussi sont comparés à cette réalité de la vie réelle, ayant des états ou propriétés et des comportements. Les états ou propriétés d’un Objet sont représentés par les différents champs que comprend la Classe et ses comportements sont les différentes méthodes de cette Classe.
Sa déclaration se fait en donnant le type qu’il est (ici juste le nom de la dite Classe), puis le nom de l’objet suivi d’un signe égal (=) puis le nom de la Classe suivi des parenthèses ( ) à l’intérieur de celles-ci les constructeurs si ils existent, ils sont entourés des signes < > (Nous en dirons plus lorsque nous serons sur les constructeurs).
Et pour accéder aux propriétés et comportements de la Classe, on prend le nom de l’objet point (.) la propriété ou le comportement.
Voyons ces théories en pratique :
//Notre Classe Ordinateur avec ses propriétés
class Ordinateur{
//Déclaration des propriétés ou état, via les variables
dynamic marque;
dynamic prix;
dynamic anneeCreation;
dynamic typeProcesseur;
//Déclaration des comportements via des fonctions
void activite(){
print('Activité : est éteint.\n');
}
void qualite(String message){
print('Qualité : $message\n');
}
void clavier(String etatClavier){
print("Clavier : $etatClavier\n");
}
}
void main() {
//La première instance de cette Classe
Ordinateur ordi1 = Ordinateur();
//On accède aux différentes propriétés de cette classe pour y ajouter les éléments
ordi1.marque = 'HP';
ordi1.prix = 420;
ordi1.anneeCreation = 2023;
ordi1.typeProcesseur = "intel";
print("L'ordinateur 1\nNom : ${ordi1.marque}\nPrix : ${ordi1.prix}\nAnnée de creation : ${ordi1.anneeCreation}\nType de processeur : ${ordi1.typeProcesseur}\n");
//Ici, ce sont les différents comportement de cette classe Ordinateur
ordi1.activite();
ordi1.qualite('bonne');
ordi1.clavier('lumineux');
//La deuxième instance de cette Classe
Ordinateur ordi2 = Ordinateur();
//Même gymnastique, on accède aux différentes propriétés de cette classe pour y ajouter les éléments
ordi2.marque = 'Lenovo';
ordi2.prix = 550;
ordi2.anneeCreation = 2024;
ordi2.typeProcesseur = "NVIDIA";
print("L'ordinateur 2\nNom : ${ordi2.marque}\nPrix : ${ordi2.prix}\nAnnée de creation : ${ordi2.anneeCreation}\nType de processeur : ${ordi2.typeProcesseur}\n");
//Même gymnastique, on accède aux comportements de la classe Ordinateur
ordi1.activite();
ordi1.qualite('super');
ordi1.clavier('lumineux');
}
Ainsi, nous pouvons voir les différentes manipulations des objets d’une classe.
3. Le Constructeur en Dart
Le Constructeur est une fonction spéciale d’une Classe qui a pour rôle d’initialiser les variables d’une classe.
Il est défini par le même nom de la Classe dont il est constructeur.
Il est une fonction et peut donc être paramétré, mais contrairement aux fonctions que nous avons vu précédemment, celui-ci ne peut pas retourner une valeur et a un nom commençant par une lettre majuscule ce qui n’est pas le cas des autres fonctions.
Si tu ne déclares pas le constructeur, un constructeur par défaut sans argument vous est fourni.
Sa syntaxe reste comme celle d’une classe mais dans les parenthèses elle peut prendre les paramètres comme les fonctions le font.
Voyons cela dans notre exemple précédent :
//Notre Classe Ordinateur avec ses propriétés
class Ordinateur{
//Déclaration des propriétés ou état, via les variables
dynamic marque;
dynamic prix;
dynamic anneeCreation;
dynamic typeProcesseur;
//Entrée d'un Contructeur par défaut
Ordinateur(){
print("\nCeci est un ordinateur\n");
}
}
void main() {
//La première instance de cette Classe
Ordinateur ordi1 = Ordinateur();
//On accède aux différentes propriétés de cette classe pour y ajouter les éléments
ordi1.marque = 'HP';
ordi1.prix = 420;
ordi1.anneeCreation = 2023;
ordi1.typeProcesseur = "intel";
print("L'ordinateur 1\nNom : ${ordi1.marque}\nPrix : ${ordi1.prix}\nAnnée de creation : ${ordi1.anneeCreation}\nType de processeur : ${ordi1.typeProcesseur}\n");
}
Constat fait : le constructeur est lu avant toutes les instructions du programme, ce qui rejoint son rôle d’initialisation des éléments d’une Classes.
Voyons comment peut-on passer les paramètres à un constructeur.
//Notre Classe Ordinateur avec ses propriétés
class Ordinateur {
//Déclaration des propriétés ou état, via les variables
dynamic marque;
dynamic prix;
dynamic anneeCreation;
dynamic typeProcesseur;
//Entrée d'un Contructeur avec paramètres
Ordinateur(
dynamic marqueParam,
dynamic prixParam,
dynamic anneeCreationParam,
dynamic typeProcesseurParam,
) {
//La partie gauche fait référence aux propriétés de la classe et elles sont initialisées avec les valeurs des paramètres
this.prix = prixParam;
this.marque = marqueParam;
this.anneeCreation = anneeCreationParam;
this.typeProcesseur = typeProcesseurParam;
}
}
void main() {
//La première instance de cette Classe
Ordinateur ordi1 = Ordinateur('HP', 420, 2023, 'intel');
print(
"L'ordinateur 1\nNom : ${ordi1.marque}\nPrix : ${ordi1.prix}\nAnnée de creation : ${ordi1.anneeCreation}\nType de processeur : ${ordi1.typeProcesseur}\n");
}
Ici, nous voyons clairement comment le constructeur permet d’initialiser les propriétés d’une Classe. Ainsi, pour prendre les valeurs des propriétés de la Classe, l’objet les prend dans les parenthèses de la Classe par référence au constructeur de cette classe.
Et en troisième lieu, nous pouvons créer nos propres constructeurs, pour ainsi passer aux éléments des différentes propriétés de la classe.
Simplement, on s’y prend en appelant le nom de la classe point (.) le nom de notre constructeur personnalisé avec la suite comme les syntaxes précédentes. Voyons cela en exemple :
//Notre Classe Ordinateur avec ses propriétés
class Ordinateur {
//Déclaration des propriétés ou état, via les variables
dynamic marque;
dynamic prix;
dynamic anneeCreation;
dynamic typeProcesseur;
//Entrée d'un premier Contructeur personnalisé
Ordinateur.constructeurPersonnalise() {
marque = "HP";
prix = 420;
anneeCreation = 2023;
typeProcesseur = "intel";
}
//Entrée d'un deuxième Contructeur personnalisé
Ordinateur.constructeurPersonnalise2() {
marque = "Lenovo";
prix = 500;
anneeCreation = 2024;
typeProcesseur = "NVIDIA";
}
}
void main() {
//La première instance de cette Classe avec le premier constructeur personnalisé
Ordinateur ordi1 = Ordinateur.constructeurPersonnalise();
//La deuxième instance de cette même Classe avec le deuxième constructeur personnalisé
Ordinateur ordi2 = Ordinateur.constructeurPersonnalise2();
print(
"L'ordinateur 1\nNom : ${ordi1.marque}\nPrix : ${ordi1.prix}\nAnnée de creation : ${ordi1.anneeCreation}\nType de processeur : ${ordi1.typeProcesseur}\n");
print(
"L'ordinateur 2\nNom : ${ordi2.marque}\nPrix : ${ordi2.prix}\nAnnée de creation : ${ordi2.anneeCreation}\nType de processeur : ${ordi2.typeProcesseur}\n");
}
Ainsi, chaque constructeur passe une initialisation selon ses instructions.
4. Getter & Setter
Les Getters et les Setters sont des méthodes spéciales qui permettent d’accéder, de modifier ou d’initialiser les attributs d’une classe en Dart.
a. Getter
- Un Getter est une méthode qui permet d’obtenir la valeur d’un attribut privé.
- Il est défini avec le mot-clé
get
. - Il n’a pas de paramètre.
- Il retourne la valeur de l’attribut.
class Ordinateur {
String marque;
String get marque{
return marque;
}
}
b. Setter
- Un Setter est une méthode qui permet de modifier la valeur d’un attribut privé.
- Il est défini avec le mot-clé
set
. - Il a un paramètre qui correspond à la nouvelle valeur de l’attribut.
- Il ne retourne aucune valeur.
class Ordinateur {
String marque;
set marque(String marqueParam){
marque = marqueParam;
}
}
Voyons cette notion dans notre grand exemple :
//Notre Classe Ordinateur avec ses propriétés
class Ordinateur{
//Déclaration de propriété ou état, via les variables
dynamic marque;
set marqueSet(dynamic marqueParam){
marque = marqueParam;
}
String get marqueGet{
return marque;
}
}
void main() {
//La première instance de cette Classe
Ordinateur ordi1 = Ordinateur();
//Setter par défaut
ordi1.marque = 'HP';
//Getter par défaut
print(ordi1.marque);
}
5. Héritage en Dart
L’héritage en Dart est un concept fondamental de la Programmation Orientée Objet (POO) qui permet à une classe d’hériter des propriétés (états) et des méthodes (comportements) d’une autre classe. Comprenons comme créer une autre classe d’une classe qui existe déjà (Rappelons nous des classes personnalisées d’Exception dans la notion précédente).
En Dart, il est implémenté via le mot-clé extends
.
Il existe des classes dites :
- Parentes : celles qui sont héritées par d’autres, autrement on les appelle les classes de base.
- Enfants : ici, ce sont celles qui héritent les propriétés et comportements de la Classe Parente.
Comprenons via le même grand exemple de ce chapitre. Nous avons déjà notre grande Classe Ordinateur, mais voudrions en créer 2 autres petites classes (Classes Enfants) à celle Ordinateur (Classe Parente), la Classe HP et la Classe Lenovo, qui prendront toutes les propriétés et comportements de la Classe Ordinateur.
Avant d’aller avec cet exemple, citons les différents types d’héritages que nous retrouvons en Dart :
- Héritage simple : Une classe hérite d’une seule classe parente. Le schéma serait de A-B.
- Héritage multiple : Une classe hérite de plusieurs classes parentes, au schéma A, B — C (Notons ici que Dart ne supporte pas directement ce style d’héritage, mais nous verrons dans les notions qui suivent comment c’est possible de le faire).
- Héritage multi-niveaux : Une classe hérite d'une classe parente qui, à son tour, a hérité aussi d’une autre classe. Au schéma A-B-C.
- Héritage hiérarchique : Plusieurs classes enfants héritent d’une seule classe parente.
Voyons cela en exemple :
//Notre Classe Ordinateur avec ses propriétés
class Ordinateur {
//Déclaration des propriétés ou états, via les variables
dynamic marque;
dynamic prix;
dynamic anneeCreation;
dynamic typeProcesseur;
}
//L'extension de la classe Ordinateur dans celle de HP
class HP extends Ordinateur {
void etatClavier() {
//Nous pouvons récupérer la propriété marque de la classe Ordinateur
print('${this.marque} a un clavier lumineux\n');
}
}
//L'extension de la classe Ordinateur dans celle de Lenovo
class Lenovo extends Ordinateur {
void etatClavier() {
//Nous pouvons récupérer la propriété marque de la classe Ordinateur
print('${this.marque} a un clavier non-lumineux\n');
}
}
void main() {
//Constat l'instance de la classe HP a aussi les propriétés de la classe Ordinateur que HP
dynamic ordi1 = HP();
ordi1.marque = 'HP';
ordi1.prix = 420;
ordi1.anneeCreation = 2023;
ordi1.typeProcesseur = 'intel';
//Constat l'instance de la classe Lenovo a les propriétés de la classe Ordinateur et de celle Lenovo
dynamic ordi2 = Lenovo();
ordi2.marque = 'Lenovo';
ordi2.prix = 500;
ordi2.anneeCreation = 2024;
ordi2.typeProcesseur = 'NVIDIA';
print(
"L'ordinateur 1\nMarque : ${ordi1.marque}\nPrix : ${ordi1.prix}\nAnnée de creation : ${ordi1.anneeCreation}\nType de processeur : ${ordi1.typeProcesseur}\n");
//Le comportement de la classe HP seulement
ordi1.etatClavier();
print(
"L'ordinateur 2\nMarque : ${ordi2.marque}\nPrix : ${ordi2.prix}\nAnnée de creation : ${ordi2.anneeCreation}\nType de processeur : ${ordi2.typeProcesseur}\n");
//Le comportement de la classe Lenovo seulement
ordi2.etatClavier();
}
6. La notion sur le remplacement de méthode
La notion de remplacement de méthode est une technique pour atteindre le polymorphisme qui, dans la POO, permet à des objets de types différents de se comporter de manière similaire ou le contraire.
Cela peut se faire en appelant une même méthode de la classe Parente dans la classe Enfant avec le même nom, les mêmes arguments et le même type de retour.
Et donc lors de l’appel de cette méthode, c’est la méthode de la classe Enfant qui est exécutée plutôt celle de la classe Parente. Ainsi, on parlerait du remplacement de méthode.
Comme avantages de cette manière, nous pouvons citer :
- Flexibilité : Permet de traiter des objets de types différents de manière uniforme et vice-versa.
- Réutilisabilité : Permet de créer des fonctions et des classes génériques qui peuvent être utilisées avec différents types d’objets.
- Extensibilité : Permet d’ajouter de nouveaux types d’objets sans avoir à modifier le code existant.
Cette notion est plus utilisée pour son troisième avantage, celle de l’extensibilité.
Les obligations à respecter pour y parvenir sont :
- La méthode de remplacement dans la classe Enfant doit avoir les mêmes configurations que celle de la classe Parente.
- Cette méthode est définie dans la classe Enfant et non dans la même classe Parente.
- Les méthodes statiques et/ou finales ne sont pas accessibles dans ce cas.
- Le constructeur lui non plus n’est pas accessible pour cette notion.
- Les méthodes non accessibles ne sont pas remplaçables.
Pour appeler, la méthode de la classe Parente dans la classe Enfant, on utilise le mot clé super
point (.) le nom de la méthode à question. Ainsi, on peut garder le comportement parentale et jouer avec un nouveau comportement dans la classe Enfant.
Voyons cela en Exemple :
//Notre Classe Ordinateur
class Ordinateur {
//Déclaration de la méthode principale
void etatClavier() {
print("Peut avoir un clavier lumineux ou non\n");
}
}
//L'extension de la classe Ordinateur dans celle de HP
class HP extends Ordinateur {
//La même méthode mais avec un nouveau comportement
void etatClavier() {
//L'appel du comportement de la classe Ordinateur
super.etatClavier();
//Nouveau comportement
print('HP avec un clavier lumineux\n');
}
}
void main() {
dynamic ordi1 = HP();
ordi1.etatClavier();
}
7. “Abstract Class” (Classe abstraite) en Dart
En Dart, une classe abstraite est une classe spéciale qui ne peut pas être directement instanciée. Elle sert de modèle pour d’autres classes, en définissant la structure et le comportement qu’elles doivent hériter.
Elles sont utilisées pour des raisons comme :
- Encapsulation : Elles permettent de définir une interface commune pour un ensemble de classes, regroupant les propriétés et les méthodes communes.
- Réutilisabilité : Elles permettent de réutiliser du code sans avoir à dupliquer les implémentations pour chaque classe concrète.
- Contrôle de l’héritage : Elles obligent les classes Enfants à implémenter les méthodes abstraites définies dans la classe abstraite.
Elles sont caractérisées par :
- Une déclaration avec le mot-clé
abstract
. - La présence des méthodes abstraites et des méthodes concrètes.
- Le manque d’implémentation.
- Les classes Enfants doivent implémenter toutes les méthodes abstraites héritées de la classe abstraite.
- Manque de possibilité de création d’une instance via un objet, mais une extension oui.
- L’implémentation de toutes ses caractéristiques dans ses classes Enfants.
Cette classe ne peut être étendue que par d’autres et non initialisée via un objet.
Voyons comment ça marche :
//Notre Classe Ordinateur
abstract class Ordinateur {
// Méthode abstraite
void activite();
// Méthode concrète
void etatClavier() {
print("Peut avoir un clavier lumineux ou non\n");
}
}
//L'extension de la classe Ordinateur dans celle de HP
class HP extends Ordinateur {
//Implémentation de la méthode abstraite
void activite() {
print('HP allumé\n');
}
}
void main() {
dynamic ordi1 = HP();
// dynamic ordi2 = Ordinateur(); //Essayez d'initialiser cela et faites le constat
ordi1.etatClavier(); // Hérite de la méthode concrète
ordi1.activite();
}
8. Notion sur l’Interface en Dart
L’Interface en Dart est un type de contrat qui définit un ensemble des méthodes qu’une classe doit implémenter.
Contrairement aux autres langages, Dart n’a pas de mot-clé “interface”. Mais pour l’implémenter, on utilise le mot-clé implements
.
Elle est utilisée pour :
- Définir des contrats : elle définit un ensemble de fonctionnalités qu’une classe doit implémenter.
- Améliorer le découplage : elle permet de découpler les classes les unes des autres, favorisant la flexibilité et la maintenabilité.
- Polymorphisme : elle permet d’utiliser des objets de types différents ayant des fonctionnalités communes.
Une classe Interface doit fournir l’implémentation complète de toutes les méthodes appartenant à l’interface.
Avec cette notion, on peut parvenir à faire de l’héritage multiple au schéma A,B — C (ce qui n’était pas faisable en Dart dans la notion d’héritage).
Voyons comment ça marche :
class Ordinateur {
void activite() {
print("Peut être allumé ou pas\n");
}
}
class Clavier {
void clavier() {
print("Peut être lumineux ou non\n");
}
}
/*Nous voyons la faisabilité de la notion de l'Héritage Multiple
La classe HP hérite les caractéristiques de 2 classes au même moment
Cette class est obligée d'implémenter toutes les fonctionnalités des
classes parentes*/
class HP implements Ordinateur, Clavier {
void activite() {
print("Pour l'instant éteint\n");
}
void clavier() {
print('Clavier allumé\n');
}
}
void main() {
dynamic ordi = HP();
ordi.activite();
ordi.clavier();
}
9. Variables et méthodes statiques
En Dart, les variables et méthodes statiques, déclarées avec le mot-clé static
, sont associées à la classe elle-même et non à une instance particulière de la classe, c-à-d que l’objet ne peut pas accéder pour modifier la valeur de celles-ci.
Elles stockent une valeur unique partagée par toutes les instances de la classe et sont initialisées une seule fois lors du chargement de la classe.
On y accède via la syntaxe : nom_classe.nom_variable_statique
Voyons cela en exemple :
class Ordinateur {
static dynamic marque;
dynamic prix;
static void activite() {
print("$marque est allumé\n");
}
void coute() {
print('Cet ordi coûte $prix\n');
}
}
void main() {
dynamic ordi = Ordinateur();
ordi.prix = 420;
ordi.coute();
//ordi.activite(); //N'est pas accessible via l'objet
//Ainsi nous pouvons accéder aux éléments statics
Ordinateur.marque = 'HP';
Ordinateur.activite();
}
10. Conclusion
Enfin ! Nous voici à la fin de ce chapitre qui a été un peu long que les précédents car ses notions demandent une bonne compréhension.
Sous ce chapitre, il a été question d’entrer dans le bain de la programmation avec la notion de la POO, une notion indispensable pour le développement des applications robustes qui soient facile à maintenir et flexibles.
Merci de rester dans la course et je vous dis à la prochaine où nous parlerons de la Programmation Fonctionnelle.
Portez-vous bien et n’oubliez pas : “Ne limitez pas vos rêves !”
😉