Serialisation: heritage & évolutivité

Résolu
Guillemouze Messages postés 991 Date d'inscription samedi 25 octobre 2003 Statut Membre Dernière intervention 29 août 2013 - 15 avril 2008 à 09:41
florenth Messages postés 1023 Date d'inscription dimanche 1 août 2004 Statut Membre Dernière intervention 17 août 2008 - 17 avril 2008 à 11:00
Salut tout le monde,
je suis sur un probleme de serialisation de mes objets, et je cherche une maniere efficace pour la sauvegarde/chargement.
Ma structure est composée d'une liste d'objets heritants tous de la meme classe de base (a travers de multiples niveaux).
on pourrai les symboliser comme ca :

  A
 /  \
 B   C
/ \ / \
D E F G

Mon but est de pouvoir sauvegarder les informations de ces objets de maniere optimale (en terme de rapidité), tout en gardant à l'esprit que ces structures changeront a l'avenir: ajout/suppression de champs, ajout de classes intermediaires (pas sur mais possible), ...

j'ai donc pensé a la structure suivante:
| Id de la classe | Version de la classe | Champs de la classe | Version de la classe mere | Champs de la classe mere | ...
ce qui me donnera par exemple, pour un objet de la classe E
| E | 2 | E1 | E2 | 5 | B1 | B2 | B3 | 1 | A1

ou les Xi sont les champs de la classe X, en supposant que la classe E est en version 2, la B en 5 et la A en 1.

J'en arrive donc (enfin) a ma question : Que pensez vous de cette structure? est elle fiable, efficace et evolutive? Peut etre avez d'autres idées/suggestions a me soumettre.

Merci de vos réactions :)

14 réponses

florenth Messages postés 1023 Date d'inscription dimanche 1 août 2004 Statut Membre Dernière intervention 17 août 2008 3
15 avril 2008 à 20:34
Salut !

Perso, je trouve que ta structure fonctionne bien.
Après, pour l'implémentation dans le code, tu ne donnes pas trop de détails.
Mais j'imagine que la version de chaque classe est bien retournée par une "class function" et que tu utilises bien l'héritage pour sauvegarder et chercher les propriétés ancestrales.

Vu le code que tu donnes, ça a l'air bon, mais j'aurais une toute petite remarque: faire sauvegarder les ancêtres AVANT la classe elle-même. En gros, appeler inherited en tout premier dans les procédures Read ET Write.
Pourquoi ?
Parce que si tu as besoin d'initialisations, ou autre, il est judicieux de s'assurer que les ancêtres ont un contenu cohérent avec ce que tu vas charger.
En effet, si TChild dérive de TParent, une instance de TParent ne peut pas accéder aux membres de TChild alors qu'une instance de TChild peut accéder aux membres de TParent. Et suivant ce que tu fais dans tes procédures, ce détail ne doit pas être négligé (surtout si tes classes évoluent, on ne peut pas prédire ce que va devenir ton code !)

En plus, ça permet facilement de faire sauvegarder ce que tu appelles l'id de classe (qui ne doit être fait qu'une fois par objet sauvé): il suffit de demander à la classe A de le faire ! Par le jeu des inherited, ce sera elle qui sera sauvée en premier alors que dans ton cas, c'est plus dur.

Voila déjà pour ça.

Ensuite, je ne pense pas que tu va pouvoir ajouter des classes intermédiaires dans ta hiérarchie. Dans tous les cas, c'est affreusement galère !

Et j'aurais une autre idée: donner la taille en octets de chaque enregistrement de classe. Car quand tes versions changeront, tu n'auras plus aucun moyen de le savoir. (ex: une classe de version 2 charge un fichier de version 4: il y a des champs en trop mais elle ne peut pas le savoir)
3
Caribensila Messages postés 2527 Date d'inscription jeudi 15 janvier 2004 Statut Membre Dernière intervention 16 octobre 2019 18
15 avril 2008 à 14:20
Salut Guillemouze,


Si j'ai bien compris...


Je reprends ton exemple pour la classe E et je considère une autre classe D (version 3) par exemple. Ca donnera, pour ces deux classes:



| E | 2 | E1 | E2 | 5 | B1 | B2 | B3 | 1 | A1
| D | 3 | D1 | D2 | 5 | B1 | B2 | B3 | 1 | A1



