Guillemouze
Messages postés991Date d'inscriptionsamedi 25 octobre 2003StatutMembreDernière intervention29 août 2013
-
15 avril 2008 à 09:41
florenth
Messages postés1023Date d'inscriptiondimanche 1 août 2004StatutMembreDernière intervention17 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.
florenth
Messages postés1023Date d'inscriptiondimanche 1 août 2004StatutMembreDernière intervention17 août 20082 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)
cs_MAURICIO
Messages postés2106Date d'inscriptionmardi 10 décembre 2002StatutModérateurDernière intervention15 décembre 20145 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 ..
Guillemouze
Messages postés991Date d'inscriptionsamedi 25 octobre 2003StatutMembreDernière intervention29 août 20136 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?
Vous n’avez pas trouvé la réponse que vous recherchez ?
florenth
Messages postés1023Date d'inscriptiondimanche 1 août 2004StatutMembreDernière intervention17 août 20082 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 !]
florenth
Messages postés1023Date d'inscriptiondimanche 1 août 2004StatutMembreDernière intervention17 août 20082 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.
Guillemouze
Messages postés991Date d'inscriptionsamedi 25 octobre 2003StatutMembreDernière intervention29 août 20136 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
florenth
Messages postés1023Date d'inscriptiondimanche 1 août 2004StatutMembreDernière intervention17 août 20082 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;
Guillemouze
Messages postés991Date d'inscriptionsamedi 25 octobre 2003StatutMembreDernière intervention29 août 20136 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).
florenth
Messages postés1023Date d'inscriptiondimanche 1 août 2004StatutMembreDernière intervention17 août 20082 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.
Guillemouze
Messages postés991Date d'inscriptionsamedi 25 octobre 2003StatutMembreDernière intervention29 août 20136 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;