Introduction à la notion de Programmation Orientée Objet
- Avant-propos
[/b][/size]
Ce tutoriel s’adresse à quelqu’un ayant déjà des bases des concepts de la programmation (variables, fonctions, conditions et boucles). Les notions seront, pour la plupart, illustrées en C++, mais je tenterai lorsque possible, de donner la version Ruby puisque c’est le langage qui intéresse probablement le plus ici, étant lié à RMXP.
La
Programmation Orientée Objet (P.O.O.) est une façon de programmer, qui s’oppose notamment à la
Programmation Procédurale. Si pour cette dernière, on construit son code avec des fonctions ici et là sans vrai rapport entre elles, en P.O.O. on cherchera à créer ce qu’on appelle des objets dans le but de structurer son code, et d’optimiser (et dans la plupart des cas, faciliter) la conception de celui-ci.
- La distinction Classe/Objet
[/b][/size]
On dit d’un objet qu’il est une instance de sa classe. Une classe est en quelque sorte le modèle sur lequel est construit l’objet. Raisonnons en exemple, je pense que ça sera plus parlant ^^
Lorsque l’on conçoit une voiture, on produit ses plans, son prototype etc... Ceci est comparable à la classe. Après, à partir de ces plans et de ces modèles, on va construire tout un tas de voitures possédant les même fonctionnalités, ce sont les objets, les instances de la classe voiture.
- Et qu’est-ce que qu’un objet alors ?
[/b][/size]
Les objets possèdent des fonctions, appelées méthodes, et des variables, nommées attributs. Ces attributs et ces méthodes définissent l'objet, et sont propres à celui-ci (pas vraiment au fond, mais on va faire comme si c’était le cas pour le moment).
Pour en revenir à notre exemple de voiture, on peut imaginer une classe voiture qui possède un nombre fixe de roues, mais un nombre variable de porte. Exemple en C++ et en Ruby:
Code en C++ | Code en Ruby |
---|
#include <iostream>
//> std::cout en C++ affiche ce qui est donné.
class Voiture { public: //> Définition des attributs de la classe Voiture int _nmbrPortes; const int _nmbrRoues = 4; };
int main() { Voiture Cli; Cli._nmbrPortes = 5; std::cout << Cli._nmbrPortes << " " << Cli._nmbrRoues; //> Affiche "5 4" return 0; } | begin
class Voiture attr_accessor :nmbrPortes attr_reader :NMBR_ROUES def initialize @NMBR_ROUES = 4 end end
cli = Voiture.new #> Crée une voiture de 5 portes cli.nmbrPortes = 5 p cli.NMBR_ROUES #> Affiche 4 p cli.nmbrPortes #> Affiche 5 end |
Vous remarquerez que pour accéder à un attributs ou à une méthode d'un objet, on utilisera souvent la forme "Objet.méthode()" / "Objet.attribut".
On remarque clairement ici la différence entre la définition de la classe Voiture et l'utilisation de celle-ci pour créer un objet Cli.
Je reviendrai dans la prochaine partie sur ce que signifie mot-clé
public en C++.
En Ruby,
attr_accessor :var indique un attribut "var" qu'il est possible de lire et dans lequel il est possible d'écrire. Alors que
attr_reader :var ne nous permet que de lire "var".
- 2 méthodes particulières d'un objet: Constructeur, Destructeur
[/b][/size]
Dans un objet, il existe 2 méthodes bien particulières, qui ne s'utilisent qu'à un moment de la vie de l'objet, il s'agit du Constructeur et du Desctructeur d'une classe.
En Ruby, le constructeur est appelé par la méthode qu'on va nommer "initialize".
En C++, le constructeur portera le nom de sa classe, alors que le destructeur portera le nom de sa classe précédé d'un ~.
Le nom nous en dit beaucoup sur leur fonctionnement (comme souvent), le constructeur sera appelé lors de la création de l'objet et le destructeur lors de la destruction de celui-ci.
Le premier nous sert à initialiser les variables et à prendre en argument ce qui est nécessaire et le second à de multiples utilités en fonction des langages. En C++ par exemple, il libère la mémoire allouée.
Exemples:
Code en C++ | Code en Ruby |
---|
#include <iostream>
class Vehicule { private: //> Ne vous occupez pas de ce mot clé, on le verra plus tard int _nmbrRoues; public: //> De même pour celui là Vehicule(int nmbrRoues) {_nmbrRoues = nmbrRoues;} //> Constructeur de Vehicule void affich() {std::cout << _nmbrRoues;} };
int main() { Vehicule voiture(4); voiture.affich(); //> Affiche "4" Vehicule moto(2); moto.affich(); //> Affiche "2" return 0; } | begin
class Vehicule attr_accessor :nmbrRoues def initialize(nmbrRoues) #> Constructeur de Vehicule @nmbrRoues = 4 end end voiture = Vehicule.new(4) p voiture.nmbrRoues #> Affiche "4" moto = Vehicule.new(2) p moto.nmbrRoues #> Affiche "2" end |
- Ce que nous apporte la POO
[/b][/size]
Faire de la POO, cela implique de respecter trois "principes" de celle-ci:
- L'encapsulation
- L'héritage
- Le polymorphisme, que je n'aborderai probablement pas ici, sauf s'il y a des intéressés. Dans ce cas, dîtes le moi et je ferai une partie sur cette notion ^^
- L'encapsulation
[/b][/size]
Derrière un terme que l'on peut considérer comme barbare (ou qui nous fait penser à une canette de cola, ça dépend :d), se cache en fait un principe tout simple. Gérer les données de manière groupée et ordonnée.
Je m'explique. Il s'agit de regrouper toute donnée sous un objet, afin de ne plus laisser de variable ou de fonction seules dans un coin reculé du code (Tentez de voir les objets comme des blocs qu'on lie entre eux). Ceux qui savent déjà programmer procéduralement me diront que l'encapsulation est déjà présente dans la programmation procédurale, sous la forme des libs ou des fichiers, d'une certaine façon. Mais avec la POO, vient une nouvelle fonctionnalité, celle de permettre ou non à l'utilisateur l'accès à certains attributs ou méthodes.
Cette méthode permet ainsi de facilité l'utilisation de certains outils, cachant de manière interne certains rouages qui pourraient provoquer des dysfonctionnements si ceux-ci étaient mal utilisés.
Je vais illustrer cela en reprenant l'exemple voiture. Imaginons qqn qui assemble une voiture, mais qui n'y connait rien en roues. Pour réfléchir en Objet, il va donc créer sa classe Voiture qui sera constituée de roues et d'un moteur par exemple (il va pas aller loin lui). Si jamais il dérègle (sans forcément le vouloir), la pression des pneus, sa voiture ne va pas fonctionner correctement. Le mieux serait donc de restreindre l'utilisateur (le concepteur de la voiture), de l'empêcher d'accéder au paramètre "pression des pneu".
Après cela, le concepteur de la voiture ne veut pas forcément que son client modifie ses Roues et son moteur, il faut donc en restreindre l'accès et ne permettre au client d'utiliser que ce qui lui servira.
Exemple en C++ :
#include <iostream>
class Moteur {
public:
bool marche() {return true;} //> Renvoie si le Moteur fonctionne ou non
};
class Roues {
public:
bool pressionBonne() {return _pression;}
private:
bool _pression=true; //> Vérifie l'état de la pression
};
//> C'est ici qu'intervient notre concepteur de voiture, il dispose des outils demandés sans y avoir accès
//> (bon, ya pas grand chose qui change ici, mais c'est le principe d'un exemple simple :d)
class Voiture {
private:
Moteur _mot; //> Le client n'a pas à avoir accès au moteur, il pourrait provoquer un dysfonctionnements.
Roues _r; //> De même pour les roues.
public:
bool peutAvancer() {if(mot.marche() && _r.pressionBonne()) return true;} //> Vérifie que la voiture peut avancer
};
int main() {
Voiture Peuge;
if (Peuge.peutAvancer())
std::cout << "En avant !";
else
std::cout << "Hop hop, ya qqch qui ne marche pas";
return 0;
}
Pour comprendre ce code, il faut noter que le mot-clé
public: indique que les attributs définis par la suite seront accessibles depuis l'extérieur, alors que
private: comme son nom l'indique, privatise l'utilisation des attributs à l'objet en lui même.
(Notez également que, dans mon cas, j'ai pris l'habitude de déclarer tout attribut en private, sauf si je ne peux faire autrement ou que c'est plus optimisé de le faire en public)
En C++ (et dans quelques autres langages), il existe également un 3ème format d'encapsulation, appelé
protected:, qui a un rapport avec l'héritage, notion que nous allons aborder juste après. Je reviendrai donc sur celui-ci dans la prochaine partie ^^
- L'Héritage
[/b][/size]
L'héritage est, à mon goût, la notion la plus puissante de l'orienté objet. On peut comprendre tout son principe dans son nom, mais rien de tel que quelques exemples pour aider à y voir plus clair

On va changer de l'exemple de la voiture qui rendrait ce tuto quelques peu monotone, et diriger notre choix vers les consoles ^^
Nous voulons donc ouvrir une entreprise qui fabriquera des consoles de jeux en tout genre. Mais cette appellation peut paraître bien vague, et il existe une multitude de console de jeux, pourtant toutes basées sur le même principes et présentant plein de ressemblances les unes avec les autres... Devons-nous créer un objet reprenant les même caractéristiques qu'un autres pour chaque console ?
Et c'est là que l'héritage entre en jeu.
Celui-ci nous permet de créer une classe Console de laquelle va dériver d'autres consoles de différents types. On peut également surcharger les méthodes des classes héritées, afin d'y ajouter des fonctionnalités ou de complètement modifier celles-ci. Je passe tout de suite à l'exemple en C++ pour que vous compreniez, notez que pour faire hériter une classe d'une autre dans ce langage, il faut indiquer un "niveau d'héritage" (qui a un rapport avec l'encapsulation), puis le nom de la classe.
#include <iostream>
class Console {
public:
Console(std::string name, int year) : _name(name), _year(year) {}
//> Affecte la variable donnée "name" donnée en argument dans l'attribut _name. De même pour year.
//> Pour voir comment est fait un constructeur en C++, se référer à un tuto
void afficherDetails() {std::cout << "La Console " << _name << " a ete concue en " << _year << std::endl;}
protected:
std::string _name;
int _year;
};
class ConsolePortable : public Console {
public:
ConsolePortable(std::string name, int year, int nmbrButton, int nmbrScreen) : Console(name, year), _nmbrButton(nmbrButton), _nmbrScreen(nmbrScreen) {}
//> Il est tout a fait possible d'appeler le constructeur d'une classe héritée
int getNmbrButton() {return _nmbrButton;}
//> On surcharge la méthode afficherDetails afin qu'elle affiche correctement pour l'objet
void afficherDetails() {std::cout << "La Console Portable " << _name << " a ete concue en "
<< _year << " possede " << _nmbrScreen << " ecrans et " << _nmbrButton
<< " bouttons."<< std::endl;}
private:
int _nmbrScreen;
int _nmbrButton;
};
int main() {
Console Playsystem("SP1", 1994); //> On définit donc la console SP1
//> Puis on définit 2 consoles portables, possédant plus de détails qu'un objet Console
ConsolePortable nitandos1SD("Nitandos1SD", 2011, 13, 2);
ConsolePortable nitandos2SD("Nitandos2SD", 2014, nitandos1SD.getNmbrButton()+3, 2);
//> Enfin, on affiche les détails des consoles
Playsystem.afficherDetails(); //> "La Console SP1 a ete concue en 1994"
nitandos1SD.afficherDetails();//> "La Console Portable Nitandos1SD a ete concue en 2011 possede 2 ecrans et 13 bouttons."
nitandos2SD.afficherDetails();//> "La Console Portable Nitandos1SD a ete concue en 2014 possede 2 ecrans et 16 bouttons.
return 0;
}
Ici,
protected: est donc utilisé pour définir ces attributs comme étant privés, donc inaccessible de l'extérieur, sauf pour les classes filles (celles qui héritent) qui les considéreront également comme privés.
- Petit Exercice
[/b][/size]
Pour ceux qui le souhaitent (et je vous le conseille grandement, rien ne vaut mieux que la pratique), je vous propose d'imaginer une classe selon un énoncé (que je donne juste un peu plus bas), ou selon votre propre volonté (mais indiquez quand même ce que vous avez chercher à faire) pour que l'on puisse comparer, optimiser et corriger s'il le faut, votre code. Cela peut très bien être écrit en un langage X que vous avez vous même inventé, le tout c'est qu'il reste compréhensible de tous ou qu'il soit fort probable que qqn le connaisse ici.
Comme exercice:
- Je vous propose de refaire la classe Voiture, en partant d'un Véhicule, puis de définir un Avion, un bateau, voire plusieurs modèles suivant chaque type de véhicules (voitures de sport, bateau de pêche, avion sous-marin... Tout ce que vous voulez
) - Vous pouvez également continuer l'ébauche de classe Console que je vous ai donnée en y intégrant d'autres console, ou en la refaisant complètement (c'était une classe faîte à l'arrache pour l'exemple donc bon ^^')
Vous pouvez trouvez des exemples pour vous entraîner à penser objet tout autour de vous, il y en a partout, que ce soit pour (*zieute autour de lui*) les meubles, les téléphones portables ou encore les instruments de musiques, on peut penser objet de partout ^^
Mot de la fin
[/b][/size]
Voilà, ici se termine ce tutoriel pour l'instant, je pense bien avoir été un peu flou sur certains points, donc si vous avez des problèmes de compréhension sur telle ou telle partie, n'hésitez pas à le signaler, on (moi ou tous ceux pouvant vous répondre) tentera de vous aider ^^
Désolé pour les potentielles et probables fautes de français ^^'
Aussi, je me rend compte que je n'ai donné que 2 exemples en Ruby au final, donc si qqn qui en a le courage et la capacité de traduire un ou les autres exemples en Ruby (ou en x autre langage hein, plus yen a, mieux c'est), eh ben je l'invite à poster son code, ou s'il en a le pouvoir, à modifier mon message pour le rajouter ^^
Je vous invite aussi à me dire (ou a modifier mon post) si vous trouvez quoi que ce soit de faux dans ce que j'ai dit d:
Merci de m'avoir lu et bonne POO !
Un 42 s'est glissé qq part, saurez-vous le retrouver ? 