edit: J'ai continué à travailler sur ces expérimentations pour produire une bibliothèque grand public.
Après un peu moins de deux mois de taf, voilà enfin le résultat : http://syllab.fr/projets/web/ObjectModel/
Bonsoir les codeurs,
J'étais en train de bricoler avec les getters/setters et Object.defineProperty pour voir si je ne pouvais pas m'en servir pour simuler l'API Proxy actuellement à l'étude en ES6 (voir https://developer.mozilla.org/fr/doc..._globaux/Proxy), et je suis arrivé à un truc très sympa.
A la base, je cherchais à éviter ce genre de tests redondant dans mes codes :
A supposer que l'adresse est un champ faculatatif pour mon utilisateur, dès que je souhaite faire une opération avec un sous-champ bien précis, je dois tester toute la chaîne par prudence pour éviter les exceptions de type Cannot read property 'work' of undefined.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 if(person != null && person.address != null && person.address.work != null && person.address.work.city != null){ fonctionAvec(person.address.work.city); }
Les Proxies pourraient permettre de simplifier ce code, en fournissant un objet à la place des valeurs null/undefined si on essaie de faire un getter dessus. Mais leur support est minoritaire et ils ne sont même pas confirmés par le W3C. Il y a aussi les monades pour ça, notamment Maybe, mais cela nous oblige à adapter notre syntaxe un peu partout. Je n'accroche pas beaucoup aux monades, elles font partie de ces choses très simples mais très difficiles à expliquer. Et puis elles ont un nom vraiment moche
Donc en remplacement des Proxies, on a Object.defineProperty dont on pourrait se servir sur un objet . Le problème est que pour s'en servir, il faut savoir quelle clé définir ! C'est là que j'ai eu l'idée de fournir un "modèle" de données pour les objets que je veux tester. C'est vrai, on peut déclarer quelque-part que le modèle Person est amené à contenir des addresses, dont une professionnelle, avec une ville définie. Même si ces champs sont facultatifs, on les a en tête et on code des instructions avec, alors autant les déclarer quelque-part.
Tout ce qu'il me fallait, c'était la liste des clés pour pouvoir les déclarer avec defineProperty sur mon objet proxy. Je n'avais pas besoin des valeurs, mais comme je ne savais pas quoi mettre, j'ai mis les types attendus. Et c'est là que m'est venu la seconde idée: faire de la validation de type à la volée. Bon, les erreurs seront remontées au runtime, ce n'est non plus du TypeScript, mais ça peut aider à prévenir certains problèmes de faire valider les types.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 var Person = ObjectModel({ name: String, age: Number, female: Boolean, address: { work: { city: String } } });
Bref, en quelques heures j'ai réussi à pondre une petite fonction qui fait des miracles :
Cette fonction permet donc de manipuler des objets au rôle de proxy qui vont être mis en parallèle avec un modèle à chaque get/set. Ce qui permet de faire ça :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 function ObjectModel(def) { function wrap(obj) { if (obj === null || obj === undefined) { return Object.create(null); } else switch (typeof obj) { case "string": return new String(obj); case "number": return new Number(obj); case "boolean": return new Boolean(obj); case "object": default: return obj; } } function isPrimitive(def){ return def === String || def === Number || def === Boolean; } function safeProxy(obj, def) { var wrapper = wrap(obj); var proxy = Object.create(Object.getPrototypeOf(wrapper)); if(isPrimitive(def)){ if (obj !== undefined && def(obj) !== obj) { throw new Error("expecting " + def.name + ", got " + (typeof obj)); } return obj; } else { Object.keys(def).forEach(function (key) { Object.defineProperty(proxy, key, { get: function () { return safeProxy(isPrimitive(def[key]) && obj instanceof Object ? obj[key] : wrapper[key], def[key]); }, set: function (val) { wrapper[key] = safeProxy(val, def[key]); } }); }); } return proxy; } return function (obj) { return safeProxy(obj, def); }; }
On peut aussi utiliser une syntaxe avec new :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 var joe = Person(); joe.name = "Joe"; joe.age = "old"; // Error: expecting Number, got string if(joe.address.work.city){ //joe.address n'est pas défini mais ce code ne lève pas d'exceptions //mais cette condition ne passe pas, car joe.address.work.city renvoie undefined throw new Error("you should not pass"); }
Je cherche à vérifier si l'utilisation de cette fonction présente des problèmes bloquants (notamment sur la pertinence de la distinction objet/primitive). J'ai déjà identifié quelques inconvénients, le principal étant joe.address !== undefined. Je pourrais contourner le problème par une propriété _isDefined mais ce n'est pas aussi élégant que le reste. Vous avez d'autres idées ? J'ai aussi pensé à permettre d'indiquer si le paramètre est facultatif ou non avec la notation age: [Number], pour imiter la syntaxe JSDoc tout en reposant sur les Array pour rester du JS valide. Mais il faut que je trouve autre chose pour indiquer les Array... et quid des Array typées ?
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 var ann = new Person; ann.female = 1; //Error: expecting Boolean, got number ann.age = 25; ann.name = joe.name+"'s wife";
Si quelques grosses têtes veulent m'aider à creuser le sujet, je fournis les pioches![]()
Partager