On voit qu'on sauvegardes deux fois les mêmes données (en rouge)... Ce qui n'est pas optimal.

De plus, s'il y a changement dans la structure, il faudra peut-être revoir toutes les classes contenant les parties qui auront changé.


Mais ai-je bien compris ton problème?...
0
cs_MAURICIO Messages postés 2106 Date d'inscription mardi 10 décembre 2002 Statut Modérateur Dernière intervention 15 décembre 2014 5
15 avril 2008 à 14:58
Effectivement c' est ce que j' ai remarqué aussi: peut-être veux tu garder pour chaque "child" la version du parent avec lequel il descend lors de la sauvegarde ..
0
Guillemouze Messages postés 991 Date d'inscription samedi 25 octobre 2003 Statut Membre Dernière intervention 29 août 2013 6
15 avril 2008 à 18:17
non il n'y a pas redondance. B1, B2, A1, ... sont les valeurs des champs (pour l'instance de E qui est en train d'etre sauvée) hérités de A et B

Petit exemple un peu plus concret :
Classe Papi - 2 champs: (version 4)
 - Toto: integer
 - Titi : word

Classe Maman - 1 champ: (version 2)

 - Tutu: char

Class Fiston - 1 champ (version 7)
 - Tata: byte

Class Fiston2 - 0 champs (version 5)

soit une instance de fiston (F1) et une instance de fiston2 (F2)
F1 = (Tata=9, Tutu=C, Titi=12, Toto=23);
F2 = (             Tutu=E, Titi=21, Toto=512);
je sauverai donc
pour F1 : ID_FISTON  | 7 | 9 | 2 | C | 4 | 12 | 23
pour F2 : ID_FISTON2 | 5 |     2 | E | 4 | 21 | 512
(en rouge les num de version)

Ainsi, si j'ajoute un champ a Maman par exemple (je passe donc en version 3), je sais que si je li une version 2, je lierai le champ "Tutu", si je lis une version superieur, il faudra aussi que je lise le nouveau champ. En gros, la version m'indique l'espace memoire ocuppée par la classe elle meme

procedure Maman.Read(S: TStream);
var
    v: byte;
begin
    S.Read(v, 1); //lecture de la version
    S.Read(Self.Tutu, 1);
    if v > 2 then //Seulement a partir de la version 3
       S.Read(MonNouveauChamp, 4);

    inherited; // a tour du parent de se lire
end;

