Pourquoi ? La réponse à cette question est simple: l'abstraction change moins vite que l'implémentation concrète. Par voie de conséquence, si mon interface est directement liée à mon implémentation et que celle ci change, alors ces modifications ont un impact fort sur le reste de mon code. Si mon code utilise non pas une interface liée à l'implémentation mais une interface liée à l'abstraction sous-jacente, alors il changera moins souvent.

Considérons le code (assez typique) suivant:

class Salarie
{
   int mSalaire;
   float mPourcentCharges;
public:
   Salarie(int salaire);
   int DonneSalaire() { return mSalaire; }
   float DonnePourcentCharges() { return mPourcentCharges; }
};
class Entreprise { std::vector<Salarie*> mSalaries; public: int DonneNbSalairies(); Salarie *DonneSalarie(int index); }

Le cahier des charge de notre application demande à ce que l'on calcule puis que l'on affiche dans une boite d'édition la masse salariale de l'entreprise. Ce n'est pas difficile:

void DialogEntreprise::AfficheMasseSalariale(Entreprise *e)
{
   float masseSalariale = 0.0f;
for (int i=0; i<e->DonneNbSalaries(); i++) { Salarie *s = e->DonneSalarie(i); int salaire = s->DonneSalaire(); float pc = s->DonnePourcentCharges(); masseSalariale += (1.0f + pc) * s->DonneSalaire(); } mEdit.Affiche(masseSalariale); }

Bien que correct, ce code est l'exact contraire d'une encapsulation propre. Pourtant, je n'accède à aucun membre public - toutes mes variables sont privées. Alors, quel est le problème ?

Le point le plus important est que j'expose les détails de l'implémentation de la classe Entreprise à la classe chargée de l'affichage de celle-ci: c'est à la boite de dialogue de compter le nombre de salariés et de demander à chaque salarié son salaire. Supposons que j'ai besoin à plusieurs endroits de calculer cette masse salariale. Dois-je réécrire systématiquement ce code ? Et dans ce cas, que devrais-je faire si j'ajoute à la liste des salariés une liste de stagiaires ? Bien évidemment, réécrire plusieurs fois le même code montre le besoin de factorisation - et dans ce cas, la factorisation passe par une nécessaire encapsulation du calcul de la masse salariale.

// le calcul de la masse salariale est déporté dans la classe Entreprise 
float Entreprise::CalculeMasseSalariale()
{
   float masseSalariale = 0.0f;
for (size_t i=0; i<mSalaries.size(); i++) { Salarie *s = mSalaries.at(i); int salaire = s->DonneSalaire(); float pc = s->DonnePourcentCharges(); masseSalariale += (1.0f + pc) * s->DonneSalaire(); } return masseSalariale; }

Le code appelant est simplifié d'autant, et j'ai maintenant plus de lattitudes pour modifier la façon dont ce calcul se fait.

Est-ce parfait pour autant ? Non. Supposons que la masse salariale d'un stagiaire est égal à sa rémunération à laquelle on ajoute des charges non plus sous forme de pourcentage mais sous la forme de charges fixes. Le code s'écrit alors:

float Entreprise::CalculeMasseSalariale()
{
   float masseSalariale = 0.0f;
for (size_t i=0; i<mSalaries.size(); i++) { Salarie *s = mSalaries.at(i); int salaire = s->DonneSalaire(); if (s->EstStagiaire()) { masseSalariale += s->DonneSalaire() + s->DonneChargesStagaire(); } else { float pc = s->DonnePourcentCharges(); masseSalariale += (1.0f + pc) * s->DonneSalaire(); } } return masseSalariale; }

Voici un exemple clair de l'impact que peut avoir la modification de la classe Salarie sur l'algorithme de calcul de la masse salariale. Il devient rapidement évident que toute modifiaction subséquente de Salarie risque d'entraîner une modification de cette méthode qui va alors se fragiliser et être une probable source de bugs.

Pour éviter ce problème potentiel, il suffit de porter un cran plus loin l'encapsulation, en encapsulant le calcul du salaire chargé au niveau de la class Salarie.

// la méthode peut être aisément modifiée 
float Salarie::DonneSalaireCharge()
{
   return (1.0f + mPourcentCharge) * mSalaire; 
}
float Entreprise::CalculeMasseSalariale() { float masseSalariale = 0.0f;
for (size_t i=0; i<mSalaries.size(); i++) { Salarie *s = mSalaries.at(i); masseSalariale += s->DonneSalaireCharge(); } return masseSalariale; }

Le code est beaucoup plus simple, et par conséquent beaucoup moins risqué.

Comme je l'ai dit plus haut, encapsuler c'est découpler l'abstraction de l'implémentation.

Une bonne encapsulation va plus loin que l'encapsulation des propriétés d'un objet - celle ci font partie de l'implementation, et non pas de l'abstraction. En fait, encapsuler seulement l'accès aux propriétés d'un objet est aussi inneficace que de mettre ces propriétés en accès public. Ce que l'on a besoin d'encapsuler, c'est la manière dont on se sert de ces propriétés. D'où la règle suivante:

On encapsule un comportement, pas des propriétés.

Comment celà se traduit-il ? Simplement comme ceci: si un algorithme utilise les données d'une classe ou accessibles via cette classe, alors cet algorithme doit être encapsulé dans la classe. En appliquant récursivement cette idée au code source donné au début de cet article, on arrive au final à la solution donnée plus haut.

Bien évidemment, il ne faut pas hésiter à créer des méthodes dont la taille parait insignifiante - telle la méthode Salarie::DonneSalaireCharge() donnée plus haut, qui se contente de faire un simple calcul. Au final, la classe peut donner l'impression d'être trop chargée, trop lourde à gérer. C'est peut-être vrai, mais ceci fera l'objet d'un autre billet.

Si l'on souhaitre être pointilleux en ce qui concerne la taxonomie des outils de l'architecture objet, on notera que l'encapsulation n'est pas à proprement parler un principe mais une simple règle. J'ai décidé de passer outre cette taxonomie, et je la présente tout de même comme un principe - pour plusieurs raisons :

  1. l'encapsulation n'est pas un artefact de langage - aucun langage ne propose l'encapsulation des comportements de matière native, même si certains peuvent proposer une encapsulation faible (encapsulation de l'accès aux données). Il s'agit donc d'un outil que l'architecte se doit de mettre en oeuvre par lui même.
  2. elle a un impact fort sur l'interface des classes, et donc par ricochet sur l'architecture, puisqu'elle autorise la programmation non plus vers une implémentation mais vers une interface[1] - objectif vers lequel une architecture doit tendre.
  3. sans encapsulation, il est impossible d'implémenter les autres principes de programmation objet[2]. L'encapsulation est en effet necessaire pour leur mise en oeuvre. Elle est en outre à la base d'un certain nombres de règles (loi de Demeter, utilisation des accesseurs, etc.).

Ces raisons font de l'encapsulation une constante de la programmation orientée objet, au même titre que les autres principes.

Notes

[1] l'idiome anglais est program to an interface

[2] OCP, LSP, SRP, IDP et autres NCP ; ces principes feront l'objets de billets ultérieurs