IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

 C++ Discussion :

Observer : renvoi du type d'objet


Sujet :

C++

  1. #1
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut Observer : renvoi du type d'objet
    Bonjour à tous,

    Je m'intéresse depuis peu aux design patterns et de tous les bénéfices que cela peut apporter. Je m'intéresse donc, dans un premier temps, au pattern "Observer" qui me semble être une base particulièrement utile dans mes différents projets.

    Pour le moment, tout va bien. J'ai compris le fonctionnement (relativement simple) et suis capable de le mettre en œuvre sur des sujets simples. Cependant, je me demandais comment mettre en place un tel type de pattern dans le cas où on se retrouve avec un grand nombre de valeurs à passer.

    Par exemple, je peux avoir un Transmetteur "Clavier" qui comme son nom l'indique envoi une notification à ses récepteurs en incluant les touches pressées. A côté de ça, un autre transmetteur "Souris" qui notifie ses récepteurs de l'état de ses boutons etc... Jusqu'à lors, ce que j'arrive à faire se limite à un seul évènement.

    Comment puis-je faire pour qu'un récepteur accepte via une fonction unique n'importe quel type de données ? J'ai bien tenté de renvoyer au récepteur un pointeur sur le transmetteur, mais étant donné que mes classes Transmetteur et Recepteur sont virtuelles le compilateur m'envoie gentiment valser dans tous les coins.



    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Sensor: public Transmitter
    {
    private:
    	int _data;
     
    public:
    	Sensor(): _data(0) {}
    	virtual ~Sensor() {}
     
    	virtual void setData(const int &i) { _data = i; update(); }
    	virtual int getData() { return _data; }
     
    };
     
    class Machine: public Receiver
    {
    public:
    	Machine() { }
    	~Machine() { }
     
    	virtual bool notify() { std::cout << "notified val : " << std::endl; return true; }
    	virtual bool notify(vx::SmartPtr<Transmitter> T) { std::cout << "notified val : "<< (*T).getData() << std::endl; return true; }
    };

    retour du compilo :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    In member function 'virtual bool Machine::notify(vx::SmartPtr<Transmitter>)':|
    error: 'class Transmitter' has no member named 'getData'|


    Merci d'avance pour vos retours.

  2. #2
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Bon, J'ai compris. Je dois mettre la fonction "getData()" dans la classe virtuelle..... Mais j'ai peur que ce soit trop permissif du coup... Une idée plus intelligente ?

  3. #3
    Expert éminent sénior
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 279
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 279
    Points : 11 015
    Points
    11 015
    Par défaut
    J'avais pondu cette preuve de concept il y a pas mal de temps. En inversant les relations, on arrive à typer fortement le pattern.

    Du coup c'est plus complexe à comprendre car moins usuel comme façon de de faire.

    https://gist.github.com/LucHermitte/...ab6a8c9ccafbf4

    A voir si les notions de signaux, ou si juste les lambdas ne pourraient pas simplifier les choses. J'avoue ne plus y avoir réfléchi depuis le temps.

  4. #4
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Merci pour ce retour ; je vais voir comment ceci est réalisé... Je pense, en parallèle, que je dois me familiariser un peu plus avec les notions de classes virtuelles / virtuelles pures etc...
    En réalité, le simple aspect "Orienté Objet" d'un langage (du c++ en tout cas) apporte tout un lot de fonctionnalités bien plus poussé que ce que l'on peux s'imaginer aux premiers abords.

    Merci.

  5. #5
    Expert éminent sénior
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 279
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 279
    Points : 11 015
    Points
    11 015
    Par défaut
    > classe virtuelle

    Ca n'existe pas. Une classe de base peut être virtuelle relativement à un héritage précis, mais aussi non virtuelle sur un autre héritage.

    Je pense que tu confonds avec la notion de classe abstraite. Si tu galères avec l'OO, il y a de fortes chances qu'il ne te soit pas présenté correctement -- un grand classique malheureusement.

    Les fonctions virtuelles viennent enrichir la notion de substituabilité en raffinant comment un point de variation doit se comporter. Mes mots sont probablement barbares et je n'ai pas trop le temps de les détailler. Il y a un livre que j'aime bien, pas orienté C++ mais OSEF: Design Patterns, tête la première. Il montre bien comment l'OO est employé pour répondre à des besoins.

  6. #6
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Effectivement, je parlais bien de "classe abstraite" ; merci pour cette précision.

    Je suis d'accord avec toi, j'apprends de manière strictement autodidacte selon toute la diversité que propose internet. Peut-être que le passage via un livre dédié accélérerais considérablement mon apprentissage ? à voir. Je vais regarder du côté de l'ouvrage que tu mentionnes. Comme tu le dis, pour le moment, il s'agit surtout de l'aspect "Orienté Objet" que je dois travailler. Les côtés techniques de l'implémentation dans un langage ou un autre seront abordés par la suite (et/ou en parallèle d'ailleurs).

    Merci pour ces conseils.

  7. #7
    Membre régulier
    Homme Profil pro
    Ingénieur validation
    Inscrit en
    Août 2018
    Messages
    37
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 46
    Localisation : France, Côtes d'Armor (Bretagne)

    Informations professionnelles :
    Activité : Ingénieur validation
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2018
    Messages : 37
    Points : 123
    Points
    123
    Par défaut
    Bonjour

    Transmettre un pointeur de Transmitter à un objet qui hérite de Receiver ne doit pas poser de problème.
    D'ailleurs la première étape du pattern Observer, c'est que le Receiver s'abonne à un objet qui hérite de Transmitter. Et il doit conserver ce pointeur pour se désabonner.

    Maintenant, rien n'empèche un Receiver de s'abonner à plusieurs Transmitter.
    Il est donc utile de fournir le pointeur à nouveau dans fonction notify() pour distinguer quel Transmitter est à l'origine de la notification.

    Au sujet de la méthode getData() que tu as implémenté dans Sensor, elle doit effectivement être déclarée (éventuellement en fonction virtuelle pure) dans sa classe mère Transmitter.
    Pour rappel, le principe de conception primordial des Design Patterns, c'est de ne travailler qu'avec des interfaces et non avec des implémentations.

    Machine ne connaît pas Sensor.
    Machine est un type particulier de Receiver,
    Sensor est un type particulier de Transmitter.
    Et seulement Receiver sait comment dialoguer avec Transmitter.

    Ce qui permet de remplacer Machine par n'importe quel autre Receiver et Sensor par n'importe quel autre Transmitter.

    Pour en revenir à ta première question, qui est de transmettre des données complexes, l'exemple du clavier et de la souris est intéressant.
    D'un côté, on peut se permettre de notifier un observateur à chaque fois qu'on appuie sur une touche du clavier. On transmet le code de la touche, ou un vector de la combinaison de plusieurs touches. On peut aussi distinguer les changements d'état "touche appuyée" et "touche relachée". Cela reste des événements "discrets".

    Pour la souris, par contre, on ne peut pas solliciter les observateurs à chaque micro-mouvement.
    Tu peux donc, du côté du Transmitter, gérer la liste des Receiver abonnés et un état booléen qui informe si chaque Receiver a appelé getData().
    Lors d'un premier mouvement, tu notifies tous les Receiver à jour que la souris a bougé et tu mets les états à false.
    Lors d'un second mouvement, tu ne notifies que les Receiver qui ont appelé getData() et dont l'état a été remis à true. Dans ce cas, getData() doit aussi contenir un pointeur du Receiver.
    Lorsqu'un Receiver appelle getData(), il reçoit la position actuelle de la souris, sans ce soucier si la position gérée par Transmitter a changé une ou N fois depuis la notification.

    Le même problème se pose avec des capteurs continus tels qu'un thermomètre. Là on peut imaginer que le Receiver s'abonne en indiquant des seuils en-deça desquels il ne veut pas être notifié.

  8. #8
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Je te remercie pour ce retour.

    J'ai pu réalisé en partie ce que je souhaite (dans le cas d'un exemple simple encore uniquement). Par contre, ce que je ne comprends pas trop c'est :
    Citation Envoyé par Grool Voir le message
    Pour rappel, le principe de conception primordial des Design Patterns, c'est de ne travailler qu'avec des interfaces et non avec des implémentations.
    Effectivement, le "receiver" peut être une interface (et l'est dans mon cas). Par contre, la classe "transmitter" doit être une classe abstraite et non une interface non ? Car je ne vois pas trop comment gérer les enregistrements sans devoir implémenter une petite partie de code... A moins d'ajouter un niveau hiérarchique mais il me semble que cela complexifierais la chose...

  9. #9
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 629
    Points : 30 692
    Points
    30 692
    Par défaut
    Salut,

    Dans l'idéal:

    1- Les accesseurs devraient être évités dans la mesure du possibles, car ils exposent des détails d'implémentation, dont l'utilisateur de la classe n'a -- a priori -- rien à faire

    Il est largement préférable de considérer les classes (et les types de données, de manière générale) comme des "fournisseurs de services": une série de questions auxquelles ils doivent répondre et d'ordre auxquels ils doivent obéir.

    2- Les mutateurs, c'est encore pire:

    Déjà, si tu n'as pas jugé bon de fournir un accesseur, il n'y a aucune raison de fournir un mutateur.

    L'idéal étant de réfléchir en terme d'ordre que l'on peut donner à l'objet, en lui laissant le soin de calculer lui-même le résultat (en veillant à respecter les règles qui lui sont imposées):

    On préférera créer une fonction move(int differenceX, int différenceY), qui pourra s'assurer que la position finale est valide et cohérente plutôt que d'obliger l'utilisateur à calculer lui-même la position finale de l'objet (au risque d'oublier l'une ou l'autre des restrictions en ce faisant)

    3- Si, dans le pire des cas, un accesseur doit être envisagé (car il correspond à une question que l'utilisateur est en droit de poser à une donnée), il est très rare qu'il doive être virtuel.

    4- Si, vraiment, tu dois faire varier le résultat d'une fonction membre en fonction du type de donnée à partir duquel elle est appelée,

    a- Le retour co-variant peut être envisagé, mais cela impose pas mal de conditions, et c'est assez difficile à mettre en oeuvre
    b1- l'utilisation de boost::variant (std::variant, depuis C++17) ou de "quelque chose" de générique (un std::vector<std::byte>, par exemple) devrait être envisagée
    b2- Cela t'obligera à savoir exactement en quoi convertir la valeur obtenue, et donc, à savoir quel était le type réel de l'objet utilisé, ce qui met tout le système de polymorphisme à terre

  10. #10
    Membre régulier
    Homme Profil pro
    Ingénieur validation
    Inscrit en
    Août 2018
    Messages
    37
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 46
    Localisation : France, Côtes d'Armor (Bretagne)

    Informations professionnelles :
    Activité : Ingénieur validation
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2018
    Messages : 37
    Points : 123
    Points
    123
    Par défaut
    Bonjour

    Pour simplifier, une classe abstraite EST une interface.

    (je laisse Luc et Philippe préciser, si nécessaire, les limites de mon affirmation péremptoire)

    D'ailleurs même dans une interface au sens Java, il est possible depuis les dernières versions d'implémenter certaines méthodes.
    Il faut distinguer la notion d'interface (concept) de la façon technique dont tu la mets en œuvre dans ton code. En C++, on peut même envisager une interface sous forme de classe template plutôt qu'une hiérarchie de classe, mais on sort du sujet...

    Au sujet de l'accesseur getData() de notre Transmitter qui expose trop de détails d'implémentation, je suis assez d'accord. Le souci est donc comment définir cet fonction de façon suffisamment générique pour ne pas violer la loi de Déméter.
    Peut-on par exemple séparer les Transmitter qui ne gèrent que des informations discrètes (et peuvent se passe de l'accesseur) des Transmitter qui gèrent des informations continues ? Puisqu'ils ont un fonctionnement fondamentalement différent. Suffisamment pour justifier une hiérarchie de classes séparée.

  11. #11
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Je te remercie pour ce retour... Donc, selon toi, une classe intermédiaire se justifie. En soit, pourquoi pas.

    En ce qui concerne la question des accesseurs, je ne peut qu'être d'accord avec koala01. D'ailleurs, jusqu'à un passé très proche (avant que je ne commence à découvrir un petit bout de la réelle puissance de la POO), mes classes n'étaient que Setters et Getters. Et pour être franc, je pense que dans 90% des cas, avec une implémentation "OO" à peine améliorée, j'aurais pu me passer de toutes ces pseudo fonctions tout en simplifiant largement mon code.

    Finalement, je pense même que le fond de ce post n'est QUE ça : apprendre à programmer en orienté objet.

    Pour reprendre un exemple du même type que la fonction move(const int &dx, const int &dy); présentée par koala01, je remarque que mon code lié à la partie "physique des matériaux indéformables" (rigid bodies) que j'implémente, je me retrouve avec des setForce(glm::vec3 &F);, addForce(glm::vec3 &F);, de même pour les accélérations, et tout ça, juste pour dire que l'on peut soumettre un solide à différentes forces quelconques et/ou le placer dans un champ de forces (ou champ d'accélération).

    Et justement, pour en revenir au sujet initial de ce post, je pense qu'un pattern de type "Observer" pourrait (je n'y ai pas encore pleinement réfléchis) supprimer ces setters qui ne font qu’alourdir mon code et le rendre imbuvable. Il faudrait que ma classe "Solid" ici, puisse s'abonner à différents Transmetteurs qui chacun viendront le configurer. Par exemple, on peut imaginer qu'un objet soit à la fois soumis à un champ de Force ainsi qu'à un autre champ d'accélération. On voit de-suite ici que, finalement, l'utilisateur n'aurait qu'à configurer les environnements, et les objets héritant de la classe "Solid" seront alors automatiquement affectés par ces environnements...

    Dites moi que je me fait bien comprendre et que je reste dans le sujet svp, mais selon moi cela me semble être une bonne idée... Il me reste à tester tout ça pour voir comment cela s'imbrique concrètement. Mais là encore, comment faire pour qu'une même fonction de type update(Transmitter *T), modifie soit l'accélération, soit la force selon le type du champ traversé ?

  12. #12
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 629
    Points : 30 692
    Points
    30 692
    Par défaut
    Citation Envoyé par BioKore Voir le message
    Et justement, pour en revenir au sujet initial de ce post, je pense qu'un pattern de type "Observer" pourrait (je n'y ai pas encore pleinement réfléchis) supprimer ces setters qui ne font qu’alourdir mon code et le rendre imbuvable. Il faudrait que ma classe "Solid" ici, puisse s'abonner à différents Transmetteurs qui chacun viendront le configurer. Par exemple, on peut imaginer qu'un objet soit à la fois soumis à un champ de Force ainsi qu'à un autre champ d'accélération. On voit de-suite ici que, finalement, l'utilisateur n'aurait qu'à configurer les environnements, et les objets héritant de la classe "Solid" seront alors automatiquement affectés par ces environnements...
    D'autant plus que le patron de conception observateur ne fait qu'une chose : permettre à un objet qui est capable d'utiliser une information d'être tenu au courant d'une information qu'un autre objet est capable de lui donner, le tout, en permettant que l'objet qui est capable de fournir l'information n'aie pas à se soucier qu'un objet soit en mesure de l'utiliser.

    Or, pour obtenir le même résultat, un système de signaux et de slot apporte énormément de souplesse au bastringue, car:
    1. l'objet qui est capable de fournir l'information n'a plus à se soucier que d'émettre le signal avec cette information
    2. l'objet capable de traiter une information peut s'y abonner sans difficulté
    3. il est même possible (surtout depuis l'ajout des expressions lambda au langage) d'adapter le slot proposé par l'objet capable de traiter l'information au signal émis

    Et, pour la petite histoire, il se fait que j'ai réussi à implémenter un tel système en moins de 200 lignes de codes (si on en retire les cartouches destinés à la génération de documentation) dans un fichier d'en-tête unique en C++11 (C++14 idéalement conseillé )
    La solution

    Le code, avec les exemples et les tests unitaires sont disponibles ==>sur mon dépot git public<== (et librement réutilisables, selon les termes de la licence MIT )


    Mais là encore, comment faire pour qu'une même fonction de type update(Transmitter *T), modifie soit l'accélération, soit la force selon le type du champ traversé ?
    Simplement, "dé généralisant" la notion de Transmitter, du moins, si tu ne veux pas **trop** te casser la tête...

    En partant de l'approche "purement orientée objet" d'une classe (ou d'un interface) qui permet de transmettre "tout et n'importe quoi" -- que tu ferais bien, au passage, de transmettre sous la forme d'une référence constante au lieu de la transmettre sous forme d'un pointeur -- tu tombes dans le gros piège de la généralisation à outrance, classique en orienté objets.

    Car, même si on peut -- effectivement -- se dire que "transmettre une distance" et "transmettre une durée", ca reste toujours le comportement de "transmettre", il faut se rendre compte que les notions de distance et de durée n'ont ... absolument aucun point commun

    A moins, bien sur, que tu parte du principe que les notions de distance et de durée représentent tous les deux ... une spécialisation de la notion de donnée et que tu décides de les faire hériter toutes les deux d'une classe Data.

    Sauf que, à ce moment là, tu vas être confronté à plusieurs problèmes:

    1- Le respect du LSP (principe de substitution de Liskov)

    Pour qu'un héritage puisse être considéré comme valide, il faut:
    1. que toutes les propriétés valides de la classe de base soient valident pour les classes dérivées
    2. que les préconditions imposées dans la classe de base ne soient pas renforcées dans les classes dérivées
    3. que les post-conditions imposée dans la classe de base ne soient pas allégées dans les classes dérivées
    4. que les invariants de la classe de base soient respectés dans la classe dérivée

    Tu auras sans doute déjà pas mal de difficulté à trouver un point commun entre la notion de vitesse et la notion de distance

    Mais, en plus, tu dois penser à toutes les autres notions que l'on pourrait vouloir qualifier de "données", telles que:
    • la notion de surface ou de volume, qui n'a rien à voir avec la notion de distance
    • la notion de date ou d'heure, qui n'a rien à voir avec la notion de durée
    • la notion de "point de vie" qui n'a rien à voir avec les notions précédentes
    • la notion de "points de dégâts" qui n'a rien à voir avec les autres,
    • j'en passe et sans doute de meilleures ...

    Si tu dois trouver un point commun à toutes ces notions (et à toutes celles que tu pourrais envisager), cela devient pour ainsi dire impossible

    2- L'héritage implique une sémantique d'entité

    On peut, pour simplifier, ranger les données que l'on manipule en deux grandes catégories: les données qui ont sémantique de valeur et celles qui ont sémantique d'entité.

    Les données qui ont sémantique de valeur sont celles dont on se fout pas mal qu'il puisse exister plusieurs instances en mémoire à un instant T de l'exécution, comme les notions de durée, de distance, de couleur, et autres :

    Après tout, ce n'est pas parce l'on a peint la cuisine en "bleu océan" qu'on ne peut pas décider de repeindre la salle de bains dans la même couleur

    C'est données sont, typiquement :
    • comparables entre elles (pour autant qu'elles soient toutes les deux de type identique, bien sur), au moins par égalité (rien ne nous empêche de comparer la couleur du salon avec celle de la cuisine)
    • assignables (on peut décider à tout moment de repeindre le salon dans une autre couleur)
    • copiables (rien ne nous empêche d'utiliser exactement la même couleur pour le salon et la chambre)

    Et surtout: totalement inadaptées à l'héritage public

    Les données qui ont sémantique d'entité sont celles pour lesquelles on veut, au contraire, s'assurer qu'il n'y ait moyen d'accéder (à un instant T de l'exécution) qu'à une seule et unique instance au travers d'un identifiant quelconque.

    Tu serais particulièrement embêté si ton salaire finissait sur mon compte en banque ou si l’excès de vitesse que je viens de commettre avec ma voiture (qui est de la même marque, du même modèle et de la même couleur que la tienne) t'était reproché (tu remarqueras au passage que je me suis arrangé pour que tout le bénéfice des erreurs commises me soit rendu ).

    Ces données sont, typiquement:
    • absolument pas comparables (dans leur ensemble): seuls les états qui les représentent l'étant (potentiellement)
    • non assignable : je ne peux pas récupérer ton compte en banque à mon propre bénéfice
    • non copiables : à un instant T de l'exécution, il ne peut y avoir qu'une seule instance du compe en banque qui est identifié par le numéro du tien

    Et, surtout: particulièrement adaptées à l'héritage public:
    une voiture (une moto, un camion, un char à voile) peut facilement être substitué à un véhicule (comprend: être transmis -- sous la forme d'un véhicule -- à une fonction qui s'attend à recevoir ... un véhicule comme paramètre).

    Tout cela pour dire que, même si on arrivait à respecter Liskov, le simple fait d'avoir recours à un héritage nous ferait perdre tout le bénéfice de la sémantique de valeur, qui serait pourtant indispensable pour la représentation de notions comme la distance ou la durée

    3- l'over-ingenieering

    Mettons même que l'on arrive à respecter Liskov, et que l'on considère les restrictions imposées par la sémantique d'entité (vu que nous serions dans une approche basée sur l'héritage public) comme "raisonnable".

    Il nous reste toujours un dernier problème à résoudre : Celui d'être en mesure de ... profiter des données qui sont fournies par notre transmitter. Et là, on est parti pour la galère

    Car, si tu défini dans ta classe une fonciton update(Transmitter const &), il faut -- d'abord et avant tout -- arriver à s'assurer que le transmetteur obtenu présente un quelconque intérêt pour l'élément à partir duquel on invoque cette fonction.

    Or, il n'y a qu'un seul moyen pour gérer de manière "efficace et maintenable" le cas où "tu as été assez bête" pour "oublier" le type réel d'une donnée au profit de son type de base: le double dispatch.

    Et, qui pense "double dispatch" en mode "patron de conception" pense -- inévitablement -- au patron "visiteur", dans lequel le transmetteur serait l'élément visité (avec sa fonction [/c]bool accept(visitor &) const[/c] et l'élément à mettre à jour le visiteur lui-même (avec ses différentes surcharges de la fonction bool visit(UnType const &))

    Oui, mais, cela implique que tu aurais un type de transmetteur (visitable) par type de donnée à transmettre. Autrement dit, si tu as dix type de données susceptibles d'être transmises, tu te retrouveras avec ... dix surcharge de cette fameuse fonction bool visit(UnType const &). Et tout cela, alors qu'il n'y aura qu'une seule de ces surcharges qui risquent de t'intéresser

    Pire encore: une fois que tu auras récupéré le type réel pour le seul transmetteur qui puisse t'intéresser, il faut te dire que la fonction qui permet de récupérer la donnée émise va renvoyer ... une référence (ou un pointeur) sur une donnée de type Data, qui n'est une fois encore que ... l'abstraction la plus générale de n'importe quelle donnée que tu pourrais vouloir manipuler.

    Or, une même cause ayant toujours les mêmes effets, si tu veux pouvoir gérer les différentes formes réelles prise par ton abstraction Data de manière "efficace et maintenable", tu n'auras qu'une seule solution : le double dispatch

    Et, du coup, rebelote : nous partons de nouveau sur un patron de conception visiteur, avec l'abstraction Data dans le role de l'objet visité, et notre récepteur dans le rôle du visiteur. Et nous serons, une fois de plus, confronté au même problème : pour que le visiteur fonctionne, il devra disposer d'une surcharge de la fonction visit(UnType const &) par type réel dérivé de Data alors qu'il ne sera intéressé que par ... un type bien particulier.

    Alors, si en plus, ton récepteur est intéressé par le fait de récupérer plusieurs types de données bien particulier, je te laisse imaginé la foire que cela pourra devenir

    La solution
    La solution à ce problème peut être obtenue "tout simplement" en abandonnant l'approche purement orientée objets.

    Après tout, C++ arrive à faire cohabiter le paradigme purement procédural et le paradigme générique avec l'orienté objets. Autant en profiter

    j'ai déjà parlé du système de signaux et de slots, qui représente sans doute la "meilleure solution".

    Mais, le simple fait de se dire que "on ne sait pas quel type de donnée on veut transmettre, mais on sait qu'elle sera transmise" peut déjà nous mener vers une alternative
    En effet, un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template <typename Data, typename Modifier>
    class Transmiter{
    public:
         /* ...*/
        T get() const{
            return data;
        }
    private:
        template<typename ... Args>
        void compute(Data sArgs ... args){
            data = Modifier::evaluate(data, args ... );
        }
        Data data;
    };
    peut nous sortir d'affaire de manière assez incroyable.

    Car il suffit de créer une structure qui expose une fonction evaluate, qui prenne comme premier paramètre le type de la donnée qui nous intéresse et comme paramètres suivants les données qui permettront d'évaluer la modification, par exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    struct PositionMover{
         Position evaluate(Position const & origin, int diffX, int diffY/*, Context const & context*/){
             /* il faut s'assurer que le déplacement est possible dans le contexte indiqué,
              * je ne présente pas la manière de s'y prendre
              */
         return Position{origin.x + diffX, origin.y + diffY};
         }
    }
    (en fait, on délègue à une classe qui ne fait que cela la responsabilité de calculer une nouvelle position à partir d'une position de départ et d'un contexte donnés)

    et, le cas échéant (pour la facilité d'utilisation) de définir un alias de type proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    using DeplacementTransmiter = Transmiter<Position, PositionMover>;
    pour que l'on puisse se contenter de fournir, à l'élément qui sera intéressé par le fait traiter l'information transmise, une fonction proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class MaClasse{
    public:
        /* ... */
        void recieve(DeplacementTransmiter const & tr){
        pos = tr.get();
        }
    private:
        Position pos;
    };
    Et, si -- sait on jamais -- MaClasse devait être intéressée par plusieurs informations susceptibles d'être transmises, hé bien, il "suffirait" de rajouter une surcharge de recieve adaptée au transmetteur adéquat

  13. #13
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Bon, après une bonne nuit de sommeil, un café et avoir pesté d'un bug me demandant de retaper l'intégralité de ma réponse, voici ce que j'en retiens :

    Tu souhaites avoir mon salaire et en plus que je paye tes amandes
    Une autre solution que le pattern Observer semblerait plus appropriée pour répondre au sujet présenté lors de mon précédent post.

    Je vais me renseigner sur le fonctionnement de signaux / slots présenté ; s'il s'agit d'un fonctionnement similaire à ce que propose QT, cela peut effectivement apporter un plus.

    Dans tous les cas, il me semble que des fonctions membres d'affectation pour la Force et pour l'accélération soient nécessaires...

  14. #14
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 629
    Points : 30 692
    Points
    30 692
    Par défaut
    Citation Envoyé par BioKore Voir le message
    Bon, après une bonne nuit de sommeil, un café et avoir pesté d'un bug me demandant de retaper l'intégralité de ma réponse, voici ce que j'en retiens :

    Tu souhaites avoir mon salaire et en plus que je paye tes amandes
    Quitte à te démontrer quelques problèmes qui pourraient survenir si tu laissait les classes ayant sémantique d'entité être copiées ou assignées, autant que j'en tire un bénéfice, non

    Une autre solution que le pattern Observer semblerait plus appropriée pour répondre au sujet présenté lors de mon précédent post.
    Peut-être ... A la condition que tu ne généralise pas les notions d'observateur et d'observés à outrance.

    Si, de manière générale, tu en arrive à créer un (ou plusieurs) "God Object(s)" pour mettre ce patron en oeuvre, tu foncera très rapidement dans le mur, parce que la maintenance deviendra à termes tout à fait impossible

    Je vais me renseigner sur le fonctionnement de signaux / slots présenté ; s'il s'agit d'un fonctionnement similaire à ce que propose QT, cela peut effectivement apporter un plus.
    (en fait, c'est Qt, car QT est -- typiquement -- l'abréviation de Quick Time, pour les gens qui utilisaient déjà un ordinateur à la fin des années 1900 )

    Le fonctionnement des systèmes de signaux et de slot sera -- forcément -- (à quelques détails près) similaire à ce que propose Qt, vu que les termes "signal" et "slot" correspondent en définitive à des "concepts", des notions générales, au même titre que les notions de "listes chainées" ou d'"arbres binaires".

    L'implémentation et les "détails de mise en oeuvre" peuvent varier quelque peu d'une implémentation à une autre (j'ai, par exemple, pris la décision de faire porter tout le poids de la décision de déconnection d'un slot sur la notion de "connexion"), mais le mode de fonctionnement reste toujours plus ou moins le même:
    1. on définit un signal
    2. on définit un slot
    3. on connecte le signal au slot
    4. on emet le signal, et tous les les slots connectés sont exécutés



    Dans tous les cas, il me semble que des fonctions membres d'affectation pour la Force et pour l'accélération soient nécessaires...
    Retiens bien que
    100 % des bugs trouvent leur origine dans l'interface entre la chaise et le clavier
    A vrai dire, j'ai oublié une caractéristique classique pour les données ayant sémantique de valeur : elles sont (ou du moins, dans l'indéal, devraient être) -- typiquement -- constantes.

    Après tout, si je fais augmenter la force de deux newton ou l'accélération de 4 m/s², j'obtiens ... une force (ou une accélération) tout à fait différente.

    Et je pourrais suivre un raisonnement tout à fait similaire pour des notions comme la distance, la date, la durée, la vitesse, les points, et ... tout ce qui est, de manière générale, comparable par égalité.

    De plus, le principe des classes (et, surtout, de leur constructeur) est de permettre à l'utilisateur (au développeur qui utilise la classe) d'obtenir directement un objet valide et cohérent, directement utilisable, ainsi que d'en assurer (au travers du constructeur et des services qu'elle rend) les pré/post conditions et les invariants.

    Si l'utilisateur de ta classe doit penser appliquer "toute une procédure" qui demande plus d'une action pour pouvoir disposer de l'objet qu'il a créé, tu dois partir du principe que la loi de Finagle fait que, tôt ou tard, l'utilisateur finira par en oublier l'une ou l'autre.

    De même, si tu décides de laisser le soin à l'utilisateur de ta classe de calculer la nouvelle valeur pour un ou l'autre élément de ta classe, tu dois partir du principe que, toujours à cause de la loi de Finagle , il finira -- tôt ou tard -- il finira par oublier d'appliquer une des règles qui permettent de garantir la cohérence de l'objet manipulé, et que la valeur qu'il aura calculée ne respectera plus l'un ou l'autre des invariants, l'une ou l'autre des préconditions imposées par la notion représentée par ta classe.

    Par exemple, tes notions de force et d'accélération ne peuvent pas être négative: au pire, c'est le sens dans lequel elles sont appliquées qui s'inverse (appliquée vers "l'avant", on obtient une accélération positive, une force plus importante, appliquée "vers l'arrière", on obtient une décélération, une "force opposée").

    Si tu laisses l'utilisateur décider de la nouvelle valeur adaptée à la force ou à l'accélération, tu dois te dire qu'il va systématiquement travailler sous une forme proche de
    1. récupérer la valeur acutelle
    2. effectuer le calcul qui l'intéresse, pour déterminer la nouvelle valeur
    3. transmettre la valeur calculé (qui a de fortes chances de ne pas respcter les invariants imposés par la classe) au mutateur de l'objet

    Ne perd pas ton temps à te demander SI il risque de faire une erreur en calculant la nouvelle valeur, car la loi de Finagle te dit qu'il le fera forcément à un moment ou à un autre.

    Poses toi directement plutôt la question de savoir QUAND il fera cette erreur! Le problème, c'est que la loi de l'emmerdement maximum fait que la réponse sera toujours la même: au pire moment qui soit

    La notion d'encapsulation est justement basée sur l'idée d'éviter ce genre de problème, en faisant en sorte que, comme le dit si bien Scott Meyers
    une interface soit facile à utiliser correctement et difficile à utiliser de manière incorrecte
    Si l'on considère effectivement d'imposer la constance à toutes les classes ayant sémantique de valeur (bien qu'il y ait pas mal d'avantages à le faire ) comme "un peu excessif", il faut faire en sorte ne puisse pas faire de conneries en calculant lui-même valeur résultant d'une modification.

    Pour la notion de vitesse, cela passera sans doute par la fourniture de fonctions comme void accelerate(int difference) et void decelerate(int difference). Pour la notion d'accélération, ce serait sans doute des fonctions void increse(int difference) et void decrese(int difference).

    Mais toutes ces fonctions auront un seul et unique but : s'assurer que le fait d'appliquer la différence ne mènera pas l'objet à présenter une valeur incohérente (que l'on n'excède pas la vitesse maximum possible, qu'on ne se retrouve pas avec une vitesse négative, qui aurait du correspondre à une vitesse positive dans "le sens inverse", et des raisonnements similaires pour la notion d'accélération).

    Je sais que cela peut être perçu comme de l'over ingenieering, mais c'est le seul moyen d'éviter "un certain nombre de bugs"

  15. #15
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Merci pour ce retour, une fois encore, bien fourni.

    Je vais devoir prendre le temps d'assimiler tout ça, mais en parallèle, j'ai compris qu'un objet se doit de ne réaliser qu'une fonction, en plus de limiter au maximum l'intervention du développeur dans l'utilisation de cette dernière. La "modification" des attributs doit donc être réalisée via une classe dédiée.

    Pour reprendre l'exemple de la position de mon objet, il faudrait que cette dernière soit modifiée via une classe (par exemple "motionControler") dans laquelle nous pourrions retrouver une méthode de type update(Object &O). De cette manière, je pourrais créer des classes "accelerationField" et/ou "forceField" (simplement pour l'exemple), instanciées dans "Object". Chacune de ces classes possède une méthode update(Object &O) venant, à leur manière, modifier les attributs de "Object". Cependant, ces attributs devront être publics...

    Ça me semble bien, mais par contre, je pense que j'aborde un autre sujet là. Il me reste à répondre à la question initiale : comment rendre mon objet sensible à son environnement sans que le développeur ait besoin d'intervenir explicitement sur cet objet ? Mon objectif est justement, comme tu le présentes si bien, de pouvoir cadrer les interventions possibles de l'utilisateur.

    Ainsi, d'autres fonctions, liées elles aussi à des classes dédiées permettraient de réaliser des actions du type accelerate(Object &O, int &diff)
    Brrr, j'ai l'impression de me noyer dans un verre d'eau là. Je ferais bien de me mettre à l'UML pour m'organiser un peu mieux.

  16. #16
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 629
    Points : 30 692
    Points
    30 692
    Par défaut
    En fait, j'ai de plus en plus l'impression que tu te fourvoies à vouloir utiliser l'orienté objets là ou une approche ECS serait bien plus adaptée

    Parce que, surtout si tu en arrive à généraliser à outrance ("tous les éléments manipulés sont des objets", ca semble cool dit comme cela... Mais ... Quel sera le point commun entre un mur -- qui est un obstacle -- un brin d'herbe -- qui n'est qu'un élément de décors que l'on peut traveser -- et un personnage ) cela va rapidement devenir ingérable.

    Imagine le bordel que cela peut devenir rien qu'à devoir gérer plusieurs races (les nains, les elfes, les humains et les orcs) en tant que personnages non joueurs et en tant que personnage joueur.

    A priori, certaines races ont plus d'affinités (ou plus d'antipathies) avec certaines autres races, mais un joueur (qui fait forcément partie d'une des races considérées) peut tout à fait décider de rendre "certains services" à certains représentant d'une race qu'il exècre en temps normal, ce qui tend à rendre les représentants de cette race "moins prompts à l'attaque" (As tu connu Gothic III Les orcs étaient sensés être nos ennemis, mais, pour avancer dans le jeu, il fallait rendre services à certains orcs, et, du coups, les autres avaient d'avantage tendance à nous accepter), et les alliés du lundi devenaient les ennemis du mardi, avant de devenir neutre le mercredi .

    A l'inverse, il se peut qu'un joueur décide d'attaquer le représentant d'une race qui est "classiquement" considérée comme aliée (un humain, un elfe ou un nain), avec, pour conséquence, le fait que cette race risque de nous apprécier beaucoup moins, voire, même, de décider de nous attaquer "à vue".

    Et puis, pour chaque race, il y a différentes catégories:
    • des guerriers
    • des éclaireurs
    • (des chasseurs )
    • des chamans / prêtres / magiciens
    • des "hauts gradés"
    • j'en passe et de meilleures

    Et chacune de ces catégories présentent des caractéristiques particulières (qui, en plus, peuvent varier en fonction de la race )

    Mais le joueur qui choisi d'être un magicien peut très bien décider d'améliorer ses compétences au corps à corps, ou celui qui décide d'être un guerrier peut décider d'améliorer ses compétences magiques, au point de donner un "magicien de combat"

    Et je te laisse imaginer le bordel que cela peut devenir si, par la suite, le joueur décide d'améliorer ses capacité de voleur!!!
    Brrr, j'ai l'impression de me noyer dans un verre d'eau là. Je ferais bien de me mettre à l'UML pour m'organiser un peu mieux.
    Malheureusement, l'UML ne te sera d'aucune aide sur ce problème

    Revenons en à notre problème...

    En considérant que "tous les éléments métiers" sont des entités (comprends : des éléments qu'il est possible d'identifier de manière strictement unique et non ambigue), nous pouvons créer un ensemble de "composants" dont la seule présence (ou la seule absence) -- associée, le cas échéant à la présence (ou à l'absence) d'autres composants -- permet à certains "services" d'être accessibles (ou non)
    pour les entités auxquelles se rapportent les composants en question.

    Hé bien, ca, figure toi que c'est exactement ce que permet un système ECS (pour Entities Components Services).

    Et, du coup, on a -- tout simplement -- un identifiant (une "entité), qui peut être réduit "à sa plus simple expression" (une simple valeur numérique entière fera très bien l'affaire), à laquelle on associe "un certain nombre" de caractéristiques (comme la position, la vitesse de déplacement, le fait qu'il s'agisse d'un obstacle), mais, uniquement si les caractéristiques en question sont "cohérentes" par rapport à ce qu'est sensé représenter l'entité:

    Il ne sert à rien, par exemple, d'associer la notion de vitesse à un mur, vu qu'un mur, par définition, cela ne se déplace pas

    Et, pourtant, si le mur en question cache l'entrée d'un passage secret, nous pouvons y associer une caractéristique "ouvrable" qui, si elle passe à true (on a demandé l'ouverture du passage) ajoutera la notion de vitesse de déplacement "juste le temps nécessaire" à l'ouverture du passage.

    Par la suite, ces deux caractéristique (ouvrable et vitesse de déplacement) seront "logiquement" remplacée par une caractéristique unique "ouvert" (et, pourquoi pas, un délais avant la fermeture )

    Mais, quoi qu'il en soit, ce sera parce que l'on sera en mesure constater la présence d'une (ou de plusieurs) caractéristique(s) associée(s) à une entité particulière et dont on pourra -- le cas échéant -- évaluer la "valeur actuelle" que l'on pourra décider d'y appliquer (ou non) certains services, dont le résultat sera -- pour certains de ces services en tout cas -- de modifier la valeur de certaines des caractéristiques prises en compte.

    Cette approche s'avère beaucoup plus souple que l'approche orientée objets, même si sa mise en oeuvre initiale n'est pas forcément des plus faciles...

    Mais j'ai vraiment l'impression, même en ne connaissant de ton projet que ce que tu nous en as dit jusqu'à présent (c'est-à-dire ... quasiment rien ) qu'elle t'éviterait bien des soucis

  17. #17
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Bonjour !

    Alors je trouve intéressant que tu abordes ce sujet car cela fait quelques jours (depuis ton premier post en fait), que je me renseigne au sujet des ECS.
    Aussi je pense avoir compris, dans les grandes lignes, le fonctionnement général d'une telle implémentation ; ce que tu expliques très bien dans ton dernier retour d'ailleurs.

    J'ai donc trouvé x implémentations différentes sur le sujet, des plus "simples" à des plus complexes. J'ai alors commencé à implémenter un système similaire, et il est vrai que, sortir du carcan POO demande une certaine concentration. Hier soir, j'ai donc finalisé le code permettant de créer/gérer des ID uniques. Ce code fonctionne plutôt bien et permet une réutilisation, si besoin, des entités préalablement supprimées, en s'assurant bien entendu d'éviter tout doublon possible.
    A voir maintenant si cette implémentation saura s'adapter correctement à la gestion des composants.

    La partie qui, je pense, va me demander un peu plus de patience, c'est justement cette gestion des composants, et tout particulièrement du type de ces composants. Et là, nous en revenons, finalement, au titre du post : la récupération du type de ces objets. Car même si chaque composant "hérite" d'une classe Component, les systèmes auront besoin de ne traiter que les composants qui les intéressent. Si je raisonne comme cela, il faut que chaque composant soit stocké dans un container dédié au type des dits composants.
    Par exemple, tous les composants de types Position doivent être stockés dans un container dédié. Idem pour les composants de type Color etc... Et cela doit pouvoir être réalisé trivialement, sans avoir besoin de retoucher tout le code lorsque l'on souhaite ajouter un nouveau type de composant...

    J'ai trouvé plusieurs implémentations répondant au moins en partie à cette question:
    1. Dynamic_cast<T> obj; mais j'ai cru comprendre que ça "tue" les performances
    2. Des tableaux dédiés de type std::vector<T> obj; pour chaque type de composants, mais il faut donner un peu plus de flexibilité à cela
    3. std::map<???> obj; Pourquoi pas. Je n'ai jamais utilisé de map, à voir ; mais j'aimerais d'abord lister tous les principaux choix dont je peux disposer
    4. un autre système que j'ai vu, basé entièrement sur un fonctionnement avec des Templates. N'ayant pas encore étudié ce fonctionnement, je ne saurais trop quoi dire de cette implémentation ; mais je compte creuser le sujet. Par contre, il me semble que je devrais revoir mon implémentation de gestion des ID. il me semble, dans ce cas, que les ID sont aussi liés aux types des composants.
    5. Enfin, et là il ne s'agit que d'une potentielle implémentation, trouver un système utilisant des std::typeid(T).... Mais Honnêtement, je pense qu'il est préférable et tout à fait envisageable de remplacer cette fonction par un template<T> quelque-part.


    Pour les systèmes, je me pencherais sur leur fonctionnement une fois que je saurais enregistrer des couples Entity / Component.


    Que pensez-vous de cette approche ?

  18. #18
    Membre expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    746
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 746
    Points : 3 667
    Points
    3 667
    Par défaut
    Dynamique_cast et typeid (c'est un opérateur, pas une classe) repose tous les 2 sur RTTI. Et RTTI est lent. Personnellement, je le désactive (-fno-rtti) et ne peux donc pas utiliser les 2 mots clefs précédemment cité. std::any et la plupart des systèmes qui ont besoin d'un mécanisme similaire repose sur CTTI (on troque le R de run-time pour un C de compile-time). Il y a une classe toute belle dans Boost.TypeIndex, mais voici le code de clang:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct  _LIBCPP_TEMPLATE_VIS __unique_typeinfo { static constexpr int __id = 0; };
    template <class _Tp> constexpr int __unique_typeinfo<_Tp>::__id;
     
    template <class _Tp>
    inline _LIBCPP_INLINE_VISIBILITY
    constexpr const void* __get_fallback_typeid() {
        return &__unique_typeinfo<decay_t<_Tp>>::__id;
    }
     
    // my_id =  __get_fallback_typeid<T>();
    Ensuite un `std::unordered_map<Id, Data>` permet de faire la correspondance. Généralement, Data sera très proche d'un void*, mais comme on est sûr de son véritable type, il sera casté en std::vector<T>* sans aucun soucis. T qui rappelons-le viens de l'utilisateur.

    Après on peut remplacer std::unordered_map par un std::vector, mais alors chaque composant doit avoir un identifiant qui se suit et probablement initialisé au démarrage.

    ------

    Une solution entièrement à base de template requière que tous les types soit connue à la compilation. C'est envisageable et le code devient très simple: un std::tuple qui contient tous les éléments.

  19. #19
    Membre actif Avatar de BioKore
    Homme Profil pro
    Dresseur d'Alpaga
    Inscrit en
    Septembre 2016
    Messages
    300
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : Canada

    Informations professionnelles :
    Activité : Dresseur d'Alpaga

    Informations forums :
    Inscription : Septembre 2016
    Messages : 300
    Points : 219
    Points
    219
    Par défaut
    Ok, c'est bien ce que je pensais pour typeid. C'est du RTTI.

    Après on peut remplacer std::unordered_map par un std::vector, mais alors chaque composant doit avoir un identifiant qui se suit et probablement initialisé au démarrage.
    J'imagine que la dernière implémentation que j'ai vu est de ce type. Chaque composant possède un identifiant unique selon son type, et le tout est traité par un array de Component* et bitset. Telle que la chose est implémentée, je dirais que l'identifiant peut être géré dynamiquement.

    Cette dernière implémentation me plait, cependant, ce n'est pas strictement un ECS dans le sens où se sont les composants qui possèdent les fonctions nécessaires à leurs initialisations, et utilisations.... Je pense néanmoins que ce type de code fait une bonne base de départ et permet déjà pas mal de choses. Je vais partir là-dessus et voir comment gérer les Systèmes de manière indépendante après.

  20. #20
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 629
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 629
    Points : 30 692
    Points
    30 692
    Par défaut
    Citation Envoyé par BioKore Voir le message
    Bonjour !

    Alors je trouve intéressant que tu abordes ce sujet car cela fait quelques jours (depuis ton premier post en fait), que je me renseigne au sujet des ECS.
    Aussi je pense avoir compris, dans les grandes lignes, le fonctionnement général d'une telle implémentation ; ce que tu expliques très bien dans ton dernier retour d'ailleurs.
    Eh, qu'est que tu crois

    L'évolution de mes réponse avait pour but de te mener graduellement à cette conclusion, tout en te permettant de prendre toi-même la décision de le faire.

    La programmation, ce n'est pas "simplement" de reprendre des "recettes" découvertes par d'autres, surtout en C++! C'est de tirer le meilleur parti de différentes recettes (si elles existent) tout en n'hésitant pas à s'éloigner des sentiers battus si l'on y trouve un intérêt!

    Quand je t'ai dit, dans ma première intervention, qu'un accesseur (et pire, un mutateur) était rarement la bonne solution, et que, dans tous les cas, un accesseur virtuel n'était vraiment pas efficace, tu aurais déjà pu "me faire confiance" sur ce point, et te rendre compte que ton approche était scabreuse.

    Mais, il semblerait que tu manquais peut-être d'un certain recul pour t'en apercevoir à ce niveau là.

    Dans ma deuxième intervention, j'ai tenté de mettre en évidence l'ensemble des problèmes auxquels tu serait confronté, en te présentant au passage quelques alternatives (dont j'ai bien conscience qu'il t'aurait été difficile de trouver par toi-même). Cela aurait encore une fois pu t'inciter à changer ton fusil d'épaule. Mais tu t'es une deuxième fois acharné à persister dans ton optique originale.

    Dans ma troisième intervention, j'ai carrément mis "les points sur les i" en expliquant comment fonctionnaient l'alternative que je te proposais, et en insistant clairement sur le fait que "des données de type différents s'utilisent de manière différentes", ce qui est rarement compatible avec le principe de base de la programmation orientée objets (le LSP ou Liskov Substitution Principle); même si je n'ai pas cité ce principe.

    Dans ma quatrième intervention, j'ai enfin compris que, si je ne t'aidais pas à sortir du carcan de la programmation objet, tu n'y arriverais pas tout seul. Ce que je trouve d'ailleurs dommage de la part d'un ingénieur, fusse-t-il généraliste

    Car il me semble que les ingénieurs devraient, justement, apprendre à ne pas se contenter de suivre des "recettes toutes faites", et à ne pas éviter à chercher des solutions différentes pour les problème que les "solutions classiques" n'arrivent pas à résoudre de manière cohérentes.

    Maintenant, je peux me tromper sur ce point, car, je dispose un "simple" "Bac +4" en informatique, et je n'ai aucune idée de ce que les ingénieurs peuvent apprendre de plus durant les trois ans d'étude supplémentaire

    J'ai donc trouvé x implémentations différentes sur le sujet, des plus "simples" à des plus complexes. J'ai alors commencé à implémenter un système similaire, et il est vrai que, sortir du carcan POO demande une certaine concentration. Hier soir, j'ai donc finalisé le code permettant de créer/gérer des ID uniques. Ce code fonctionne plutôt bien et permet une réutilisation, si besoin, des entités préalablement supprimées, en s'assurant bien entendu d'éviter tout doublon possible.
    A voir maintenant si cette implémentation saura s'adapter correctement à la gestion des composants.
    C'est toujours le gros problème, lorsque l'on décide d'utiliser du travail fourni par quelqu'un d'autre : il faut "un certain temps" (parfois important) pour arriver à se rendre compte si ce travail est -- effectivement -- adapté à notre situation personnelle ou non.

    Le seul conseil que je puisse te donner, sur ce coup là, si une implémentation s'avère inefficace sur certains points, c'est de ne pas hésiter à en changer dés que tu te rends compte de ce fait. Car, plus tu attendra pour ce faire, plus tu auras difficile (à raison) de décider de le faire:

    N'importe quel projet s'apparente à quelque "chose de vivant" qui évolue en permanence au fil du temps. Et le temps passé à utiliser une implémentation inadéquate est -- forcément -- perdu, car, pour obtenir un résultat similaire dans l'implémentation adéquate, il faudra sans doute réécrire l'ensemble

    La partie qui, je pense, va me demander un peu plus de patience, c'est justement cette gestion des composants,
    Là encore, le seul conseil que je puisse te donner, c'est d'y aller progressivement, en privilégiant la réflexion à l'écriture du code.

    Commence par réfléchir à trois ou quatre des "type d'entité" dont tu auras besoin et aux composants nécessaires à cette représentation (par exemple : un sort, une arme, une potion de mana et une potion de santé).

    Attention, je ne dis absolument pas que tu dois directement réfléchir à tous les types d'entité dont tu auras besoin dans ton programme! D'abord, parce que cela en représenterait un nombre important, et donc un nombre "encore plus important" de composants permettant de les représenter. Mais aussi parce que tu dois t'attendre, au fil du temps, à te dire que "ben, tel nouveau type d'entité serait peut-être sympa".

    N'hésites pas à garder une trace écrite (sous la forme que tu préfère) des composants nécessaire pour chaque type d'entité, ni à ajouter de nouvelles entités (et les composants qui permettent de les représenter) au fur et à mesure que tu y penses, car cela pourra te servir de "fil d'Ariane" pour la suite .

    Tu te rendras facilement compte que certains composants sont communs : (tous peuvent être placé dans un inventaire ou "abandonné à terre"; certains peuvent être "équipés", d'autres occasionnent des dégâts, d'autres encore améliorent une capacité particulière).

    Puis, commence par mettre en place les composants qui te semblent utilisés par le "plus grand nombre" des types d'entités auxquels tu aura pensé.

    Car, même si les chiffres diffèrent selon les gens, on peut estimer que 90% du temps d'exécution d'un programme se passe dans 10% du code écrit.

    Si tu trouves un composant qui est déjà utilisé par trois des quatre premiers type d'entités auxquels tu as pensé au tout début, tu peux te dire que tu retrouveras ce composant particulier (au moins) dans trois des types d'entités sur quatre que tu voudra ajouter par la suite.

    Réfléchis, en priorité, au services qui n'ont besoin que du composant mis en place pour fonctionner.

    Une fois que c'est fait (de la même manière), met en place en priorité:
    • les composants qui apparaissent le plus souvent dans les différents types d'entités (de préférences les plus simples) auxquels tu as déjà pensé
    • les composants qui n'ont réellement de l'intérêt que lorsqu'ils sont utilisés avec des composants déjà créé (la notion de déplacement n'ayant un intérêt que si tu dispose déjà de la notion de position )
    • les composants qui te permettront de "terminer" la représentation d'un type d'entité particulier

    En y allant "un pas après l'autre", tu te rendra compte, à un moment donné, que ton ECS commence tout doucement à être "utilisable", et que l'ajout de nouveaux type d'entités (ou de nouveaux services) ne nécessite -- pour une grande partie -- que de "réutiliser" les différents composants (et les services qui permettent de les manipuler) déjà existants .

    et tout particulièrement du type de ces composants. Et là, nous en revenons, finalement, au titre du post : la récupération du type de ces objets.
    En fait, le besoin est bien moindre que ce que tu ne pourrais croire à la base

    Car la grosse difficulté n'est pas de savoir quel composant est manipulé (cela, on le sait déjà grâce aux différents services mis en place pour chaque composants), mais bien de savoir ... à quelle entité particulière chaque composant (d'un type particulier) est associé; et; le cas échéant, l'ensemble des composants qui permettent de représenter une entité particulière (de manière à pouvoir décider s'il est "cohérent" ou non d'essayer d'appliquer un service nécessitant la présence de deux composant ou plus sur une entité bien précise).

    "Dans l'idéal", chaque type de composant devrait donc disposer -- au minimum -- de l'identifiant de l'entité à laquelle il appartient, de manière à pouvoir ... "récupérer" cet identifiant en cas de besoin.

    Dans l'autre sens, il peut "s'avérer utile" d'avoir un système de "passe partout (pass-key)" qui permet de comparer la liste des composants requis (pour un service particulier) à celle des composants assignés à une entité particulière.

    Ce système peut, tout simplement, être mis en place par un "tableau de bits" (un std::bitset, vu que l'on est en C++) associé à chaque entité existante; dans lequel chaque bit représentera la présence (si sa valeur est égale à true) ou l'absence (si sa valeur est égale à false) d'un type de composant particulier. une simple comparaison prenant la forme de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    if(entitykey(id) & requirements == requirements) {
        // on fait appel au(x) service(s) qui nous intéresse(nt) 
    }
    devenant alors particulièrement intéressante et efficace pour décider (ou non) d'appliquer un (ou plusieurs) services en fonction du fait que certain(s) type(s) de composant(s) sont assigné à l'entité en question
    Car même si chaque composant "hérite" d'une classe Component, les systèmes auront besoin de ne traiter que les composants qui les intéressent. Si je raisonne comme cela, il faut que chaque composant soit stocké dans un container dédié au type des dits composants.
    Justement!!! Les composants n'ont pas à hériter (du moins, selon l'approche "correcte" de la POO) de quoi que ce soit!!!

    Chaque type de composant est -- par nature -- absolument indépendant de tous les autres, et doivent être maintenus en mémoire de manière absolument séparée (on créera des "tableaux" pour chaque type de composant, mais il n'y aura aucun tableau susceptible de contenir indifféremment deux types de composants différents) !!!

    Par exemple, tous les composants de types Position doivent être stockés dans un container dédié. Idem pour les composants de type Color etc... Et cela doit pouvoir être réalisé trivialement, sans avoir besoin de retoucher tout le code lorsque l'on souhaite ajouter un nouveau type de composant...
    Tout à fait...

    Mais, contrairement, à ce que tu crois, la meilleure approche de ce point de vue n'est pas l'approche orientée objets

    Nous sous trouvons -- typiquement -- dans une situation dans laquelle on ne sait pas "quel type de donnée" sera manipulé, mais dans laquelle on sait en revanche "comment ces données seront manipulées" (pour les maintenir en mémoire de manière efficace).

    Et, cette situation sera traitée de manière bien plus efficace en utilisant l'approche générique (à l'instar des différentes collection proposés par la bibliothèque standard)

    Quand je te dis qu'il faut absolument "sortir du carcan" de la programmation orientée objets, tu peux me croire sur parole! Il ne s'agit pas de dévaloriser cette approche d'une manière ou d'une autre! Il s'agit -- essentiellement -- de se rendre compte "honnêtement" de ses limites, et de voir si d'autres approches nous permettent de faire "autrement".

    Or, il se fait que C++ nous donne -- justement -- l'opportunité "unique" de pouvoir faire cohabiter trois paradigmes différents (le procédural, l'orienté objets et le générique) et de profiter "du meilleur de tous les mondes" pour contourner les "limitations imposées par les autres".

    Nous serions vraiment idiots de ne pas profiter de cette opportunité
    J'ai trouvé plusieurs implémentations répondant au moins en partie à cette question:
    1. Dynamic_cast<T> obj; mais j'ai cru comprendre que ça "tue" les performances
    2. Des tableaux dédiés de type std::vector<T> obj; pour chaque type de composants, mais il faut donner un peu plus de flexibilité à cela
    3. std::map<???> obj; Pourquoi pas. Je n'ai jamais utilisé de map, à voir ; mais j'aimerais d'abord lister tous les principaux choix dont je peux disposer
    4. un autre système que j'ai vu, basé entièrement sur un fonctionnement avec des Templates. N'ayant pas encore étudié ce fonctionnement, je ne saurais trop quoi dire de cette implémentation ; mais je compte creuser le sujet. Par contre, il me semble que je devrais revoir mon implémentation de gestion des ID. il me semble, dans ce cas, que les ID sont aussi liés aux types des composants.
    5. Enfin, et là il ne s'agit que d'une potentielle implémentation, trouver un système utilisant des std::typeid(T).... Mais Honnêtement, je pense qu'il est préférable et tout à fait envisageable de remplacer cette fonction par un template<T> quelque-part.


    Pour les systèmes, je me pencherais sur leur fonctionnement une fois que je saurais enregistrer des couples Entity / Component.


    Que pensez-vous de cette approche ?
    Il y a moyen de faire bien mieux, en abandonnant l'approche orientée objets, et en s'intéressant à l'approche générique, et à l'instanciation explicite

+ Répondre à la discussion
Cette discussion est résolue.
Page 1 sur 2 12 DernièreDernière

Discussions similaires

  1. Réponses: 4
    Dernier message: 01/04/2019, 18h52
  2. [XL-2013] Code qui renvoie le Type d'Objet dans un UserForm qui a le focus
    Par pickatshou dans le forum Excel
    Réponses: 5
    Dernier message: 16/07/2015, 20h35
  3. Quel type d'objet renvoie Workbooks(nomFichier).Worksheets(1)?
    Par netoale dans le forum Macros et VBA Excel
    Réponses: 2
    Dernier message: 24/03/2011, 11h50
  4. Initialisation d'un type d'objet
    Par fdraven dans le forum Oracle
    Réponses: 3
    Dernier message: 28/10/2005, 11h05
  5. [Appli] Recherche d'un type d'objet précis pour interface
    Par superpatate dans le forum Interfaces Graphiques en Java
    Réponses: 3
    Dernier message: 05/08/2005, 12h02

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo