RSS

Blog en anglais

Finalement, traduire systématiquement mes articles en français me prends tant de temps que je le fais de moins en moins.
Si vous le voulez, vous pouvez trouver mes articles en anglais sur mon autre blog. Je le tiens plus à jour que celui-ci.

https://chrisbertrandprogramer.wordpress.com/

Un traducteur automatique y est disponible en option, mais la traduction laisse franchement à désirer. Les machines sont loin de savoir traduire correctement.

Publicités
 
Poster un commentaire

Publié par le 2018-09-05 dans Uncategorized

 

Le dilemme du XML

Le dilemme de la propriété d’un élément

Lorsqu’on construit un modèle de document xml, on est confronté à dilemme:
Cette propriété doit-elle être un attribut ou bien un sous-élément ?

La réponse à cette question est lourde de conséquences en cas d’erreur, car un changement de statut dans l’avenir obligera à modifier le modèle et posera de gros problèmes aux logiciels qui utilisent le format que l’on aura modifié. Le fonctionnement interne du logiciel devra être modifié, et il devra transcoder les documents entre l’ancien format et le nouveau.

Exemple d’un changement de modèle:

Dans l’élément « a », on a une propriété « value1 » que l’on écrit sous la forme simple d’un attribut:

<a value1="100" value2="200"/>

Plus tard on décide que cette propriété pourrait être plus complexe dans certains cas.
On la transforme donc en un sous-élément de « a »:

<a value2="200">
    <
value1>
        <
pixel>
            <
x>10</x>
            <
y>20</y>
        </
pixel>
    </value1>
</
a>

La propriété « value1 » ayant changé de structure, on doit donc transcoder tous les documents qui suivent ce modèle.

Palliatif

Il existe une astuce pour pallier à cet inconvénient, en gardant la propriété sous forme d’attribut mais en lui donnant une syntaxe plus complexe:

<a value1="pixel x=10 y=20" value2= "200">

L’ennui, c’est qu’on se retrouve avec une syntaxe particulière pour chaque attribut un peu complexe, ce qui est précisément l’inverse de ce qu’on recherche en xml: une syntaxe régulière et vérifiable par n’importe quel outil xml.

Et en XAML ?

Il est intéressant de noter que les concepteurs du langage XAML, le principal descripteur d’interface graphique utilisateur sous Windows, ont choisit de laisser le choix aux programmeurs qui écrivent un document XAML.

Voici un élément XAML représentant un contrôle graphique, TextBox, qui affiche et permet d’éditer un texte simple.
On peut exprimer le texte dans un attribut:

<TextBox Text="mon texte"/>

Ou bien dans un sous-élément XML:

<TextBox>
	<TextBox.Text>mon texte</TextBox.Text>
</TextBox>

En fait, pour l’élément TextBox, le Texte est son contenu principal, on peut donc aussi l’exprimer comme la valeur de l’élément XML:

<TextBox>mon texte</TextBox>

On peut dire que les concepteurs du XAML ont rendu leur langage assez souple pour faciliter la vie des utilisateurs (développeurs) et leur permettre d’écrire le code le plus court possible selon le cas.
Certains pourraient critiquer ce choix, qui assimile allègrement les concepts d’attribut, de sous-élément et de valeur. Mais, au fond, est-ce que ça n’est pas le signe qu’ici le Texte a tout simplement une seule signification, être la propriété principale de l’élément TextBox, et que si XML a plusieurs façons d’exprimer cela, c’est peut-être sa faute à lui ?
Je trouve que le XAML met bien en valeur les incohérences du XML, en s’y adaptant tant bien que mal.

Et ailleurs ?

Il existe des méta-langages textuels qui ne font pas de distinction entre propriétés simples et propriétés complexes.
Par exemple en JSON:

{ "a" : { "value1":"100", "value2":"200" } }

peut devenir, lorsqu’on choisit de rendre la propriété « value1 » plus complexe:

{ "a" : { "value1": { "Pixel" : { "x":"10", "y":20 } } , "value2":"200" } }

J’ai pu apprécier la différence lorsque j’ai un écrit un sérialiseur généraliste (UniversalSerializer).
En JSON, j’ai pu écrire un modèle de données en arbre très vite.
Alors qu’en XML, il m’a fallu cogiter longuement pour de nombreuses propriétés d’éléments, au risque de devoir modifier mon modèle plus tard, avec tous les inconvénients que ça suppose.

Le problème du XML

En fait, la syntaxe XML ne permet pas l’imbrication des balises.
Pour obtenir l’équivalent de ce que j’ai écrit plus haut en JSON, il faudrait que le XML permette ceci:

<a <value1><Pixel><x>10</x><y>20</y></value1> value2="200"/>

Ce qui bien entendu est strictement interdit en XML: un élément (une balise en fait) ne peut pas contenir une autre balise (c’est à dire un signe « <« ).

Dans cet exemple imaginaire et impossible, on voit que le vrai problème du XML, c’est justement qu’il propose deux façons d’exprimer la propriété d’un élément: attribut et sous-élément.
Cette particularité est non seulement inutile d’un point de vue structurel, mais elle rend le concept même du XML plutôt irrégulier. Un comble pour un langage qui se veut à la fois simple et très régulier.
Tout système régulier où une chose peut être exprimée de deux façons a forcément un problème.

