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

Langage C++ Discussion :

Petite problématique d'héritage/polymorphisme


Sujet :

Langage C++

  1. #1
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut Petite problématique d'héritage/polymorphisme
    Bonjour, je recherche une solution à la situation suivante:

    J'ai un classe abstraite A déclarant plusieurs méthodes virtuelle pures, qui me sert d'interface.

    J'ai une première classe B dérivant de A et qui gère en interne des ressources qui doivent absolument rester unique dans mon projet.

    Et j'ai une seconde classe C, qui implémente différemment l'interface A, mais qui doit se servir des même ressources que B.


    Ma première idée (et qui reste ma solution par défaut, si je ne trouve pas mieux), est de faire dériver C de A, et de mettre en relation B et C à l'initialisation, afin qu'elles se partagent la ressource (J'aurai C qui référencera les ressources de B, et B qui en gardera la responsabilité).

    Mais comme C partage, non seulement les ressources de B, mais également beaucoup de code interne très proches (plusieurs sous-méthodes privés identiques), j'aurai trouvé plus élégant de faire directement dériver C de B.

    La ressource devant rester unique, je n'instancierai dans mon projet qu'un seul objet C, mais j'aimerai en obtenir une seconde référence, qui présenterait l'implémentation de B.
    C'est à dire que lorsque je rencontre une fonction prenant un pointeur A, j'aimerai en faire un premier appel qui lancera l'implémentation C (type réel de l'objet instancié) et un second appel qui lancera l'implémentation de B (un de ses types parents).

    Butant dans ma réflexion, j'ai testé méthodiquement tous les types de cast (static, dynamic et reinterpret), ainsi qu'avec et sans 'virtual' dans la classe A, mais rien ne m'a permit d'atteindre l'implémentation de B.
    A l'intérieur de C, j'arrive à accédé à une méthode de B par B::foo(), mais existe-t-il une syntaxe qui m'aurait échappé, permettant d'obtenir une référence à mon objet C appelant automatiquement l'implémentation de B, lors d'appel de type foo(A* obj) ?



    Merci

  2. #2
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut
    Voici un petit code pour mieux illustrer ce que je cherche à faire
    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
     
    class base{
        base() {}
        ~base(){}
     
        virtual void print() { std::cout << "A"; }
    };
     
    class derive : public base{
        derive() {}
        ~derive() {}
     
        void print() { std::cout << "B"; }
    }
     
    void print(base *obj){
        obj->print();
    }
     
    int main(){
        derive *objB = new derive();
        base *objA = objB;
     
        print(objA);
        print(objB);
     
    }
    Je souhaite qu'il m'imprime "base" et "derive", mais pour le moment je ne parvient qu'à imprimer 2x "derive".

    Puisqu'à l'intérieur de la classe derive, j'arrive à atteindre le print de la classe de base par un base::print();, j'ai même tenter ceci depuis le main():
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    std::function<void(void)> f = std::bind(&base::print, ObjB);
    f();
    Mais même lui m'imprime "derive". De toute façon j'imagine que j'aurai eu du mal à utiliser cette syntaxe dans mon projet.


    Finalement, si je ne dis pas trop de bêtise, ce que je cherche à faire c'est d'obtenir une référence de ObjB qui aurait sa propre vtable. Ou au moins de pouvoir spécifier à print(base *obj) quelle vtable utiliser, lors de ses appels.
    J'imagine que C++ ne permet pas de réaliser cela?
    Intrinsèquement, la classe derive doit bien posséder les définitions de la classe de base (c'est confirmé, puisque je parviens à faire des appels base::print(), à l'intérieur d'elle-même). C'est dommage qu'il n'est pas possible d'atteindre ces définitions depuis "le monde extérieur".



    Pour en revenir à mon problème de base (classe A, B, C), je vais mettre mes ressources et les méthodes protégés associés en static, et créer deux instances distinctes, de B et de C.

    Mais si quelqu'un a des commentaires à faire ça m'intéresserait bien.
    Merci.

  3. #3
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 115
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 115
    Points : 32 965
    Points
    32 965
    Billets dans le blog
    4
    Par défaut
    Je n'ai rien compris au problème mais c'est le principe même de l'héritage et du virtual dont tu parles...
    Le but est d'appeler la méthode de l'objet réel, depuis un objet de base*.
    Si tu veux forcer A::print, tu peux faire ptr->A::print. Enfin, si tenté que print soit public.
    Mais bon, au mieux tu as pas besoin de l'héritage, au pire tu n'en as pas compris l'utilisation.
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  4. #4
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    B possède des ressources uniques qu'elles doivent gérer elle-même ; mais C doit accéder à ses ressources. Pour moi, il y a une contradiction : ces ressources ne sont pas franchement réservées à B si C y accède

    N'as-tu pas plutôt besoin d'une classe R pour gérer les ressources (notamment l'unicité) ?

    B prendrait en paramètre une référence sur R quand C prendrait en paramètre un const-référence sur R ?

    Si B et C partagent bcp de code, tu peux toujours faire une classe intermédiaire avec l'implémentation commune :
       A
       |
       |
    Common
    |    |
    |    |
    B    C

  5. #5
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut
    Citation Envoyé par Bousk Voir le message
    Je n'ai rien compris au problème mais c'est le principe même de l'héritage et du virtual dont tu parles...
    Le but est d'appeler la méthode de l'objet réel, depuis un objet de base*.
    Si tu veux forcer A::print, tu peux faire ptr->A::print. Enfin, si tenté que print soit public.
    Mais bon, au mieux tu as pas besoin de l'héritage, au pire tu n'en as pas compris l'utilisation.
    Ah super, c’est cette syntaxe là que je ne connaissais pas. Mais finalement quand je la regarde, je me rend compte que je ne pourrai pas l’utiliser.

    Mon but est d’utiliser le polymorphisme, non pas en appelant l’implémentation final de l’objet, mais une de ses implémentations intermédiaires, dans sa hiérarchie d’héritage.

    Sur un objet c dérivé de ceci : A->B->C,
    En le passant deux fois de suite à une fonction de prototype foo(A*), j’aurai aimé qu’il m’imprime une première fois « C », puis une seconde fois « B », en se faisant passer pour un de ses parents (avec un cast quelconque).
    Aujourd’hui, en mettant ‘virtual’ dans A, j’obtiens « C » et « C », et en retirant le ‘virtual’ j’ai « A » et « A ». Mais jamais je n’arrive à atteindre « B ».

    Ca aurait été sympa si il aurait été possible (pour des cas très exceptionnel et bien alambiqué) de paramétrer un objet ou une référence pour qu’il se comporte, non pas pour ce qu’il est vraiment, mais comme l’un de ses parent, lors de l'utilisation du polymorphisme.
    Avec l’héritage, il possède bien l’intégralité des méthodes de sa hiérarchie, alors pourquoi pas ?


    Comme le C++ hérite du C, je me suis dit que je pouvais peut-être m’en sortir avec un cast C bien dégueu et unsafe sur un pointer, mais rien. Et j’ai bien compris que c’est la vtable qui m’empêche de faire ça.
    Alors peut-être avec un hardcopy de mon objet, en définissant une nouvelle vtable pointant sur les méthodes que je souhaite, tout en référençant les même données originales… mais je vais arrêter là les conneries.


    [edit]
    En résumé, j'aurai souhaité faire ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    foo(A* obj);
     
    C c;
     
    foo( (B*)&c );
    Et que ça m'imprime "B" et non pas "C" ou "A"

  6. #6
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut
    Citation Envoyé par Bktero Voir le message
    B possède des ressources uniques qu'elles doivent gérer elle-même ; mais C doit accéder à ses ressources. Pour moi, il y a une contradiction : ces ressources ne sont pas franchement réservées à B si C y accède
    D'où mon idée de faire dériver C de B. Comme ça C aurait posséder les ressources, par le biais de son héritage, et aurait ainsi pu accéder aux données utiles, mais le code de gestion (pour faire simple, 'ouverture'/'fermeture' même si c'est pas vraiment de ça qu'il s'agit) aurait été implémenté dans B et rester de son seul ressort.



    Citation Envoyé par Bktero Voir le message
    N'as-tu pas plutôt besoin d'une classe R pour gérer les ressources (notamment l'unicité) ?

    B prendrait en paramètre une référence sur R quand C prendrait en paramètre un const-référence sur R ?
    A la base, j'avais beaucoup de mal à me résoudre à cette approche, mais au final c'est effectivement là-dessus que je suis parti, plutôt que d'opter pour des membres et données statiques.
    Il y a une intégration très profonde entre B/C et ces ressources. Et c'était plutôt cohérent, conceptuellement, que ces ressources soit géré par B et/ou C. Elles sont sur un même pied d'égalité et l'un comme l'autre aurai pu s'en occuper. Le seule problème c'était l'unicité, il ne fallait qu'il n'y en ait qu'un à s'en occuper.
    Mais j'ai finalement réussi à faire une découpe plutôt propre. Niveau encapsulation c'est pas top, puisque B et C accède à la totalité des membres de la nouvelle classe qui gère la ressource. Mais bon, au moins ça passe par des accesseurs.

    Et au final, une fois de code lié aux ressources retiré, C n'est même plus besoin de dérivé de B. Elles n'ont aujourd'hui presque plus aucun code en commun.

  7. #7
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    L'héritage est une manière pratique de récupérer du comportement mais il faut revenir à la sémantique de l'héritage.

    L'héritage public modélise une relation "est un". Si B hérite de C, alors cela veut dire que C est un B. Pas juste que C se comporte un peu / en partie / beaucoup comme un B. L'héritage public est une relation très forte, la sémantique n'est pas anodine.

    Il y a d'autres façon de récupérer du comportement sans avoir cette sémantique de "est un". Il y a l'héritage privé qui a une sémantique de "est implémenté en terme de". Par exemple, tu as une classe List et tu veux gérer un groupe de personne pour ta classe Group, peut-être que Group va hériter de manière privée de List. Il y a surtout l'encapsulation : Group possède une List.

    L'héritage public ne doit pas être la relation entre 2 classes qui doit te venir à l'idée. On apprend à tous les débutants "la POO c'est trop bien tu peux faire de l'héritage" mais les vieux sages savent que la POO c'est beaucoup de choses avant l'héritage.

    N'oublie pas non plus qu'une classe doit avoir une seule responsabilité : https://en.wikipedia.org/wiki/Single...lity_principle

  8. #8
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut
    Je ne suis pas super pointu en POO et il y a probablement bien des concepts avancé qui m'échappent encore.

    Mais pour le coup, c'est bien parce que j'avais bien compris que C "est un" B, que je n'avais pas vu de mal à utiliser une syntaxe de ce style:


    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     
    foo(A* obj);
     
    C c;
     
    foo( &c );  //doit imprimer "c" 
    foo( (B*)&c );  //doit imprimer "b"
    C est un B, alors utilisons le comme un B (ou en tant que B), lorsque j'en ai envie. C'est bien le rôle du bon vieux cast C. Et il n'y a aucun risque de violation mémoire, puisque C est bien un B.

    Mais le problème vient de la vtable (ou en tout cas, mon compilateur utilise une vtable).
    Si C est bien un B et qu'il est donc intégralement composé des membres et méthodes non virtual de B, il possède finalement une vtable qui lui est propre, et les implémentations des méthodes virtual de B lui sont donc étrangères.

  9. #9
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut
    Intéressant ce concept de responsabilité unique.

    Mais comme tous grand principe, tout dépend de la granularité à laquelle on l’applique. Dans mon cas, aucune classe n’atteint encore le « people(actor) ».
    En séparant les tâches ‘compile’ et ‘print’ dans deux classe différentes, il faudra alors définir une interface commune pour que les deux tâches puissent communiquer et s’échanger des donnée.
    Et que se passe-t-il si cette interface doit changer ? Faut-il aussi découper en deux la classe compile (une classe qui compile, et une qui expose les donnée à l’interface)?

    Dans mon nouveau code, la classe R (qui gère la ressource) et la classe B (ou C) réalisent toutes deux ensemble la compilation. La classe abstraite A est la définition de l’interface, qui leur serviront pour communiquer avec les futures classes d’impression.
    J’ai utilisé de terme de « ressources » pour être expressif dans les explications, mais en réalité c’est plus subtils que ça. Ce serait peut-être plutôt « traitement bas niveau » / « traitement haut-niveau », et il n’était pas du tout évident au départ de devoir scinder ces traitements en deux. Seul l’unicité d’un des membres de « traitement bas niveau » m’y a finalement contraint.

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Citation Envoyé par wistiti1234 Voir le message
    Intéressant ce concept de responsabilité unique.

    En séparant les tâches ‘compile’ et ‘print’ dans deux classe différentes, il faudra alors définir une interface commune pour que les deux tâches puissent communiquer et s’échanger des donnée.
    Et que se passe-t-il si cette interface doit changer ? Faut-il aussi découper en deux la classe compile (une classe qui compile, et une qui expose les donnée à l’interface)?
    Justement, c'est là tout l'intérêt du SRP!!!

    On décide de créer une nouvelle application pour remplir certains besoins. L'analyse technique (fonctionnelle) que l'on effectue concernant l'application a pour but de décrire très précisément l'ensemble des besoins qui devront être remplis.

    A la fin du compte, nous voudrons "valider" l'application en nous assurant que l'ensemble des besoins que l'on a décrit sont bel et bien remplis exactement de la manière indiquée.

    Et le meilleur moyen d'y arriver, c'est encore de s'assurer que l'ensemble des besoin que l'on décrit soient clairement exprimés dans le code, selon une règle relativement simple:
    chaque nom (groupe nominal) rencontré dans l'analyse fonctionnelle doit être représenté par une donnée ou un type de donnée dans le code
    chaque verbe (groupe verbal) rencontré dans l'analyse fonctionnelle doit être représenté par une fonction dans le code
    Alors, nous sommes bien d'accord avec le fait qu'un projet évolue dans le temps

    Seulement, il faut bien comprendre que toute les fonctionnalités (pour regrouper les types de données, les variables et les fonctions) que nous allons créer en écrivant le code ne prendront réellement leur sens qu'à partir du moment où ... elles seront utilisées:

    Si tu crées une variable, une fonction ou un type de donnée et que tu ne l'utilises pas, il (ou elle) devient "tout bonnement" inutile, et le temps passé à mettre cette fonctionnalité au point est "du temps perdu", qui aurait sans doute été bien mieux utilisé s'il avait servi à mettre au point... une fonctionnalité utilisée.

    Si bien que la très grosse majorité des besoins que l'on décrit dans l'analyse fonctionnelle ne font en réalité que... permettre la mise au point d'autres besoins plus complexes.

    Et cela implique que dés le moment où un besoin particulier, aussi simple fut-il, est décrit dans l'analyse fonctionnelle (et transcrit dans le code), il se retrouve "gravé dans le marbre", parce que, si on venait à décider de le supprimer, on empêcherait d'autres fonctionnalités de travailler "comme elle le doivent".

    Donc, si un projet évolue dans le temps, tu peux avoir l'absolue certitude que cela se fera toujours en ajoutant de nouveaux besoin, de nouveaux type de données, de nouvelles variables, de nouvelles fonctions, et qu'il ne sera JAMAIS question (sauf en cas de re factorisation en profondeur) de supprimer des fonctionnalités existante.

    Au final, une interface est sans doute la chose la plus stable que l'on puisse trouver dans un projet. Parce qu'il n'y a rien à faire : si tu décide de créer une fonction int add(int , int), c'est parce que tu as eu besoin, à un moment donné, de créer une fonction prenant deux int comme paramètre et renvoyant une valeur de type int dont le nom indique clairement que la fonction exécutera l'addition (par opposition à la possibilité d'effectuer une soustraction, une multiplication ou une division) des deux valeurs fournies.

    Et, dés le moment où cette fonction aura été créée, il est "plus que probable" qu'elle aura été ... utilisée, ce qui t'interdira d'en modifier le prototype par la suite, sous peine de te retrouver avec un tas d'erreurs émises par le compilateur qui se plaint de "ne pas trouver la fonction 'int add(int, int)'"

    Par contre, le fait que chaque variable déclarée, chaque type de donnée, chaque fonction ne soit utilisé(e) que dans un but bien précis te permettra de décrire (dans le code) les comportements les plus complexes comme n'étant rien d'autre que ... le fait d'invoquer les comportements "les plus simples" dans un ordre bien déterminé, susceptible de fournir le résultat attendu lorsque tu fais appel à ce comportement complexe.

    Et, du coup, lorsque tu écris
    Mais comme tous grand principe, tout dépend de la granularité à laquelle on l’applique. Dans mon cas, aucune classe n’atteint encore le « people(actor) ».
    je suis obligé de m'élever violemment contre cette pensée et de m'écrier NON!!!.

    Si je ne pouvais t'obliger à respecter qu'un seul principe AU PIED DE LA LETTRE ce serait sans doute celui-là

    Pour mon malheur, il y a en réalité six règle (cinq principes et une loi) que je voudrais t'inciter à respecter scrupuleusement: les cinq principes SOLID et la loi de Déméter. SOLID étant l'acronyme de
    • SRP (Single Responsability Principle ou principe de la responsabilité unique) : chaque fonction, chaque type de donnée, chaque donnée n'est utilisée que dans un seul et unique but
    • OCP (Open Closed Principle ou principe "ouvert / fermé"): un code doit être "ouvert aux évolutions", mais "fermé au modifiications": quand on a pu déterminer (prouver) qu'un code fournissait le résultat attendu, il n'y a aucune raison d'aller le modifier par la suite, même si c'est pour introduire une évolution
    • LSP (Liskov Substitution Principle ou principe de substitution de liskov): le principe qui régit les règles qui permettent l'héritage multiple et la mise en place de la relation EST-UN (j'en ai vagument parlé, sans le citer, dans mon intervention précédante)
    • ISP (Interface Segregation Principle ou principe de ségrégation des interfaces): il faut garder les interfaces le plus simple possible, ne multipliant les interface plutôt que les fonctions qu'elles exposent
    • DIP (Dépendance Inversion Principle ou principe des inversions des dépendance) : en inversant les dépendances, on s'ouvre parfois des possibilités nouvelles

    Quant à la loi de Déméter, elle nous dit que
    Si le type A utilise en interne une donnée de type B, l'utilisateur d'une donnée de type A ne devrait pas avoir à connaitre le type B pour pouvoir manipuler sa donnée de type A
    Dans mon nouveau code, la classe R (qui gère la ressource) et la classe B (ou C) réalisent toutes deux ensemble la compilation. La classe abstraite A est la définition de l’interface, qui leur serviront pour communiquer avec les futures classes d’impression.
    J’ai utilisé de terme de « ressources » pour être expressif dans les explications, mais en réalité c’est plus subtils que ça. Ce serait peut-être plutôt « traitement bas niveau » / « traitement haut-niveau », et il n’était pas du tout évident au départ de devoir scinder ces traitements en deux. Seul l’unicité d’un des membres de « traitement bas niveau » m’y a finalement contraint.[/QUOTE]

    Dans mon nouveau code, la classe R (qui gère la ressource) et la classe B (ou C) réalisent toutes deux ensemble la compilation. La classe abstraite A est la définition de l’interface, qui leur serviront pour communiquer avec les futures classes d’impression.
    J’ai utilisé de terme de « ressources » pour être expressif dans les explications, mais en réalité c’est plus subtils que ça. Ce serait peut-être plutôt « traitement bas niveau » / « traitement haut-niveau », et il n’était pas du tout évident au départ de devoir scinder ces traitements en deux. Seul l’unicité d’un des membres de « traitement bas niveau » m’y a finalement contraint.
    Nous arrivons, de toutes évidences, au point de la discussion où il devient indispensable d'abandonner les notions totalement abstraites pour s'intéresser à tes besoins réels. Il devient donc absolument nécessaire que tu nous explique très clairement:
    • la situation dans laquelle tu te trouve et quelles sont les données dont tu dispose et
    • ce que tu veux obtenir au final

    pour que nous puissions (si besoin en est) t'aider à exprimer tes besoins de manière plus cohérente et, surtout, t'aider à choisir la solution qui sera la plus adaptée à tes besoins

    D'après ce que tu nous dis, j'aurais tendance à abandonner l'approche orientée objets pure et dure pour une approche basée sur la généricité et les politiques d'exécution, mais, je n'exclus absolument pas le fait de n'avoir compris qu'à moitié ta problématique, et donc de revenir sur cette proposition

    Quoi qu'il en soit, tant que nous ne saurons pas exactement quels sont tes besoins réels, il sera difficile (voire impossible) de t'aider d'avantage
    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

  11. #11
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut
    En prenant un peu de recule sur le concept de mon projet, en fait, à la base, B et C ne devaient formé qu'une seule et unique classe (puisqu'elles ont la même source), qui aurait exposé un gros paquet de données dans une grande interface (x1, x2, x3, x4, x5, x6, x7 et x8).

    Mais j'ai rapidement réalisé que toutes ces données pouvaient être regroupé dans deux grandes familles (x1, x2, x3, x4 et y1, y2, y3, y4) et que les couples x1/y1, x2/y2, x3/y3 et x4/y4 pouvaient rentrer dans un même moule sans trop de contorsions.

    Cela simplifiait mon interface et j'y gagnais en généricité. Une interface (ma classe A) en I1, I2, I3, I4 étant moins spécialisé et plus générale que l'idée initiale en I1...I8, elle pourra sans problème convenir à de futur 'compiler' que j'aurai à écrire à l'avenir (tandis qu'une interface en I1...I8 aurait facilement pu aboutir à de nombreux accesseurs inutilisé, suivant le type compileurs que j'aurai à créer dans le futur).

    Et comme, dans le fond, les données x1...x4 et y1...y4 représentes deux jeux de données assez différentes, on peut considérer qu'il peut y avoir deux raisons distinctes qui pourraient pousser à modifier un jour cette classe (pour en revenir au concept de responsabilité unique).
    Donc j'ai très rapidement exclus de faire sortir ces données par une même classe.

    Seule problème: je savais que je ne pouvais pas faire cohabiter deux objets B et C en même temps (à cause d'un membre unique).
    D'où mon idée de faire dériver C de B, afin d'avoir un unique objet qui aurait été à la fois un B et à la fois un C.

    Mais je me suis finalement heurté à la fonction 'void foo(A*)', incapable de considérer un objet C comme étant un B.

  12. #12
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut
    Merci pour ton explication, koala01, que j'ai lu très attentivement.
    Je suis plutôt en accord avec tout ça. Ca m'a permis de mettre des mots précis, sur des design que je cherche souvent à atteindre (inconsciemment ou non), et me permettront de les suivre plus scrupuleusement à l'avenir, ou de m'aider à tranché entre plusieurs approches possibles.

    Il y a juste sur les variables et fonctions inutilisé où j'ai un peu tiqué. J'ai souvent tendance à définir des interfaces assez complètes, même si je ne compte pas tous utiliser immédiatement. Mais jamais de méthodes trop spécialisé (toujours affaire de compromis, mais je ne pense pas aller trop loin). Mon but est de faire des composants réutilisables et générique, et de remonter les spécialisations le plus haut possible dans le code.

    Donc l'ESR s'applique plutôt d'un point de vue de l'utilisateur et des fonctionnalité finale rendu par le logiciel, pas de l'implémentation interne? Donc définitivement, ce n'est pas l'ESR qui justifiait de scinder mes classe B et C en B/R et C/R.

    Pour ma question original, c'est vraiment un point technique que j'attendais à la base, pas conceptuel.
    Le fameux
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     
    foo(A* obj);
     
    C c;
     
    foo( &c );  //doit imprimer "c" 
    foo( (B*)&c );  //doit imprimer "b"
    Pour mon projet en particulier: en gros, j'ai une donnée à analyser, je doit en extraire des éléments clé et les exposé au reste de mon programme à travers l'interface que j'ai défini (plus tard, j'aurai très certainement d'autres ressources très différentes à analyser, mais elles devraient pouvoir rentrer dans le moule que j'ai défini, il me parait suffisamment générique pour ça).
    Pourquoi j'ai choisi de scinder cette analyse/fourniture de donnée en deux classe séparé: je l'ai expliqué dans mon message précédent: deux famille de données très différentes à extraire et j'ai préféré que chacune des classe se concentre sur l'extraction son type de données en particulièr.

  13. #13
    Membre habitué
    Profil pro
    Inscrit en
    Mars 2008
    Messages
    112
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Mars 2008
    Messages : 112
    Points : 165
    Points
    165
    Par défaut
    Et je tiens à utiliser la POO pour pouvoir utiliser le polymorphisme et pouvoir permuter mes extracteurs de donnés à travers mon interface, sans que mes futurs classes utilisatrice ne sachent quel générateur elle sont en train d'utiliser.

    Y-a-t-il une autre approche que la POO pour arriver à ça?

  14. #14
    Expert éminent sénior
    Homme Profil pro
    Analyste/ Programmeur
    Inscrit en
    Juillet 2013
    Messages
    4 629
    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 629
    Points : 10 554
    Points
    10 554
    Par défaut
    Après 15 tubes de doliprane j'ai toujours autant mal au crane : tes explications sont imbuvables

    Mais à vue de pif, je dirais que ce que tu veux faire c'est le patron de conception Stratégie.

    La strategie (strategy) serait l'interface de tes générateurs (et ce serait l'action generate et non pas execute)
    Le contexte (context) serait ton gros paquet de données (du moins la classe qui permet de le manipuler)

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

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 612
    Points : 30 612
    Points
    30 612
    Par défaut
    Citation Envoyé par wistiti1234 Voir le message
    Il y a juste sur les variables et fonctions inutilisé où j'ai un peu tiqué. J'ai souvent tendance à définir des interfaces assez complètes, même si je ne compte pas tous utiliser immédiatement.
    En Xtrem-Programming -- on pourrait parler d'une "philosophie", tirée des méthodes Agile, qui tend à rendre le développement plus évolutif -- il existe l'"exression" YAGNI, qui est l'abréviation de You Ain't Gonna Need it (tu n'as pas encore besoin de cela).

    On utilise cette expression pour essayer de faire comprendre à notre interlocuteur qu'il "perd son temps" à développer "quelque chose" dont il n'a pas encore besoin

    Et là, tu as largement mérité qu'on te fasse la remarque : YAGNI!!!.

    D'abord parce qu'il faut savoir que ta mémoire est souvent ton pire ennemi. Pour te donner une idée, tu n'imagine pas le nombre de fois ou, comme tout bon développeur professionnel, je me suis retrouvé à la fin d'une dure journée de labeur en étant tout juste capable (sans aller voir dans les différents logs, s'entend) de te dire que "aujourd'hui, j'ai corrigé trois bugs et développé une nouvelle fonctionnalité", en étant dans l'incapacité totale de te dresser la liste des bug corrigés et de la fonctionnalité développée. Il va sans dire que j'aurais été incapable de retrouver les lignes de code corrigées et / ou écrites à cette occasion

    Alors, imagine ce qui risque de se passer si tu décides de créer une classe ou d'ajouter dix fonctions à une interface alors que tu n'en as pas le besoin immédiat, simplement parce que tu te dis que "j'en aurai peut être besoin dans trois mois ou dans six mois".

    D'ici à ce que tu aies besoin de ces fonctions ou de cette classe, il y a 99 chances sur 100 (et je suis encore gentil en te laissant une chance que ce ne soit pas le cas ) que tu aies complètement oublié que ces fonctions ou que ces classes existent.

    Tu te retrouveras donc, avec une quasi certitude, à développer (encore une fois) la classe ou les fonctions en question. Avec un résultat aberrant: tu te retrouveras avec deux classe ou 10 paires de fonctions dont les objectifs sont fondamentalement identiques, et dont les seules différences porteront sur le nom, le type des paramètres attendus (ou leur nombre) ou le type de la valeur de retour.

    Dans le meilleur des cas, si tu te souviens que "oui, mais ces fonctions ou cette classe a déjà été créée", tu te rendras compte que la classe n'expose pas du tout l'interface qui correspond à l'usage que tu en as réellement et que le type des paramètres attendus par les fonction et / ou la valeur de retour de celles-ci ne correspondent absolument pas à tes besoins.

    Dans tous les cas, tu n'auras donc réussi qu'une seule chose : perdre ton temps à développer des fonctionnalités totalement inutiles parce que tu n'avais pas encore en main toutes les informations qui t'auraient permis de faire directement les choses de manière correcte.


    Mais jamais de méthodes trop spécialisé (toujours affaire de compromis, mais je ne pense pas aller trop loin).
    Non, il n'y a aucun compromis : chaque fonctionnalité ne doit prendre qu'une seule responsabilité, point à la ligne. La responsabilité peut être complexe, mais alors, la fonctionnalité doit systématiquement déléguer les "petits bouts" moins complexes de cette responsabilité à... d'autres fonctionnalités spécifiquement créées pour prendre ces "responsabilités moins complexe" en charge.

    Mon but est de faire des composants réutilisables et générique, et de remonter les spécialisations le plus haut possible dans le code.
    Justement! Pour que tes composants puissent être réutilisables et générique, il faut que chaque composant ne prenne qu'une responsabilité en charge.

    Imaginons trente seconde que je sois occupé à développer un robot ménager. J'ai déterminé qu'une de ses "journées type" serait composée de différentes actions, dans l'ordre
    1. faire le café,
    2. préparer le petit-déjeuner
    3. faire la lessive
    4. préparer le diner
    5. faire la vaisselle
    6. faire les courses
    7. préparer le souper
    8. border les enfants
    9. sortir le chien
    10. se mettre en charge pour la nuit

    Je pourrais parfaitement envisager de tout faire dans une unique "méga fonction" qui serait 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
    void workingDay(){
         /* XXX instructions pour faire le café
          * suivies de YYY instruction pour préparer le petit déjeuner
          * puis de ZZZ instructions pour faire la lessive
          * encore suivies de AAA instructions pour préparer le diner
          * puis de BBB instructions pour faire la vaisselle
          * de CCC instructions pour faire les courses
          * de DDD instructions pour préparer le souper
          * de EEE instructions pour sortir le chien
          * de FFF instructions pour border les enfants
          * et enfin de GGG instructions pour se mettre en charge
          */
    }
    Nous pourrions croire que tout cela ne représente qu'une seule responsabilité, vu qu'il s'agit d'un emploi du temps journalier. Et pourtant: Peu après avoir rendu mon robot disponible sur le marché, j'ai un client qui m'appelle et me dit qu'il préférerait que mon robot borde les enfants avant d'aller sortir le chien.

    je me retrouverais donc à écrire une fonction 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
    void workingDay2(){
         /* XXX instructions pour faire le café
          * suivies de YYY instruction pour préparer le petit déjeuner
          * puis de ZZZ instructions pour faire la lessive
          * encore suivies de AAA instructions pour préparer le diner
          * puis de BBB instructions pour faire la vaisselle
          * de CCC instructions pour faire les courses
          * de DDD instructions pour préparer le souper
          * de FFF instructions pour border les enfants
          * de EEE instructions pour sortir le chien
          * et enfin de GGG instructions pour se mettre en charge
          */
    }
    et je rajoute un menu permettant de choisir l'emploi du temps du robot.

    Encore un peu plus tard, j'ai un autre client qui m'appelle et me dit qu'il a l'habitude de sortir le chien tôt le matin et qu'il voudrait donc que le robot sorte le chien avant même de préparer le café.

    J'en arriverais donc "naturellement à écrire une troisième fonction 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
    void workingDay2(){
         /*  EEE instructions pour sortir le chien 
          * suivies de XXX instructions pour faire le café
          * de YYY instruction pour préparer le petit déjeuner
          * puis de ZZZ instructions pour faire la lessive
          * encore suivies de AAA instructions pour préparer le diner
          * puis de BBB instructions pour faire la vaisselle
          * de CCC instructions pour faire les courses
          * de DDD instructions pour préparer le souper
          * de FFF instructions pour border les enfants
          * et enfin de GGG instructions pour se mettre en charge
          */
    }
    Et les problèmes ne font que commencer, parce que le propriétaire d'un chien m'appelle peu après pour m'engueuler parce que son chien, qui ne sort pas assez longtemps, fait systématiquement pipi sur son beau tapis persan tous les jours depuis qu'il est sorti par le robot, que je dois donc faire en sorte que la balade du chien dure au moins dix minutes de plus.

    A ce jour, je vais devoir aller corriger trois fonctions pour résoudre ce problème, et tu peux être sur que... j'oublierai d'en corriger au moins une -- voire deux -- dans le lot

    Imaginons maintenant que, au lieu de ne créer qu'une seule grande fonction, je découpe l'emploi du temps pour lui donner la forme de "petites fonctions simples" proches des
    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
    void preparerLeCafe(){
         /* xxx instructions pour préparer le café  */
    }
    void preparerLePetitDejeuner(){
        /* YYY instructions pour préparer le petit-déjeuner */
    }
    void faireLaLessive(){
        /*...*/
    }
    void sortirLeChien(){
        /* ... */
    }
    void preparerLeDiner(){
        /* ... */
    }
    void preparerLeSouper(){
        /* ... */
    }
    void faireLesCourses(){
        /* ... */
    }
    void faireLaVaisselle(){
        /* ... */
    }
    void borderLesEnfants(){
        /* ...*/ 
    }
    void seMettreEnCharge(){
        /* ... */
    }
    Les trois fonctions décrivant l'emploi du temps de mon robot pourraient se contenter d'appeler les fonctions bien spécifiques créées sous une forme proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void workingDay(){
        faireLeCafe();
        preparerLePetitDejeuner();
        faireLaLessive();
        preparerLeDiner();
        faireLaVaisselle();
        faireLesCourses();
        preparerLeSouper();
        sortirLeChien();
        borderLesEnfants();
        seMettreEnCharge();
    }
    Et, s'il faut adapter l'emploi du temps par la suite (en faisant exécuter certaines actions avant d'autres), je n'aurai "qu'à" ... changer l'ordre dans lequel ces fonctions sont appelées.

    L'avantage est absolument énorme, car, si un client vient un jour se plaindre que le bacon de son petit déjeuner est systématiquement brulé, il suffira que je corrige la fonction preparerLePetitDejeuner (afin de corriger le temps de cuisson du bacon) pour que ... tous les emplois du temps que j'aurai pu créer prennent systématiquement la correction en compte. Et cela, même si je devais me retrouver au final avec 150 ou "300 journées" type différentes!

    La "réutilisabilité" ne peut s'obtenir qu'avec des composants suffisamment simples que pour pouvoir effectivement être utilisés n'importe quand
    Donc l'ESR s'applique plutôt d'un point de vue de l'utilisateur et des fonctionnalité finale rendu par le logiciel, pas de l'implémentation interne? Donc définitivement, ce n'est pas l'ESR qui justifiait de scinder mes classe B et C en B/R et C/R.
    ESR??? KESSAKO??? peut-être veux tu parler du SRP ???

    L'utilisateur du logiciel final n'a ABSOLUMENT RIEN A VOIR avec la grosse majeure partie des décisions que tu prend lors du développement du logiciel. Tout au plus, il sera content de disposer de l'interface graphique lui permettant d'utiliser le logiciel de la manière dont ... le client (celui qui aura décidé de la manière dont le logiciel doit fonctionner) aura décidé qu'il devra l'utiliser

    La plupart des décisions que tu prendras, en tant que développeur, n'auront jamais qu'un seul but : trouver le moyen d'exprimer des besoins (et d'exprimer la manière de s'y prendre pour les remplir) de manière suffisamment simple que pour qu'ils puissent être compris par "quelque chose d'aussi bête qu'un ordinateur".

    Et, pour que cette description soit "aussi facile" à faire évoluer, à corriger et à entretenir que possible, il n'y a pas trente six solutions : la conception doit être absolument parfaite, surtout lorsque tu utilises un langage comme le C++.
    Pour ma question original, c'est vraiment un point technique que j'attendais à la base, pas conceptuel.
    Le fameux
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     
    foo(A* obj);
     
    C c;
     
    foo( &c );  //doit imprimer "c" 
    foo( (B*)&c );  //doit imprimer "b"
    C'est bien là qu'est toute ton erreur, qui est triple dans le sens ou
    1. tu veux transposer en C++ une technique issue d'un langage totalement différent (le C)
    2. tu veux mentir au compilateur en lui faisant passer une donnée d'un type tout à fait particulier pour une donnée d'un type qu'elle n'est absolument pas
    3. tu semble considérer que le langage, le code et la technique sont la partie importante du problème, alors que ces trois éléments ne sont que des outils et que l'écriture du code n'est, en définitive, qu'un long et fastidieux travail de traduction des décisions prises lors de la conception

    Je m'explique:

    (1) On dit, avec raison, que le C++ hérite directement du C. Mais on dit la même chose du francais par rapport au latin de Jules Cesar. Or, tu sera d'accord avec moi quand je dis que le français est une langue totalement différente du latin. Il en va exactement de même pour la relation qui existe entre le C++ et le C

    (2) le transtypage, quelle que soit la forme qu'il prend, a toujours pour objectif de "mentir" au compilateur en lui imposant de considérer une donnée pour "autre chose" que ce qu'elle est réellement, ce qui nous amène naturellement au point suivant:

    (3) Le langage et le compilateur (dans le cas du C++) ou l'interpréteur qui y est associé ne sont que des outils, qui n'ont pas d'autres choix que d'accepter "sans discuter" les décisions que nous leur imposons au travers du code que nous écrivons.

    Si on force le compilateur (au travers d'un transtypage, par exemple), à considérer la voiture de bob comme s'il s'agissait d'un arbre, il n'a absolument pas d'autre choix que de dire "oui chef, bien chef"; peu importe (pour le compilateur) que cette décision ait du sens ou non: si on a décidé qu'il en serait ainsi, c'est qu'il devait en être ainsi; Point à la ligne.

    Le problème, c'est que la technique nous offre énormément de libertés nous permettant de faire prendre des vessies pour des lanternes par le compilateur et que beaucoup des décisions allant dans ce sens vont systématiquement provoquer des catastrophes sans nom, à court ou à long terme.

    Le seul moyen d'éviter les catastrophes est donc d'appuyer toutes les décisions que nous prendrons sur des conception absolument "sans tache".

    Alors, il y a -- effectivement -- des solutions techniques permettant de faire ce genre de chose. Mais je peux te garantir, avec une certitude de près de 100%, que ce n'est pas ce que tu veux, car, dans l'état actuel de ma compréhension de ton souhait, cela t'apporterait bien plus de problèmes que les solutions que tu en espères

    Pour mon projet en particulier: en gros, j'ai une donnée à analyser,
    Quel type de donnée
    je doit en extraire des éléments clé
    Pourquoi
    et les exposé au reste de mon programme à travers l'interface que j'ai défini
    Sauf que, jusque là, on n'a pas la moindre idée de l'interface que tu veux exposer
    (plus tard, j'aurai très certainement d'autres ressources très différentes à analyser, mais elles devraient pouvoir rentrer dans le moule que j'ai défini, il me parait suffisamment générique pour ça).
    Dans l'état actuel de ce que j'en sais (c'est à dire : pas grand chose, je dois bien l'avouer), permet moi d'en douter

    Pourquoi j'ai choisi de scinder cette analyse/fourniture de donnée en deux classe séparé: je l'ai expliqué dans mon message précédent: deux famille de données très différentes à extraire et j'ai préféré que chacune des classe se concentre sur l'extraction son type de données en particulièr.
    Extraire deux types de données très distincts (ou deux familles de données très distinctes) à partir d'une interface unique c'est ... peu recommandé dirons nous.

    Enfin, extraire les données peut se faire "assez facilement", exposer les données extraites, c'est une toute autre paire de manche
    Citation Envoyé par wistiti1234 Voir le message
    Et je tiens à utiliser la POO pour pouvoir utiliser le polymorphisme et pouvoir permuter mes extracteurs de donnés à travers mon interface, sans que mes futurs classes utilisatrice ne sachent quel générateur elle sont en train d'utiliser.
    Alors, pourquoi te tourner vers le C++ et non vers le java, qui t'obligera à travailler en orienté objet
    Y-a-t-il une autre approche que la POO pour arriver à ça?
    En C++ Et comment donc!

    C++ est un langage qui permet sans aucun problème de mélanger trois paradigmes:
    • le procédural "pur et dur" (le paradigme proposé par C, par exemple)
    • l'orienté objets "classique" (les classes, l'héritage public et le polymorphisme d'inclusion)
    • et le générique

    Si bien que tu peux tout aussi bien transmettre une classe (POO) à une fonction libre qu'à une fonction template, ou décider de faire hériter une classe générique (template) d'une classe "normale", voire l'inverse.

    Le gros avantage du C++ est la totale liberté qu'il te laisse à ce sujet, pour autant que tu puisse justifier chacune des décisions que tu prendras.

    Mais, je le répète : pour t'orienter vers la solution réellement adaptée à ton problème, il faudrait que nous en sachions un peu plus sur tes besoins réels. Autrement, cette discussion ne pourra être que théorique, et nous courrons un risque énorme de passer à coté de celle qui s'adaptera le mieux à tes souhaits
    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

  16. #16
    Modérateur

    Avatar de Bktero
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Juin 2009
    Messages
    4 481
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 36
    Localisation : France, Loire Atlantique (Pays de la Loire)

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Juin 2009
    Messages : 4 481
    Points : 13 679
    Points
    13 679
    Billets dans le blog
    1
    Par défaut
    Citation Envoyé par wistiti1234 Voir le message
    C est un B, alors utilisons le comme un B (ou en tant que B), lorsque j'en ai envie. C'est bien le rôle du bon vieux cast C. Et il n'y a aucun risque de violation mémoire, puisque C est bien un B.
    Woh woh woh ! Arrête ton char Ben Hur !

    Un "bon vieux cast C", qu'on devrait appeler un "gros cast C dégeulasse" n'est pas une sémantique "est un". Exemple : quand tu castes un int en float, un int n'est pas un float, tu veux juste le considérer comme tel ! Faire que C hérite de B force C être un B. Tu peux faire ça en C++ :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    struct Voiture {};
    struct Pigeon : public Voiture {};
    Hop ! Un pigeon est une voiture.

    Sauf que moi je te parle de conception, pas d'implémentation. Il faut s'assurer que la relation bien "est un" d'un point de vue conceptuel. Si c'est vrai, alors on utilise l'héritage public pour implémenter cette relation.

    Je ne dis pas que ton C n'est pas un B conceptuellement, je te dis qu'il faut faire très attention avant de l'affirmer. Partager du comportement n'est pas une raison suffisante.

    Perso, il m'arrive extrêmement souvent de dériver une classe abstraite pour fournir un comportement particulier dans une classe concrète. En revanche, il m'arrive assez peu de dériver une classe concrète. Et souvent, dans un tel cas, je me rend compte qu'en fait il me faut une classe abstraite intermédiaire pour regrouper du comportement.

Discussions similaires

  1. Petit projet pour héritage & polymorphisme
    Par proginfme dans le forum Débuter avec Java
    Réponses: 10
    Dernier message: 21/02/2015, 21h41
  2. [débutant] héritage, polymorphisme, etc !
    Par remsrock dans le forum C#
    Réponses: 4
    Dernier message: 31/10/2008, 12h33
  3. Problème d'héritage - polymorphisme
    Par antoine2405 dans le forum Langage
    Réponses: 16
    Dernier message: 28/04/2008, 13h51
  4. Réponses: 6
    Dernier message: 08/02/2008, 14h58
  5. [Héritage] [Polymorphisme] Question de débutant ?
    Par TSnarfK dans le forum Langage
    Réponses: 9
    Dernier message: 12/09/2006, 15h39

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