Donc comme tu le dis mauricio, je ne peux pas savoir qu'une sauvegarde de la version X d'une classe correspond a une version Y de sa classe mere (mes classes n'evoluent pas toutes en meme temps).

Ai-je été clair?
0

Vous n’avez pas trouvé la réponse que vous recherchez ?

Posez votre question
cs_MAURICIO Messages postés 2106 Date d'inscription mardi 10 décembre 2002 Statut Modérateur Dernière intervention 15 décembre 2014 5
15 avril 2008 à 18:22
Haa bem oui, mais c' est qui le père de "Fiston" et "Fiston2" ?  ^ ^
0
florenth Messages postés 1023 Date d'inscription dimanche 1 août 2004 Statut Membre Dernière intervention 17 août 2008 3
15 avril 2008 à 20:37
Tiens, mes dires sont confirmés par Wikipédia:
http://fr.wikipedia.org/wiki/S%C3%A9rialisation#Types_hi.C3.A9rarchiques
0
florenth Messages postés 1023 Date d'inscription dimanche 1 août 2004 Statut Membre Dernière intervention 17 août 2008 3
15 avril 2008 à 20:53
Sinon, si tu cherches en plus la performance (tant au niveau occupation disque que vitesse de lecture/écriture) j'ai une autre solution, radicale cette fois:

- Comme les versions des classes seront les mêmes pour tout le fichier que tu vas écrire, tu écris une fois toutes ces versions au début du fichier. Comme ça:
1. C'est fait une fois pour de bon.
2. Tu gagnes en rapidité et en place.

- Du coup, tu vas devoir faire quelque chose de beaucoup plus complexe sur le plan structural: chaque classe devra être référencée auprès d'une classe de gestion (comme TBitmap et TJpegImage se référencent sur TPicture) qui se chargera de gérer le chargement/sauvegarde de tout ton bazar.

C'est de l'introspection en sorte, mais fait-maison puisque Delphi ne propose rien** pour le faire automatiquement, à la différence de Java et .net.

**: c'est pas très juste (voire complètement faux) car les composants fonctionnent aussi selon ce principe d'introspection (enregistrement dans les dfm) qu'il est tout à fait possible de réutiliser en faisant descendre tes classes de TComponent, en lui faisant surcharger les méthodes adéquates et en utilisant les classes de Delphi faites pour cela (TReader, TWriter, ...)
Il n'y a "juste" pas de compatibilité descendante puisqu'une exception est produite dans ces cas là (mais peut être que tu ne souhaites pas garantir de compatibilité descendante)

Voila, avec ça tu as de quoi pas mal réfléchir !
[d'ailleurs ce problème aurait du être réfléchi AVANT de coder quoi que ce soit !]
0
florenth Messages postés 1023 Date d'inscription dimanche 1 août 2004 Statut Membre Dernière intervention 17 août 2008 3
15 avril 2008 à 20:55
Pardon, je parlais de compatibilité ascendante (cas où on lit un fichier provenant d0une version plus récente) et non pas descendante dans mon dernier paragraphe.

Voir ici: http://fr.wikipedia.org/wiki/Compatibilit%C3%A9_ascendante_et_descendante
0
Guillemouze Messages postés 991 Date d'inscription samedi 25 octobre 2003 Statut Membre Dernière intervention 29 août 2013 6
16 avril 2008 à 00:06
merci BEAUCOUP florenth pour toutes ces remarques

"Mais j'imagine que la version de chaque classe est bien retournée par
une "class function" et que tu utilises bien l'héritage pour
sauvegarder et chercher les propriétés ancestrales."
Non j'ai choisi d'utiliser des constantes dans le code pour gerer chaque version. Cette info de version n'est utilisée que par la classe elle meme pour savoir quoi lire. En clair, seules les methodes Read et Write de la classe utilisent cette info, donc je ne suis pas allé jusqu'a creer une class function. Peut etre vais-je faire une fonction Read[Write]Version pour genericiser tout ca... a voir.
par contre il est sur que j'utilise l'heritage, sinon je perd tout l'interet de ce versionning !

"faire sauvegarder les ancêtres AVANT la classe elle-même."
Carrement, tres bonne idée. mes objets n'utilisent aucune donnée de leur parent lorsqu'ils chargent les leurs, mais ca pourrait tres bien arriver. A changer absolument.

"En plus, ça permet facilement de faire sauvegarder ce que tu appelles
l'id de classe (qui ne doit être fait qu'une fois par objet sauvé): il
suffit de demander à la classe A de le faire ! Par le jeu des
inherited, ce sera elle qui sera sauvée en premier alors que dans ton
cas, c'est plus dur."
C'est peut etre la que se trouve le gros probleme de ma conception. En effet, l'id est sauvé qu'une fois par objet, pour la classe la plus profonde. Mais cette action est faite par le superviseur (la liste d'objets). Cela vient du fait qu'il faut que j'appele le bon TClass.Create .
Mon code ressemble a peu pres a ca :

procedure TSuperviseur.Load(s: TStream);
begin
    while not EndOfStream(s) do
    begin
       s.read(id, 1);
       classe := Self.GetClassOfId(id);
       MonObjet := classe.Create;
       MonObjet.Load(s);
    end;
end;

procedure TSuperviseur.Save(s: TStream);

begin

    for i := 0 to self.Count-1 do

    begin

       id := Self.GetIdOfClass(self[i].classType);


       s.write(id, 1);
       MonObjet.Save(s);

    end;

end;

"donner la taille en octets de chaque enregistrement de classe"
Ca me semble aussi tres important pour lire des version ulterieures avec un ancien code.

"Comme les versions des classes seront les mêmes pour tout le fichier
que tu vas écrire, tu écris une fois toutes ces versions au début du
fichier"
C'est vrai que c'est pas mal, mais un peu trop galere a mettre en place (utilisation de variables globales, ...). Je garde cette idée de coté, mais je ne vais pas la mettre en place pour l'instant (faute de temps un peu aussi).

"en utilisant les classes de Delphi faites pour cela (TReader, TWriter, ...)
"
J'etais parti sur l'etude de ces classes au debut, je pensai les utiliser ou m'en inspirer fortement, mais j'ai laissé ca de coté pour voir si je trouvai pas une autre solution ... que j'ai finalement choisie.