Hors, les attributs et les sous-éléments sont justement la base du XML. Tout le reste de sa structure en découle ou s’y adapte.
La seule façon de résoudre le problème serait de supprimer les attributs, de permettre l’imbrication des balises, et, tant qu’on y est, de ne pas permettre de contenu/valeurs dans les éléments.
Toute structure se réduirait à un arbre.
Autant dire que ce serait tout simplement un autre langage que le XML.

Que faire ?

On peut très bien décider de continuer à utiliser un langage à syntaxe bancale, sans se poser de questions, par habitude ou par nécessité. Et continuer d’en subir les défauts: sémantique discutable, problème d’évolution des modèles, transcodage à revoir régulièrement, etc.

D’ailleurs on a de bonnes raisons de continuer: il existe une masse énorme de standards, de formats de fichiers, basés sur le XML, dans tous les domaines, et dans les principaux logiciels.
Et puis, les habitudes ont la vie dure. Et on peut se consoler en se disant qu’on a à notre disposition toute une infrastructure, des milliers de logiciels et de bibliothèques qui gèrent, éditent, vérifient, transcodent le code xml.

Ou bien on peut se mettre en quête d’autre chose, un méta-format de donnée plus intéressant.
Reste à savoir lequel, et s’il restera ou deviendra un standard sur lequel on peut miser.

Personnellement, j’ai beaucoup travaillé avec le XML, bien qu’avec une maitrise modeste de ses possibilités, et je sens que le passage à autre chose sera long et difficile (beaucoup de programmes à modifier, et d’habitudes à changer).

Par ailleurs, il existe un autre problème au XML, et en fait à tous les méta-langages textuels comme JSON, c’est leur incapacité à gérer efficacement de gros fichiers, surtout lorsqu’il faut y insérer ou y retirer des informations.
Beaucoup ont proposé des versions binaires de XML, aucune n’a l’air de devenir un standard reconnu.
D’ailleurs, à partir d’une certaine complexité et d’une certaine taille, les bases de données semblent incontournables.. et toujours aussi peu lisibles par le commun des mortels.

 
Poster un commentaire

Publié par le 2017-05-08 dans Programmation, XML

 

Ne sérialisez pas des listes vides avec XmlSerializer

Lorsque vous sérialisez une collection/liste vide avec XmlSerializer, un élément xml est écrit.
Si vous voulez que rien ne soit sérialisé, vous pouvez utiliser ce truc: ajouter une fonction ShouldSerialize..().

Exemple:

public class MaClasse
{
    public List Éléments;
    public bool ShouldSerializeÉléments()
    { return Éléments != null && Éléments.Count > 0; }
}

Le nom de la fonction est « ShouldSerialize »+<le nom du champ ou propriété de la collection>

XmlSerializer détectera automatiquement cette fonction et lui demandera s’il doit sérialiser ou non le champ ou la propriété, ou non.

 
Poster un commentaire

Publié par le 2017-05-08 dans .Net, C#, Programmation

 

Dispose parallèle et efficace

Dans .NET, pour chaque classe utilisée en parallèle (par plusieurs tâches, ou « thread safe » pour les anglophones) et implémentant IDisposable, il faut péniblement implémenter une méthode « Dispose » fonctionnant en parallèle aussi.
Ici je propose une petite structure qui nous facilite la vie, et qui en plus a le mérite d’être efficace: rapide et petite.

Code de la structure:

internal struct DisposeParallèle
{
enum LibéréOuPas { PasEncoreSupprimé, DéjàSupprimé }

int déjàSuppriméOuPas;

internal void Dispose(Action MéthodeDeSuppression)
{
// "Interlocked" est bien plus rapide que "lock".

LibéréOuPas l = (LibéréOuPas)
System.Threading.Interlocked.CompareExchange(
ref this.déjàSuppriméOuPas,
(int)LibéréOuPas.DéjàSupprimé,
(int) LibéréOuPas.PasEncoreSupprimé);

// Ici, "déjàSuppriméOuPas" a toujours pour valeur DéjàSupprimé, et il n'en changera plus. 😉

if (l == LibéréOuPas.PasEncoreSupprimé)
MéthodeDeSuppression();
}
}

Remarques:

  1. On utilise « Interlocked », qui est bien plus rapide que « lock ».
    La preuve: http://www.drdobbs.com/windows/boosting-performance-with-atomic-operati/226900048
  2. On se contente de mémoriser un simple entier 32 bits, là où un « lock » aurait nécessité un objet et un booléen.
    J’aurais aimé ne mémoriser qu’un booléen, d’ailleurs, mais « Interlocked »
    ne propose pas de méthode « CompareExchange » sur un booléen.
  3. En tant que simple structure, on économise les ressources objets, et on n’a pas besoin d’appeler un constructeur.

Exemple d’utilisation:

internal class MaClasse : IDisposable
{
DisposeParallèle disposeParallèle;

void IDisposable.Dispose()
{
this.disposeParallèle.Dispose(this.suppression);
}

void suppression()
{
// votre code...
}

~MaClasse() // si nécessaire
{
((IDisposable)this).Dispose();
}
}

Ça reste assez simple à utiliser, avec un code bien découpé donc facile à maintenir.

 

Étiquettes : , , , , , , , ,

