L'un des principes les plus importants de la programmation orientée objet est de délimiter l'interface interne de l'interface externe.
C’est une pratique « indispensable » pour développer quelque chose de plus complexe qu’une application « bonjour tout le monde ».
Pour comprendre cela, éloignons-nous du développement et tournons notre regard vers 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ème.
Par exemple, une machine à café. Simple de l'extérieur : un bouton, un écran, quelques trous… Et, sûrement, le résultat : un bon café ! :)
Mais à l'intérieur… (une photo 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 un pendant des années, et seulement en cas de problème, apportez-le pour réparation.
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 l’on retire le capot de protection de la machine à café, alors son utilisation sera beaucoup plus complexe (où appuyer ?), et dangereuse (elle peut électrocuter).
Comme nous le verrons, en programmation, les objets sont comme des machines à café.
Mais afin de cacher les détails internes, nous n'utiliserons pas une enveloppe de protection, mais plutôt une syntaxe particulière du langage et des conventions.
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 depuis d'autres méthodes de la classe, mais pas de l'extérieur.
Interface externe – méthodes et propriétés, accessibles également depuis l’extérieur de la classe.
Si nous poursuivons l'analogie avec la machine à café – ce qui se cache à 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 fixé à l'élément chauffant.
Mais de l'extérieur, une machine à café est fermée par le couvercle de protection, afin 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.
Ainsi, tout ce dont nous avons besoin pour utiliser un objet est de connaître son interface externe. Nous ignorons peut-être complètement comment cela fonctionne à l’intérieur, et c’est formidable.
C'était une introduction générale.
En JavaScript, il existe deux types de champs d'objet (propriétés et méthodes) :
Public : accessible de partout. Ils constituent l’interface externe. Jusqu'à présent, nous utilisions uniquement des propriétés et des méthodes publiques.
Privé : accessible uniquement depuis 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 depuis l'intérieur de la classe et ceux qui l'étendent (comme privés, mais plus l'accès depuis les classes héritantes). Ils sont également utiles pour l'interface interne. Ils sont en un sens plus répandus que les classes privées, car nous souhaitons généralement que les classes héritières y aient accès.
Les champs protégés ne sont pas implémentés en JavaScript au niveau du langage, mais en pratique ils sont très pratiques, ils sont donc émulés.
Nous allons maintenant créer une machine à café en JavaScript avec tous ces types de propriétés. Une machine à café comporte beaucoup de détails, nous ne les modéliserons pas pour rester simple (même si nous pourrions le faire).
Faisons d'abord un simple cours de machine à café :
classe Machine à café { quantité d'eau = 0 ; // la quantité d'eau à l'intérieur constructeur (puissance) { this.power = puissance; alert( `Création d'une machine à café, puissance : ${power}` ); } } // crée la machine à café laissez coffeeMachine = new CoffeeMachine (100); // ajoute de l'eau coffeeMachine.waterAmount = 200 ;
À l'heure actuelle, les propriétés waterAmount
et power
sont publiques. Nous pouvons facilement les obtenir/régler de l'extérieur à n'importe quelle valeur.
Changeons la propriété waterAmount
en protected pour avoir plus de contrôle sur elle. Par exemple, nous ne voulons pas que quiconque le fixe en dessous de zéro.
Les propriétés protégées sont généralement préfixées par 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.
Notre propriété s'appellera donc _waterAmount
:
classe Machine à café { _montanteau = 0 ; définir la quantité d'eau (valeur) { si (valeur < 0) { valeur = 0 ; } this._waterAmount = valeur ; } obtenir la quantité d'eau() { renvoie this._waterAmount ; } constructeur (puissance) { this._power = puissance; } } // crée la machine à café laissez coffeeMachine = new CoffeeMachine (100); // ajoute de l'eau coffeeMachine.waterAmount = -10 ; // _waterAmount deviendra 0, pas -10
L’accès étant désormais sous contrôle, il devient impossible de régler la quantité d’eau en dessous de zéro.
Pour la propriété power
, rendons-la en lecture seule. Il arrive parfois qu'une propriété doive être définie au moment de sa création uniquement, 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 créer le getter, mais pas le setter :
classe Machine à café { //... constructeur (puissance) { this._power = puissance; } obtenir du pouvoir() { renvoie this._power; } } // crée la machine à café laissez coffeeMachine = new CoffeeMachine (100); alert(`La puissance est : ${coffeeMachine.power}W`); // La puissance est : 100W coffeeMachine.puissance = 25 ; // Erreur (pas de setter)
Fonctions getter/setter
Ici, nous avons utilisé la syntaxe getter/setter.
Mais la plupart du temps, les fonctions get.../set...
sont préférées, comme ceci :
classe Machine à café { _montanteau = 0 ; setWaterAmount (valeur) { si (valeur < 0) valeur = 0 ; this._waterAmount = valeur ; } getWaterAmount() { renvoie this._waterAmount ; } } new CoffeeMachine().setWaterAmount(100);
Cela semble un peu plus long, mais les fonctions sont plus flexibles. Ils peuvent accepter plusieurs arguments (même si nous n'en avons pas besoin pour le moment).
En revanche, la syntaxe get/set est plus courte, donc finalement il n'y a pas de règle stricte, c'est à vous de décider.
Les champs protégés sont hérités
Si nous héritons class MegaMachine extends CoffeeMachine
, alors rien ne nous empêche d'accéder à this._waterAmount
ou this._power
depuis les méthodes de la nouvelle classe.
Les champs protégés sont donc naturellement héritables. Contrairement aux privés que nous verrons ci-dessous.
Un ajout récent
Il s'agit d'un ajout récent à la langue. Non pris en charge dans les moteurs JavaScript, ou partiellement encore pris en charge, nécessite un polyfilling.
Il existe une proposition JavaScript terminée, 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 éléments privés doivent commencer par #
. Ils ne sont accessibles que depuis l’intérieur de la classe.
Par exemple, voici une propriété privée #waterLimit
et la méthode privée de vérification de l'eau #fixWaterAmount
:
classe Machine à café { #waterLimit = 200 ; #fixWaterAmount(valeur) { si (valeur < 0) renvoie 0 ; if (value > this.#waterLimit) renvoie this.#waterLimit ; } setWaterAmount (valeur) { this.#waterLimit = this.#fixWaterAmount(value); } } laissez coffeeMachine = new CoffeeMachine(); // impossible d'accéder aux éléments privés depuis l'extérieur de la classe coffeeMachine.#fixWaterAmount(123); // Erreur coffeeMachine.#waterLimit = 1000; // Erreur
Au niveau linguistique, #
est un signe spécial indiquant que le champ est privé. Nous ne pouvons pas y accéder de l'extérieur ou depuis des classes héritées.
Les champs privés n'entrent pas en conflit avec les champs publics. Nous pouvons avoir à la fois des champs privés #waterAmount
et des champs publics waterAmount
.
Par exemple, faisons waterAmount
un accesseur pour #waterAmount
:
classe Machine à café { #montanteau = 0 ; obtenir la quantité d'eau() { renvoie ceci.#waterAmount; } définir la quantité d'eau (valeur) { si (valeur < 0) valeur = 0 ; this.#waterAmount = valeur ; } } laissez machine = new CoffeeMachine(); machine.waterAmount = 100 ; alert(machine.#waterAmount); // Erreur
Contrairement aux champs protégés, les champs privés sont appliqués par la langue elle-même. C'est une bonne chose.
Mais si nous héritons de CoffeeMachine
, alors nous n'aurons pas d'accès direct à #waterAmount
. Nous devrons nous appuyer sur le getter/setter waterAmount
:
la classe MegaCoffeeMachine étend CoffeeMachine { méthode() { alert( this.#waterAmount ); // Erreur : accès uniquement depuis CoffeeMachine } }
Dans de nombreux scénarios, une telle limitation est trop sévère. Si nous étendons une CoffeeMachine
, nous pouvons avoir des raisons légitimes d'accéder à ses composants internes. C'est pourquoi les champs protégés sont plus souvent utilisés, même s'ils ne sont pas pris en charge par la syntaxe du langage.
Les champs privés ne sont pas disponibles en tant que this[name]
Les champs privés sont spéciaux.
Comme nous le savons, nous pouvons généralement accéder aux champs en utilisant this[name]
:
Utilisateur de classe { ... disBonjour() { laissez fieldName = "nom" ; alert(`Bonjour, ${this[fieldName]}`); } }
Avec des champs privés, c'est impossible : this['#name']
ne fonctionne pas. C'est une limitation de syntaxe pour garantir la confidentialité.
En termes de POO, la délimitation de l’interface interne de l’interface externe est appelée encapsulation.
Il 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é. Il a été fabriqué par la société « Best CoffeeMachine » et fonctionne bien, mais un capot de protection a été retiré. L'interface interne est donc exposée.
Tous les développeurs sont civilisés – ils utilisent la machine à café comme prévu. Mais l'un d'eux, John, a décidé qu'il était le plus intelligent et a apporté quelques modifications aux composants internes de la machine à café. La machine à café est donc tombée en panne deux jours plus tard.
Ce n'est sûrement pas la faute de John, mais plutôt de la personne qui a retiré le capot de protection et a laissé John faire ses manipulations.
La même chose en programmation. Si un utilisateur d’une classe change des choses qui ne sont pas censées être modifiées de l’extérieur, les conséquences sont imprévisibles.
Supportable
La situation en matière de programmation est plus complexe que celle d'une machine à café réelle, car nous ne l'achetons pas une seule fois. Le code est constamment développé et amélioré.
Si nous délimitons strictement l'interface interne, alors le développeur de la classe peut librement modifier ses propriétés et méthodes internes, même sans en informer les utilisateurs.
Si vous êtes un 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é, leurs paramètres peuvent être modifiés et même supprimés, car aucun code externe n'en dépend.
Pour les utilisateurs, lorsqu'une nouvelle version sort, il peut s'agir d'une refonte totale en interne, mais toujours simple à mettre à niveau si l'interface externe est la même.
Cacher la complexité
Les gens adorent utiliser des choses simples. Du moins de l'extérieur. Ce qu’il y a à l’intérieur est une autre chose.
Les programmeurs ne font pas exception.
C'est toujours pratique lorsque les détails de l'implémentation sont masqués et qu'une interface externe simple et bien documentée est disponible.
Pour masquer une 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, qui n'est pas appliquée au niveau linguistique. Les programmeurs ne doivent accéder à un champ commençant par _
qu'à partir de sa classe et des classes qui en héritent.
Les champs privés commencent par #
. JavaScript garantit que nous ne pouvons accéder à ceux-ci que depuis l'intérieur de la classe.
À l'heure actuelle, les champs privés ne sont pas bien pris en charge par les navigateurs, mais peuvent être poly-remplis.