Introduction à la notion de Programmation Orientée Objet

16 Réponses • 2238 Vues

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 :3)
  • 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 ? :ahde:

Tuto Script

GiraKoth

Programmeur

J'ai vu ça en cours y a environ une semaine, du coup je savais déjà à peu près tout :ahde:

Le tout est intéressant mais je pense qu'un minimum de connaissance en programmation peut être utile pour mieux comprendre cela dit.

Un 42 s'est glissé qq part, saurez-vous le retrouver ? :ahde:
A part dans le numéro des lignes de codes, pas trouvé x)

Toujours en vie (je crois)
Citer
Un 42 s'est glissé qq part, saurez-vous le retrouver ? :ahde:
Oui, il est dans cette phrase :P

Sinon très instructif ce tuto (pour ma par en tout cas), même si je connaissais déjà
une peu la POO (très vite fais en c++, mais plus en Pyhton), ça ma permis d'éclaircir 2-3 truc sur les mystère du Rubis dont j'ai du mal
à comprendre x)

Gros pouce vert de ma part ^^
J'ai vu ça en cours y a environ une semaine, du coup je savais déjà à peu près tout :ahde:

Le tout est intéressant mais je pense qu'un minimum de connaissance en programmation peut être utile pour mieux comprendre cela dit.

Ah mince, j'ai une semaine de retard alors x)
Ouep, j'ai précisé au début qu'il fallait voir qq bases en programmation pour comprendre, mais c'est vrai que parfois, le "minimum de programmation" connu ne suffira peut être pas :/

Sinon très instructif ce tuto (pour ma par en tout cas), même si je connaissais déjà
une peu la POO (très vite fais en c++, mais plus en Pyhton), ça ma permis d'éclaircir 2-3 truc sur les mystère du Rubis dont j'ai du mal
à comprendre x)

Gros pouce vert de ma part ^^


Très content que ça t'aie plu dans ce cas, merci :3


A part dans le numéro des lignes de codes, pas trouvé x)

Haha j'y avais pas pensé à celui là, mais nan, c'est un 42 que j'ai définit moi-même :p

Oui, il est dans cette phrase :P

Et non ! (enfin si, mais...) ça serait trop facile si je parlais de celui là x)

Ptit indice, on ne le voit pas de manière explicite :3
Ah, je crois que j'ai compris :
Ultra Spoiler
Citer
  voiture = Vehicule.new(4)
  p voiture.nmbrRoues #> Affiche "4"
  moto = Vehicule.new(2)
  p moto.nmbrRoues #> Affiche "2"

Si on lit de haut en bas, ça fais 42 :)

« Modifié: 11 février 2015, 13:36:25 par maror »

Spoiler
Ouep, c'est à peut près ça, plus précisément, si on compile le code C++ ou si on interprète le code Ruby correspondant, ça nous affichera respectivement "42" ou
"4
2" d:
(cache ça maintenant, pour laisser les autres chercher :ahde:)

« Modifié: 11 février 2015, 13:45:05 par Tokeur »

Nuri Yuri

HostMaster

print "Voiture 4 roues. Moto 2 roues.".gsub(/[A-Za-z \.]+/) do nil end #Affiche 42
Tu n'as pas trop parlé de destructeur ou montré d'exemples à ce propos...
Peut-être est-ce parce qu'en Ruby le destructeur n'a pas d'existence propre ? Ou du moins c'est un truc très spéciale :perv:

Voici un exemple de la destruction d'objet en Ruby, je vais expliquer ça après car le concept est très spécial...
Un objet ruby normal n'a pas de destructeur explicite.
Le Garbage Collector se charge de détruire les objets si c'est nécessaire, comme vous pouvez le voir dans cet exemple, seule la voiture a été détruite, pourtant la moto n'est pas plus accessible que la voiture après l'appel de la méthode. Les différences entre ces deux objets sont leur date de création et leurs contenu.

Bref, dans une utilisation normale de Ruby définir des destructeurs n'est pas nécessaire c'est au programmeur de gérer ses objets, le Garbage Collector lui ne devrait qu'avoir à se charger de la mémoire, ainsi, lorsqu'un n'est plus utile, nous devons faire ce qu'il faut pour indiquer qu'il a fini sa vie. Par exemple dans la classe file c'est la méthode close qu'on appelle. Dans la classe Bitmap en RGSS c'est dispose. Après l'appel de ces méthodes, le GC aura juste à se charger de "libérer" la mémoire de l'objet puis-ce que pour File, il a été indiqué que le fichier n'est plus utilisé (fermé) donc Windows peut à nouveau utiliser le fichier sans corrompre les données qu'il contient, et pour Bitmap on a libéré le contenu qui prend sa petite place. Après File et Bitmap sont des objets qui ont été défini en C / C++ et qui ont donc un destructeur fonctionnel déclaré dans leur structure ce qui fait que si vous oubliez d'appeler la méthode "dispose" / "close" les destructeurs définis se chargeront de faire le travail que vous avez délibérément oublié de faire.