Dans Firefox, refaire apparaître l’annonce de mise à jour

Contexte

Depuis quelques années, Firefox propose une mise à jour
(semi-)automatique. Une petite fenêtre apparaît en bas à droite de
l’écran pour nous avertir qu’une nouvelle version du logiciel a été
publiée, et nous propose un lien facilitant cette mise à jour.

Malheureusement, ce message n’apparait qu’une seule fois, et reste
même très peu de temps présent sur l’écran.
Une fois cette occasion de cliquer partie, une mise à jour de Firefox
devient nettement plus difficile, obligeant en fait à télécharger
l’installeur à partir du site de Mozilla, à entrer le mot de passe
administrateur, etc.

Il nous faut donc un moyen de faire réapparaître la petite fenêtre
annonçant la mise à jour.

La solution

Une méthode assez simple consiste à modifier un paramètre dans Firefox:

  1. Dans la barre d’adresse de Firefox, entrez ce texte:
    about:config
    Puis validez.
  2. Confirmez l’accès à la zone des paramètres, si cela vous est
    demandé.
  3. En haut, dans la partie permettant d’effectuer des recherches
    dans les paramètres, entrez ce texte:
    app.update.lastUpdateTime.background-update-timer
  4. Faites un clic droit sur la seule ligne qui apparait dans la
    liste en dessous, qui porte le même nom,
    puis choisissez l’option Modifier.
  5. Remplacez le nombre par 0 (zéro).
  6. Redémarrez Firefox.
  7. Naviguez normalement avec Firefox.
    Au bout d’environ 1 minute, notre fameuse petite fenêtre indiquant la
    disponibilité d’une mise à jour apparait. À vous de cliquer assez vite
    sur le lien qu’elle contient !

Voila.

 
Poster un commentaire

Publié par le 2013-11-12 dans Astuces

 

Étiquettes : , ,

UniversalSerializer

Le code source se trouve dans l’article original en anglais et mis à jour, voir ici
Le site Web officiel est maintenant universalserializer.com

Un sérialiseur universel pour .Net .

C’est quoi ?

UniversalSerializer est un sérialiseur pour .Net.
En d’autres termes, il transcode un arbre d’instances d’objets en un tableau d’octets, et vice versa.

En résumé

L’objectif de UniversalSerializer est d’être capable de sérialiser tout type sans effort.

  • Pas besoin d’ajouter des attributs ni des interfaces aux types (classes & structures).

  • Lorsqu’une instance de classe est référencée plusieurs fois, elle n’est sérialisée qu’une seule fois.

  • Les références circulaires sont autorisées.

  • Les mécanismes de sérialisation et de transcodage existant sont réutilisés.
    Actuellement: [Serializable], ISerializable, [ValueSerializer] et [TypeConverter].

  • Les types sans constructeur par défaut (sans paramètres) sont autorisés, si un constructeur paramétrique peut être exploité (c’est automatique).

  • Les classes ICollection non génériques peuvent être sérialisées si une méthode Add ou Insert peut être exploitée (c’est automatique).

Bien entendu, toutes les constructions ordinaires telles que les classes, structures, propriétés publiques, champs publics, énumérations, collections, dictionnaires, etc.. sont sérialisées par UniversalSerializer.

Lorsqu’un type n’est pas directement sérialisable, UniversalSerializer propose deux mécanismes:

  • ITypeContainers
    Nous pouvons englober le type (ou l’ensemble de types) qui pose problème dans une classe personnalisée qui gérera sa sérialisation et sa désérialisation.

  • Un ensemble de filtres
    Nous pouvons bloquer certains types, et demander au sérialiseur de stocker certains champs privés de la source.

Mon plus grand souhait est que les gens ajoutent de nouveaux Conteneurs et filtres dans le futur, et qu’ainsi peut-être un jour tous les types pourront être sérialisés.

Exemples d’utilisation

var data = new Window();
byte[] DataBytes = UniversalSerializer.Serialize(data);
var data2 = UniversalSerializer.Deserialize(DataBytes);

C’est aussi simple que ça !

Exemple pour WPF

Il existe une DLL spécialisée pour WPF, qui gère plus de types de WPF:

var data = new Window();
byte[] bytesData = UniversalSerializerWPF.SerializeWPF(data);
var data2 = UniversalSerializerWPF.DeserializeWPF(bytesData);

Exemple pour WinForms

Il existe une DLL spécialisée pour WinForms, qui gère plus de types de WinForms:

var data = new Form();
byte[] DataBytes = UniversalSerializerWinForm.SerializeWinForm(data);
var data2 = UniversalSerializerWinForm.DeserializeWinForm(DataBytes);

Pourquoi et comment est-ce fait ?

J’avais besoin d’un sérialiseur universel, capable de sérialiser n’importe quoi sans modification.

Mais les sérialiseurs existant posent des problèmes:

  • Certains nécessitent des attributs ou des interfaces spéciaux.
  • D’autres ne prennent pas en compte les champs.
  • Les références sont un vrai problème. En général, les instances sont dupliquées, et les références des objets désérialisés pointent vers différentes instances.
    Et les références circulaires ne sont jamais gérées.
  • Aucun ne peut gérer des types sans constructeur par défaut (sans paramètres), pour autant que je sache.
  • Certaines classes .Net particulières nécessitent plus d’attention mais sont scellées et donc ne peuvent pas être héritées avec des attributs de sérialisation.

