Les règles d’encapsulations
L’encapsulation est une pratique en programmation orientée objet, qui consiste à regrouper les données et les méthodes qui les manipulent au sein d’une même classe. Ce principe vise à cacher la complexité et les détails techniques sous-jacents d’un objet, en ne révélant que les fonctionnalités utiles aux autres développeurs. Grâce à l’encapsulation, les données sont protégées contre les manipulations accidentelles ou non autorisées, ce qui rend le code plus robuste, plus facile à maintenir et plus-evolutif.
L’encapsulation facilite la collaboration entre les développeurs en leur permettant de se concentrer sur l’utilisation des objets plutôt que sur leurs détails d’implémentation. En exposant uniquement les interfaces publiques nécessaires, il est plus facile pour les développeurs de comprendre et d’utiliser les objets sans avoir à se soucier de leurs mécanismes internes. Cela favorise également la réutilisation du code et limite les erreurs, car les développeurs sont guidés dans l’utilisation correcte des objets.
L’encapsulation facilite la maintenance et l’évolution du code à long terme. En séparant les détails d’implémentation de l’interface publique, vous pouvez modifier ou améliorer le fonctionnement interne d’un objet sans impacter les parties du code qui l’utilisent. Cela permet de réduire le risque d’introduire de nouvelles erreurs lors des modifications et d’assurer une meilleure compatibilité entre les différentes versions de votre code.
Pour assurer une encapsulation efficace, il est important de respecter les règles suivantes :
Utilisez des modificateurs d’accès appropriés
La déclaration des variables d’instance (attributs) en tant que private
permet de limiter leur accès aux méthodes de la classe elle-même. Cela empêche les autres classes et les développeurs d’accéder directement à ces attributs, garantissant ainsi l’intégrité des données et la cohérence de l’objet. En cachant les détails d’implémentation, on s’assure que les modifications apportées aux attributs sont effectuées de manière contrôlée et conforme aux règles métier définies dans la classe.
Les méthodes d’accès (getters) et de modification (setters) sont des fonctions public
utilisées pour contrôler l’accès aux attributs private
d’une classe. Les getters permettent de récupérer la valeur d’un attribut sans le modifier, tandis que les setters autorisent à modifier la valeur d’un attribut tout en respectant les contraintes définies dans la classe. Ces méthodes proposent une interface sécurisée et contrôlée pour interagir avec les attributs d’un objet, tout en préservant l’encapsulation et en respectant les règles métier.
Dans l’exemple ci-dessous, les attributs age
et nom
sont déclarés comme private
. L’attribut age
est en lecture seule, donc seul un getter est fourni. L’attribut nom
est en lecture et écriture, donc un getter et un setter sont fournis pour accéder et modifier sa valeur de manière contrôlée.
class Personne: def __init__(self, unNom: str): self.__nom = unNom self.__age = 0 @property def nom(self) -> str: return self.__nom @nom.setter def nom(self, nouveauNom: str) -> None: self.__nom = nouveauNom @property def age(self) -> int: return self.__age
#include <string> class Personne { private: std::string _nom; int _age; public: Personne(std::string unNom) : _nom(unNom), _age(0) {} std::string getNom() const { return _nom; } void setNom(std::string nouveauNom) { _nom = nouveauNom; } int getAge() const { return _age; } };
public class Personne { private string _nom; private int _age; public Personne(string unNom) { Nom = unNom; Age = 0; } public string Nom { get { return _nom; } set { _nom = value; } } public int Age { get { return _age; } private set { _age = value; } } }
Validez les données en entrée
Avant d’affecter de nouvelles valeurs aux attributs, il est possible de valider les données en entrée pour s’assurer qu’elles respectent certaines contraintes ou conditions. Cette validation permet d’éviter l’affectation de valeurs incohérentes ou inattendues, qui pourraient entraîner des erreurs ou des comportements indésirables dans le fonctionnement de l’objet. La validation des données peut impliquer plusieurs types de vérifications, en fonction des exigences spécifiques du programme. Les contraintes de validation courantes incluent :
- Les vérifications de plage : s’assurer qu’une valeur est comprise entre une valeur minimale et maximale.
- Les vérifications de type : s’assurer qu’une valeur est d’un certain type de données.
- Les vérifications d’état : s’assurer qu’une valeur est conforme à un ensemble de conditions spécifiques.
Prenons l’exemple d’une classe Voiture
qui dispose d’un attribut vitesseMax
représentant la vitesse maximale que la voiture peut atteindre. Supposons que les voitures de notre application ne peuvent pas dépasser une vitesse maximale de 300 km/h et ne peuvent pas être inférieures à 50 km/h pour des raisons de sécurité. Nous pouvons créer un setter qui valide les données fournies avant de les affecter à la variable d’instance vitesseMax :
class Voiture: def __init__(self, vitesse: int): self.vitesse_max = vitesse @property def vitesse_max(self) -> int: return self.__vitesse_max @vitesse_max.setter def vitesse_max(self, vitesse: int): if 50 < vitesse < 300: self.__vitesse_max = vitesse else: raise ValueError("La vitesse doit être comprise entre 50 et 300.")
class Voiture { private: int _vitesse_max; public: Voiture(int vitesse) { set_vitesse_max(vitesse); } int get_vitesse_max() const { return _vitesse_max; } void set_vitesse_max(int vitesse) { if (vitesse > 50 && vitesse < 300) { _vitesse_max = vitesse; } else { throw std::invalid_argument("La vitesse doit être comprise entre 50 et 300."); } } };
public class Voiture { private int _vitesseMax; public int VitesseMax { get { return _vitesseMax; } set { if (value > 50 && value < 300) { _vitesseMax = value; } else { throw new ArgumentException("La vitesse doit être comprise entre 50 et 300."); } } } public Voiture(int vitesse) { VitesseMax = vitesse; } }
Minimisez l’exposition des détails d’implémentation
L’exposition des fonctionnalités minimum consiste à ne rendre accessibles aux autres développeurs que les méthodes et attributs nécessaires pour interagir avec un objet, sans dévoiler les détails d’implémentation internes. Cela permet de simplifier l’utilisation de la classe, d’éviter les erreurs involontaires et de faciliter la maintenance et l’évolution du code. En cachant les mécanismes internes, vous créez une interface claire et cohérente qui guide les développeurs dans l’utilisation de vos objets et assure une meilleure compatibilité entre les différentes versions du code.
Prenons l’exemple d’une classe CarteBancaire
qui dispose d’un attribut codePin
qui ne doit jamais être exposé ou modifié directement par les utilisateurs. On peut y ajouter une méthode verifierCode est déclarée public et permet de vérifier si le code PIN fourni est correct. Cette méthode renvoie un booléen indiquant si le code PIN est valide ou non, sans exposer le code PIN lui-même. Ainsi, les autres développeurs peuvent utiliser la classe CarteBancaire pour vérifier le code PIN sans jamais avoir accès au code PIN réel, ce qui respecte les principes d’encapsulation et garantit la sécurité des données.
class CarteBancaire: def __init__(self, code): self.__code_pin = code def verifier_code(self, code): if self.__code_pin == code: return True else: return False
class CarteBancaire { private: int codePin; public: CarteBancaire(int code) : codePin(code) {} bool verifier_code(int code) { if (codePin == code) { return true; } else { return false; } } };
public class CarteBancaire { private int codePin; public CarteBancaire(int code) { codePin = code; } public bool VerifierCode(int code) { if (codePin == code) { return true; } else { return false; } } }
Maintenez une séparation claire entre l’interface publiques et les mécanismes internes
En suivant les principes d’encapsulation, vous facilitez la maintenance et l’évolution du code en permettant des modifications internes sans impacter les parties du code qui utilisent les objets. En isolant les détails d’implémentation à l’intérieur des classes, vous pouvez apporter des changements à la logique interne sans affecter la manière dont les autres développeurs interagissent avec vos objets. Cela permet de réduire le risque d’introduire de nouvelles erreurs lors des modifications et d’assurer une meilleure compatibilité entre les différentes versions de votre code.
Reprenons l’exemple de la classe CarteBancaire
et ajoutons une fonction privée de cryptage pour améliorer la sécurité des données. Ajoutons une fonction privée crypterCodePin
qui crypte un code PIN. Cette fonction est déclarée private
, ce qui signifie qu’elle ne peut être utilisée qu’à l’intérieur de la classe CarteBancaire
. La méthode verifierCode
a été modifiée pour comparer le code PIN crypté plutôt que le code PIN en clair.
class CarteBancaire: def __init__(self, code): self.__codePinCrypté = self.__crypter_code_pin(code) def verifier_code(self, code): if self.__codePinCrypté == self.__crypter_code_pin(code): return True else: return False def __crypter_code_pin(self, code): return code * code
class CarteBancaire { private: int codePinCrypté; int crypter_code_pin(int code) { return code * code; } public: CarteBancaire(int code) { codePinCrypté = crypter_code_pin(code); } bool verifier_code(int code) { if (codePinCrypté == crypter_code_pin(code)) { return true; } else { return false; } } };
public class CarteBancaire { private int codePinCrypté; public CarteBancaire(int code) { codePinCrypté = CrypterCodePin(code); } public bool VerifierCode(int code) { if (codePinCrypté == CrypterCodePin(code)) { return true; } else { return false; } } private int CrypterCodePin(int code) { return code * code; } }
En encapsulant la logique de cryptage à l’intérieur de la classe, nous avons pu améliorer la sécurité des données sans impacter les parties du code qui utilisent la classe CarteBancaire
. Les autres développeurs peuvent continuer d’utiliser la méthode verifierCode
de la même manière qu’auparavant, sans se soucier des détails de l’implémentation du cryptage. Cela démontre comment l’encapsulation facilite la maintenance et l’évolution du code tout en préservant la compatibilité et la stabilité des interfaces publiques.
Notez que dans cette nouvelle version de CarteBancaire
, nous avons remplacé l’attribut codePin
par codePinCrypté
. Comme cet attribut est déclaré private
, seules les méthodes internes de la classe CarteBancaire
peuvent y accéder et le manipuler. Ainsi, lorsque nous effectuons ce changement, il n’affecte en aucun cas les autres développeurs qui utilisent notre objet.
Cela démontre l’importance de l’encapsulation pour assurer la modularité et la flexibilité du code. En gardant les détails d’implémentation et les attributs privés à l’intérieur de la classe, nous pouvons apporter des modifications sans impacter les parties du code qui utilisent nos objets. Les autres développeurs peuvent donc continuer d’interagir avec la classe CarteBancaire
via ses méthodes publiques sans se soucier des changements internes, ce qui facilite la maintenance, l’évolution et la réutilisation du code.
En UML
Dans un diagramme de classe UML, les attributs sont généralement représentés en premier, suivis des méthodes. Les méthodes se distinguent par des parenthèses après leur nom. Concernant la visibilité, un +
indique que l’attribut ou la méthode est public, donc accessible depuis toutes les classes, tandis qu’un -
signifie qu’il est privé et accessible uniquement dans la classe où il est défini. Enfin, il existe aussi le symbole #
qui signifie protégé, c’est-à-dire modifiable par la classe et ses dérivées, mais pas par les autres classes. Lorsque cette visibilité n’est pas précisée, l’appréciation est laissée au développeur.