Puyb Inside

samedi 11 décembre 2010

Serialize object instances in Mojo WebOS

Mojo, the javascript framework in HP WebOS offer convenient ways to save data like Mojo.Model.Cookie...

Cookies can save javascript objects. Unfortunately, as this API is based on a JSON serialization, it can't naturally deserialize all objects.
JSON can only serialize numbers, strings, booleans, arrays and objects. But when it serialize objects, obviously, only the object properties that can be serialized are serialized. Functions and prototype are not saved, and as a consequence not restored... In fact, arrays are the only objects that truly can be serialized...

For example, we can't save Date instances... In fact, by default Date instance are serialized into ISO 8601 strings...

Mojo is based on the prototype javascript framework. To serialize an object in JSON, we just call the Object.toJSON function. When we look at the source code, we can see that this function call, if exists, a _toJSON method of the current serialized object (toJSON without underscore, in the real prototype framework, with underscore in Mojo). So if we implement a Date#toJSON method, we can change the way Date are serialized...

We can implement something like this :
Date.prototype._toJSON = function() {
    return 'new Date(' + this.getFullYear() + ', ' + this.getMonth() + ', ' + this.getDay() + ', ' + this.getHours() + ', ' + this.getMinutes() + ', ' + this.getSeconds() + ')';
};

This will broke the JSON specification, but if the string is deserialized in javascript with an eval(), this will create a new Date instance with the right parameters... But, prototype offer a String#evalJSON method. This method check the validity of the JSON string before evaluating it. The check is done with the String#isJSON method. And this method will broke on serialized date... We must update it to allow our new way to serialize dates...
String.prototype.isJSON = (function(orig) {
    return function() {
        var str = this.replace(/new Date\([0-9, ]\)/g, '"@"');
        return orig.call(str);
    };
})(String.prototype.isJSON);

So now, we can serialize date objects :
    var cookie = new Mojo.Model.Cookie('date');
    var d ={ date: new Date() };
    cookie.put(d);

And the deserialization is automagically done with a :
    var cookie = new Mojo.Model.Cookie('date');
    var d = cookie.get();
    Mojo.Log.info(d.date.getTime());

That's it... And this can be done with any objects...
Of course, this can also be done in non Mojo javascript environment...

Commentaires sur le DevCast Palmpre-France #4

Marc Aurélien de Palm Pré France continue sa série d'article sur le développement sur HP WebOS...

Je viens de voir les articles 3 et 4 et j'ai quelques commentaires à faire sur son code... Le code, c'est un art, il y a toujours une meilleur façon de faire, et il y a autant de façons de faire que de développeurs ;-). Le code de Marc Aurélien n'est pas mauvais, c'est juste que j'ai envie de donner mon avis ;-)

Les variables :

Mon premier conseil est qu'il ne faut jamais utiliser des variables globales... Des variables globales ? Quezako ? Et bien, il faut un petit cour sur les variables et surtout sur leur visibilité... En fonction de l'endroit où l'on crée une variable et de la façon dont on la déclare, sa visibilité peut varier... Il y a 2 types de variables : les variables locales et les variables globales...

Une variable locale se déclare avec le mot clef var... Sa visibilité est limitée au sein de la fonction courante et de toutes les sous fonctions qui peuvent avoir été crées dans la fonction courante (on appelle cela une closure)...
Par contre, une variable globale n'a pas besoin d'être déclarée avec un mot clef. Sa visibilité n'est pas restreinte. Tout code dans l'application peut utiliser cette variable, même en dehors de la fonction qui l'a déclarée.
function test() {
    var ma_variable_locale = 0;
    ma_variable_globale = 0;
}

Mais pourquoi ne faut-il pas les utiliser ?
Premièrement, car c'est souvent une source de bugs... On utilise une variable dans une fonction, mais on à oublié que cette variable est aussi utilisée dans une autre... On se retrouve avec un jolie bug... Même si l'on a une mémoire infaillible, il faut toujours se dire qu'un jour, quelqu'un d'autre sera peut être amené à travailler sur votre code, et lui, il sera peut être faillible ;-)
Deuxièmement, dans un environnement objet comme le javascript, utiliser des variables globales veut dire que l'on ne pourra pas avoir plusieurs instances du même objet... L'objet dont je parle, dans le cas qui nous intéresse, c'est l'assistant de la carte actuelle (fenêtre / écran dans WebOS). Si l'on veut pouvoir avoir plusieurs cartes simultanément, on se retrouvera avec plusieurs assistant qui tenteront tous d'utiliser les mêmes variables... Aie, on se retrouve encore avec un bug ;-)

Donc, moralité : il ne faut jamais utiliser des variables globales... Point. Comment faire ? C'est simple, il faut toujours déclarer ses variables avec le mot clef var !

Mais comment faire pour avoir des variables qui doivent être partagées entre les différentes fonctions de l'assistant, mais qui ne doivent pas partagées entre les différentes instances de l'assistant ? Et bien c'est simple, on n'utilise pas de variable ;-) On utilise les propriétés que l'on ajoute à l'assistant courant... En javascript, on peut accéder aux propriétés d'un objet de deux façon : en nommant l'objet, suivi d'un point (.) et du nom de la propriété, ou en nommant l'objet, puis en ajoutant une chaine de caractères entre crochets ( [ et ] )...