Donc la solution nécessitait un ensemble de techniques et de mécanismes.
Nous allons passer en revue ces mécanismes dans les prochains chapitres.

Le code original

UniversalSerializer est construit à partir de fastBinaryJSON 1.3.7, de Mehdi Gholam, qui sérialise vers un format binaire inspiré par JSON.
UniversalSerializer y ajoute une certaine universalité en permettant de sérialiser n’importe quel type.
Notez que j’ai dû modifier fastBinaryJSON en profondeur, donc je ne peux plus me synchroniser avec le code source original de fastBinaryJSON. Par conséquent, UniversalSerializer n’est une extension de fastBinaryJSON et je ne maintiendrai pas de compatibilité dans le futur.

Sérialiser des classes sans constructeur par défaut

Dans .Net, les constructeurs de classes par défaut (sans paramètres) ne sont pas imposés. Mais presque tous les sérialiseurs en ont besoin.
Et des classes sans constructeur par défaut sont fréquentes dans la plateforme. Exemple: System.Windows.Controls.UIElementCollection.

La solution dans UniversalSerializer est de rechercher les autres constructeurs, et de trouver une correspondance entre leurs paramètres et des champs de la classe, même s’ils sont privés.

Par exemple, dans UIElementCollection, nous trouvons ce constructeur:

public UIElementCollection( UIElement visualParent, FrameworkElement logicalParent )

Et ces champs sont disponibles dans la même classe:

  • private readonly UIElement _visualParent;
  • private readonly FrameworkElement _logicalParent;

Les types sont les mêmes et leur nom très proches. Assez pour que UniversalSerializer essaie de créer une instance avec ces valeurs.
Et ça marche !

Sérialiser des classes ICollection non génériques

Pour d’obscures raisons, l’interface ICollection ne fournit pas de méthodes Add et Insert.
En d’autres termes, elle définit une collection en lecture-seule, au contraire de l’interface générique ICollection.
Normalement, nous ne pourrions pas désérialiser une classe qui implémente ICollection mais pas ICollection.

Heureusement, dans le monde réel, la quasi-totalité de ces classes possèdent une méthode Add ou Insert, au moins pour un usage interne.
UniversalSerializer détecte ces méthodes et les utilise pour désérialiser une instance de classe collection.

Réutiliser les mécanismes existant et les gérer à l’aide de ITypeContainers

Dans certains cas, il est plus efficace ou seulement possible d’utiliser les mécanismes de transcodage existant.

Lorsque j’ai essayé de sérialiser des contrôles WPF, j’ai découvert que:

  • [TypeConverter] permet de transcoder un objet vers des types facilement sérialisables (string, byte[ ], etc.).
    Exemple: System.Windows.Input.Cursor
  • [ValueSerializer] permet de transcoder à partir et vers un string.
    Exemple: System.Windows.Media.FontFamily
  • [Serializable] (et ISerializable) permet d’utiliser BinaryFormatter.
    Exemple: System.Uri

Si vous examinez FontFamily, vous vous apercevrez que la transcoder vers un string est bien plus facile que d’essayer de sauvegarder ses propriétés.
Et plus sûr, car modifier une propriété peut entraîner des conséquences imprévisibles sur des classes inconnues ou complexes.

Pour ces attributs, j’ai créé le mécanisme de ITypeContainer. Un conteneur remplace l’instance source par sa valeur transcodée, en général un string ou un byte[ ].
Un conteneur peut s’appliquer à un ensemble de types, par exemple tous les types affublés d’un certain attribut.

Des exemples et des détails suivent.

Les Filtres

Certains types nécessitent une gestion particulière, qui peut être faite par un ensemble de filtres.

Le filtre de validation de type

Ce filtre permet d’éviter à UniversalSerializer de sérialiser des types problématiques.

Par exemple, j’ai rencontré certaines classes qui utilisent System.IntPtr .
Sérialiser ce type mène droit à des problèmes vu que ses instances sont uniquement utilisées en interne dans les classes, même lorsqu’elles sont stockées dans des propriétés publiques.

Les filtres d’ajout de champs privés

Ce filtre ordonne au sérialiseur d’ajouter certains champs privés aux informations de sérialisation.

Par exemple, System.Windows.Controls.Panel a besoin de _uiElementCollection pour remplir sa propriété Children, car Children est en lecture-seule.
Grâce à ce filtre, la solution est simple. Et tous les types héritant de Panel, comme StackPanel, bénéficient du même filtre.

ForcedParametricConstructorTypes

Ce n’est pas un filtre mais une liste de types.
Lorsqu’un type est dans cette liste, UniversalSerializer ignore son constructeur par défaut (sans paramètres) et recherche un constructeur paramétrique.
Exemple: System.Windows.Forms.PropertyManager . Il est ici bien plus simple d’utiliser son constructeur paramétrique que d’écrire un ITypeContainer pour ce type.

Les références

Examinons ce code:

