Sommaire
Programmation Orientée Objet, Design/Architectural Patterns, … Vous en avez déjà entendu parler. Mais savez vous précisément ce en quoi ça consiste ?
Ces 8 articles vous permettront de comprendre les grands principes qui se cachent derrière ces termes techniques, mais également d’apprendre comment et pourquoi les utiliser.
- La Programmation Orientée Objet
- Introduction à UML : Diagrammes de classe
- Concepts de base de POO
- Les Patrons de Conception (Design Patterns)
- Les Patrons d’Architecture (Architectural Patterns)
- Concepts avancés de POO
- Qualité de Code
- Web Services
Introduction
Pour maîtriser l’art de la Programmation Orientée Objet, les patrons de conception, et les patrons d’architecture, il est important de comprendre les principaux concepts.
La Programmation Orientée Objet permet de faire énormément de choses, dans le but d’améliorer la qualité du code produit, la maintenance, la réutilisabilité, mais aussi de simplifier d’éventuelles évolutions.
Concept de Responsabilité Unique
Ce concept très basique est probablement le plus important en termes de Programmation Orientée Objet. Son principe est simple :
Une classe ne doit avoir qu’une seule et unique responsabilité
Qu’est-ce que ça signifie ?
Prenons un exemple concret. Dans une entreprise, des employés sont embauchés pour des raisons très précises. On ne demandera jamais à la secrétaire de faire une mise à jour des serveurs. En Programmation Orientée Objet, ce sera pareil, et même poussé encore plus loin.
Quel est l’intérêt ?
Il n’y a aucun coût supplémentaire à la création d’une nouvelle classe, alors pourquoi se priver d’en créer ? Au contraire, la Programmation Orientée Objet apporte une réelle amélioration en termes d’organisation de code. Ce principe permet de créer des classes, ayant chacune leur responsabilité, et donc, lorsqu’il sera nécessaire de modifier une portion de code, ou bien d’ajouter une fonctionnalité, etc… Le fichier à modifier apparaîtra clairement !
Si nous placions tout le code dans une seule classe, ça deviendrait vite ingérable n’est-ce pas ?
Comment l’appliquer ?
Lorsque vous réfléchissez à la logique de votre application, posez-vous la question suivante :
Quelles sont les compétences que mes classes doivent avoir ?
Et même plus :
Si je disposais d’une grande équipe, comment séparer au maximum les tâches pour que chacun ait quelque chose à faire ?
En effet, en partant sur cette dernière question, si on voulait faire un formulaire de connexion, on pourrait dire :
- Un développeur crée une classe pour effectuer des requêtes sur la base de données
- Un autre développeur crée une classe pour récupérer l’utilisateur X dans la base de données
- Un autre développeur crée une classe pour vérifier si le mot de passe de l’utilisateur X est correct
- Un autre développeur crée le formulaire
- Un autre développeur crée la page qui affiche le formulaire
- Un autre développeur met tout cela en relation
Je vous l’accorde, au premier abord ça semble compliqué. Pourtant, avec l’expérience, toute cette logique devient intuitive et automatique.
Grâce à cette séparation des responsabilités, chaque classe devient réutilisable. En effet, si on crée une nouvelle fonctionnalité dans notre application qui aura besoin de récupérer un utilisateur dans la base de données depuis son “login”, la classe “UserRepository” existe déjà et le permet. De même, on aura certainement besoin d’effectuer d’autres requêtes en base de données, et la classe “DatabaseManager” nous permettra de les exécuter.
Résultat : On ne code qu’une fois ce qui peut-être utilisé plusieurs fois.
En termes de maintenabilité, si la connexion à la base de données ne fonctionne plus pour certaines raisons, on sait tout de suite où le problème se situe, et on peut effectuer la correction à cet endroit seulement. De même, le risque que d’autres fonctionnalités de l’application ne fonctionnent plus est moindre lorsqu’on effectue une correction.
L’encapsulation
Nous avons vu ce qu’était le concept de responsabilité. Et bien figurez vous que l’encapsulation est le premier concept qui se base dessus. En effet, dans une classe, qui est le mieux placé pour gérer ses attributs ? La classe elle-même !
Bien sûr, il y a toujours des exceptions, mais en général, personne n’est plus à même de gérer les attributs d’une classe que cette dernière.
Repensez à ce qu’est la Programmation Orientée Objet dans le fond. Il s’agit d’un moyen permettant de séparer les fonctionnalités d’une application, sans que chacune n’ait à se préoccuper de l’implémentation de l’autre. En d’autres termes, ma classe A n’a pas besoin de savoir comment ma classe B fonctionne pour l’utiliser. Elle connaît les méthodes publiques, et ça lui suffit ! Imaginez que la classe A (ou le développeur qui a codé la classe A) devine l’implémentation de la classe B, et convaincu d’avoir compris son fonctionnement dans son intégralité, se permet de modifier manuellement un attribut de la classe B. Dans la plupart des cas, aucun problème, mais parfois, il arrive que le développeur n’ait pas prévu certains cas particuliers qui ont bien été gérés par le développeur de la classe B. La conséquence ? Si la valeur de l’attribut n’est plus parfaitement formatée par exemple, une pourrait se retrouver avec une belle exception !
Quel est l’intérêt ?
L’intérêt est simple : garantir l’intégrité des données. C’est-à-dire, s’assurer que les données des attributs sont valides.
Comment l’appliquer ?
Première étape : définir tous les attributs d’une classe en “private”/”protected”.
Deuxième étape : définir deux méthodes publiques pour chaque attribut. Un getter, et un setter, permettant respectivement de récupérer une valeur, et de la modifier. On appelle ces méthodes des “accesseurs”.
Exemple :
class Foo
{
private float bar;
public float getBar()
{
return bar;
}
public float setBar(float _bar)
{
bar = _bar;
}
}
Dans l’exemple ci-dessus, la méthode “getBar” permet de récupérer la valeur de “bar” depuis l’extérieur de la classe, et la méthode “setBar” permet de modifier la valeur de “bar”.
Imaginez maintenant que pour certaines raisons, il soit nécessaire que la valeur de bar soit toujours positive (dans un logiciel de comptabilité par exemple). Etant donné que l’application toute entière utilise “setBar” pour modifier la valeur de “bar”, il nous suffit d’ajouter un traitement dans la méthode “setBar” pour convertir “_bar” en valeur absolue.
De même, si nous ne souhaitons pas communiquer la véritable valeur de bar, qui pourrait être stockée sous une forme très pratique pour la classe “Foo”, et moins pratique à l’extérieur. Nous pourrions convertir la valeur retournée, sans modifier la vraie valeur de “bar”.
Dans tous les cas, il est conseillé de toujours utiliser l’encapsulation, de manière à ne pas avoir à modifier l’intégralité de l’application lorsque nous aurons besoins des accesseurs (getter/setter).
De plus, les IDEs intègrent généralement une fonctionnalité permettant de générer les getters/setters automatiquement. Vous n’avez donc plus aucune raison de ne pas vous en servir !
Attention : Cela ne veut pas dire que vous devez implémenter des accesseurs pour tous les attributs de votre classe. Certains attributs sont parfois créés juste pour le fonctionnement interne de la classe, et ne doivent pas être modifiés/lus depuis l’extérieur de la classe.
Le couplage
Vos classes peuvent dépendre les unes des autres. Suivant la difficulté à séparer deux classes, on parlera de couplage faible ou fort. Si la suppression d’une classe implique beaucoup de modifications dans l’autre classe, on parle de couplage fort. A l’inverse, si le système a bien été pensé afin de rendre les classes assez indépendantes, on parle de couplage faible.
Le problème du couplage fort, c’est que le code est difficile à faire évoluer. En cas de modification d’une classe, les répercussions peuvent être énormes sur les autres classes, et les liens qui unissent les classes de l’applications forment un vrai “plat de spaghettis”, difficile à démêler (pour info, le plat de spaghettis est un anti-pattern, c’est-à-dire une mauvaise pratique).
L’avantage du couplage faible, c’est que l’architecture qui en résulte est plus simple et flexible. Avec le couplage faible, on définit un standard, permettant à nos classes de communiquer de manière claire. Prenez l’exemple d’un casque audio, il suivra en général un standard qui est celui de la prise “Jack”. Et les smartphones/ordinateurs/etc… respectent ce protocole, ce qui permet de faire communiquer différents éléments simplement, comme s’il s’agissait de plugins. Grâce à ce protocole, on peut débrancher un casque pour le remplacer par des écouteurs sans problème. En Programmation Orientée Objet, on utilise le même principe.
En général, on met en place des interfaces, au minimum une par dépendance.
De plus, le concept de couplage est très important pour les tests unitaires.
L’immuabilité
Il existe des cas où des objets ne doivent pas pouvoir être modifiés. En Programmation Orientée Objet, les objets sont souvent passés par référence, afin d’optimiser les performances en évitant la copie complète d’un objet. Pourtant, parfois, il s’avère nécessaire d’empêcher la modification d’un objet.
L’immuabilité consiste à retourner une copie d’un objet, à chaque fois qu’on effectue une modification sur un objet, afin de garder l’objet de base intact/immuable, ou bien d’empêcher la modification de ses attributs.
Si votre applications utilise des Threads par exemple (processus exécutés en parallèle), il pourrait y avoir des conflits si deux threads modifient en même temps un même objet. L’immuabilité permet d’éviter ce problème.
La classe “String” par exemple, implémente ce concept. C’est à dire qu’à chaque fois que vous modifiez un objet “String”, c’est une copie qui est générée, l’objet d’origine restant intact. Exemple :
String foo = “azerty”;
String bar = foo.toUpperCase();
Dans cet exemple, l’objet “foo” n’est pas modifié lorsqu’on fait appel à la méthode “toUpperCase”, et l’objet “bar” n’est qu’une copie de l’objet “foo”, mis en majuscules.
Il y a plusieurs façon d’implémenter l’immuabilité. Tout ce qui importe, c’est de rendre une instance impossible à modifier (attributs en readonly, ou bien de retourner une copie de l’objet à modifier, etc…).
Réutilisabilité
Il vous est certainement déjà arrivé de dupliquer du code, pour une fonctionnalité qui fait à peu près la même chose qu’une autre, mais avec quelques légères différences. Vous avez pourtant déjà compris qu’il fallait factoriser au maximum son code en utilisant des fonctions par exemple. L’intérêt étant de pouvoir réutiliser ces fonctions plusieurs fois dans le code. Ainsi, en termes de maintenabilité, c’est tout de suite plus simple, car si un bug est présent, il ne suffirait en théorie que de corriger la fonction concernée. Le bug lui n’aura, dans ce cas, pas été dupliqué.
En Programmation Orientée Objet, vous pouvez pousser ce niveau de factorisation bien plus loin encore. En effet, vous disposez de bien plus d’outils (attributs, héritage, etc…) pour pouvoir créer des classes puissantes, et réutilisables.
Comment penser “réutilisabilité” ?
Il n’y a rien de compliqué. Durant la phase d’analyse, ou même lorsque vous codez, posez-vous la question suivante :
Quelles sont les tâches unitaires qui composent la fonctionnalité ?
Le terme important dans cette question, c’est “unitaire”, autrement dit, les processus les plus simples et petits. Si votre fonctionnalité consiste à authentifier un utilisateur, on pourrait définir comme tâches unitaires :
- Connexion à la base de données
- Requête vers la base de données
- Vérification du format du login/password
- Transformation des données saisies par l’utilisateur en données manipulables
- Enregistrement en session des informations de l’utilisateur connecté
- Etc…
Suite à cela, vous pouvez vous poser une deuxième question :
Une de ces tâches pourrait-elle être utilisée pour une autre fonctionnalité (même non-existante, ou non prévue pour l’instant) ?
Si on prend la tâche “Enregistrement en session des informations de l’utilisateur connecté”, on peut se dire que “l’enregistrement de données en session pourrait bien être utilisé pour une autre fonctionnalité, si on souhaite enregistrer des messages d’erreurs par exemple”. Dans ce cas, on pourrait créer une classe “Session”, permettant de simplifier l’enregistrement/récupération de données en session. Cette classe pourrait avoir une méthode “set(name : string, value : mixed)” et une méthode “get(name : string) : mixed”. Cette classe serait très simple à utiliser, finalement très pratique, et pourrait être testée unitairement ! (Les tests unitaires seront traités dans un autre chapitre, dites vous tout simplement qu’il s’agit d’une pratique permettant de garantir un haut niveau de qualité de code et de maintenabilité).
Polymorphisme
Certains langages de Programmation Orientée Objet, comme le Java ou le C++, sont fortement typés, c’est-à-dire qu’il est nécessaire de déclarer des variables en précisant le type (le compilateur ne le devinera pas pour vous). Et même si vous utilisez un langage faiblement typé (Script, PHP, Python, etc…), il est très important de comprendre les mécanismes internes de ces langages.
Le polymorphisme, c’est la capacité de manipuler différents objets de la même façon.
Pensez aux types “int” et “float”. L’un est un nombre entier, l’autre un nombre à virgule. Lorsque vous faites une addition, une soustraction, etc… vous n’avez pas vraiment à vous préoccuper du type de votre variable, pourtant derrière, le traitement est totalement différent. Le polymorphisme est dans ce cas la capacité des types “int” et “float” de permettre une manipulation commune, en faisant en interne les traitements qui leur sont propres.
Ce concept est puissant, et permet de réunir sous un même contrat des classes différentes, afin de pouvoir les utiliser de la même manière. On utilisera pour cela l’héritage, ou les interfaces (ces dernières ayant été créées dans ce but principalement).
Revenons à nos langages fortement typés, et prenons un exemple simple pour illustrer ce concept : Nous voulons faire une application permettant de générer des factures pour les clients d’un cinéma, et disposons d’une classe “Facture”, d’une classe “Ticket”, et d’une classe “Nourriture”. Une “Facture” peut lister des “Ticket”, et des “Nourriture”. Ces deux dernières classes sont différentes, car dans le premier cas il faudrait peut-être renseigner l’heure de la séance, alors que pour la “Nourriture”, il faudrait définir le nom de l’article (ex: Popcorn). Pourtant, notre classe “Facture” doit afficher les deux de la même manière, et ne souhaite pas se préoccuper du type des objets qui seront listés (imaginez qu’on doive par la suite ajouter une classe “PlaceParking”, une classe “Lunnettes3D”, etc…). Il faudrait pour chaque classe ajouter une liste d’objets de ce type dans la classe “Facture”, et on finirait pas avoir une classe “Facture” avec les attributs suivants :
- Ticket[] tickets;
- Nourriture[] nourritures;
- PlaceParking[] placeParkings;
- Etc…
Pourtant, tout ce dont notre classe Facture a besoin, c’est le libellé et le prix de chaque “Ticket”/”Nourriture”/etc…
Interface à la rescousse !
Les voilà nos belles interfaces, toujours prêtes à nous sauver la mise. Grâce à elles, nous allons pouvoir définir une interface “Facturable”, afin d’imposer une méthode “getFactureLibelle() : string”. Ensuite, nos classes “Ticket”, “Nourriture”, etc… n’auront qu’à implémenter cette interface, et définir la méthode “getFactureLibelle”. Pour la classe “Ticket”, cette méthode pourrait retourner le nom du film et l’heure de la séance, alors que pour la classe “Nourriture”, cette méthode pourrait retourner le nom de l’aliment. Le traitement interne de chaque classe diffère, pourtant, elles ont au moins une méthode “getFactureLibelle” qui est commune, bien qu’implémentée de façon différente.
Grâce à ça, notre classe “Facture” n’aura plus besoin de définir une liste pour chaque type d’objet. Un seul attribut “facturables : Facturable[]” permettrait de stocker à la fois des “Ticket”, des “Nourriture”, etc… Ensuite, nous pourrons faire appel à la méthode “getFactureLibelle” de chaque objet, quelque soit son type, car nous savons que peu importe qu’il s’agisse d’un “Ticket” ou d’un “Nourriture”, l’objet a une méthode “getFactureLibelle” qui retourne un String. On peut donc faire une boucle sur “facturables”, et appeler la méthode “getFactureLibelle” de chaque objet. Lors de l’exécution, si l’objet est un “Ticket”, alors le nom du film et l’heure de la séance sera retourné.
On pourrait également ajouter une méthode « getMontant », ou toute autre méthode simplifiant la vie de la classe Facture.
Dans cette exemple, nous avons mis en place une interface permettant de traiter de la même manière des objets différents, il s’agit du polymorphisme.
KISS : Keep It Simple Stupid
Enfin, nous terminerons sur un concept très simple : votre code doit être “tout bêtement simple”. En effet, si votre code/architecture est compliqué à comprendre, c’est très certainement parce que quelque chose a mal été pensé. Alors à chaque fois que vous codez, gardez cette notion en tête, et faites quelque chose de simple à comprendre et à manipuler !
Pour résumer
Concept de responsabilité unique: Une classe ne doit avoir qu’une seule et uniquement responsabilité, ni plus, ni moins.
Encapsulation : C’est à la classe de gérer ses attributs. Implémentation d’accesseurs pour y accéder/les modifier depuis l’extérieur.
Couplage : Niveau de liaison entre classes. Le couplage doit être faible de préférence.
Immuabilité : Un objet ne peut être modifié. Retourne une copie, ou définit les attributs comme invariables.
Réutilisabilité : Les tâches doivent être découpées afin de créer des classes réutilisables.
Polymorphisme : Définition d’une interface commune à plusieurs classes implémentant la même logique.
KISS : Le code/architecture doit toujours rester simple.