2. L'encapsulation
Après cette quête, tu sauras comment modifier la portée des attributs et méthodes d'une classe
Objectifs
-
Comprendre l'utilité de l'encapsulation
-
Comprendre la notion de portée et de visibilité
-
Utiliser l'encapsulation pour mettre à disposition une interface simple
Le brasseur

Récemment, tu as décidé de te lancer dans la fabrication de bière. Au début, c'était un hobbie pratiqué à la maison. Lorsque tu as vu tes stocks de bière s'écouler à la vitesse de la lumière lorsque tu faisais goûter ton breuvage à tes convives et que tu recevais uniquement de bons retours concernant le goût et la texture de ta bière, tu as décidé de changer de job et de te lancer dans le brassage de bière.
Seulement, le matériel dont tu disposes à la maison ne suffit pas à construire une entreprise fabriquant de la bière de manière industrielle. Tu vas avoir besoin de locaux et d'encapsuler des bouteilles remplies de ton merveilleux breuvage.
La phase d'encapsulation d'une bouteille à la main est un peu compliquée et surtout très rébarbative quand il s'agit de 1000 bouteilles. Tu ne peux plus te permettre de réaliser le travail à la main à cause du temps que cela te fait perdre. Pour cela, tu décides d'acheter une machine qui accomplira le travail pour toi.
Tu ne sais absolument pas comment la machine fonctionne, tu sais uniquement qu'elle a besoin de capsules, de bière et de bouteilles. Si elle en manque, elle ne peut pas continuer l'assemblage des trois éléments requis. A la lecture du mode d'emploi, tu t'aperçois qu'il faut programmer la machine pour qu'elle fonctionne de la manière souhaitée et, pour cela, il va te falloir un peu de culture concernant la manière dont elle a été conçue !
La portée dans les blocs
Qu'est-ce qu'une portée ? Un tireur d'élite te dirait qu'il s'agit de la distance maximum qu'une balle peut parcourir une fois tirée. Si tu es programmeur, eh bien ce n'est pas si éloigné que cela et tout aussi fondamental.
Si dans le corps d'une méthode, tu déclares des variables, lesdites variables ne seront accessibles que dans le corps de ladite fonction. On appelle ces variables des variables locales, puisqu'elles sont détruites dès lors que l'exécution de ta méthode est terminée. De la même manière - et cela paraît un peu moins évident - les variables que tu déclares dans le corps d'une condition ou d'une boucle ne seront pas accessible en dehors. Eh oui, en C#, les variables que tu déclares dans un bloc n'ont qu'une portée qui leur permet d'être utilisée à l'intérieur du bloc où elle sont déclarées et dans ses blocs fils.
C'est un peu du charabia, tout ça ...
Je te l'accorde, mais je t'assure que tout deviendra plus simple après ce petit exemple. Je t'enjoins vivement de copier-coller ce code et de tenter de le compiler :
using System;
namespace ScopeTest
{
public class MainProgram
{
public static void Main()
{
for (int i=0; i < 20; i++)
{
string sentence = "I have encapsulated " + i + " bottles of beer.";
}
Console.WriteLine(sentence);
}
}
}
Le code ci-dessus itère 20 fois. A chaque itération, une variable de type string est instanciée avec comme valeur affectée une chaîne de caractères affichant le nombre de bières encapsulées. A la fin de la boucle, on désire afficher le résultat de la dernière itération qui sera Repetition 19. Si tu essaies de compiler ce programme, le compilateur te remontera une erreur. La description de l'erreur est : The name 'sentence' does not exist in the current context.
Qu'est-ce que ça peut bien vouloir dire ?
Le nom sentence n'existe pas dans le contexte courant, voilà ce que ça veut dire.
J'avais compris, mais ça ne m'aide pas plus que ça !
Eh bien, c'est une question de portée. La variable sentence a été déclarée dans le bloc itératif for. Elle ne peut donc pas sortir de ce bloc. En fait, la variable n'existe tout simplement pas en-dehors du bloc. C'est ce que tu dois comprendre du message d'erreur.
Arf, c'est embarrassant ! Comment je peux faire pour résoudre ça, alors ?
Eh bien, il y a plusieurs solutions. Et la meilleure est de remettre en question la portée de ta variable sentence. Dans le code qui ne compile pas, la variable sentence est déclarée dans un bloc et n'est accessible qu'à l'intérieur du bloc. Que faudrait-il faire, alors ?
Il faut changer la portée de la variable ?
Eh oui ! Comme une variable déclarée dans un bloc y a accès ainsi qu'à ses blocs enfants, il te suffit de monter ta portée d'un seul niveau :
using System;
namespace ScopeTest
{
public class MainProgram
{
public static void Main()
{
string sentence = "";
for (int i=0; i < 20; i++)
{
sentence = "I have encapsulated " + i + " bottles of beer.";
}
Console.WriteLine(sentence);
}
}
}
L'exemple ci-dessus compile, et affiche bien I have encapsulated 19 bottles of beer à la fin de la boucle. Tout ce qu'il y avait à faire était de rendre ta variable accessible dans le contexte de la fonction Main, en la déclarant à l'intérieur de ta fonction Main. Gérer les contextes est quelque chose de vital en C#, puisqu’une variable déclarée dans un bloc ne peut être atteinte dans le bloc parent.
Et qu'est-ce que ça a à voir avec le modèle objet ?
La visibilité des attributs
La portée ne s'applique pas qu'aux variables définies dans des blocs. Tu peux aussi paramétrer la portée des attributs et des méthodes d'une classe. Bien que la notion de portée représente le même concept, la portée des attributs et méthodes se nomme la visibilité. La visibilité est la portée des méthodes et attributs.
Deux mots-clés (actuellement trois, mais nous ne verrons le troisième que plus tard) permettent de configurer la visibilité et tu en connais déjà au moins un : public. Le mot-clé public rend accessible un attribut ou une méthode en-dehors de sa déclaration. Ainsi, si j'ai déclaré une méthode publique IsAlive() dans une classe InvicibleCharacter, je peux y accéder avec l'opérateur .:
using System;
namespace RolePlayGame
{
public class InvincibleCharacter
{
public bool IsAlive()
{
return true;
}
}
public class MainProgram
{
public static void Main()
{
InvicibleCharacter character = new InvicibleCharacter();
character.IsAlive();
}
}
}
La visibilité publique de la méthode IsAlive() la rend accessible partout, même en dehors de la déclaration de la classe. C'est-à-dire qu'une instance de la classe permet d'utiliser cette méthode.
Il existe la portée inverse à public et je te la donne en mille.
Ro...c'est pas sympa. Tu me prends pour un jambon !
Non, je veux juste que tu réfléchisses.
Bah, c'est simple : l'inverse de publique est privée, c'est donc le mot-clé
private!
Et voilà ! Donc, le mot-clé private réalise l'inverse du mot-clé public, c'est-à-dire qu'il restreint la visibilité de l'attribut ou de la méthode à la classe en question. Par conséquent, si un attribut ou une méthode est privé(e), il/elle ne peut être appelé/e en-dehors du corps de la classe.
A quoi sert une visibilité privée ? C'est plus simple de pouvoir accéder à tout, quand on veut et où on veut, non ?
Eh bien en théorie, oui, en pratique, non. Un beau design suit des règles rigoureuses. Si elles ne sont pas respectées, il y a là de grandes chances que le code devienne petit à petit un fouillis inintelligible. On fait toujours au plus simple, et ensuite on améliore si nécessaire. Et dans la vraie vie, tu te fiches à coup sûr de connaître la position exacte du rotor d'un moteur à un instant donné où tu l'actionnes. D'ailleurs, tu n'as même aucun pouvoir dessus. C'est lorsque le moteur tourne qu'il modifie la position de son rotor, et non parce que tu l'actionnes.
Le modèle objet se veut le plus proche de la réalité possible, et ton cerveau abstrait aussi le monde en terme de visibilité publique ou privée. Ecrire un code propre revient à produire une abstraction cohérente de la réalité, et la réalité n'expose pas tous les détails à la vue de l'utilisateur.
Ah, mais au fait, je ne t'ai pas donné d'exemple d'utilisation du mot-clé private ! Le voici :
using System;
namespace RolePlayGame
{
public class MainProgram
{
public static void Main()
{
Character character = new Character();
while (character.IsAlive())
{
character.LoseLifePoints(10);
if (character.IsAlive())
{
Console.WriteLine("The character is still alive");
}
}
Console.WriteLine("The character is dead");
}
}
public class Character
{
private int _lifePoints=100;
public bool IsAlive()
{
if (_lifePoints > 0)
{
return true;
}
else
{
return false;
}
}
public void LoseLifePoints(int loosedLifePoints)
{
_lifePoints = _lifePoints - loosedLifePoints;
}
}
}
L'exemple ci-dessus représente un caractère de jeu vidéo avec une classe Character. Ce caractère de jeu vidéo peut mourir. Il meurt lorsque ses points de vie sont à zéro. Un attribut privé représente le nombre de points de vie qu'il lui reste. Cet attribut n'est pas accessible en dehors du corps de la classe. Il est uniquement modifiable par une méthode LoseLifePoints(int) qui diminue le nombre de points de vie du personnage. La méthode IsAlive() permet de vérifier si le personnage est toujours en vie. A aucun moment, l'attribut _lifePoints ne peut être accédé en dehors de la classe. Il est manipulé par deux méthodes qui permettent de décrire les fonctionnalités, sans en connaître les détails. La barre horizontale (underscore) précédant le nom de l'attribut représentant les points de vie restants du caractère est une convention de nommage. En C#, il est d'usage de préfixer tous les attributs et méthodes privés avec un underscore (_), ceci afin de rendre clair le fait que l'attribut est privé. Mais les conventions changent selon les équipes et les époques. Pour ma part, je resterai sur celle-ci, car elle est très claire.
Le code dans la méthode Main enlève dix points de vie à chaque itération et la boucle s'arrête lorsque le personnage meurt (n'a plus de points de vie). C'est très simple à comprendre puisque les méthodes ont des noms représentatifs et il est difficile de mal les interpréter. On voit clairement que l'on manipule un objet qui a des fonctionnalités.
On met l'attribut _lifePoints en visibilité privée, car on ne veut pas que le codeur fasse n'importe quoi avec cet attribut. Mieux encore, on ne veut même pas qu'il sache que la classe Character a des points de vie. Cela lui simplifie la vie, car il n'a pas à se demander comment sont achevées les opérations, tant qu'elles le sont.
Mais il y a quand même un problème ! Comment est-ce qu'on peut accéder au nombre de points de vie restants pour les afficher, par exemple ?
Les accesseurs
En programmation orientée objet, le programmeur a pour responsabilité de paramétrer la visibilité des attributs et méthodes qu'il définit dans une classe. Il se doit de fournir une interface claire : il choisit très précisément, et après mûre réflexion, les spécificités d'une classe. Il se doit de penser aux autres développeurs et les empêcher au maximum de leur faire faire n'importe quoi. Le code doit être clair pour permettre aux autres programmeurs de vite prendre en main la solution écrite. A l'inverse, le développeur ne doit pas faire preuve de paranoïa et doit laisser suffisamment d'espace pour permettre à un autre d'améliorer son code et/ou d'utiliser une de ses classes comme il l'entend (et il y a des règles pour ça).
Cependant, la classe ne doit exposer que les attributs utiles pour utiliser les fonctionnalités de la classe qu'il crée. C'est ce qu'on appelle l'encapsulation.
En principe, on déclare tous les attributs d'une classe avec une portée privée, puis ensuite on décide de l'accès ou non à l'attribut en-dehors de la classe.
Mais si l'attribut est privé, je ne peux pas y accéder ? Alors, ça n'a pas de sens, ton truc ?
Eh bien, un attribut ayant une visibilité privée est accessible dans le corps de classe. Donc, pour paramétrer l'accès ou non à un attribut, on utilise des méthodes publiques qui ont le pouvoir de modifier ou non l'attribut. Ce type de méthode a un nom : accesseurs. Les accesseurs permettent de modifier ou de récupérer la valeur d'un attribut privé. Les accesseurs sont publics, car ils doivent pouvoir être utilisés à l'extérieur du corps de la classe, sinon ils ne peuvent pas être qualifiés d’ accesseurs.
Un exemple :
using System;
namespace RolePlayGame
{
public class Character
{
private int _lifePoints = 100;
private string _name = "";
public bool IsAlive()
{
if (_lifePoints > 0)
{
return true;
}
else
{
return false;
}
}
public void LooseLifePoints(int loosedLifePoints)
{
_lifePoints = _lifePoints - Math.Abs(loosedLifePoints);
}
public int GetLifePoints()
{
return _lifePoints;
}
public void SetName(string name)
{
_name = name;
}
public string GetName()
{
return _name;
}
}
}
J'ai repris l'exemple de la classe de l'étape précédente pour résoudre la problématique d'accès à l'attribut privé. Comme tu peux le lire, une méthode GetLifePoints() permet de récupérer l'attribut. Une autre méthode permet d'accéder à l'attribut privé _lifePoints : LooseLifePoints qui diminue les points de vie. On permet l'accès en lecture à l'attribut _lifePoints et un accès partiel en écriture. En effet, la méthode LooseLifePoints(int) ne permet que d'enlever des points de vie, et non d'en rajouter, et ceci grâce à la méthode Math.Abs(int) qui renvoie la partie absolue d'un nombre. Dans ce cas, même si le développeur passe un nombre négatif, il ne pourra pas augmenter le nombre de points de vie.
Est exposé un seul attribut qui peut uniquement être utilisé en-dehors de la classe de la façon dont on l'a voulue. Nous avons donc bien réalisé notre encapsulation et notre classe est prête à être utilisée correctement. De plus, on peut maintenant récupérer les points de vie pour les afficher. On peut aussi affecter un nom à notre caractère grâce à SetName(string) et récupérer sa valeur grâce à GetName(). Tout va bien dans le meilleur des mondes : nous avons rendu notre classe lisible et nous faisons savoir à nos confrères comment ils devraient l'utiliser pour utiliser correctement ses fonctionnalités.
Challenge
Une machine industrielle
Je veux que tu représentes une machine industrielle qui verse de la bière dans une bouteille puis l'encapsule. Le nom de la classe est BeerEncapsulator.
Le nombre de capsules dont la machine dispose doit être accessible en modification, ainsi que son nombre de bouteilles. En revanche, la quantité de bière disponible ne peut pas diminuer à l'extérieur de la machine, uniquement augmenter :
-
_avalaibleBeerVolumeest un nombre décimal qui représente en centilitres le volume de bière disponible dans la machine -
_avalaibleBottlesest un nombre entier qui représente le nombre de bouteilles disponibles -
_avalaibleCapsulesest un nombre entier qui représente le nombre de capsules disponibles -
Aucun attribut n'est accessible en-dehors de la classe, ils sont déclarés en private. On y accède de l'extérieur avec des accesseurs.
-
Une méthode
AddBeerajoute de la bière -
Chaque bouteille est remplie de 33 centilitres de bière, nécessite une capsule et une bouteille. Lorsqu'une bouteille est réalisée, le nombre de composants diminue.
-
Dès qu'un composant manque, un message est affiché spécifiant la quantité d'un composant à ajouter nécessaire à la fabrication du nombre de bouteilles.
-
Une méthode
ProduceEncapsulatedBeerBottlesréalise la mise en bouteille:-
Elle prend un seul argument: le nombre de bouteilles qu'elle doit fabriquer
-
Elle renvoie un entier: le nombre de bouteilles qui ont pu être faites
-
Si il n'y a pas assez de composants, la méthode renvoie 0
-
Critères de validation
-
Un lien vers ta solution sur GitHub
-
L'utilisateur doit saisir combien de litres de bière, de bouteilles vides et de capsules sont disponibles
-
Le nombre de capsules et de bouteilles ne peut pas être négatif
-
Une bière peut être brassée s’il y a au moins une bouteille, 33 centilitres de bière et une capsule
-
Un message spécifiant la quantité de composant à ajouter est affiché dès lors que la quantité disponible du composant en question est inférieure à ce qui est requis pour fabriquer un nombre de bouteilles.
-
Des accesseurs permettent d'accéder au nombre de bouteilles, au volume de bière et au nombre de capsules disponibles dans la machine