Lâun des principes les plus importants de la programmation orientée objet â délimiter lâinterface interne de lâinterface externe.
Câest une pratique «incontournable» dans le développement de quelque chose de plus complexe que lâapplication «hello world».
Pour comprendre cela, écartons nous du développement et tournons nos yeux dans le monde réel.
Habituellement, les appareils que nous utilisons sont assez complexes. Mais délimiter lâinterface interne de lâinterface externe permet de les utiliser sans problèmes.
Un exemple concret
Par exemple, une machine à café. Simple de lâextérieur : un bouton, un écran, quelques trous⦠Et le résultat : du bon café ! :)
Mais à lâintérieur⦠(une image du manuel de réparation)
Beaucoup de détails. Mais on peut lâutiliser sans rien savoir.
Les machines à café sont assez fiables, nâest-ce pas ? Nous pouvons en utiliser une pendant des années, et seulement en cas de problème, la faire réparer.
Le secret de la fiabilité et de la simplicité dâune machine à café â tous les détails sont bien réglés et cachés à lâintérieur.
Si nous retirons le capot de protection de la machine à café, son utilisation sera beaucoup plus complexe (où appuyer ?) et dangereuse (elle peut électrocuter).
Comme nous le verrons, les objets de programmation ressemblent à des machines à café.
Mais pour masquer les détails intérieurs, nous nâutiliserons pas une couverture de protection, mais une syntaxe spéciale du langage et des conventions.
Interface interne et externe
En programmation orientée objet, les propriétés et les méthodes sont divisées en deux groupes :
- Interface interne â méthodes et propriétés, accessibles à partir dâautres méthodes de la classe, mais pas de lâextérieur.
- Interface externe â méthodes et propriétés, accessibles aussi de lâextérieur de la classe.
Si nous continuons lâanalogie avec la machine à café â ce qui est caché à lâintérieur : un tube de chaudière, un élément chauffant, etc. -, câest son interface interne.
Une interface interne est utilisée pour que lâobjet fonctionne, ses détails sâutilisent les uns les autres. Par exemple, un tube de chaudière est attaché à lâélément chauffant.
Mais de lâextérieur, une machine à café est fermée par le capot de protection, de sorte que personne ne puisse y accéder. Les détails sont cachés et inaccessibles. Nous pouvons utiliser ses fonctionnalités via lâinterface externe.
Il suffit donc de connaître son interface externe pour utiliser un objet. Nous ne savons peut-être pas comment cela fonctionne à lâintérieur, et câest très bien.
Câétait une introduction générale.
En JavaScript, il existe deux types de champs dâobjet (propriétés et méthodes) :
- Publique : accessible de nâimporte où. Ils comprennent lâinterface externe. Jusquâà présent, nous utilisions uniquement des propriétés et méthodes publiques.
- Privée : accessible uniquement de lâintérieur de la classe. Ce sont pour lâinterface interne.
Dans de nombreux autres langages, il existe également des champs âprotégésâ : accessibles uniquement de lâintérieur de la classe et de ceux qui en héritent (comme privé, mais avec accès des classes héritées). Ils sont également utiles pour lâinterface interne. En un sens, elles sont plus répandues que les méthodes privées, car nous souhaitons généralement que les classes héritées puissent y accéder.
Les champs protégés ne sont pas implémentés en JavaScript au niveau du langage, mais dans la pratique, ils sont très pratiques, ils sont donc imités.
Nous allons maintenant créer une machine à café en JavaScript avec tous ces types de propriétés. Une machine à café a beaucoup de détails, nous ne les modéliserons pas pour rester simples (bien que nous puissions).
Protection de âwaterAmountâ
Faisons dâabord une classe de machine à café simple :
class CoffeeMachine {
waterAmount = 0; // la quantité d'eau à l'intérieur
constructor(power) {
this.power = power;
alert( `Created a coffee-machine, power: ${power}` );
}
}
// créer la machine à café
let coffeeMachine = new CoffeeMachine(100);
// ajoutez de l'eau
coffeeMachine.waterAmount = 200;
à lâheure actuelle, les propriétés waterAmount et power sont publiques. Nous pouvons facilement les accéder/muter de lâextérieur à nâimporte quelle valeur.
Changeons la propriété waterAmount en protégée pour avoir plus de contrôle sur celle-ci. Par exemple, nous ne voulons pas que quiconque la règle en dessous de zéro.
Les propriétés protégées sont généralement précédées dâun trait de soulignement _.
Cela nâest pas appliqué au niveau du langage, mais il existe une convention bien connue entre les programmeurs selon laquelle ces propriétés et méthodes ne doivent pas être accessibles de lâextérieur.
Donc notre propriété sâappellera _waterAmount :
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(value) {
if (value < 0) {
value = 0;
}
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
}
// créer la machine à café
let coffeeMachine = new CoffeeMachine(100);
// ajoutez de l'eau
coffeeMachine.waterAmount = -10; // _waterAmount va devenir 0, pas -10
Maintenant, lâaccès est sous contrôle, donc le réglage de lâeau en dessous de zéro échoue.
âpowerâ en lecture seule
Pour la propriété power, rendons-la en lecture seule. Il arrive parfois quâune propriété doive être définie au moment de la création, puis ne jamais être modifiée.
Câest exactement le cas pour une machine à café : la puissance ne change jamais.
Pour ce faire, il suffit de définir lâaccesseur, mais pas le mutateur :
class CoffeeMachine {
// ...
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
}
// créer la machine à café
let coffeeMachine = new CoffeeMachine(100);
alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W
coffeeMachine.power = 25; // Error (no setter)
Ici, nous avons utilisé la syntaxe accesseur/mutateur.
Mais la plupart du temps, les fonctions get... / set... sont préférées, comme ceci :
class CoffeeMachine {
_waterAmount = 0;
setWaterAmount(value) {
if (value < 0) value = 0;
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
Cela semble un peu plus long, mais les fonctions sont plus flexibles. Elles peuvent accepter plusieurs arguments (même si nous nâen avons pas besoin maintenant).
Dâun autre côté, la syntaxe accesseur/mutateur est plus courte, donc il nây a pas de règle stricte, câest à vous de décider.
Si nous héritons de classe MegaMachine extends CoffeeMachine, rien ne nous empêche dâaccéder à this._waterAmount ou this._power à partir des méthodes de la nouvelle classe.
Les champs protégés sont donc naturellement héritables. Contrairement aux champs privés que nous verrons ci-dessous.
â#waterLimitâ privée
Il existe une proposition JavaScript finie, presque dans la norme, qui fournit une prise en charge au niveau du langage pour les propriétés et méthodes privées.
Les propriétés privées devraient commencer par #. Elles ne sont accessibles que de lâintérieur de la classe.
Par exemple, voici une propriété privée #waterLimit et la méthode privée de vérification du niveau de lâeau #fixWaterAmount :
class CoffeeMachine {
#waterLimit = 200;
#fixWaterAmount(value) {
if (value < 0) return 0;
if (value > this.#waterLimit) return this.#waterLimit;
}
setWaterAmount(value) {
this.#waterLimit = this.#fixWaterAmount(value);
}
}
let coffeeMachine = new CoffeeMachine();
// ne peut pas accéder aux propriétés privées de l'extérieur de la classe
coffeeMachine.#fixWaterAmount(123); // Error
coffeeMachine.#waterLimit = 1000; // Error
Au niveau du langage, # est un signe spécial spécifiant que le champ est privé. Nous ne pouvons pas y accéder de lâextérieur ou des classes héritées.
Les champs privés nâentrent pas en conflit avec les champs publics. Nous pouvons avoir les champs privés #waterAmount et publics waterAmount en même temps.
Pour lâexemple, faisons de waterAmount un accesseur pour #waterAmount :
class CoffeeMachine {
#waterAmount = 0;
get waterAmount() {
return this.#waterAmount;
}
set waterAmount(value) {
if (value < 0) value = 0;
this.#waterAmount = value;
}
}
let machine = new CoffeeMachine();
machine.waterAmount = 100;
alert(machine.#waterAmount); // Error
Contrairement aux champs protégés, les champs privés sont imposés par le langage lui-même. Câest une bonne chose.
Mais si nous héritons de CoffeeMachine, nous nâaurons aucun accès direct à #waterAmount. Nous aurons besoin de compter sur lâaccesseur/mutateur waterAmount :
class MegaCoffeeMachine extends CoffeeMachine {
method() {
alert( this.#waterAmount ); // Error: can only access from CoffeeMachine
}
}
Dans de nombreux scénarios, une telle limitation est trop sévère. Si nous étendons une CoffeeMachine, nous pouvons avoir une raison légitime dâaccéder à ses composants internes. Câest pourquoi les champs protégés sont utilisés plus souvent, même sâils ne sont pas pris en charge par la syntaxe du langage.
Les champs privés sont spéciaux.
Comme nous le savons, nous pouvons généralement accéder aux champs en utilisant this[name] :
class User {
...
sayHi() {
let fieldName = "name";
alert(`Hello, ${this[fieldName]}`);
}
}
Câest impossible avec les champs privés : this['#name'] ne fonctionne pas. Câest une limitation de syntaxe pour assurer la confidentialité.
Résumé
En termes de POO, la délimitation de lâinterface interne de lâinterface externe est appelée encapsulation.
Cela offre les avantages suivants :
- Protection des utilisateurs pour quâils ne se tirent pas une balle dans le pied
-
Imaginez, il y a une équipe de développeurs utilisant une machine à café. Elle a été fabriquée par la société âBest CoffeeMachineâ et fonctionne parfaitement, mais une coque de protection a été retirée. Donc, lâinterface interne est exposée.
Tous les développeurs sont civilisés â ils utilisent la machine à café comme prévu. Mais lâun dâentre eux, John, a décidé quâil était le plus intelligent et a apporté quelques modifications aux éléments internes de la machine à café. La machine à café a donc échoué deux jours plus tard.
Ce nâest sûrement pas la faute de John, mais bien de la personne qui a enlevé le capot de protection et laissé John manipuler.
La même chose en programmation. Si un utilisateur dâune classe va changer des choses qui ne sont pas destinées à être modifiées de lâextérieur, les conséquences sont imprévisibles.
- Maintenable
-
La programmation est plus complexe quâune machine à café réelle, car nous ne lâachetons pas une seule fois. Le code est en constante évolution et amélioration.
Si nous délimitons strictement lâinterface interne, le développeur de la classe peut modifier librement ses propriétés et méthodes internes, même sans en informer les utilisateurs.
Si vous êtes développeur dâune telle classe, il est bon de savoir que les méthodes privées peuvent être renommées en toute sécurité, que leurs paramètres peuvent être modifiés, voire supprimés, car aucun code externe ne dépend dâeux.
Pour les utilisateurs, lorsquâune nouvelle version est disponible, il peut sâagir dâune refonte totale en interne, mais reste simple à mettre à niveau si lâinterface externe est la même.
- Cacher la complexité
-
Les gens adorent utiliser des choses simples. Au moins de lâextérieur. Ce qui est à lâintérieur est une chose différente.
Les programmeurs ne sont pas une exception.
Câest toujours pratique lorsque les détails de lâimplémentation sont cachés et quâune interface externe simple et bien documentée est disponible.
Pour masquer lâinterface interne, nous utilisons des propriétés protégées ou privées :
- Les champs protégés commencent par
_. Câest une convention bien connue, non appliquée au niveau du langage. Les programmeurs doivent uniquement accéder à un champ commençant par_depuis sa classe et les classes qui en héritent. - Les champs privés commencent par
#. JavaScript garantit que nous ne pouvons accéder à ceux la que de lâintérieur de la classe.
Pour le moment, les champs privés ne sont pas bien supportés par les navigateurs, mais peuvent être polyfilled.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâ¦)