{ // Two references to the same object.
  var data = new TextBox[2];
  data[0] = new TextBox() { Text = "TextBox1" };
  data[1] = data[0]; // Same reference
  byte[] bytesData = UniversalSerializer.Serialize(data);
  var data2 = UniversalSerializer.Deserialize(bytesData);
  data2[0].Text = "New text"; // Affects the two references.
  bool sameReference = object.ReferenceEquals(data2[0], data2[1]);
}
  1. UniversalSerializer sérialise une seule instance du TextBox.
  2. Il désérialise ensuite seulement une seule instance de TextBox, et deux références pointant vers elle.
    Les preuves: sameReference est vrai, et Text est identique dans les deux références.

Des filtres et des ITypeContainer personnalisés

Étant donné que l’objectif majeur de UniversalSerializer est de permettre de sérialiser n’importe quel type, il est essentiel que nous partagions notre expérience.

Par conséquent, j’ai essayé de rendre la création de conteneurs et de filtres aussi facile que possible, pour vous permettre de les essayer dans tous les sens et de partager ensuite vos solutions.

Créer un ITypeContainer

Le but est de remplacer une instance problématique par une instance facile à sérialiser. Le plus souvent, le conteneur contiendra des types très simples (string, int, byte[ ], etc..).

Prenons un exemple:

/// 
/// . No default (no-param) constructor.
/// . The only constructor has a parameter with no corresponding field.
/// . The field ATextBox has no public 'set' and is different type from constructor's parameter.
///

public class MyStrangeClassNeedsACustomerContainer
{
    ///
    /// It is built from the constructor's parameter.
    /// Since its 'set' method is not public, it will not be serialized directly.
    ///

    public TextBox ATextBox { get; private set; }
    public MyStrangeClassNeedsACustomerContainer(int NumberAsTitle)
    {
        this.ATextBox = new TextBox() { Text = NumberAsTitle.ToString() };
    }
}

Comme noté dans son résumé, cette classe pose quelques problèmes au(x) sérialiseur(s).

Pour régler ce problème, nous créons un conteneur:

class ContainerForMyStrangeClass : UniversalSerializerLib.ITypeContainer
{
#region Here you add data to be serialized in place of the class instance
public int AnInteger; // We store the smallest, sufficient and necessary data.
#endregion Here you add data to be serialized in place of the class instance

public UniversalSerializerLib.ITypeContainer CreateNewContainer(object ContainedObject)
{
MyStrangeClassNeedsACustomerContainer sourceInstance = ContainedObject as MyStrangeClassNeedsACustomerContainer;
return new ContainerForMyStrangeClass() { AnInteger = int.Parse(sourceInstance.ATextBox.Text) };
}
public object Deserialize()
{
return new MyStrangeClassNeedsACustomerContainer(this.AnInteger);
}
public bool IsValidType(Type type)
{
return Tools.TypeIs(type, typeof(MyStrangeClassNeedsACustomerContainer));
}
public bool ApplyEvenIfThereIsANoParamConstructor
{
get { return false; }
}
public bool ApplyToStructures
{
get { return false; }
}
}

Un détail: toutes les méthodes se comportent comme des méthodes statiques (mais sans l’être officiellement), à l’exception de Deserialize().

Voyons-les plus en détail:

  • public int AnInteger

    Ça ne fait pas partie de l’interface ITypeContainer.
    C’est ici que nous stockerons les informations qui serviront plus tard à la désérialisation.

  • ITypeContainer CreateNewContainer(object ContainedObject)

    Utilisé durant la sérialisation.
    C’est un genre de constructeur pour une instance de ce conteneur. Le paramètre sera l’instance de la classe source à sérialiser.

  • object Deserialize()

    Utilisé durant la désérialisation.
    L’instance du conteneur produira une nouvelle instance, une copie de l’instance de la classe source, en utilisant notre champ AnInteger.

  • bool IsValidType(Type type)

    Utilisé durant la sérialisation.
    Renvoie vrai si le type hérite, ou est, du type source.
    C’est un filtre.
    Nous pouvons choisir d’accepter les types hérités ou non, d’accepter des types compatibles, etc..

  • bool ApplyEvenIfThereIsANoParamConstructor

    Utilisé durant la sérialisation.
    Renvoie vrai si ce conteneur s’applique aux types de classes possédant un constructeur par défaut (sans paramètres).
    Peut être utile avec des conteneurs très généraux.

  • bool ApplyToStructures

    Utilisé durant la sérialisation.
    Renvoie vrai si ce conteneur s’applique aux types structure, et pas seulement aux types classe.
    Peut être utile avec des conteneurs très généraux.

Les étapes sont:

  1. Le sérialiseur teste si le type source (MyStrangeClassNeedsACustomerContainer) est géré par ce conteneur.
    Notre classe de conteneur (ContainerForMyStrangeClass) réponds que oui, via IsValidType(), ApplyEvenIfThereIsANoParamConstructor et ApplyToStructures.
  2. Le sérialiseur construit une instance de notre conteneur, via CreateNewContainer().
    CreateNewContainer construits une instance et modifie son champ AnInteger.
  3. Le sérialiseur stocke (sérialise) ce conteneur à la place de l’instance source.
  4. Le sérialiseur retrouve (désérialise) l’instance du conteneur.
  5. Le désérialiseur appelle Deserialize() et obtient une copie de l’instance de la classe source.
    Deserialize() crée cette copie en utilisant son champ AnInteger.

Plus qu’a sérialiser:

