Partage
  • Partager sur Facebook
  • Partager sur Twitter

Visitor Pattern et XML

Amis ou ennemis ?

    9 juin 2008 à 23:57:07

    Bonjour les petits cplusplus o/

    Il y a de ça quelques temps j'ai entendu parlé du Visitor Pattern (sur ce forum je crois bien) et, curieux comme un pou je me suis empressé d'aller voir de quoi il s'agissait. Aprennant les concepts de ce pattern où (dois-je le rappeller ?) un objet visite une collection d'autres afin de... euh... faire ce qu'il veut.

    Je n'ai put m'empècher de penser : "Mortel pour générer une image XML de la partie métier de mes applications !"

    Comme je n'ai pas beaucoup de temps a moi (j'ai des obligations comme dormir, manger, m'affaler dans l'herbe au soleil), j'ai mis quelques temps à me motiver à tester cette idée. Quoi qu'il en soit j'ai prit un exemple tout con pour tester : Un monde contient des êtres vivants qui sont des plantes mais d'autres classes pourront dériver ensuite d'être vivant (en gras c'est mes classes ;) )

    Donc voilà mon premier jet en suivant exactement le visitor pattern :

    (note: le but c'était de tester, c'est donc normal de ne pas retrouver les classes de base visitor et visitable, comme vous pourrez vous en douter VisitorXML est un visitor alors que monde, être vivant et plante sont visitables)

    class Monde {
    public:
       Monde();
       ~Monde();
       
       void accueil(VisitorXML& squatter) const;
       
    private:
       enum {NB_VIVANTS = 10};
       
       std::vector<EtreVivant*> _peuplades;
    };
    
    void Monde::accueil(VisitorXML& squatter) const {
       squatter.visite(*this);
       
       for (int i = 0; i < NB_VIVANTS; ++i) {
          _peuplades[i]->accueil(squatter);
       }
    }
    


    class Plante : public EtreVivant {
    public:
       Plante();
       
       virtual void nextTurn();
       virtual void accueil(VisitorXML& squatter);
       int getAge() const;
       
    private:
       int _nbTours;
    };
    
    void Plante::accueil(VisitorXML& squatter) {
       squatter.visite(*this);
    }
    


    class VisitorXML {
    public:
       VisitorXML(std::ostream& os);
       
       void visite(const Monde& monde);
       void visite(const Plante& plante);
       
    private:
       void _genereIndentation();
       void _printLine(const std::string& ligne);
       
       std::ostream& _os;
       int _niveauIndentation;
    };
    
    VisitorXML::VisitorXML(ostream& os)
     : _os(os), _niveauIndentation(0)
    {
    }
    
    void VisitorXML::visite(const Monde& monde) {
       _printLine("<monde>");
       ++_niveauIndentation;
       
       /* Je ne peux pas fermer <monde> ici  */
    }
    
    void VisitorXML::visite(const Plante& plante) {
       _printLine("<plante>");
       ++_niveauIndentation;
       
       ostringstream oss;
       oss << "<age>" << plante.getAge() << "</age>";
       _printLine(oss.str());
       
       oss.str("");
       oss << "<sante>" << plante.getSante() << "</sante>";
       _printLine(oss.str());
       
       --_niveauIndentation;
       _printLine("</plante>");
    }
    
    // Note : _printLine ecrit une ligne précédée de l'indentation adéquate
    


    Ce code serait parfait si je pouvais fermer mes balises. Là je n'ai que les feuilles de mon arbre XML (soit les Plantes :) ) qui sont fermées. Je me suis donc attelé à trouver une solution pour fermer toutes mes balises.

    La première idée qui m'était venue fut d'appeller une seconde methode du visitor une foi la visite des "enfants" terminée, ce qui donnerai, pour Monde::accueil() quelque chose comme :

    void Monde::accueil(VisitorXML& squatter) const {
       squatter.visite(*this);
       
       for (int i = 0; i < NB_VIVANTS; ++i) {
          _peuplades[i]->accueil(squatter);
       }
    
       squatter.cassesToi(*this);
    }
    


    La méthode VisitorXML::cassesToi(const visitable&) fermant la balise correspondant au type passé en paramètre. Mais cette solution double le nombre de méthodes de mes visitors (qui sont déjà gattés en nombre de méthodes dans un projet d'ampleur réèlle) et si, par étourderie au a cause d'un throw mal calculer, la méthode accueil() oublie d'appeller cassesToi(), je me retrouverais avec un XML n'ayant plus aucune sémantique.

    J'ai donc pensé à utiliser la durée de vie des objets sur la pile d'exécution pour m'assurer de la fermeture des balises que j'ouvre. Cette idée à donner naissance à une modification de mon code de base :

    class Monde {
    public:
       /*... Cf. premier extrait de code ...*/
       
       void save(std::ostream& cout) const; // Remplace accueil()
       
    private:
       /*... Cf. premier extrait de code ...*/
    };
    
    void Monde::save(ostream& os) const {
       VisitorXML squatter(*this, os);
       
       for (int i = 0; i < NB_VIVANTS; ++i) {
          _peuplades[i]->save(os);
       }
    }
    


    class Plante : public EtreVivant {
    public:
       /*... Cf. premier extrait de code ...*/
    
       virtual void save(std::ostream& os); // Remplace accueil()
       
    private:
       /*... Cf. premier extrait de code ...*/
    };
    
    void Plante::save(std::ostream& os) {
       VisitorXML squatter(*this, os);
    }
    


    class VisitorXML {
    public:
       
       VisitorXML(const Monde& monde, std::ostream& os);
       VisitorXML(const Plante& plante, std::ostream& os);
       
       ~VisitorXML();
       
       static void reset();
       
    private:
       void _printLine(const std::string& ligne) const;
       void _genereIndentation() const;
       
       std::string _baliseFermante;
       std::ostream& _os;
       
       static int _niveauIndentation;
    };
    
    int VisitorXML::_niveauIndentation = 0;
    
    VisitorXML::VisitorXML(const Monde& monde, ostream& os)
     : _baliseFermante("</monde>"), _os(os)
    {
       _printLine("<monde>");
       ++_niveauIndentation;
    }
    
    VisitorXML::VisitorXML(const Plante& plante, ostream& os)
     : _baliseFermante("</plante>"), _os(os)
    {
       _printLine("<plante>");
       ++_niveauIndentation;
       
       ostringstream oss;
       oss << "<age>" << plante.getAge() << "</age>";
       _printLine(oss.str());
       oss.str("");
       oss << "<sante>" << plante.getSante() << "</sante>";
       _printLine(oss.str());
    }
    
    VisitorXML::~VisitorXML() {
       --VisitorXML::_niveauIndentation;
       _printLine(_baliseFermante);
    }
    
    void VisitorXML::reset() {
       _niveauIndentation = 0;
    }
    


    Contrairement au premier, ce code permet d'obtenir le résultat attendu, toutes les balises sont fermées. Par contre deux choses me chagrinent.

    La première devrait être gérable si je prennait un peu le temps de réfléchir, c'est le int static pour gérer l'indentation. Peut-être qu'en ne prenant pas un ostream comme base pour les sauvegardes mais un objet de journalisation sachant gérer l'indentation... Enfin si quelqu'un à une idée plus simple je suis preneur :)

    Et la deuxième chose qui me chagrine est que le visitable (ici Monde et Plante) est obligé de connaitre la classe qui sert de visitor et pas seulement l'interface "Visitor" comme c'est le cas dans le pattern d'origine. J'y perd beaucoups en souplesse le jours ou je veux pouvoir choisir dynamiquement entre une sauvegarde XML ou une autre sérialisation.

    Bref si quelqu'un a une idée, une opinion ou une expérience sur le sujet, je suis preneur. Et par le sujet, j'entend autant la génération d'un XML reflettant la partie métier d'un programme que l'utilisation du Visitor pattern, ses attouts et ses limites ;)
    • Partager sur Facebook
    • Partager sur Twitter
      10 juin 2008 à 1:25:05

      Là, c'est un peu tard pour des réponses ciblées et intelligentes.

      À la place, j'ai un peu de littérature pour toi:
      - diverses formes du pattern visitor, adapté aux arborescences, sont discutées sur LE wiki des DPs (http://c2.com/cgi/wiki?VisitorPattern)
      - Dans l'ASL d'adobe (sorte de boost orienté IHM, et avec plein d'algos à la STL), il y a adobe.forrest auquel ton problème m'a fait penser. (http://stlab.adobe.com)

      (Je remettrai le cerveau en marche demain matin)
      • Partager sur Facebook
      • Partager sur Twitter
      C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
        10 juin 2008 à 21:25:15

        Merci pour le wiki sur les design pattern, c'est une belle invention ça =)

        Pour ce qui est du adobe::forest j'avouerais que j'ai un peu de mal a saisir comment est foutu la structure de données, si un schéma peut se trouver ça me ferais le plus grand bien (saleté de google qui pour une requete "adobe forest" me trouve des pdf sur la forêt :'( ), sinon je conuerais a éplucher la doc jusqu'a ce que je comprenne ou que je meurt, tant pis :)
        • Partager sur Facebook
        • Partager sur Twitter
          10 juin 2008 à 22:07:41

          L'intégralité de la doc sur adobe forrest, tu la trouveras sur le site dont j'ai donné l'adresse (entre autres: http://stlab.adobe.com/wiki/index.php/Edge_Interface_For_Forest_Iterators).

          Eventuellement, Sean Parent peut en avoir parlé ailleurs comme p.ex. la mailing list de boost (archives consultables via gmane)
          • Partager sur Facebook
          • Partager sur Twitter
          C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
            11 juin 2008 à 0:28:03

            <intru>ça sert à quoi les design pattern?
            </intru>
            • Partager sur Facebook
            • Partager sur Twitter
            :)
              11 juin 2008 à 0:43:30

              Un DP (soyons intimes (avec eux!)) n'est rien d'autre qu'une recette de cuisine qui revient régulièrement, et à laquelle on a donné un nom (c'est très important les noms (va falloir que je me procure les tomes 4 à 6 de terremer!))

              Tout part de la constatation que souvent on a toujours les mêmes problèmes récurrents qui se posent à nous, et qu'à force on a pu converger vers une façon reconnue de les solutionner.

              Un DP est en gros un canevas de solution à adapter au problème précis que l'on a à résoudre.

              Souvent, c'est après avoir rencontré un problème que l'on est à même de regarder le DP (associé au pb) et de se dire "ah ben oui, c'est évident que c'est ainsi qu'il faut procéder". Ce sont des choses que l'on assimile mieux avec l'expérience des "difficultés".

              Bref. Si vous vous orientez dans développement professionnel, ce sont des choses que vous verrez, quel que soit le langage de développement.


              EDIT: (fichu contrôle anti-flood!)
              Sinon, la présentation de l'ACCU (lien probablement temporaire) contient une petite démo de forrest (pp30+)
              (NB: le lien est légal, mais leur wiki n'est pas copain avec mon firefox d'où que je donne un lien direct, mais tout pourri, à la place)
              • Partager sur Facebook
              • Partager sur Twitter
              C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                11 juin 2008 à 22:26:05

                Bon visiblement la méthode pronnée par Adobe c'est de metre des iterator partout, ce qui permet de créer des fonctions draw sur ce modèle :

                // Draw pour une branche
                void draw(const monConteneur& branche) {
                   cout << "<monconteneur>" << endl;
                
                   monConteneur::const_iterator it;
                   for (it = branche.begin(); it != branche.end(); ++it) {
                      draw(*it);
                   }
                
                   cout << "</monconteneur>" << endl;
                }
                
                // Draw pour une feuille
                void draw(const monTypeEvaluable& feuille) {
                   cout << "<monTypeEvaluable>" << monTypeEvaluable << "</monTypeEvaluable>" << endl;
                }
                


                Bien entendu il faut une fonction draw par type que l'on risque de rencontrer. Un avantage certain dans mon cas c'est qu'il n'y a rien de spécifique à faire dans la partie à sauvegarder, adieu la méthode "accueil()", draw() se demrede toute seule avec les iterator.

                Au niveau de la souplesse, si on veux plusieurs formats possible de sortie, seul l'appellant a à les connaitres pour lancer le premier draw() sur la racine de l'arbre à sauver.

                Par contre ça ne règle pas le problème de l'indentation, mais qu'est-ce qui nous empècherais de reprendre une classe comme dans le premier visitor pattern que j'ai exposer ?

                L'aboutissement de cette réflexion donnerais donc une classe telle que :

                class Visitor {
                public:
                   Visitor() {}
                   virtual ~Visitor() {}
                
                   virtual void visite(const Monde&) =0;
                   virtual void visite(const Plante&) =0;
                };
                
                
                class VisitorXML : public Visitor {
                public:
                   VisitorXML(std::ostream& os);
                   
                   void visite(const Monde& monde);
                   void visite(const Plante& plante);
                   
                private:
                   void _genereIndentation();
                   void _printLine(const std::string& ligne);
                   
                   std::ostream& _os;
                   int _niveauIndentation;
                };
                
                VisitorXML::VisitorXML(ostream& os)
                 : _os(os), _niveauIndentation(0)
                {
                }
                
                void VisitorXML::visite(const Monde& monde) {
                   _printLine("<monde>");
                   ++_niveauIndentation;
                   
                   typedef typename Monde::const_iterator iterator_t;
                   for (iterator_t it(monde.begin()), fin(monde.end()); it != fin; ++it) {
                      visite(*it);
                   }
                
                   _printLine("</monde>");
                }
                
                void VisitorXML::visite(const Plante& plante) {
                   _printLine("<plante>");
                   ++_niveauIndentation;
                   
                   ostringstream oss;
                   oss << "<age>" << plante.getAge() << "</age>";
                   _printLine(oss.str());
                   
                   oss.str("");
                   oss << "<sante>" << plante.getSante() << "</sante>";
                   _printLine(oss.str());
                   
                   --_niveauIndentation;
                   _printLine("</plante>");
                }
                


                Une sorte de visitor qui se demerde tout seul :D Le seul truc chiant c'est qu'il faut soigneusement que je code mes iterateurs partout, mais bon une foi fait, ça pourra toujours reservir des iterateurs =)

                En espérant que ça aide quelqu'un d'autre que moi,

                Merci pour la doc ;)


                Edit: J'ai rajouter l'interface qui va bien dans la solution, histoire de faire plus pro :-°
                • Partager sur Facebook
                • Partager sur Twitter
                  12 juin 2008 à 8:59:37

                  Ton iterator renvoi des objets de type EtreVivant, non? Comment tu fais dans ce cas pour que ce soit la bonne fonction visite qui soit appelé?
                  • Partager sur Facebook
                  • Partager sur Twitter
                    12 juin 2008 à 9:27:53

                    En effet, me voilà revenu au point de départ. J'oubliais que l'intérêt d'aller réellement visiter l'objet de l'intérieur est que l'objet, lui, connait son type.
                    Je suis parti pour approfondir plus mon idée... Enfin, pas avant ce soir, en attendant je veux bien un maximum d'idées ;)

                    Note:
                    Je pourrais me démerder a grand coups de dynamic cast, mais c'est hyper laid, surtout si j'ai plusieurs classes dérivées
                    • Partager sur Facebook
                    • Partager sur Twitter

                    Visitor Pattern et XML

                    × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                    × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                    • Editeur
                    • Markdown