ObjectSpace.define_finalizer est très compliqué à utilisé et s'utilise de manière pas très propre donc plutôt que de définir des destructeurs appelés automatiquement selon les désires de Ruby, avant de lâcher les objets il faut appeler une méthode qui fait un peu le ménage :p

Je regarderais pour traduire les codes C++ :d (Sinon, j'ai été dépassé par maror :( )

Édit : Non Tokeur, ça affiche :
4
2
ln(yo) = <3
Okey, merci pour cet approfondissement en Ruby, si je l'ai pas trop abordé, c'est parce que je pensais que peu de personnes ici maîtrisent /veulent maîtriser vraiment la gestion de la mémoire et tout, donc aborder le sujet du destructeur aurait pu en décourager certains ou compliquer les choses pour les parties qui suivent ^^

En plus de ça, je ne m'y connaissait pas du tout en destructeur Ruby (mis à part pour le dispose ou pour fermer un fichier), donc en plus de ça, je n'aurai que pu parler qu'au travers du destructeur en Ruby :d

Effectivement, le Ruby affiche bien le 42 de haut en bas, pardon ^^'. Le Cpp cependant l'affiche "42" par contre :3

En tout cas, merci pour ces précisions :)

Nuri Yuri

HostMaster

La destruction des objets en Ruby est surtout très implicite. Si t'utilises ObjectSpace.define_finalize c'est que t'en a vraiment besoins. Ce qui n'est pas le cas de 99% des choses que tu feras en Ruby. (Je ne l'ai mis à part pour cet exemple, jamais utilisé).
ln(yo) = <3
Ouaip, le fait qu'elle soit implicite, je l'avais deviné :p

Pendant que j'y pense, faudrait que je fasse aussi une petite partie où je fais allusion au pointeur interne...

Nuri Yuri

HostMaster

En Ruby ou en C++ ?
Si c'est en C++ je pense qu'un tutoriel complet sur les pointeurs serait assez intéressant. En Ruby à moins de programmer des extensions ça sert pas à grand choses x)
ln(yo) = <3

Palbolsky

Staff CSP

Bien joué pour le tutoriel. :)

Je pense que ça va être utile pour tout ceux qui s'intéressent à la programmation.
Intéressé par le jeu de cartes Heartstone ? Cliquez ici.
En Ruby ou en C++ ?
Si c'est en C++ je pense qu'un tutoriel complet sur les pointeurs serait assez intéressant. En Ruby à moins de programmer des extensions ça sert pas à grand choses x)
(Ups, j'ai cliqué sur remercié au lieu de Citer, à vouloir aller trop vite hein ^^' :ahde:)

Ben, rien qu'y faire allusion, pour savoir que ça existe, c'est dans les bases des objets quand même le "self" ou le "this" :d

Bien joué pour le tutoriel. :)

Je pense que ça va être utile pour tout ceux qui s'intéressent à la programmation.

Merci ! :3

« Modifié: 11 février 2015, 16:17:01 par Tokeur »

Nuri Yuri

HostMaster

En ruby self n'est pas une variable propre mais un truc qui indique l'occurrence, tu peux faire self sans être dans un objet ça te diras quelque chose donc en soit c'est plus un machin qu'un pointeur. Après en C++ oui on peut parler de pointeur puis-ce que c'est le premier argument passé dans l'appel de fonction (au niveau du compilateur).

Bref, je suis d'accord sur un truc, il faut parler de self et this.
ln(yo) = <3
Est-ce qu'en Ruby, on se trouve dans un classe de base ? (L'objet main en l'occurrence).
Ça expliquerai pourquoi self renvoi qqch où qu'on soit c:

Je sais qu'en ISN, ils nous apprennent à coder avec Processing, en gros c'est du Java, et dans le programme, c'est caché puisque rien de tout ça n'est affiché, mais on se trouve dans une classe à la base. Alors ça pourrait être le même principe (après je sais pas, j'ai pas zieuté dans les sources de Ruby moi :p)

There was an error while thanking
Thanking...