/* This example needs a custom ITypeContainer.
Normally, this class can not be serialized (see details in its source).
But thanks to this container, we can serialize the class as a small data (an integer).
*/
var Params = new UniversalSerializerLib.CustomParameters();
Params.Containers = new UniversalSerializerLib.ITypeContainer[] {
new ContainerForMyStrangeClass()
};
var data = new MyStrangeClassNeedsACustomerContainer(123);
byte[] bytesData = UniversalSerializer.Serialize(data, Params);
var data2 = UniversalSerializer.Deserialize(bytesData, Params);
bool ok = data2.ATextBox.Text == "123";

Comme vous pouvez le voir, cette implémentation est très facile.

La classe statique Tools nous offre de l’aide:

  • Type Tools.TypeIs(Type ObjectType, Type SearchedType)

    C’est l’équivalent du ‘is’ en C#, mais pour les Types.
    Par exemple, TypeIs((typeof(List), typeof(List)) renvoie true.

  • Type DerivedType(Type ObjectType, Type SearchedType)

    Renvoie le type correspondant à SearchedType qui est hérité par OjectType.
    Par exemple, DerivedType(typeof(MyList), typeof(List)) renvoie typeof(List) lorsque MyList est un

    MyList: List { }.

  • FieldInfo FieldInfoFromName(Type t, string name)

    Renvoie le FiedInfo du champ nommé de ce type.
    Nous l’utiliserons dans le prochain chapitre.

Créer un ensemble de filtres

Notez que le mécanisme des filtres est complètement indépendant des ITypeContainers.
On peut les utiliser ensemble, ou séparément.

Prenons un exemple:

public class ThisClassNeedsFilters
{
public ShouldNotBeSerialized Useless;
private int Integer;
public string Value { get { return this.Integer.ToString(); } }
public ThisClassNeedsFilters()
{
}
public ThisClassNeedsFilters(int a)
{
this.Integer = a;
this.Useless = new ShouldNotBeSerialized();
}
}
public class ShouldNotBeSerialized
{
}

Cette classe (ThisClassNeedsFilters) pose quelques problèmes:

  • Elle contient un ShouldNotBeSerialized.
    Nous supposerons que la classe ShouldNotBeSerialized doit être évitée pour certaines raisons, je ne sais pas pourquoi, peut-être qu’elle est empoisonnée !
  • Le champ Integer n’est pas public et donc est ignoré par le(s) sérialiseur(s).
  • Même le nom du paramètre du constructeur est différent d’aucun champ ou propriété.
    De toute façon, le sérialiseur n’a pas besoin du constructeur, puisqu’il y a un constructeur par défaut.

Pour dépasser ces problèmes, nous écrivons un ensemble personnalisé de filtres:

/// 
/// Tells the serializer to add some certain private fields to store the type.
///

FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)
{
if (Tools.TypeIs(t, typeof(ThisClassNeedsFilters)))
return new FieldInfo[] { Tools.FieldInfoFromName(t, "Integer") };
return null;
}
///
/// Returns 'false' if this type should not be serialized at all.
/// That will let the default value created by the constructor of its container class/structure.
///

bool MyTypeSerializationValidator(Type t)
{
return ! Tools.TypeIs(t, typeof(ShouldNotBeSerialized));
}

Ils parlent d’eux-même:

  • FieldInfo[] MyAdditionalPrivateFieldsAdder(Type t)

    dit au sérialiseur d’ajouter un champ privé (Integer) à chaque instance source de ce type (ThisClassNeedsFilters).

  • bool MyTypeSerializationValidator(Type t)

    empêche le sérialiseur de stocker toute instance de ce type (ShouldNotBeSerialized).
    En conséquence, tout instance de ThisClassNeedsFilters ne fixera pas son champ Useless (il sera null après la désérialisation).

Maintenant on sérialise la classe:

/* This example needs custom filters.
Normally, this class can be serialized but with wrong fields.
Thanks to these filters, we can serialize the class appropriately.
*/
var Params = new UniversalSerializerLib.CustomParameters();
Params.FilterSets = new FilterSet[] {
new FilterSet() {
AdditionalPrivateFieldsAdder=MyAdditionalPrivateFieldsAdder,
TypeSerializationValidator=MyTypeSerializationValidator } };
var data = new ThisClassNeedsFilters(123);
byte[] bytesData = UniversalSerializer.Serialize(data, Params);
var data2 = UniversalSerializer.Deserialize(bytesData, Params);
bool ok = data2.Value == "123" && data2.Useless == null;

L’implémentation est même plus facile qu’avec ITypeContainer.

Le code source de UniversalSerializer

Toutes les sources sont en C#.

La solution a été créée avec Visual Studio 2012, mais elle doit être compatible avec VS 2010.
La compilation nécessite seulement .Net 4, aucune DLL tierce, le code source est complet.

Il y a 3 DLLs:

  • UniversalSerializer1.dll
    La DLL principale générale, avec peu de dépendances.
  • UniversalSerializerWPF1.dll
    Une DLL spécialisée pour WPF, qui gère plus de types de WPF.
  • UniversalSerializerWinForm1.dll
    Une DLL spécialisée pour WinForms, qui gère plus de types de WinForms.

De cette façon, votre application n’aura pas de dépendances inutiles.

