Il était une fois un langage appelé C, 1 et ce langage avait quelque chose appelé un struct
, et vous pouviez l’utiliser pour créer des structures de données agrégées de manière hétérogène qui avaient des membres. La chose clé à savoir sur C est que lorsque vous avez une structure appelée currentUser
, et un membre comme id
, et que vous écrivez quelque chose comme currentUser.id = 42
, le complicateur C a transformé cela en instructions d’assembleur extrêmement rapides. Idem pour int id = currentUser.id
.
Il était également important que vous puissiez avoir des pointeurs vers des fonctions dans les structures, de sorte que vous puissiez écrire des choses comme currentUser->setId(42)
si vous préférez faire de la définition d’un id
une fonction, et cela a également été traduit en assembleur rapide.
Et enfin, la programmation C a une culture très forte de préférer « extrêmement rapide » à ”rapide », et donc si vous vouliez l’attention d’un programmeur C, vous deviez vous assurer de ne jamais faire quelque chose qui soit juste rapide alors que vous pouviez faire quelque chose qui soit extrêmement rapide. C’est une généralisation, bien sûr. Je suis sûr que si nous posons la question, nous finirons par rencontrer les deux programmeurs C qui préfèrent les abstractions élégantes au code extrêmement rapide.
java et javascript
Ensuite, il y avait un langage appelé Java, et il a été conçu pour fonctionner dans les navigateurs, et être portable sur toutes sortes de matériels et de systèmes d’exploitation, et l’un de ses objectifs était d’amener les programmeurs C à écrire du code Java dans le navigateur au lieu d’écrire du C qui vivait dans un plugin. Ou plutôt, c’était l’une de ses stratégies, l’objectif était que Sun Microsystems reste pertinent dans un monde que Microsoft banalisait, mais c’est un autre chapitre du livre d’histoire.
Les gens sympas derrière Java lui ont donc donné une syntaxe de type C avec les accolades et la dichotomie déclaration / expression et la notation par points. Ils ont des « objets » au lieu de structures, et les objets ont beaucoup plus de choses à faire que des structures, mais les concepteurs de Java ont fait une distinction entre currentUser.id = 42
et currentUser.setId(42)
, et se sont assurés que l’un était extrêmement rapide et que l’autre était juste rapide. Ou plutôt, celui-ci était rapide, et l’autre était juste correct par rapport à C, mais les programmeurs C pouvaient avoir l’impression de faire une réflexion importante pour décider si id
devait être directement accessible pour la performance ou indirectement accessible pour l’élégance et la flexibilité.
L’histoire a montré que c’était la bonne façon de vendre une nouvelle langue. L’histoire a également montré que la distinction de performance réelle n’était pas pertinente pour presque tout le monde. La performance n’est que pour l’instant, la flexibilité du code est éternelle.
Eh bien, il s’est avéré que Sun avait raison d’amener les programmeurs C à utiliser Java (cela a fonctionné sur moi, j’ai abandonné CodeWarrior et Lightspeed C), mais j’ai tort d’utiliser Java dans les navigateurs. Au lieu de cela, les gens ont commencé à utiliser un autre langage appelé JavaScript pour écrire du code dans les navigateurs et ont utilisé Java pour écrire du code sur les serveurs.
Cela vous surprendra-t-il d’apprendre que JavaScript a également été conçu pour amener les programmeurs C à écrire du code? Et que cela allait avec la syntaxe de type C avec des accolades, la dichotomie déclaration / expression et la notation par points? Et bien que JavaScript ait une chose qui ressemble un peu à un objet Java et un peu à un dictionnaire Smalltalk, cela vous surprendra-t-il d’apprendre que JavaScript a également une distinction entre currentUser.id = 42
et currentUser.setId(42)
? Et qu’à l’origine, l’un était lent et l’autre lent, mais les programmeurs pouvaient réfléchir de manière importante au moment d’optimiser les performances et au moment de parler de la santé mentale des programmeurs?
Non, cela ne vous surprendra pas d’apprendre que cela fonctionne en quelque sorte comme C de la même manière que Java fonctionne en quelque sorte comme C, et pour exactement la même raison. Et la raison n’a plus vraiment d’importance.
le problème de l’accès direct
Très peu de temps après que les gens ont commencé à travailler avec Java à grande échelle, ils ont appris que l’accès direct aux variables d’instance était une idée terrible. Les compilateurs JIT ont réduit la différence de performances entre currentUser.id = 42
et currentUser.setId(42)
à presque rien de pertinent pour quiconque, et le code utilisant currentUser.id = 42
ou int id = currentUser.id
était remarquablement inflexible.
Il n’y avait aucun moyen de décorer de telles opérations avec des préoccupations transversales comme la journalisation ou la validation. Vous ne pouvez pas remplacer le comportement de définition ou d’obtention d’un id
dans une sous-classe. (Les programmeurs Java adorent les sous-classes!)
Pendant ce temps, les programmeurs JavaScript écrivaient également currentUser.id = 42
, et finalement eux aussi ont découvert que c’était une idée terrible. L’un des catalyseurs du changement a été l’arrivée de frameworks pour les applications JavaScript côté client. Disons que nous avons une classe de personne ridiculement simple :
class Person { constructor (first, last) { this.first = first; this.last = last; } fullName () { return `${this.first} ${this.last}`; }};
Et une vue tout aussi ridicule:
class PersonView { constructor (person) { this.model = person; } // ... redraw () { document .querySelector(`person-${person.id}`) .text(person.fullName()) }}
Chaque fois que nous mettons à jour la classe de personne, nous devons nous rappeler de redessiner la vue:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUserView.redraw();currentUser.first = 'Ragnvald';currentUserView.redraw();
Pourquoi cela importe-t-il?
Eh bien, si vous ne pouvez pas contrôler où certaines responsabilités sont gérées, vous ne pouvez pas vraiment organiser votre programme. Les sous-classes, les méthodes, les mixins et les décorateurs sont des techniques: ce qu’ils rendent possible, c’est de choisir quel code est responsable de quelle fonctionnalité.
Et c’est tout le problème de la programmation: organiser la fonctionnalité. L’accès direct ne vous permet pas d’organiser les fonctionnalités associées à l’obtention et à la définition des propriétés, il oblige le code effectuant l’obtention et la définition à être également responsable de tout ce qui est associé à l’obtention et à la définition.
obtenir et définir
Il n’a pas fallu longtemps aux auteurs de la bibliothèque JavaScript pour comprendre comment faire disparaître cela en utilisant une méthode get
et set
. Dépouillé à l’essentiel à des fins d’illustration, nous pourrions écrire ceci:
class Model { constructor () { this.listeners = new Set(); } get (property) { this.notifyAll('get', property, this); return this; } set (property, value) { this.notifyAll('set', property, value); return this = value; } addListener (listener) { this.listeners.add(listener); } deleteListener (listener) { this.listeners.delete(listener); } notifyAll (message, ...args) { for (let listener of this.listeners) { listener.notify(this, message, ...args); } }}class Person extends Model { constructor (first, last) { super(); this.set('first', first); this.set('last', last); } fullName () { return `${this.get('first')} ${this.get('last')}`; }};class View { constructor (model) { this.model = model; model.addListener(this); }}class PersonView extends View { // ... notify(notifier, method, ...args) { if (notifier === this.model && method === 'set') this.redraw(); } redraw () { document .querySelector(`person-${this.model.id}`) .text(this.model.fullName()) }}
Notre nouvelle superclasse Model
gère manuellement les objets permettant d’écouter le get
et set
méthodes sur un modèle. S’ils sont appelés, les ”auditeurs » sont notifiés via la méthode .notifyAll
. Nous l’utilisons pour que la PersonView
écoute sa Person
et appelle sa propre méthode .redraw
lorsqu’une propriété est définie via la méthode .set
.
Afin que nous puissions écrire:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.set('first', 'Ragnvald');
Et nous n’avons pas besoin d’appeler currentUserView.redraw()
, car la notification intégrée à .set
le fait pour nous.
Nous pouvons faire d’autres choses avec .get
et .set
, bien sûr. Maintenant que ce sont des méthodes, nous pouvons les décorer avec une journalisation ou une validation si nous le souhaitons. Les méthodes rendent notre code flexible et ouvert à l’extension. Par exemple, nous pouvons utiliser un ES.décorateur ultérieur pour ajouter des conseils de journalisation à .set
:
const after = (behaviour, ...methodNames) => (clazz) => { for (let methodName of methodNames) { const method = clazz.prototype; Object.defineProperty(clazz.prototype, methodName, { value: function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }, writable: true }); } return clazz; }function LogSetter (model, property, value) { console.log(`Setting ${property} of ${model.fullName()} to ${value}`);}@after(LogSetter, 'set')class Person extends Model { constructor (first, last) { super(); this.set('first', first); this.set('last', last); } fullName () { return `${this.get('first')} ${this.get('last')}`; }};
Alors que nous ne pouvons rien faire de tel avec un accès direct aux propriétés. La médiation de l’accès aux propriétés avec des méthodes est plus flexible que l’accès direct aux propriétés, ce qui nous permet d’organiser notre programme et de répartir correctement les responsabilités.
Remarque: Tous les ES.les décorateurs de classe ultérieurs peuvent être utilisés dans le code vanilla ES 6 en tant que fonctions ordinaires. Au lieu de
@after(LogSetter, 'set') class Person extends Model {...}
, écrivez simplementconst Person = after(LogSetter, 'set')(class Person extends Model {...})
getters et setters en javascript
Le problème avec les getters et les setters était bien compris, et les stewards derrière l’évolution de JavaScript ont répondu en introduisant un moyen spécial de transformer l’accès direct aux propriétés en une sorte de méthode. Voici comment nous écririons notre classe Person
en utilisant « getters » et « setters:”
class Model { constructor () { this.listeners = new Set(); } addListener (listener) { this.listeners.add(listener); } deleteListener (listener) { this.listeners.delete(listener); } notifyAll (message, ...args) { for (let listener of this.listeners) { listener.notify(this, message, ...args); } }}const _first = Symbol('first'), _last = Symbol('last');class Person extends Model { constructor (first, last) { super(); this.first = first; this.last = last; } get first () { this.notifyAll('get', 'first', this); return this; } set first (value) { this.notifyAll('set', 'first', value); return this = value; } get last () { this.notifyAll('get', 'last', this); return this; } set last (value) { this.notifyAll('set', 'last', value); return this = value; } fullName () { return `${this.first} ${this.last}`; }};
Lorsque nous préfaçons une méthode avec le mot clé get
, nous définissons un getter, une méthode qui sera appelée lorsque le code tentera de lire à partir de la propriété. Et lorsque nous préfaçons une méthode avec set
, nous définissons un setter, une méthode qui sera appelée lorsque le code tentera d’écrire dans la propriété.
Les getters et les setters n’ont pas besoin de lire ou d’écrire de propriétés, ils peuvent tout faire. Mais dans cet essai, nous parlerons de les utiliser pour faciliter l’accès à la propriété. Avec les getters et les setters, nous pouvons écrire:
const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.first = 'Ragnvald';
Et tout fonctionne toujours comme si nous avions écrit currentUser.set('first', 'Ragnvald')
avec le code de style .set
.
Les getters et les setters nous permettent d’avoir la sémantique de l’utilisation des méthodes, mais la syntaxe de l’accès direct.
un combinateur après qui peut gérer les getters et les setters
Les getters et les setters semblent à première vue être une combinaison magique de syntaxe familière et de puissante capacité à méta-programmer. Cependant, un getter ou un setter n’est pas une méthode au sens habituel. Nous ne pouvons donc pas décorer un setter en utilisant exactement le même code que nous utiliserions pour décorer une méthode ordinaire.
Avec la méthode .set
, nous pourrions accéder directement à Model.prototype.set
et l’envelopper dans une autre fonction. C’est ainsi que fonctionnent nos décorateurs. Mais il n’y a pas de méthode Person.prototype.first
. Au lieu de cela, il existe un descripteur de propriété que nous ne pouvons introspecter qu’en utilisant Object.getOwnPropertyDescriptor()
et mettre à jour en utilisant Object.defineProperty()
.
Pour cette raison, le décorateur naïf after
donné ci-dessus ne fonctionnera pas pour les getters et les setters.2 Nous devrions utiliser un type de décorateur pour les méthodes, un autre pour les getters et un troisième pour les setters. Cela ne semble pas amusant, alors modifions notre combinateur after
afin que vous puissiez utiliser une seule fonction avec des méthodes, des getters et des setters:
function getPropertyDescriptor (obj, property) { if (obj == null) return null; const descriptor = Object.getOwnPropertyDescriptor(obj, property); if (obj.hasOwnProperty(property)) return Object.getOwnPropertyDescriptor(obj, property); else return getPropertyDescriptor(Object.getPrototypeOf(obj), property);};const after = (behaviour, ...methodNames) => (clazz) => { for (let methodNameExpr of methodNames) { const = methodNameExpr.match(/^(?:(get|set) )(.+)$/); const descriptor = getPropertyDescriptor(clazz.prototype, methodName); if (accessor == null) { const method = clazz.prototype; descriptor.value = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.writable = true; } else if (accessor === "get") { const method = descriptor.get; descriptor.get = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.configurable = true; } else if (accessor === "set") { const method = descriptor.set; descriptor.set = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.configurable = true; } Object.defineProperty(clazz.prototype, methodName, descriptor); } return clazz; }
Maintenant, nous pouvons écrire:
const notify = (name) => function (...args) { this.notifyAll(name, ...args); };@after(notify('set'), 'set first', 'set last')@after(notify('get'), 'get first', 'get last')class Person extends Model { constructor (first, last) { super(); this.first = first; this.last = last; } get first () { return this; } set first (value) { return this = value; } get last () { return this; } set last (value) { return this = value; } fullName () { return `${this.first} ${this.last}`; }};
Nous avons maintenant découplé le code pour notifier les auditeurs du code pour obtenir et définir les valeurs. Ce qui provoque une question simple: Si le code qui suit les auditeurs est déjà découplé dans Model
, pourquoi le code de déclenchement des notifications ne devrait-il pas être dans la même entité?
Il y a plusieurs façons de le faire. Nous utiliserons un mixin universel au lieu de bourrer cette logique dans une superclasse :
const Notifier = mixin({ init () { this.listeners = new Set(); }, addListener (listener) { this.listeners.add(listener); }, deleteListener (listener) { this.listeners.delete(listener); }, notifyAll (message, ...args) { for (let listener of this.listeners) { listener.notify(this, message, ...args); } }}, { notify (name) { return function (...args) { this.notifyAll(name, ...args); } }});
Cela nous permet d’écrire:
@Notifier@after(Notifier.notify('set'), 'set first', 'set last')@after(Notifier.notify('get'), 'get first', 'get last')class Person { constructor (first, last) { this.init(); this.first = first; this.last = last; } get first () { return this; } set first (value) { return this = value; } get last () { return this; } set last (value) { return this = value; } fullName () { return `${this.first} ${this.last}`; }};
Qu’avons-nous fait? Nous avons intégré des getters et des setters dans notre code, tout en conservant la possibilité de les décorer avec des fonctionnalités supplémentaires comme s’il s’agissait de méthodes ordinaires.
C’est une victoire pour décomposer le code. Et cela indique quelque chose à penser: Lorsque vous n’avez que des méthodes, vous êtes encouragé à créer des superclasses poids lourds. C’est pourquoi tant de frameworks vous obligent à étendre leurs classes de base spéciales comme Model
ou View
.
Mais lorsque vous trouvez un moyen d’utiliser des mixins et des méthodes de décoration, vous pouvez décomposer les objets en petits morceaux et les appliquer là où ils sont nécessaires. Cela conduit à utiliser des collections de bibliothèques au lieu d’un cadre lourd.
résumé
Les getters et les setters nous permettent de conserver le style d’écriture de code hérité qui semble accéder directement aux propriétés, tout en faisant la médiation de cet accès avec des méthodes. Avec soin, nous pouvons mettre à jour nos outils pour nous permettre de décorer nos getters et setters, en répartissant la responsabilité comme bon nous semble et en nous libérant de la dépendance aux classes de base lourdes.
(discutez sur les nouvelles des hackers)
Post Scriptum
Cet article utilise l’écoute des setters de propriété comme excuse pour discuter des mécanismes getter et setter, et des moyens de les décorer afin que nous puissions organiser le code autour des préoccupations.
Bien sûr, la propagation des modifications via une notification explicite n’est pas le seul moyen d’organiser le code qui doit gérer les dépendances à la modification des données. Il est hors de portée de cet article de discuter des nombreuses alternatives, mais les lecteurs ont suggéré d’explorer l’objet.observez et travaillez avec des données immuables.
encore une chose
Les Rubyistes se moquent de:
get first () { return this;}set first (value) { return this = value;}get last () { return this;}set last (value) { return this = value;}
Les Rubyistes utiliseraient la méthode de classe intégrée attr_accessor
pour les écrire pour nous. Donc, juste pour les coups de pied, nous allons écrire un décorateur qui écrit des getters et des setters. Les valeurs brutes seront stockées dans une carte attributes
:
function attrAccessor (...propertyNames) { return function (clazzOrObject) { const target = clazzOrObject.prototype || clazzOrObject; for (let propertyName of propertyNames) { Object.defineProperty(target, propertyName, { get: function () { if (this.attributes) return this.attributes; }, set: function (value) { if (this.attributes == undefined) this.attributes = new Map(); return this.attributes = value; }, configurable: true, enumerable: true }) } return clazzOrObject; }}
Maintenant, nous pouvons écrire:
@Notifier@attrAccessor('first', 'last')@after(Notifier.notify('set'), 'set first', 'set last')@after(Notifier.notify('get'), 'get first', 'get last')class Person { constructor (first, last) { this.init(); this.first = first; this.last = last; } fullName () { return `${this.first} ${this.last}`; }};
attrAccessor
prend un liste des noms de propriétés et renvoie un décorateur pour une classe. Il écrit une fonction getter ou setter simple pour chaque propriété, et toutes les propriétés définies sont stockées dans le hachage .attributes
. Ceci est très pratique pour la sérialisation ou d’autres mécanismes de persistance.
(Il est trivial de créer également des fonctions attrReader
et attrWriter
en utilisant ce modèle. Il suffit d’omettre le set
lors de l’écriture de attrReader
et d’omettre le get
lors de l’écriture de attrWriter
.)
-
Il y avait aussi un langage appelé BCPL, et d’autres avant cela, mais notre histoire doit commencer quelque part, et cela commence par C.Neither
-
La recette
mixin
nous avons évolué dans des articles précédents comme Utiliser ES.décorateurs plus tard comme Mixins. Il peut être amélioré pour ajouter un cas spécial pour les getters, les setters et d’autres problèmes comme travailler avec des POJO. Par exemple, le Mixin universel d’Andrea Giammarchi. ↩