Mais comment accéder à l'objet courant ? Grâce à la variable spéciale this... this est une variable locale qui dépend de la façon dont la fonction est appelée... Ce qu'il faut retenir, c'est qu'en générale, elle pointe sur l'objet courant...

Donc, voici comment définir des propriétés dans un objet :
Assistant.prototype.maFonction = function() {
    this.property_1 = 0;
    this['property_2'] = 0;
}

Dans l'application de Marc Aurélien, les variables globales que j'ai recensées sont au nombre de 4 (vent, nuage, meteo et meteophrase)...
Les deux dernières sont strictement locales à la fonction meteo... Par contre, les deux premières sont des candidates idéales pour être des propriétés de l'assistant.

Simplification du code

Marc Aurélien utilise un événement propertyChanged sur chaque sélecteur pour la logique de son application. Chaque sélecteur commence par convertir la valeur, enregistre le résultat dans la variable globale, puis lance la fonction métier, qui va se charger d'afficher le résultat du calcul.

1 - la conversion de valeur :
Pourquoi ne pas mettre directement la bonne valeur dans les champs value des listes de choix des sélecteurs... Mettre directement l'entier correspondant à la valeur plutôt qu'une chaine de caractères intermédiaire... Par contre, en faisant mes essais, je me suis rendu compte que Mojo retournait une chaine de caractères plutôt que la valeur qui était définie dans les attributs de l'objet... Je pense que je vais me fendre d'un petit rapport de bug sur le sujet ;-) Qu'a cela ne tienne, il suffit d'appliquer un petit parseFloat pour obtenir une valeur numérique...

2 - l'enregistrement de la valeur :
Les widgets de Mojo se chargent tout seul d'enregistrer la valeur comme une propriété de l'objet qui est passée en troisième argument de la fonction setupWidget... Le nom de la propriété est par défaut 'value', mais il peut être changé grâce au paramètre modelProperty... Plusieurs widgets peuvent ainsi se partager le même modèle en utilisant des propriétés différentes...

Ces deux points permettent de factoriser le code et de ne garder qu'un seul gestionnaire d'événement commun aux deux sélecteurs... Ce gestionnaire d'événement se contentera juste de lancer la fonction meteo qui s'occupe du traitement des données.

Voici maintenant à quoi va ressembler le code tel que je l'aurais écrit :
function FirstAssistant() { }
FirstAssistant.prototype.setup = function() {
        this.model = {
            vent: 1,
            nuage: 1,
        };

		this.selectorChanged = this.selectorChanged.bindAsEventListener(this);
		Mojo.Event.listen(this.controller.get('mySelector'),  Mojo.Event.propertyChange, this.selectorChanged);
		Mojo.Event.listen(this.controller.get('mySelector2'), Mojo.Event.propertyChange, this.selectorChanged);

		this.setupChoices();
		this.controller.setupWidget('mySelector',  { label: $L('Force du vent'), choices: this.mychoice,  modelProperty: 'vent' }, this.model);
		this.controller.setupWidget('mySelector2', { label: $L('Nuages'),        choices: this.mychoice2, modelProperty: 'nuage'}, this.model);
};
FirstAssistant.prototype.setupChoices = function(){
	this.mychoice = [ 
		{ label: $L("Peu de vent"),         value: 1, secondaryIcon: 'status-away' }, 
		{ label: $L("Un peu plus de vent"), value: 2, secondaryIcon: 'status-unavailable' }, 
		{ label: $L("du vent normalement"), value: 3, secondaryIcon: 'status-available' }, 
		{ label: $L("Vent fort"),           value: 4, secondaryIcon: 'status-available' }, 
		{ label: $L("Vent tres fort"),      value: 5, secondaryIcon: 'status-available' }, 
		{ label: $L("Vent violent"),        value: 6, secondaryIcon: 'status-available' }, 
    ];
	this.mychoice2 = [ 
		{ label: $L("Pas de nuages"),    value: 1, secondaryIcon: 'status-away' }, 
		{ label: $L("Un peu de nuages"), value: 2, secondaryIcon: 'status-unavailable' }, 
		{ label: $L("Quelques  nuages"), value: 3, secondaryIcon: 'status-available' }, 
		{ label: $L("Couvert"),          value: 4, secondaryIcon: 'status-available' }, 
		{ label: $L("Tres couvert"),     value: 5, secondaryIcon: 'status-available' }, 
		{ label: $L("Nuages noirs"),     value: 6, secondaryIcon: 'status-available' }, 
    ];
}
FirstAssistant.prototype.selectorChanged = function(event){
    this.meteo();
}
FirstAssistant.prototype.meteo = function(){
    var meteo = parseFloat(this.model.vent) + parseFloat(this.model.nuage);
    var meteophrase = "il va faire beau";
    if( meteo >= 7 ) { meteophrase = "il va faire mauvais"; }
    $("prevision").update(meteophrase);
}
FirstAssistant.prototype.activate = function(event) { };
FirstAssistant.prototype.deactivate = function(event) { };
FirstAssistant.prototype.cleanup = function(event) { };

Et voilà... Il y a surement plein de chose à redire sur ce code, mais voilà ma vision des choses ;-)