Dans l’archive, vous trouverez ces solutions pour VS:

  • \UniversalSerializer Tester\UniversalSerializer Tester.sln
    Une application WPF avec de nombreux exemples.
  • \UniversalSerializerWinForm\UniversalSerializerWinForm.sln
    Une application WinForm avec quelques exemples.
  • \UniversalSerializerReleaseLibs\UniversalSerializerReleaseLibs.sln
    Cette solution génère la version Release des trois DLLs.

Quelques points important

  • Le processus de sérialisation/désérialisation  identifie les types par leur nom complet (grâce à type.AssemblyQualifiedName).
    Ce genre de nom dépend de la version de l’assemblage (assembly), alors faites attention aux problèmes de versions pour la plateforme et pour les DLLs !

  • Le format produit actuel est sujet à changements dans le futur.
    C’est pour cela que le nom de la DLL porte un numéro de version.
    Si vous sauvegardez le tableau d’octets dans un fichier, je vous suggère d’ajouter une information de version à votre arbre d’objets.

Le futur

Je voudrais améliorer certains aspects:

  • Ajouter de nouveaux ITypeContainers et filtres.
    Vous pouvez m’aider. Prévenez-moi lorsque des Types ne sont pas sérialisés correctement. Et merci de partager votre solution, telle que des conteneurs et des filtres.
  • Réduire la taille des données produites.
    J’ai besoin de créer un nouveau format binaire.
  • Accélérer le traitement.

Faciliter le travail des sérialiseurs

Cette expérience m’a enseigné quelles difficultés rencontrent les sérialiseurs avec certains types et comment on peut leur faciliter le travail lorsque nous créons une classe.

  1. Écrivez un constructeur par défaut (sans paramètres) lorsque c’est possible.
    Le sérialiseur reconstruira l’instance à partir des champs et des propriétés.
  2. Si vous ne pouvez pas écrire un constructeur par défaut (sans paramètres), écrivez un constructeur paramétrique dont les paramètres correspondent à des champs privés.
    Chaque paramètre doit être du même type que le champ, et leurs noms doivent être semblables. En fait les noms peuvent différer légèrement: Param -> param ou _param ou encore _Param.
  3. Implémentez un ValueSerializerAttribute ou un TypeConverterAttribute lorsqu’une instance peut être construite à partir de quelque chose d’aussi simple qu’un string.
    En particulier lorsque votre classe contient de nombreux champs et propriétés d’optimisation publics.
    Par exemple, FontFamily peut être construit à partir d’un simple string; peu importe s’il contient bien d’autres informations, elles peuvent toutes être retrouvées à partir de ce simple string.
    Je ne sais pas si beaucoup de sérialiseurs prennent ces attributs en compte, mais au moins UniversalSerializer le fait.
  4. Tous les champs et les propriétés d’optimisation devraient être privés, lorsque c’est possible. Voir plus haut.
  5. Lorsque vous créez une collection d’objets, implémentez IList.
      Parce que ICollection ne permet pas d’ajouter des éléments à une collection.
    Lorsque vous créez une collection générique, implémentez ICollection.
      Parce que IEnumerable ne permet pas d’ajouter des éléments à une collection.

Remerciements

Je remercie Mehdi Gholam pour son fastBinaryJSON.
J’ai appris des choses intéressantes en lisant son code.
Merci pour avoir partagé cela.

 
Poster un commentaire

Publié par le 2013-07-13 dans .Net, Programmation

 

Étiquettes : , , ,

Le respect du client… de l’API

Les API changent, évoluent, s’améliorent, et ne sont pas toujours compatibles entre leurs versions.

C’est ce qui fait qu’un logiciel vieux de deux ans ne fonctionne parfois plus correctement, souvent après une mise à jour du système d’exploitation.
Le problème est général à la programmation, donc il concerne tous les systèmes d’exploitation.

Au final, ce qui cause ces problèmes, particulièrement énervants pour l’utilisateur final, c’est avant-tout un choix.

Choix de la compatibilité entre versions d’une API

[ Ici, je parle de versions majeures des API, et non des mises à jour mineures ]

Pour clarifier un peu le problème, voici une liste des différents choix que j’ai pu rencontrer jusqu’à présent.

Aucune compatibilité

Ça peut paraître absurde à beaucoup, mais certains programmeurs ou architectes font le choix de ne tout simplement pas permettre à un logiciel prévu pour une version A d’une API de fonctionner avec la version B de la même API.

Schématiquement, ça ressemble à ceci:

Ici les différentes versions ne proposent aucune compatibilité.
C’est évidemment ce qui demande le moins de travail aux programmeurs.
Par contre, un logiciel basé sur une API A devra être livré avec la même version A de la bibliothèque de fonctions, ce qui peut représenter une grosse taille, du temps de téléchargement et des incompatibilités à l’exécution.

Redirection vers la version immédiatement suivante

Note: C’est un concept différent de la compatibilité descendante.

Schématiquement:

Ici, lorsqu’on crée une nouvelle version (B) de l’API, la 1ère version (A) est réécrite pour renvoyer vers la version suivante (B). Et ainsi de suite.

L’intérêt de cette technique, c’est qu’un logiciel écrit pour une très vieille version de l’API (A) peut utiliser l’API la plus récente (E), sous forme de sa bibliothèque de fonctions.
Cela évite le problème de l’Enfer des bibliothèques, c’est à dire essentiellement qu’on n’a pas besoin de livrer chaque logiciel avec ses bibliothèques associées.