En tout cas merci beaucoup pour toutes ces information qui me sont d'une aide precieuse
0
florenth Messages postés 1023 Date d'inscription dimanche 1 août 2004 Statut Membre Dernière intervention 17 août 2008 3
16 avril 2008 à 14:51
"Comme les versions des classes seront les mêmes pour tout le fichier que tu vas écrire, tu écris une fois toutes ces versions au début du fichier"
C'est vrai que c'est pas mal, mais un peu trop galere a mettre en place (utilisation de variables globales, ...). Je garde cette idée de coté, mais je ne vais pas la mettre en place pour l'instant (faute de temps un peu aussi).


> Bah non, vu ton code du superviseur, ça me semble simple:

procedure TSuperviseur.Save(s: TStream);
begin
[ enregistrement de toutes les versions des classes via une boucle ]
suite de ton code normal (sauf que tu n'enregistres plus aucune version)
end;


procedure TSuperviseur.Save(s: TStream);
var
ListOfVersions: array of record
classID: Byte;
version: Byte;
end;

function GetVersionOfClass(AClassID: Byte): Byte;
var
I: Integer;
begin
Result := 0;
for I := 0 to High(ListOfVersions) do
if ListOfVersions[I].classID = AClassID then
begin
Result := ListOfVersions[I].version;
Exit;
end;
end;

begin
[ Chargement de toutes les versions des classes via une boucle
(dans une variable locale, tu n'en a pas besoin ailleurs) ]

while not EndOfStream(s) do
begin
s.read(id, 1);
classe := Self.GetClassOfId(id);
MonObjet := classe.Create;
MonObjet.Load(s, GetVersionOfClass(id)); // Rajout d'un paramètre ici.
end;
end;

Voili voilou !
0
Guillemouze Messages postés 991 Date d'inscription samedi 25 octobre 2003 Statut Membre Dernière intervention 29 août 2013 6
16 avril 2008 à 15:07
et bien non, ce n'est pas si simple que ca !
si je fais
MonObjet.Load(s, GetVersionOfClass(id)); // Rajout d'un paramètre ici.

quand MonObjet fera appel a la procedure inherited, je n'aurai plus acces au tableau ListOfVersions pour me dire quelle est la version du pere de MonObjet. Avec cette methode, je n'ai accès qu'a la version de MonObjet.
CE serait limite possible si mon objet connaissait le superviseur, mais ce n'est pas le cas, et je doute que ce soit judicieux dans mon cas (un objet peut etre contenu dans plusieurs superviseurs).
0
florenth Messages postés 1023 Date d'inscription dimanche 1 août 2004 Statut Membre Dernière intervention 17 août 2008 3
16 avril 2008 à 19:00
Ah ouais bien vu !
J'oubliais que les classes ancêtres pouvaient avoir une version différente (c'est comme ça dans mon code - tout évolue en même temps !)

Dans ce cas là en effet, ça se corse.
Mais il suffirait "juste" de transmettre un pointeur vers le tableau de liste de versions après tout.
Après, faut voir ce qui est le plus simple.
0
Guillemouze Messages postés 991 Date d'inscription samedi 25 octobre 2003 Statut Membre Dernière intervention 29 août 2013 6
17 avril 2008 à 09:23
En tout cas merci encore pour le temps passé sur mon probleme.

Petite question subsidiaire, est-ce que vous connaitriez une astuce pour que chaque classe enregistre sa version et sa taille sans faire de copier-coller pour chaque classe?

par exemple si C herite de B qui herite de A, je suis obligé de faire

procedure A.Save
begin
    WriteSizeAndVersion(Sz, Ver); // <- fonction protected de la classe de base
    //ecriture des données propres a la classe A
end;

procedure B.Save

begin
    WriteSizeAndVersion(Sz, Ver);

    //ecriture des données propres a la classe B

end;

procedure C.Save

begin

    inherited;

    WriteSizeAndVersion(Sz, Ver);

    //ecriture des données propres a la classe C

end;
0
florenth Messages postés 1023 Date d'inscription dimanche 1 août 2004 Statut Membre Dernière intervention 17 août 2008 3
17 avril 2008 à 11:00
Héhé, c'est justement ce que j'étais en train de chercher !
Mais ça ne me semble pas simple, du moins en premier abord
0
Rejoignez-nous