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 :

Implémenter une fonction virtuelle equals


Sujet :

C++

  1. #1
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 110
    Points
    6 110
    Par défaut Implémenter une fonction virtuelle equals
    Dans un autre fil, j'ai commencé un hors sujet sur la comparaison virtuelle.

    Admettons que l'on ait envie de stocker, dans une même table de hachage, des objets de type hétérogène, mais qui dérivent tous de la même classe de base Base. Ces objets ont une sémantique de valeur.

    Dans ce cas, je pense que l'on peut faire un code dans ce genre-là :
    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
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    class Base
    {
        friend bool operator==(const Base& lhs, const Base& rhs);
    public:
        bool equals(const Base& other) const
        {
            return canEqual_1(other) && other.canEqual_2(*this);
        }
        size_t hash() const
        {
            return virtHash();
        }
        // ...
    private:
        virtual bool canEqual_1(const Base& other) const
        {
            return (*this == other);
        }
        virtual bool canEqual_2(const Base& other) const
        {
            return true;
        }
        virtual size_t virtHash() const
        {
            // ...
        }
        // ...
    };
     
     
    class Deriv : public Base
    {
        friend bool operator==(const Deriv& lhs, const Deriv& rhs);
    private:
        // Ici, par exemple, on impose que le type dynamique de other soit Deriv ou dérive de Deriv
        // pour que this->equals(other) puisse être vrai.
        bool canEqual_1(const Base& other) const override
        {
            const Deriv* that = dynamic_cast<const Deriv*>(&other);
            return that != nullptr && (*this == *that);
        }
        bool canEqual_2(const Base& other) const override
        {
            const Deriv* that = dynamic_cast<const Deriv*>(&other);
            return that != nullptr;
        }
        size_t virtHash() const override
        {
            // ...
        }
        // ...
    };
     
     
    class BaseWrapper
    {
        friend bool operator==(const BaseWrapper& lhs, const BaseWrapper& rhs)
        {
            return lhs.m_base->equals(*rhs.m_base);
        }
    private:
        std::unique_ptr<Base> m_base;
    public:
        explicit BaseWrapper(std::unique_ptr<Base> base) : m_base(std::move(base)) {}
        size_t hash() const
        {
            return m_base->hash();
        }
    };
     
    namespace std
    {
        template<>
        struct hash<BaseWrapper>
        {
            typedef BaseWrapper argument_type;
            typedef std::size_t result_type;
            result_type operator()(argument_type const& bw) const
            {
                return bw.hash();
            }
        };
    }
     
     
    int main()
    {
        std::unordered_set<BaseWrapper> mySet;
        mySet.insert(BaseWrapper(std::make_unique<Base> (/* paramètres */)));
        mySet.insert(BaseWrapper(std::make_unique<Deriv>(/* paramètres */)));
        return 0;
    }
    J'ai implémenté bool Base::equals(const Base& other) const de telle sorte que l'on puisse facilement créer de nouvelles classes qui dérivent de Base ou de Deriv, tout en respectant la contrainte selon laquelle equals reste une relation d'équivalence, c'est à dire :
    • réflexive : pour tout x, x.equals(x) ;
    • symétrique : pour tout couple (x, y), x.equals(y) == y.equals(x) ;
    • transitive : pour tout triplet (x, y, z), si x.equals(y) && y.equals(z) alors x.equals(z).

    (Les objets x, y et z peuvent être de types dynamiques différents.)

    Mon implémentation de equals utilise indirectement des dynamic_cast, mais je pense que c'est justifié.
    Qu'en pensez-vous ?

  2. #2
    Membre expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    739
    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 : 739
    Points : 3 627
    Points
    3 627
    Par défaut
    (Ce serait plus simple avec std::type_index)

    Combiner polymorphisme et classe de valeur me gêne beaucoup et n'a pas lieu d'être.
    Les comparaisons dans le conteneur peuvent se faire avec un visiteur (std::variant si le nombre de types est connu) ou un dérivé de std::any qui ferrait une comparaison si les 2 types internes sont égaux et implémente une fonction hash. Une surcouche bien plus efficace et indépendante.

  3. #3
    Expert confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2012
    Messages
    1 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2012
    Messages : 1 711
    Points : 4 442
    Points
    4 442
    Par défaut
    Du coup, je recopie ici mon post :

    Citation Envoyé par Pyramidev Voir le message
    -Si ColoredPoint dérive de Point,
    -si on pose comme contrat que deux objets de type Point ou qui dérivent de Point sont égaux ssi ils ont les mêmes coordonnées et
    -si on pose comme contrat que deux objets de type ColoredPoint sont égaux ssi ils ont à la fois les mêmes coordonnées et aussi la même couleur
    alors, oui, forcément, si on veut que ces conditions soient remplies par la même fonction de comparaison, on arrive à une contradiction.

    Mais, ça, c'est la faute de celui qui rédige les contrats, pas de l'OO. Il faut juste plusieurs fonctions de comparaison.

    Par exemple, en C++, on pourrait faire ceci :
    Dans 99% des cas dynamic_cast est synonyme d'erreur de conception, j'ai pas l'impression qu'on soit dans les 1% ici.

    Vu l'énoncé, je verrais plutôt une composition.
    Code C++ : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Point {
       int x;
       int y;
    };
     
    struct Color { ... };
     
    struct ColoredPoint {
       Point point;
       Color color;
    };

    Si ColoredPoint hérite de Point, il doit ÊTRE un Point (c'est le principe de l'héritage; et donc se comporter comme tel).
    Aussi, un Point sera (très souvent) un POD -> pas d'héritage donc.

    -- fin recopie --

    L'énoncé change un peu : on veut aussi pouvoir stocker des Point (Base) et ColoredPoint (Deriv) dans le même conteneur, ce qui force un héritage.( edit : ou std::variant / std::any)

    Citation Envoyé par Pyramidev Voir le message
    J'ai implémenté bool Base::equals(const Base& other) const de telle sorte que l'on puisse facilement créer de nouvelles classes qui dérivent de Base ou de Deriv, tout en respectant la contrainte selon laquelle equals reste une relation d'équivalence, c'est à dire :
    • réflexive : pour tout x, x.equals(x) ;
    • symétrique : pour tout couple (x, y), x.equals(y) == y.equals(x) ;
    • transitive : pour tout triplet (x, y, z), si x.equals(y) && y.equals(z) alors x.equals(z).

    (Les objets x, y et z peuvent être de types dynamiques différents.)

    Mon implémentation de equals utilise indirectement des dynamic_cast, mais je pense que c'est justifié.
    Qu'en pensez-vous ?
    A moins d'avoir la même fonction equals (ou operator==) pour la classe de base et classe dérivée, tu n'as pas cette équivalence.
    Code c++ : 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
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    ]struct Point {
    	int x, y;	
    };
     
    struct Color { };
     
    struct ColoredPoint : Point {
    	Color color;
    };
     
    bool operator==(Point const& lhs, Point const& rhs) {
    	return lhs.x == rhs.x && lhs.y == rhs.y;
    }
     
    bool operator==(ColoredPoint const& lhs, Point const& rhs) {
    	return lhs.x == rhs.x && lhs.y == rhs.y;
    }
     
    bool operator==(Point const& lhs, ColoredPoint const& rhs) {
    	return lhs.x == rhs.x && lhs.y == rhs.y;
    }
     
    bool operator==(ColoredPoint const& lhs, ColoredPoint const& rhs) {
    	return lhs.x == rhs.x && lhs.y == rhs.y && lhs.color == rhs.color;
    }
     
    ColoredPoint a(0, 0, Color::blue);
    Point b(0, 0);
    ColoredPoint c(0, 0, Color::green);
     
    assert(a == b);
    assert(b == c);
    assert(a != c);

  4. #4
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 110
    Points
    6 110
    Par défaut
    Je précise que je me suis inspiré de l'article Java :
    http://www.artima.com/lejava/articles/equality.html
    qui aborde le sujet à partir de "Pitfall #4: Failing to define equals as an equivalence relation".

    @jo_link_noir : Si on fait une comparaison directe de types telle que x.equals(y) est forcément faux si x n'a pas le même type dynamique que y, c'est plus restrictif que la solution que je propose.
    Par exemple, si j'ai une classe Employee qui dérive de Person, je peux décider de ne pas redéfinir canEqual_1 et canEqual_2 dans Employee. Alors, un objet de type Employee pourra être égal à un objet de type Person.

    Citation Envoyé par Iradrille Voir le message
    Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    ColoredPoint a(0, 0, Color::blue);
    Point b(0, 0);
    ColoredPoint c(0, 0, Color::green);
     
    assert(a == b);
    assert(b == c);
    assert(a != c);
    J'ai déjà pris en compte ce piège. Avec mon code, on aurait :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    ColoredPoint a(0, 0, Color::blue);
    Point b(0, 0);
    ColoredPoint c(0, 0, Color::green);
     
    assert(!a.equals(b)); // car ColoredPoint::canEqual_1 retourne false (dynamic_cast retourne nullptr)
    assert(!b.equals(c)); // car ColoredPoint::canEqual_2 retourne false (dynamic_cast retourne nullptr)
    assert(!a.equals(c)); // car ColoredPoint::canEqual_1 retourne false (couleurs différentes)
    A part ça, pour revenir à la flexibilité de canEqual_1 et canEqual_2, on pourrait même modifier ColoredPoint::canEqual_1 et ColoredPoint::canEqual_2 de telle sorte qu'un ColoredPoint ne puisse être égal à un Point que si ce premier est de couleur noire.

  5. #5
    Expert confirmé
    Homme Profil pro
    Étudiant
    Inscrit en
    Juin 2012
    Messages
    1 711
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juin 2012
    Messages : 1 711
    Points : 4 442
    Points
    4 442
    Par défaut
    Citation Envoyé par Pyramidev Voir le message
    J'ai déjà pris en compte ce piège. Avec mon code, on aurait :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    ColoredPoint a(0, 0, Color::blue);
    Point b(0, 0);
    ColoredPoint c(0, 0, Color::green);
     
    assert(!a.equals(b)); // car ColoredPoint::canEqual_1 retourne false (dynamic_cast retourne nullptr)
    assert(!b.equals(c)); // car ColoredPoint::canEqual_2 retourne false (dynamic_cast retourne nullptr)
    assert(!a.equals(c)); // car ColoredPoint::canEqual_1 retourne false (couleurs différentes)
    A part ça, pour revenir à la flexibilité de canEqual_1 et canEqual_2, on pourrait même modifier ColoredPoint::canEqual_1 et ColoredPoint::canEqual_2 de telle sorte qu'un ColoredPoint ne puisse être égal à un Point que si ce premier est de couleur noire.
    Mais dans ce cas on aurait des copies non égales ?
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    ColoredPoint p0(1, 1, Color::green);
    Point p1(p0);
     
    assert(p0 != p1); // ??

  6. #6
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 110
    Points
    6 110
    Par défaut
    En fait, en tout, j'ai 3 fonctions de comparaison différentes :
    • bool operator==(const Point& lhs, const Point& rhs)vérifie que les coordonnées sont identiques. N'appelle aucune fonction virtuelle.
    • bool operator==(const ColoredPoint& lhs, const ColoredPoint& rhs)vérifie que les coordonnées et les couleurs sont identiques. N'appelle aucune fonction virtuelle.
    • bool Point::equals(const Point& other) constvérifie que les objets sont égaux. Appelle deux fonctions virtuelles.


    On a :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    ColoredPoint p0(1, 1, Color::green);
    Point p1(p0); // slicing
    assert(p0 == p1);
    assert(!p0.equals(p1));

  7. #7
    Expert éminent sénior
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 275
    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 275
    Points : 10 985
    Points
    10 985
    Par défaut
    Comme je le disais dans l'autre fil, le problème est que l'on perd le LSP en voulant hériter. Or, qui dit héritage (public en C++, ou n'importe lequel en Java), dit possibilité syntaxique d'utiliser un enfant en place d'un parent.

    Quand on manipule des points sans savoir leur type exact, qu'ils sont toujours comparables sauf dans certains cas induits pas leur combinaison de types, on perd des fonctionnalités. Et perdre des fonctionnalités, c'est briser le LSP.

    Si en C++ on sait que héritage et sémantique de valeur ne font pas bon ménage à cause des problèmes de slicing, de manière plus générale, on le sait aussi à cause du fait que l'on n'est pas sûr de toujours pouvoir comparer deux points.

    NB: Le problème est également abondamment décrit dans Effective Java de J.Bloch (§8, pp33-45 dans la 2nde éd.). L'exemple qu'il est prend est un HashSet<Point>, et il passe des FilsDePoint pour tester s'ils sont contenus (HashSet<>.contains() qui vérifie equals()). Dans cet ensemble (qui ne devrait que tester les propriétés propres aux points, peu importe que le type fils introduit -- dans son cas un compteur d'instances-- aie aucun champ supplémentaire). Or, vu que equals renvoie faux sur les mélanges (pour pouvoir implémenter correctement symétrie, réflexivité et transitivité), on ne peut jamais vérifier si un fils de point serait contenu dans l'ensemble.
    Alors certes, on peut dire que l'on ne tient pas exactement à comparer les points et les pointscomptés. Alors pourquoi hériter puisque que composer n'introduit aucune surprise ?

    Et vous savez ce qui me chagrine énormément dans l'affaire? Vous en connaissez beaucoup des livres d'introduction à l'objet dans le langage X qui me montre pas l'héritage en dérivant un Point pour définir un PointColoré? (L'autre point est que beaucoup d'ouvrages et d'exo font factoriser les données communes, et non les comportements communs. On final on va modéliser des bases de données (de livres/CD/DVD, ou de produits pharmaceutiques), et non des systèmes OO où prime le comportement -- mais c'est un autre sujet encore)
    Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
    Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...

  8. #8
    Membre expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    739
    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 : 739
    Points : 3 627
    Points
    3 627
    Par défaut
    Citation Envoyé par Pyramidev Voir le message
    @jo_link_noir : Si on fait une comparaison directe de types telle que x.equals(y) est forcément faux si x n'a pas le même type dynamique que y, c'est plus restrictif que la solution que je propose.
    C'est exacte. On peut adapter pour retourner l'identifiant ¹ minimal de comparaison, même s'il n'y en réalité aucune raison de le faire puisque normalement les classes de valeurs n'ont pas d'héritage et les classes polymorphes changent les comportements même si elles n'ajoutent pas de champs.
    Finalement, canEquals est là pour contourner la composition de Point manquante dans ColoredPoint.

    ¹ Dans mon idée, l'identifiant est un pointeur de fonction qui prend des void*, cast et fait fait la comparaison.

    Avoir == et equals asymétrique n'est vraiment pas naturel .

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

    Informations professionnelles :
    Activité : aucun

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

    De manière générale, une fonction de type equals (ou tout autre nom similaire impliquant une comparaison d'égalité) entre deux objets dont le type est clairement assimilé à la sémantique d'entité est une pure et simple aberration.

    Or, toute classe intervenant dans une hiérarchie de classes (qu'il s'agisse de la classe de base de la hiérarchie ou de n'importe quelle classe dérivée) a forcément sémantique d'entité, et ce, pour une raison toute bête : il est indispensable de pouvoir identifier chaque élément de manière strictement unique et non ambigüe par rapport aux autres.

    Mais, comme chaque élément créé est sensé être identifié de manière strictement unique, il semble évident qu'il ne pourra jamais être considéré que comme... le double de lui-même: même si on venait à trouver deux éléments dont l'ensemble des états auxquels nous sommes en mesure de nous intéresser sont strictement identiques, il devrait au minimum y avoir un état particulier dont la valeur ne pourrait correspondre qu'à un et un seul élément, nous permettant de l'identifier parmi tous les autres: Pour une voiture ou pour un avion, ce sera son numéro de chassis. Pour un compte en banque, ce sera son code IBAN, etc.

    Du coup, tu peux envisager de comparer différents états de deux éléments "de même type" : "j'ai trois moteur à hélice; et moi j'ai cinq moteur à reaction. J'ai des (50) sièges en tissus bleu; et moi des (180) sièges en cuir bruns". Mais toute tentative de comparaison globale telle qu'elle apparaitrait à l'aide d'une fonction equals ne pourrait de toute manière jamais donner un autre résultat que "faux", dés le moment où l'on ne compare pas... un élément avec lui-même, simplement parce qu'il y a forcément un (ou plusieurs) état qui ne pourra être identique pour aucun autre élément de même type, afin de permettre d'identifier très précisément chaque élément parmi les siens.
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  10. #10
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 110
    Points
    6 110
    Par défaut
    Citation Envoyé par jo_link_noir Voir le message
    Combiner polymorphisme et classe de valeur me gêne beaucoup et n'a pas lieu d'être.
    Citation Envoyé par koala01 Voir le message
    même si on venait à trouver deux éléments dont l'ensemble des états auxquels nous sommes en mesure de nous intéresser sont strictement identiques, il devrait au minimum y avoir un état particulier dont la valeur ne pourrait correspondre qu'à un et un seul élément, nous permettant de l'identifier parmi tous les autres: Pour une voiture ou pour un avion, ce sera son numéro de chassis. Pour un compte en banque, ce sera son code IBAN, etc.
    Allez, je vais créer un exemple concret.

    Admettons que l'on modélise une ville sous la forme d'une grille dans laquelle chaque cellule peut être un bâtiment, un morceau de route, etc.
    Un bâtiment peut avoir plusieurs états différents, en fonction de son type. Par exemple, si le bâtiment est celui d'une entreprise, parmi les états, on peut y mettre le nombre de personnes qui y travaillent.
    Admettons que l'on ait deux objets Ville, le deuxième représentant la même ville que le premier, mais à une date différente. On n'a pas de delta immédiatement disponible qui détermine les différences entre les deux objets, mais on veut récupérer la liste des positions dans la grille où quelque chose a changé. Par exemple, un bâtiment est apparu ou bien plus de salariés travaillent dans un certain bâtiment. On aimerait que l'implémentation soit flexible quand on ajoute un nouveau type d'élément dans la ville. Comment l'implémenter ?

    Une manière orientée objet possible de le faire, ce serait de créer une classe CelluleVille, qui correspondrait plus ou moins à la classe Base de mon premier poste, à part qu'elle serait abstraite. On pourrait avoir une classe BatimentAvecTravailleurs qui dérive de CelluleVille. La fonction bool CelluleVille::equals(const CelluleVille& other) const retourne vrai si les cellules sont comparables et ont le même état. On appelle alors equals cellule par cellule pour voir où sont les changements.

    Alors ? Est-ce une "pure et simple aberration" ?

  11. #11
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

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

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 189
    Points : 17 141
    Points
    17 141
    Par défaut
    Pour moi, tu n'as pas là un héritage sensé être apparent.
    C'est un pattern état, et chaque case est comme un automate.
    Les cases sont comparables, mais il n'y a pas de raison de rendre les états.
    Cela dit, pour moi, les états sont des valeurs, et s'ils ont une interface commune, je ne le rentre pas dans les hiérarchies d'héritages.

    Car tes objets ne sont pas une comparaison entre un tronçon de route et un immeuble de bureau.

    Pas plus que les choux et les carottes.
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  12. #12
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 110
    Points
    6 110
    Par défaut
    @ leternel : Je n'ai pas compris ce que tu veux dire.
    Si, dans un objet Ville, j'ai un tronçon de route en position (1,2) et un champ de choux en position (4,5) et si, dans un deuxième objet Ville, j'ai un immeuble de bureau en position (1,2) et un champ de carottes en position (4,5) alors, quand je comparerai ces deux objets Ville pour savoir pour quelles positions il y a des différences, je comparerai bien d'une part un tronçon de route avec un immeuble de bureau et d'autre part des choux avec des carottes.

  13. #13
    Membre expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    739
    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 : 739
    Points : 3 627
    Points
    3 627
    Par défaut
    Citation Envoyé par Pyramidev Voir le message
    pour savoir pour quelles positions il y a des différences, je comparerai bien d'une part un tronçon de route avec un immeuble de bureau et d'autre part des choux avec des carottes.
    Mon avis que c'est là le problème. L'objet n'est comparable qu'avec un objet du même type.
    À part dire tout est différent, que vas-tu sortir en comparant un tronçon de route avec un immeuble de bureaux ?

    Du coup, la comparaison n'a pas de sens est devrait être fait sur l'objet du dessus (la cellule), qui s'occupe de propager la comparaison aux membres internes seulement si leurs types sont identiques. Ce qui revient à mon message du début, l'extérieur s'occupent de comparer les types et les valeurs ne sont comparables qu'entres même type.

    Ce n'est plus CelluleBatimentAvecTravailleurs, CelluleTronçonDeRoute mais Cellule{type, data -> BatimentAvecTravailleurs}, Cellule{type, data -> TronçonDeRoute}.
    (Même si BatimentAvecTravailleurs est trop précis et devrait être quelque chose comme Bâtiment + list de {type, data}.)

    Tu devrais regarder un peu les ECS. Les arbres d'héritage pour représenter des données sont tous sauf flexible.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Citation Envoyé par Pyramidev Voir le message
    Allez, je vais créer un exemple concret.

    Admettons que l'on modélise une ville sous la forme d'une grille dans laquelle chaque cellule peut être un bâtiment, un morceau de route, etc.
    Un bâtiment peut avoir plusieurs états différents, en fonction de son type. Par exemple, si le bâtiment est celui d'une entreprise, parmi les états, on peut y mettre le nombre de personnes qui y travaillent.
    Admettons que l'on ait deux objets Ville, le deuxième représentant la même ville que le premier, mais à une date différente. On n'a pas de delta immédiatement disponible qui détermine les différences entre les deux objets, mais on veut récupérer la liste des positions dans la grille où quelque chose a changé. Par exemple, un bâtiment est apparu ou bien plus de salariés travaillent dans un certain bâtiment. On aimerait que l'implémentation soit flexible quand on ajoute un nouveau type d'élément dans la ville. Comment l'implémenter ?
    Tu aurais bel et bien une ville, avec un quadrillage contenant des "éléments susceptible d'exister dans une ville" (sans aucune précision), qui pourraient être, au choix
    • un tronçon de route
    • un bâtiment "d'affaires"
    • un bâtiment de logements
    • un bâtiment de stockage
    • un bâtiment de fournitures (eau, gaz, electricité, ...)
    • j'en passe et de meilleures


    Mais chaque élément qui prendrait place dans cette grille serait à strictement parler absolument unique : même si deux bâtiments d'affaires venaient à appartenir à la même société, il n'empêche qu'ils seraient tous les deux identifiés de manière strictement distincte (au travers de leur position dans la grille, de leur adresse postale, ou que sais-je d'autre), et les employés qui travaillent dans l'un n'ont même pas forcément accès à l'autre.
    Une manière orientée objet possible de le faire, ce serait de créer une classe CelluleVille, qui correspondrait plus ou moins à la classe Base de mon premier poste, à part qu'elle serait abstraite. On pourrait avoir une classe BatimentAvecTravailleurs qui dérive de CelluleVille.
    Ce serait une solution potentiellement possible, avec néanmoins une restriction qui risque de "tout faire foirer": Pour qu'un héritage publique puisse être envisagé, il faut impérativement que le principe de substitution de Liskov soit respecté. Ce qui implique qu'il faut que l'interface publique de la classe de base soit au minimum commune à l'ensemble des classes qui pourraient en dériver, et que l'on puisse clairement dire que "tel objet de type dérivé est bel et bien un objet du type de la classe de base".

    Ce que tu sembles envisager tend réellement à créer ce qu'il convient d'appeler un "god object" : une classe de base qui puisse servir de base à des choses aussi différentes que peuvent l'être des légumes et des voitures. Et ca, c'est déjà une très mauvaise base de travail
    La fonction bool CelluleVille::equals(const CelluleVille& other) const retourne vrai si les cellules sont comparables et ont le même état.
    Non, equals ne peut avoir qu'un seul comportement : comparer (par égalité) l'intégralité de tous tes objets.

    Or, si l'un des éléments est un champs de bettraves et que l'autre est un batiment industriel, il n'y a absolument rien de commun entre les deux, et la comparaison est donc totalement impossible.

    Si tu veux pouvoir réagir différemment (et surtout de manière cohérente) aux différentes possibilités qui existent pour représenter un "élément de ta grille" en respectant l'approche orientée objets, la seule solution passe par le polymorphisme (à condition d'arriver à définir un comportement qui puisse être considéré comme commun à l'ensemble des classes dérivées) et le double disptach, afin de repartir le plus tôt possible sur le type réel de l'élément qui t'intéresse.

    Comprends moi bien : comme je l'ai dit, il est toujours possible, lorsque tu arrive à récupérer le type réel de ton élément, d'envisager de comparer certaines données qui le composent (certains des états qu'il accpete d'exposer), à condition que ces données soient effectivement disponible pour les deux éléments comparés. Mais:
    1. la comparaison n'a réellement de sens que pour les inégalité (plus grand ou plus petit), afin d'envisager un tri éventuel
    2. la comparaison ne peut effectivement avoir lieu que si l'état qui t'intéresse est effectivement accessible pour les deux éléments que tu envisages de comparer, ce qui n'est absolument pas garanti

    On appelle alors equals cellule par cellule pour voir où sont les changements.

    Alors ? Est-ce une "pure et simple aberration" ?
    Oui, bien sur... Allez, je vais y aller de mon propre exemple :

    Imagine la notion de "compte banquaire". Cette notion pourrait être dérivée en "compte courant" et en "compte d'épargne". Ces deux notions fourniraient un service commun qui est "de permettre de récupérer le solde courant".

    Il se peut tout à fait que mon compte d'épargne et que ton compte courant présentent exactement le même solde courant à un instant T. Mais ce n'est jamais -- dans le meilleur des cas -- qu'une "curiosité aléatoire et temporaire", car :
    • si ce montant est sur mon compte d'épargne, c'est sans doute que je n'ai pas forcément un besoin immédiat de cet argent, alors que
    • si ce montant est sur ton compte courant, c'est peut être
      • parce que tu en as besoin "dans l'immédiat" (pour faire tes courses, payer tes factures, ...)
      • parce que tu n'a pas encore eu l'occasion de faire passer l'ordre de transfert vers ton compte d'épargne
      • parce que tu as prévu de t'offrir un plaisir particulier grâce au montant que tu as mis de coté pendant plusieurs mois
      • que sais-je encore

    Pire encore : ce n'est pas parce que nos deux comptes présentent, à un instant particulier, un solde similaire que tu accepterais que le payement de ma facture d'électricité parte de ton compte, ni que tu accepterais (ce qui te plairait sans doute encore moins) que ton salaire soit versé sur mon compte d'épargne.

    Et pourtant, il est possible de comparer le solde de deux comptes (quel que soit le type réel des comptes) afin, pourquoi pas, de trier les différents comptes qui existent. Mais, je le répète encore une fois : ce n'est pas parce que deux comptes présentent, à un instant T donné, des soldes similaires que l'on peut considérer ces deux compte comme étant "intervertibles" : il y aura toujours un compte qui t'appartient, et un compte qui m'appartient, et il s'agit que les ordres que je donnerai concernant mon compte soient effectivement... débitées ou créditées de mon compte et non du tien.

    De la même manière, si tu envisage de parler d'avions. Il se peut tout à fait que deux avions affichent, à un instant T, un nombre d'heures de vol tout à fait égal. Mais, même si ces deux avions se trouvent, à cet instant T particulier, sur un même aérodrome, ce n'est absolument pas une raison pour arriver à en considérer que les deux avions ont fait exactement les mêmes trajets, ni même qu'ils sont passés par les mêmes aérodromes tout au long de leur existence : nous sommes bel et bien face à une "curiosité tout à fait aléatoire", et parfaitement temporaire (car la similitude disparaitra dés que le premier des deux avions décollera).

    A coté de cela, nous pourrions envisager de comparer les caractéristiques techniques, en allant du diamètre de la jante du train d'atterrissage à la puissance des différents moteurs qui équipent l'avion. Mais, de deux choses l'une : soit on utilise ces différentes caractéristiques pour déterminer l'avion que l'on préfère (mais on effectue alors des comparaisons par inégalité), soit on compare des avions de même marque, de même modèle et de même année de construction. Mais il n'y aura encore une fois que deux avions de même modèle qui pourront être considérés comme "égaux".

    Et, dans ce cas, il ne sert à rien de commencer à comparer l'ensemble des caractéristiques techniques de l'avion pour obtenir la réponse : il suffit de comparer ... la notion de modèle (et éventuellement l'année de construction).

    Ce qu'il faut comprendre, c'est qu'il existe deux grandes catégorie de sémantiques applicables aux données : la sémantique de valeur, qui s'applique à tout ce qui peut être considéré comme un "jalon" (dans un référentiel quelconque), comme les dates, les poids, les distances, etc et la sémantique d'entité, qui s'applique à tout ce qui doit pouvoir être systématiquement identifié de manière unique et non ambigue.

    La sémantique de valeur autorise potentiellement n'importe quel type de comparaison (que ce soit par égalité ou par inégalité), ainsi d'ailleurs que la copie et l'affectation. Il n'y a en effet absolument rien qui t'empêche d'avoir quarante instances de dates, et de te rendre compte que deux de ces instances correspondent exactement à la même date (du fait du principe du tiroir, il serait d'ailleurs étonnant qu'il n'en aille pas de la sorte).

    Par contre, la sémantique d'entité n'a pas besoin de la comparaison, vu que chaque élément est -- par définition -- unique. Pour garantir cette unicité, la copie et l'affectation doivent être interdites, ce qui exclut tout besoin de comparaison "globale". Par contre, rien n'interdit la comparaison de certains états bien particuliers, mais il ne s'agit alors forcément plus d'une comparaison globale
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  15. #15
    Expert éminent sénior

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2007
    Messages
    5 189
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Localisation : France, Essonne (Île de France)

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

    Informations forums :
    Inscription : Juin 2007
    Messages : 5 189
    Points : 17 141
    Points
    17 141
    Par défaut
    Merci Koala, je n'aurai certainement pas répondu mieux
    Mes principes de bases du codeur qui veut pouvoir dormir:
    • Une variable de moins est une source d'erreur en moins.
    • Un pointeur de moins est une montagne d'erreurs en moins.
    • Un copier-coller, ça doit se justifier... Deux, c'est un de trop.
    • jamais signifie "sauf si j'ai passé trois jours à prouver que je peux".
    • La plus sotte des questions est celle qu'on ne pose pas.
    Pour faire des graphes, essayez yEd.
    le ter nel est le titre porté par un de mes personnages de jeu de rôle

  16. #16
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 110
    Points
    6 110
    Par défaut
    Je vais pousser plus loin mon exemple.

    J'ai un objet villeJanvier de type Ville avec, entre autres, le contenu suivant :
    • Position (1,1) : Un tronçon de route de type TronconDeRoute.
    • Position (2,3) : Un tas d'herbes de type Herbe.
    • Position (5,4) : Un bâtiment de logement de type BatimentLogement. Pour faire plaisir à koala01, ledit bâtiment a un identifiant unique, par exemple "ghetto456". Il a aussi plein de variables membres, dont le nombre de personnes qui l'habitent, par exemple 50.
    • Position (6,6) : Un bâtiment de logement de type BatimentLogement. l'identifiant est "ghetto745". Le nombre de personnes qui l'habitent est 60.


    J'ai un objet villeFevrier de type Ville qui représente la même ville un mois plus tard. Elle a, entre autres, le contenu suivant :
    • Position (1,1) : Aucun changement.
    • Position (2,3) : Un champ de carottes de type ChampDeCarottes.
    • Position (5,4) : Aucun changement.
    • Position (6,6) : Un bâtiment de logement de type BatimentLogement. l'identifiant est toujours "ghetto457". Le nombre de personnes qui l'habitent n'est plus 60 mais 70. Les autres variables membres n'ont pas changé.


    Je veux afficher en surbrillances les cellules de villeFevrier où il y a des différences par rapport à celles de villeJanvier, à savoir les cellules de position (2,3) et (6,6).

    Pour l'instant, ce que propose jo_link_noir fonctionne : on compare le type des objets. Si les types sont différents, les objets sont différents. Si les types sont identiques, on compare en fonction de l'opération de comparaison du type donné.

    Mais admettons que je veuille faire le changement suivant :
    • Objet villeJanvier, position (5,4) : on a toujours un objet de type BatimentLogement avec l'identifiant "ghetto456" et qui est habité par 50 personnes. Mais, maintenant, BatimentLogement dérive de BatimentLogementAbstrait.
    • Objet villeFevrier, position (5,4) : on a un objet de type BatimentLogementProxy qui dérive de BatimentLogementAbstrait. Ledit objet contient un pointeur vers l'objet BatimentLogement de la position (5,4) de villeJanvier. Ainsi, on a la même chose, à part que la représentation des données en mémoire est différente.


    Avec ma solution, au début, la classe BatimentLogement dérivait de CelluleVille et redéfinissait les fonctions virtuelles canEqual_1 et canEqual_2 pour se distinguer des autres contenus de cellule comme TronconDeRoute.
    Après le changement, BatimentLogementAbstrait dérive de CelluleVille et redéfinit les fonctions virtuelles canEqual_1 et canEqual_2. Les classes BatimentLogement et BatimentLogementProxy dérivent de BatimentLogementAbstrait mais n'ont pas besoin de redéfinir les fonctions virtuelles canEqual_1 et canEqual_2, puisqu'on veut que deux bâtiments de logement puissent être comparés. Et l'égalité entre un objet BatimentLogement et un objet BatimentLogementProxy ne sera pas une "curiosité aléatoire et temporaire", puisque ce sera le même bâtiment avec deux implémentations différentes.

    Avec la solution de jo_link_noir où on impose que deux objets de type différents sont forcément différents, on peut se débrouiller pour faire le changement, mais je pense que cela demandera plus de code et sera donc moins flexible.
    Dans ce cas, j'aurais crée une classe BatimentLogementWrapper qui dérive de BatimentLogementAbstrait et qui peut contenir soit un objet BatimentLogement, soit un objet BatimentLogementProxy. Comme ça, dans villeJanvier et villeFevrier, à la position (5,4), l'objet sera de même type dynamique dans les deux cas : BatimentLogementWrapper. Mais, par rapport à ma solution, cela a nécessité de créer une classe en plus.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    A ce moment là, si tu veux pouvoir disposer en même temps des données de villeJanvier et de villeFevrier, ce qu'il te faut, c'est une sorte de système "undo redo", qui te permette de garder les modifications apportées entre les différents repères temporels dont tu dispose.

    Mais, même si tu peux récupérer ce qui se trouvait (et / ou dans quel nombre) à une case de ta grille un, deux, ou trois mois plus tôt, il n'empêche que ta ville se compose... de ce qui se trouve dedans à l'instant T de l'exécution. Ce qui s'y trouvait en janvier, ca fait -- purement et simplement -- partie du passé lorsque tu es en février ou en mars

    Et, quoi qu'il en soit, la représentation sous la forme d'une grille est déjà fort proche du concept de vue et devrait donc déjà être très clairement séparé de tes données métiers à proprement parler.

    Car, si tous les éléments sont susceptibles d'apparaitre dans la grille, il y a fort à parier (si tu ne t'oriente pas vers un système ECS, ce qui semblerait sans doute la meilleure idée à avoir) que, au niveau de tes données métier proprement dites, tu t'arrange pour systématiquement maintenir une liste à jour des éléments qui se trouvent dans ta ville en les regroupant selon leur type réel, en ayant -- par exemple :
    une liste des troncons de route d'une part
    une liste des batiment industriels ailleurs
    une liste des champs de coquelicots à un troisième endroit
    une liste des maisons encore ailleurs.
    Mais, quoi qu'il en soit, cela ne change absolument rien au fait que la comparaison globale de "n'importe quel élément pouvant prendre place dans ta ville" reste une aberration sans nom, simplement parce que, pour qu'une comparaison puisse se faire, il faut impérativement que les éléments soient comparables, et que, pour que les éléments soient comparables, on dispose d'états similaires pour nos différents éléments.

    Or, on te l'a sans doute répété depuis la primaire : on ne peut pas comparer les pommes avec les poires. Et bien, ici, c'est pareil : on ne peut pas comparer un champs de coquelicots avec une maison d'habitation ou avec un batiment industriel!

    Comme je te l'ai dit plus haut : les classes ayant sémantique de valeur (comme les dates ou les classes permettant de représenter des quantité, des montants, des distances, et bien d'autres choses encore) peuvent éventuellement être comparables (du moins tant que l'on compare deux données de types similaire : on n'arrivera jamais à comparer une durée et un montant entre eux), mais les classes ayant sémantique d'entité (ce qui regroupe n'importe quel classe intervenant dans une hiérarchie de classe) n'ont absolument aucune raison d'être comparable, car chaque instance est -- par nature -- absolument unique; et ce, même si cela n'interdit absolument pas de comparer, à un instant T de l'exécution deux états similaires, bien que cette comparaison (et l'égalité qui peut en ressortir) ne puisse être considérée que comme "strictement temporaire" (et potentiellement parfaitement aléatoire).
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  18. #18
    Expert éminent sénior
    Homme Profil pro
    Analyste/ Programmeur
    Inscrit en
    Juillet 2013
    Messages
    4 630
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Analyste/ Programmeur

    Informations forums :
    Inscription : Juillet 2013
    Messages : 4 630
    Points : 10 556
    Points
    10 556
    Par défaut
    Citation Envoyé par Pyramidev Voir le message
    Dans ce cas, j'aurais crée une classe BatimentLogementWrapper qui dérive de BatimentLogementAbstrait et qui peut contenir soit un objet BatimentLogement, soit un objet BatimentLogementProxy. Comme ça, dans villeJanvier et villeFevrier, à la position (5,4), l'objet sera de même type dynamique dans les deux cas : BatimentLogementWrapper. Mais, par rapport à ma solution, cela a nécessité de créer une classe en plus.
    /MODE Grosse pincette parce que je ne connais pas bien le truc

    jo_link_noir te proposait aussi E.C.S. (Entity component system)

    Et dans ce système tu aurais 2 entités (BatimentLogement et BatimentLogementProxy), mais [au moins] 1 seul composant struct { identifiant: "ghetto456", nb: 50 }.

    Et si tu t'intéresses à la flexibilité de ce patron , bien c'est le [gros] problème: c'est tellement théorique que c'est ultra flexible

Discussions similaires

  1. Réponses: 2
    Dernier message: 02/10/2008, 16h37
  2. Fonction appelant une fonction virtuelle pure
    Par oodini dans le forum C++
    Réponses: 12
    Dernier message: 19/09/2008, 08h24
  3. Une fonction virtuelle ne peut pas retourner un template!
    Par coyotte507 dans le forum Langage
    Réponses: 10
    Dernier message: 08/02/2008, 20h39
  4. Réponses: 2
    Dernier message: 05/03/2006, 19h29
  5. [MySQL] Implémenter une fonction de recherche approximative
    Par Chromatic dans le forum PHP & Base de données
    Réponses: 4
    Dernier message: 28/02/2006, 11h54

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