Avantage supplémentaire: les anciennes API étant maintenues, par le biais des renvois vers des versions plus récentes, on trouve moins de bogues.

Du point de vue du travail, cela oblige, à chaque nouvelle version d’un API, de revoir la précédente pour remplacer ses fonctions obsolètes par des renvois vers la nouvelle.
Ça peut sembler représenter beaucoup de travail, mais en pratique ce n’est pas tellement le cas, car:

  1. En général une faible partie de l’API change d’une version à une autre.
    Les plus gros changements ont lieu dans l’implémentation des fonctions, ce qui en soi ne change rien à l’API.
  2. Il est classique que les fonctions et les concepts d’une nouvelle version d’une API englobent et surpassent ceux de l’ancienne version.
    Par exemple, une fonction pouvant calculer le sinus d’un nombre flottant sera étendue pour pouvoir calculer aussi le sinus d’un nombre imaginaire (pourquoi pas ?).
    Dans un tel cas, l’ancienne fonction peut tout simplement renvoyer vers la nouvelle, avec des paramètres par défaut. Car qui peut le plus peut le moins.

Ce système, bien que limité à des paires de versions, permet tout de même à la plus ancienne version (A) de renvoyer, par bonds successifs, vers la plus récente (E). Pour une somme de travail acceptable, ça reste d’un intérêt non négligeable.

Redirection vers la dernière version

Schématiquement:

L’avantage de ce principe, c’est que toutes les anciennes versions renvoient vers la dernière, ce qui les maintient le plus à jour possible, évitant les vieux bogues et utilisant les toutes dernières techniques.

Par exemple, si c’est l’API d’un système de fenêtrage, alors même une très vieille application utilisera la toute dernière bibliothèque et donc elle aura un affichage dernier cri, tout-à-fait compatible avec les applications les plus récentes, et elle sera bien intégrée dans le système.

L’inconvénient est bien sûr que le travail est de plus en long à chaque version.
De plus il faut avoir des programmeurs connaissant bien les plus anciennes versions de l’API, ce qui est de plus en plus rare au fil du temps car les plus jeunes gens sont formés sur les API les plus récentes.

Par contre, du point de vue de l’utilisateur final, c’est la meilleure solution.

Réutilisation d’une plus ancienne version

Schématiquement:

Ici, une nouvelle version renvoie vers une plus ancienne.
Étrangement, ce concept rétrograde est très présent dans les systèmes d’exploitation.

Il est d’ailleurs systématiquement utilisé dans cet autre concept: les plateformes.

Exemple de Windows (8)

Ce choix paraît justifié par la grande différence qui existe entre l’API de la nouvelle plateforme et celle de l’ancienne.
Toutefois, je ne peux pas m’empêcher de penser qu’il serait bien plus logique de choisir un des autres concepts de compatibilité.

Dans ce dernier exemple, DotNet aurait pu être créé à partir de zéro et les parties obsolètes de Win32 réécrites pour n’être plus qu’un renvoi vers l’API la plus récente.
Idem pour WinRT.
Bien entendu, il est clair que cela aurait demandé bien plus de travail et causé bien plus de difficultés.
Il aurait fallu réfléchir longuement et mettre au point de vrais nouveaux concepts, les expérimenter tout aussi longuement. Et ensuite il aurait fallu réécrire toute la partie de Win32 devant renvoyer vers WinRT (par exemple).

Ceci dit, le résultat aurait été d’un grand avantage: Windows 8 aurait été un système homogène, au lieu de proposer deux types d’interface graphique différents et séparés.

On peut aussi remarquer que pour Windows 8 Microsoft a dû réécrire de nombreux logiciels et panneaux de configuration pour WinRT, ce qui représente également du travail. D’un certaine façon, ils ont économisé du travail avec ce choix de sur-plateforme, mais en ont gaspillé avec la réécriture des applications.

Toutefois, il faut aussi prendre en compte le bénéfique renouvellement des applications. Une application réécrite l’est en tentant compte de nouveaux concepts et des dernières techniques (par nécessairement liés à la plateforme).

Conclusion

Il me semble clair qu’il n’existe aucune méthode parfaite, et qu’on doit faire des choix difficiles.

Toutefois si on a une vision d’une informatique pérenne, alors il existe au moins une façon de faciliter l’évolution d’une API: la créer la plus simple et de plus haut niveau d’abstraction que possible, pour éviter que ses fonctions et ses concepts ne soient vite périmés.

Il me semble nécessaire d’éviter les fonctions trop spécialisées.
Nous avons une culture de l’optimisation qui nous vient des temps anciens lorsque les ordinateurs étaient si peu puissants qu’il fallait compter chaque octet et chaque temps d’horloge.
Il faut donc désormais penser différemment, plus tournés vers l’avenir et donc vers des solutions plus universelles et plus pérennes, au moins pour les API.
Cela ne nous empêchera pas d’optimiser l’implémentation des fonctions. Après tout, l’API n’est qu’une façade, une porte d’accès.

 
Poster un commentaire

Publié par le 2012-11-06 dans Programmation

 

Étiquettes : , , , , ,