Partage
  • Partager sur Facebook
  • Partager sur Twitter

Bonne pratique en POO

Utilisation des méthodes d'objets dans le constructeur

    18 janvier 2018 à 10:41:47

    Bonjour,

    Depuis longtemps je me suis posé la question de si on devais utiliser ou non les méthodes d'objets dans un constructeur.
    Je sais que cela est autoriser par bien des langages, mais en C++, en activant une option (dont je ne me rappel plus le nom) lors de la compilation avec g++, il est déconseiller d'écrire un code comme celui-ci :

    // Doit-on utiliser tous de même la liste d'initialisation ?
    Lifebar::Lifebar(int max, int min): m_max(0), m_min(0)
    {
        this->setMax(max);
        this->setMin(min);
    
        /*
         * Que je peut aussi écrire :
         * 
         * setMax(max)
         * setMin(min)
         */
    }
    
    void Lifebar::setMax(int max)
    {
        m_max = max;
    }
    
    void Lifebar::setMin(int min)
    {
        m_min = min;
    }

    Je sais que l'utilisation du mot clé "this" n'est pas recommandé dans le constructeur car this est un pointeur sur l'instance d'objet, et dans le constructeur, il n'y a pas encore d'instance à proprement parler (c'est le but d'un constructeur).

    Cependant, en retirant le mot clé this, est-ce que le compilateur fait quand même appel implicitement à lui ?

    Et si oui, il parait évident qu'il ne faut pas faire appel aux méthodes dans le constructeur. Cependant, je trouve qu'on perd une partie de l’intérêt des accesseurs/mutateurs, car on duplique le code de vérification à 2 endroits différents dans le code (dans le code supposons que je ne veux pas que min et max soit inférieur à 1).

    J'aimerais avoir l'avis de d'autres développeurs afin de mieux comprendre ce genre de situations.

    Merci d'avance, et bonne journée ! :)

    -
    Edité par peridot69 18 janvier 2018 à 10:42:28

    • Partager sur Facebook
    • Partager sur Twitter
    printf("Les rudiments de la programmation ? Nan mais Hello quoi !");
      18 janvier 2018 à 10:54:05

      Lu'!

      Ton constructeur devrait être écrit :

      LifeBar::Lifebar(int max, int min) : //on pourrait se dire que "unsigned" serait plus adapté
        m_max(max),
        m_min(min)
      {
      
      }

      TOUJOURS utilise la liste d'initialisation à moins de ne pas pouvoir.

      Pour le mot-clé this, il n'est utile que pour transmettre une référence à l'objet à travers un appel de fonction, résoudre une ambiguité n'est pas un cas valide : si c'est ambigu le nommage des variables est mauvais. L'instance est implicite quand on est dans le code d'une classe.

      peridot69 a écrit:

      Et si oui, il parait évident qu'il ne faut pas faire appel aux méthodes dans le constructeur. Cependant, je trouve qu'on perd une partie de l’intérêt des accesseurs/mutateurs, car on duplique le code de vérification à 2 endroits différents dans le code (dans le code supposons que je ne veux pas que min et max soit inférieur à 1).

      Ce qui est encore mieux c'est de penser en objet. Un peu de lecture récente :

      https://openclassrooms.com/forum/sujet/accesseurs-quelle-utilite

      Et tous les messages suivants.

      • Partager sur Facebook
      • Partager sur Twitter

      Posez vos questions ou discutez informatique, sur le Discord NaN | Tuto : Preuve de programmes C

        18 janvier 2018 à 11:06:12

        En effet, j'aurais pu aussi utiliser un unsigned int, seulement si un jour je veux que les min et max puissent être négatifs (supposons que sa pourrais être le cas avec de fortes contraintes d’évolutivité ou un patron qui changerais d'avis toutes les 5 minutes).

        Cependant, dans ce cas-ci on peut se poser la question, mais concernant une chaine de caractère la je n'aurais pas pu contourner le problème avec simplement un changement de type. Au final la question de la vérification se poserais toujours.

        Je devrais écrire un code de vérification dans le constructeur ET dans les accesseurs. Ce que tu me dis c'est que je dois effectivement dupliquer ce code la ?

        EDIT : D'ailleurs dans mon code écrit plus haut, j'utilise effectivement la liste d'initialisation malgrés tous avec des valeurs que j'estime valide (correspondant à mon cas en particulier) et aussi neutre que possible, bien que niveau logique cela me pose un problème j'avais déjà bien conscience que la liste d'initialisation doit être souvent utilisée.

        -
        Edité par peridot69 18 janvier 2018 à 11:08:52

        • Partager sur Facebook
        • Partager sur Twitter
        printf("Les rudiments de la programmation ? Nan mais Hello quoi !");
          18 janvier 2018 à 11:12:21

          Salut,

          Comme l'a si bien dit Ksass ` Peuk : il faut toujours utiliser la liste d'initialisation, à moins de ne pas pouvoir.

          A partir de là, il n'y a -- a priori -- aucun problème à  utiliser une fonction membre de la classe dans le constructeur, vu que les données membres ont  été initialisées.

          Sauf pour les fonctions membres virtuelles, dont le comportement correspondra à l'élément en cours de création: si tu es dans le constructeur de la classe de base, ce sera le comportement de la fonction virtuel tel qu'il est défini pour la classe de base qui sera appelé, vu que l'ordre de construction se fait de la classe de base vers la classe la plus dérivée. 

          Et, bien sur, comme il y a toujours "un risque" pour qu'un fonction virtuelle soit déclarée comme une fonction virtuelle pure dans la classe de base, le compilateur refusera purement et simplement que tu y fasse appel ;)

          Ceci étant dit, tu ne devrais -- a priori -- pas avoir besoin d'appeler des fonctions (qu'il s'agisse de fonctions membre ou de fonctions libres) dans ton constructeur, vu que les données membres sont sensées avoir été définies aux bonnes valeurs (au travers de la liste d'initialisation), et que le seul rôle du constructeur est d'initialiser l'instance de ta classe de telle sorte qu'elle soit directement utilisable ;)

          Par contre, la présence d'un mutateur (set_XXX) est loin d'être une bonne pratique en POO, car elle rend l'utilisateur de la classe responsable du calcul des valeurs à donner aux données membres.  Or, les données membres sont généralement soumises à certaines obligations (on parle de "préconditions" ou d'"invariants") qui doivent être respectées, et la loi de finagle aidant, on cours le risque que l'utilisateur oublie de prendre l'une ou l'autre de ces obligations en compte ;)

          • Partager sur Facebook
          • Partager sur Twitter
          Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
            18 janvier 2018 à 11:21:31

            D'accord, je comprend mieux pour la liste d'initialisation et que l'on peut, sous certaines conditions utiliser une méthode dans un constructeur. En revanche je comprend moins en quoi un mutateurs est une mauvaise pratique en POO ?

            Car la responsabilité du calcul ET de la vérification n'est pas nécessairement délégué à l'utilisateur de la classe si on écrit quelque chose comme cela :

            void Lifebar::setMax(int max)
            {
                // Vérification de la valeur donnée par l'utilisateur de la classe
                if (max < 1)
                {
                    throw new String("Error")
                }
            
                m_max(max);
            }


            Ou alors j'ai très mal compris le message ?

            De plus, la liste d'initialisation ne permet pas de vérifier si la valeur donnée par l'utilisateur de la classe est correcte. Exemple :

            // lifebar.cpp
            
            Lifebar::Lifebar(int min, int max) : m_min(min), m_max(max)
            {
                // Aucune vérification supplémentaire ?
            }
            
            // main.cpp
            
            // L'utilisateur peut renseigner des valeurs négativse que je ne veux bien évidemment pas
            Lifebar lifebar(-1, -1);

            -
            Edité par peridot69 18 janvier 2018 à 11:25:36

            • Partager sur Facebook
            • Partager sur Twitter
            printf("Les rudiments de la programmation ? Nan mais Hello quoi !");
              18 janvier 2018 à 12:26:00

              As tu lu ma première interventions dans le lien proposé par Ksass ` Peuk?  Elle devrait donner toutes les réponses aux questions que tu  te pose ;).  Allez, je la recopie ici:

              j'ai écrit:

              Salut,

              Le truc, c'est qu'il faut envisager de concevoir les classes comme des fournisseurs de services, et non comme des fournisseurs de données. Les données membres d'une classe n'étant alors présentes que... pour permettre à la classe de fournir les services que l'on attend de sa part.

              Les accesseurs et mutateurs sont apparus parce qu'il a fallu "un peu de temps" avant de se rendre compte de cette erreur (pour beaucoup, au début de l'orienté objet, une classe n'était ... qu'un agrégat de données).

              La notion d'accesseurs (get_truc) peut avoir un intérêt, à partir du moment où l'on peut considérer le fait de fournir l'accès à une information fait partie des services que la classe doit pouvoir rendre.

              Ainsi, si tu veux concevoir une classe "personne", tu vas partir du principe qu'elle va devoir rendre "un certain nombre de services", comme

              • pouvoir répondre à la question "quel est ton nom?"
              • pouvoir répondre à la question "quel est ton prénom?"
              • pouvoir obéir à l'ordre "déménage à <telle adresse>"
              • un ou deux autres services supplémentaires

              Pour que la classe puisse répondre aux deux questions indiquées, il faut bien lui fournir des fonctions qui -- on peut l'imaginer -- se contenteront sans doute de renvoyer la valeur d'une donnée particulière, et qui sont donc des accesseurs "purs et durs".

              De plus, il faut se dire que, même les accesseurs ne sont intéressant que s'ils permettent, effectivement, de répondre à un besoin particulier. Ainsi, si l'on voulait concevoir une classe voiture, il ne faut pas vraiment avoir fait d'études de mécaniques pour savoir qu'elle va disposer d'un réservoir à carburant. Mais, à moins d'être mécanicien et d'avoir une bonne raison pour changer le réservoir, nous n'avons absolument aucune raison de vouloir y accéder à partir de la voiture.

              Par contre, on s'attend à ce que la voiture nous fournisse "un certain nombre" de services relatifs à la présence de ce réservoir, comme

              • le fait de pouvoir le remplir (au travers de la trappe à carburant)
              • le fait de connaître son niveau de remplicage (au travers de la jauge à carburant)
              • (éventuellement) le fait de connaitre le nombre de kilomètres que l'on peut encore effectuer (sur base d'une évaluation de la quantité de carburant dont le réservoir dispose et de la consommation actuelle)
              • ... d'autres informations auxquelles je ne pense pas maintenant

              On se rend donc compte qu'il n'y a absolument aucune raison de proposer une fonction get_reservoir(), car la présence même du réservoir est ... rendue "transparente" grâce aux services qui ont trait à cette présence.

              Les choses sont, par contre, beaucoup plus claires pour ce qui concerne les mutateurs (set_xxx), car, de manière générale, c'est toujours une très mauvaise idée de les fournir:

              D'abord, il n'y aurait aucune raison pour qu'une personne nommé Jean Durant n'en vienne à s'appeler Jaques Dupont. Et, bien sur, si tu n'as déjà pas fourni d'accesseur sur une donnée (comme c'est le cas pour le réservoir), tu n'as de toute évidence aucune raison de fournir un mutateur sur cette donnée.

              Mais en plus, il faut comprendre que les données d'une classe sont systématiquement soumises à certaines règles qui devront être respectées si l'on veut que la classe puisse agir "comme on est en droit de s'y attendre". Or, la présence d'un mutateur va obliger l'utilisateur de notre classe à prendre lui-même "toutes les précautions" pour que la valeur (qu'il devra calculer "par lui-même") qu'il souhaite donner à notre classe soient... cohérentes.

              La loi de Finagle aidant, tu peux te dire qu'il y a "toutes les chances" pour qu'il oublie de prendre une des règles en compte, à un moment ou à un autre. Et ce, même si ces règles sont écrites en gros et en fluo juste à coté de lui ;)

              Or, comme disait l'autre

              Il faut faire en sorte de rendre l'utilisation correcte de notre classe aussi simple que possible, et de rendre l'utilisation incorrecte de celle-ci aussi difficile que possible

              Et, pour ce faire, il n'y a pas de miracle: nous devons fournir des fonctions qui se chargeront de vérifier que "toutes les règles" sont respectées avant de modifier une donnée de la classe

              Ainsi, nous préférerons avoir une fonction move(distanceX, distanceY) ou moveTo(newPosition) qui pourra s'assurer que la position de destination ne fait pas entrer notre personnage dans un mur qu'une fonction set_position(newPosition) qui obligera l'utilisateur de la classe à calculer lui-même la position de destination, au risque qu'il oublie de prendre en compte le fait qu'elle est occupée par un feu, un mur ou n'importe quel autre obstacle.

              Ceci étant dit: toute donnée qui est accessible à l'utilisateur d'une classe risque d'être modifiée de manière incongrue par l'utilisateur, et cela peut poser pas mal de problèmes (je viens de les expliquer, je ne vais pas recommencer ;) )

              C'est la raison pour laquelle il est effectivement préférable que les données soient dans l'accessibilité privée.

              On peut d'ailleurs aller plus loin en disant que les données ne peuvent être que dans l'accessibilité privée, y compris si elle doivent être accessibles aux classes dérivées. Seules des fonctions devraient être présentes dans l'accessibilité protégée ou publique (selon les besoins) ;)

              Des fonctions accelerate(diffVitesse) / decelerate(diffVitesse), move(diffX, diffY), receive(montant), transfert(montant, compteBeneficiaire) correspondent à des services que tu peux attendre de la part de différents objets.  Et ces fonctions prennent elles-même les différents calculs en charge (pas de risque d'oublier les frais de gestion, qui peuvent varier énormément soit dit en passant ;) )

              Les mutateurs équivalents (setVitesse, setPosition, setSolde) sont, non seulement, beaucoup moins précis sur "ce qui est effectivement fait", mais, en plus, tu ne peux (en tant qu'utilisateur) avoir aucune garantie quant aux vérifications qui seront effectuées.

              Or, a priori, une fonction setXXX elle se contente de...  définir la valeur de XXX (c'est en tout cas ce que dis le nom, hein !).  Et puis, elle implique que ... c'est l'utilisateur qui doit s'occuper du calcul.  Et comme l'utilisateur est -- par nature -- un imbécile distrait, on court le risque qu'il fasse... un mauvais calcul  ;)

              -
              Edité par koala01 18 janvier 2018 à 12:26:32

              • Partager sur Facebook
              • Partager sur Twitter
              Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                18 janvier 2018 à 14:38:22

                Je vois très bien ce que tu veux dire (en effet j'avais lu ton post sur l'autre sujet), cependant ce que j'en retire (outre le cas d'exemple du moteur de voiture que tu nous donne qui me parait évident, je ne donne pas accès forcément à toutes mes données, même au travers d'accesseurs/mutateurs) c'est que c'est surtout le nommage setXXX qui te pose problème, qui indique à l'utilisateur que la méthode ne devrais en théorie que modifier la valeur d'une donnée membre de l'instance, c'est un point de vue qui se défend en effet.

                Cependant, je vois mal pourquoi le setXXX serais en toute logique "interdit" de faire plus qu'une simple modification de valeur sans effectuer de vérifications, surtout si on l'indique dans une documentation ou autre ? Bien que je comprenne qu'au sens stricte un nommage pareil ne devrais pas le faire, mais si on indiquais tous fonctionnement interne d'une classe à l'utilisateur, la POO perdrais une partie de sont intérêt. l'encapsulation est la pour sa. Pour cacher des comportements en interne.

                Dans mon cas précis, ma classe, telle que je l'ai définie à besoin que l'utilisateur puisse définir "en dur" une valeur précise pour le niveau minimum et maximum de la barre de vie, pourquoi ? Parce que le besoin exprimer vis à vis de ma classe est comme sa.

                Après dans mon post je n'ai pas préciser les autres méthodes que je pourrais avoir. Je peux très bien avoir une méthode permettant d'incrémenter le minimum/maximum ou de les décrémenté (à l'image des fonctions exemples accelerate/decelerate). Ou encore les besoins évolutif de ma classe qui peuvent m'amener à ajouter/supprimer des méthodes.

                Les règles d'utilisations générales d'une classe doivent, à mon sens être préciser dans une documentation, accessible par l'utilisateur de celle-ci. S'il utilise une méthode contenant des paramètres, alors je dois lui indiquer les contraintes associés, et qu'en cas de non respect de celles-ci, il y auras des conséquences, telles que des levées d'exceptions ou des retours négatifs ect...

                En terme de réponse par rapport au début en effet je demandais surtout "Est-ce recommander d'utiliser les méthodes d'une classe dans le constructeur de celle-ci ?". J'ai montré l'exemple de mutateurs, mais j'aurais aussi pu montrer une méthode permettant d'incrémenter les valeurs maximum/minimum, cela aurais été la même chose. Mais la discussion sur l'utilisation (pas l’intérêt, pas dans mon cas, personnellement je suis convaincu qu'aucune données ne devrais être en public ou protected) est très intéressante tous de même pour moi.

                Justement, dans mon cas, concernant mon cas d'utilisation, à la place d'un setXXX que mettrais tu ? Sachant que la méthode doit permettre d'indiquer la nouvelle valeur de la données, et qu'elle doit faire des vérifications (sinon c'est pas la peine de laisser en privé ma variable membre).

                • Partager sur Facebook
                • Partager sur Twitter
                printf("Les rudiments de la programmation ? Nan mais Hello quoi !");
                  18 janvier 2018 à 17:02:21

                  peridot69 a écrit:

                  Je vois très bien ce que tu veux dire (en effet j'avais lu ton post sur l'autre sujet), cependant ce que j'en retire (outre le cas d'exemple du moteur de voiture que tu nous donne qui me parait évident, je ne donne pas accès forcément à toutes mes données, même au travers d'accesseurs/mutateurs) c'est que c'est surtout le nommage setXXX qui te pose problème, qui indique à l'utilisateur que la méthode ne devrais en théorie que modifier la valeur d'une donnée membre de l'instance, c'est un point de vue qui se défend en effet.

                  C'est, effectivement, l'une des raisons majeures

                  Dans le même ordre d'idée, on peut faire valoir le fait que le nom des fonctions doit être "suffisamment précis" que pour qu'il ne laisse aucun doute quant au comportement adapté par la fonction.

                  Cependant, je vois mal pourquoi le setXXX serais en toute logique "interdit" de faire plus qu'une simple modification de valeur sans effectuer de vérifications, surtout si on l'indique dans une documentation ou autre ?

                  Hé bien, revenons aux fondamentaux, si tu veux bien:

                  Le but de n'importe quel programme est de ... manipuler des données. Mais pas n'importe comment!

                  Car le but de n'importe quel programme est de manipuler de manière déterministe et reproductible des données:

                  Un traitement est dit "reproductible" si en exécutant deux fois le même programme avec le même jeu de données, tu obtiennes le même résultat

                  Un traitement est dit déterministe, si tu peux déterminer le résultat de ce traitement sur un jeu particulier de données avant même d'avoir effectué le traitement en question.

                  Pour y arriver, on tend -- surtout avec l'approche orientée objet -- de plus en plus à s'appuyer sur la notion de "programmation par contrat": si tu me donnes les données auxquelles je m'attend, tu obtiendra le résultat que tu espères.

                  Cette notion met en évidence trois autres notions:

                  • La notion de préconditions : des conditions qui doivent être vérifiées au plus tard lorsqu'un  traitement commence
                  • la notion de postconditions : des conditions qui doivent être vérifiée au plus tard lorsqu'un traitement s'achève
                  • la notion d'invariants : des conditions qui doivent être vérifiées en permanence.

                  Un exemple de précondition serait -- par exemple -- que l'on ne peut calculer la racine carrée d'une valeur que... sur les valeurs strictement positives.  Typiquement, c'est le genre de condition dont le non respect indique très clairement une erreur de logique de la part de l'utilisateur de la fonction: le développeur qui fait appel à la fonction racineCarree aurait du s'assurer que la valeur qu'il transmet à cette fonction est exclusivement positive avant d'appeler la fonction  ;)

                  Les postconditions sont là pour nous garantir ... que le traitement a eu l'effet que l'on pouvait en attendre. 

                  Si ce n'est pas le cas, cela peut être du à une erreur de celui qui a développé la fonction (imagines une fonction racineCarree codée sous la forme de double racineCarree(double d){return d * d;} :D ) ou à un "problème externe" auquel le développeur n'a pas d'autre choix que ... de prendre acte (si le serveur distant plante pendant que tu essaye de le contacter, tu ne sais pas faire grand chose :D )

                  Les invariants, enfin, correspondent à toutes les caractéristiques qui font qu'un objet d'un type particulier peut être considéré comme étant... de ce type particulier  (ou d'un type "fort proche".

                  Pour faire simple, ce sont les caractéristiques qui feront que tu peux considérer qu'une voiture comme étant un véhicule, mais que tu ne pourras jamais considérer qu'une voiture est... un animal ou un végétal.

                  Ces trois notions mettent un point en évidence : pour que la programmation par contrat ait une chance de donner de bons résultats, il faut que l'on puisse valider les données, car nous n'aurons les résultats auxquels on est en droit de s'attendre que... si les données que l'on manipule sont valides "de bout en bout" (comprend : du moment où elles sont créées jusqu'au moment où elles sont détruites).

                  Mais cela sous entend que toutes les manipulations que nous pourrons envisagées de nos données devraient (idéalement) suivre une même logique, qui peut se résumer en trois étapes:

                  • vérification des préconditions,
                  • traitement proprement dit
                  • vérification des postconditons.

                  Et c'est là que les problèmes apparaissent

                  Cependant, je vois mal pourquoi le setXXX serais en toute logique "interdit" de faire plus qu'une simple modification de valeur sans effectuer de vérifications, surtout si on l'indique dans une documentation ou autre ? Bien que je comprenne qu'au sens stricte un nommage pareil ne devrais pas le faire, mais si on indiquais tous fonctionnement interne d'une classe à l'utilisateur, la POO perdrais une partie de sont intérêt. l'encapsulation est la pour sa. Pour cacher des comportements en interne.

                  Si bien que l'on pourrait -- effectivement -- envisager de faire en sorte qu'une fonction setXXX vérifie les postconditions après avoir modifié la valeur de XXX .  Mais, comment faire pour ce qui a trait au préconditions, qui -- faut-il le rappeler -- doivent être vérifiées AVANT que le traitement ne commence???

                  Car, il ne faut pas se leurrer: si tu fournis une fonction setXXX, tu t'attends à ce que le traitement utilisé pour définir la valeur de XXX soit pris en charge par... l'utilisateur de ta classe!  Et cela implique ... la vérification des préconditions au traitement en question (vu qu'elle doit avoir lieu avant que le traitement ne commence).

                  Et ca, ca commence à faire beaucoup, car, comme je l'ai déjà dit: l'utilisateur d'une classe est -- par nature -- un imbécile distrait : si tu lui laisses l'occasion de faire une connerie, tu ne dois même pas te poser la question de savoir SI  il fera cette connerie un jour ou l'autre, car la réponse est forcément "oui". 

                  Poses toi tout de suite la question suivante : "quand l'utilisateur va-t-il faire la connerie que je lui laisse l'occasion de faire?"  Et la réponse à cette question sera toujours... "au pire moment qui soit" ;)

                  Or, des occasions de faire des conneries, tu en laisses tant et plus à l'utilisateur de la classe, car, comme il peut faire appel à setXXX depuis "n'importe où" il peut décider "à n'importe quel moment" de calculer la nouvelle valeur de XXX.

                  Et tu auras beau avoir documenté autant que tu veux  la manière de définir XXX de manière correcte, tu dois t'attendre, au mieux à ce que l'utilisateur ait lu cette doc (mais en ait oublié la moitié), au pire... à ce qu'il n'ait purement et simplement jamais lu la documentation.

                  Et je ne parle même pas des préconditions, dont tu dois partir du principe qu'il oubliera de les vérifier une fois sur deux (en étant très optimiste)!

                  Enfin, il y a un dernier problème majeur : L'Xtrem programming utilise l'interjection DRY (Don't Repeat Yourself ou, si tu préfère en francais : ne vous répétez pas) pour en parler.  Si tu trouve un même morceau de code à plus d'un endroit de ton projet, tu vas au devant de problèmes sans noms, car il suffit que, pour une raison ou une autre, tu décide de modifier ce code pour que, la loi de finagle aidant, tu oublies forcément de modifier l'un ou l'autre des endroits de ton projet dans lequel le code apparaît.

                  Or, la présence d'une fonction setXXX va ... forcément (ou, du moins, "a de très grosses chances"  de) provoquer la répétition de code, vu qu'elle peut être appelée depuis n'importe où et que, pour pouvoir l'appeler, il faudra que l'utilisateur ait ... calculé la nouvelle valeur à donner à XXX.

                  "Avec un peu de chance", il se rendra compte qu'il se copie, et envisagera d'écrire une fonction proche de

                  Type computeNewXXX(Type x/* , autres paramètres */ ){
                      /* il est vraiment doué  */
                      checkPreconditions(x, autre paramètres);
                     newX = /* calculs */
                     return newX;
                  } 

                  Et il veillera à l'appeler à chaque fois sous une forme proche de

                  void foo(MaClasse & c){
                      auto newX = computeNewXXX(c.getXXX()/*, autres données*/);
                      c.setXXX(newX);
                  }
                  /* il veut aussi modifier c dans bar */
                  void bar(MaClasse & c){
                      auto newX = computeNewXXX(c.getXXX()/*, autres données*/);
                      c.setXXX(newX);
                  }

                  Mais, à ce moment là, pourquoi utiliser une fonction libre, au lieu de ... définir un service clair et précis que l'on est en droit d'attendre de la part de notre classe?

                  Car, si les fonctions checkPreconditions et computeNewXXX étaient des fonctions membres de la classe (soyons précis : de sordides détails d'implémentation de la classe, car elles seraient sans doute dans l'accessibilité privé ou, au pire, dans l'accessibilité protégée), nous pourrions présenter plusieurs services clairement nommés qui y feraient appel, sous une forme proche de

                  class MaClasse{
                  public:
                      void someSpecificService(/* autres données "externes" */){
                         checkPreconditons(/* autres données "externes" */)
                         xxx_ = computeNewXXX(/* autres données "externes" */);
                     }
                  private:
                      void checkPrecondition(/* autres données "externes" */){
                          /* différentes assertions concernant xxx_ et les donnees externes*/
                      }
                      Type computeNewXXX(/* autres données "externes" */){
                          /* ... */
                      }
                      Type xxx_;
                  }

                  Dans mon cas précis, ma classe, telle que je l'ai définie à besoin que l'utilisateur puisse définir "en dur" une valeur précise pour le niveau minimum et maximum de la barre de vie, pourquoi ? Parce que le besoin exprimer vis à vis de ma classe est comme sa.

                  Non: l'utilisateur doit connaitre le niveau minimum et maximum au moment de créer une instance de ta classe.

                  Par la suite, s'il veut faire varier ces niveaux, tout ce qu'il doit savoir, c'est... dans quelle mesure faire varier ces niveau (la différence).

                  Si bien que des fonctions membres addMaximum(difference) et substractMaximum(difference) ont tout leur sens, vu qu'elles permettent à l'utilisateur de la classe de... n'avoir rien "à foutre" des valeurs actuelles utilisées par ton instance de classe ;)

                  Après dans mon post je n'ai pas préciser les autres méthodes que je pourrais avoir. Je peux très bien avoir une méthode permettant d'incrémenter le minimum/maximum ou de les décrémenté (à l'image des fonctions exemples accelerate/decelerate). Ou encore les besoins évolutif de ma classe qui peuvent m'amener à ajouter/supprimer des méthodes.

                  Si tu as déjà des fonctions de manipulation du minimum et du maximum, c'est une raison de plus pour... n'avoir aucun besoin d'une fonction setXXX...

                  Car, les fonctions de manipulation sont facile à utiliser correctement (on peut partir du principe que "toutes les vérifications seront faites"), mais le fait de rajouter une fonction setMax (setMin) ne ferait que... fournir à l'utilisateur une possibilité d'utiliser la classe de manière incorrecte (car sans le moindre contrôle quant à la manipulation qu'il aura jugée utile d'appliquer sur les données

                  Les règles d'utilisations générales d'une classe doivent, à mon sens être préciser dans une documentation, accessible par l'utilisateur de celle-ci. S'il utilise une méthode contenant des paramètres, alors je dois lui indiquer les contraintes associés, et qu'en cas de non respect de celles-ci, il y auras des conséquences, telles que des levées d'exceptions ou des retours négatifs ect...

                  Tout à fait d'accord, mais il ne faut pas faire l'erreur de croire que l'utilisateur va forcément lire (ou retenir) les informations contenues dans la documentation ;)

                  En terme de réponse par rapport au début en effet je demandais surtout "Est-ce recommander d'utiliser les méthodes d'une classe dans le constructeur de celle-ci ?".

                  Et je t'ai répondu de manière claire à ce sujet, me semble-t-il... non?

                  J'ai montré l'exemple de mutateurs, mais j'aurais aussi pu montrer une méthode permettant d'incrémenter les valeurs maximum/minimum, cela aurais été la même chose.

                  Cela aurait été techniquement la même chose.

                  Seulement, si l'on se bat contre la présence des mutateurs, ce n'est pas à cause d'un problème technique, mais bien à cause d'un problème... conceptuel ;)

                  Mais la discussion sur l'utilisation (pas l’intérêt, pas dans mon cas, personnellement je suis convaincu qu'aucune données ne devrais être en public ou protected) est très intéressante tous de même pour moi.

                  Tant mieux :D Et j'espère que cette intervention t'auras permis de comprendre encore mieux notre point de vue ;)

                  Justement, dans mon cas, concernant mon cas d'utilisation, à la place d'un setXXX que mettrais tu ? Sachant que la méthode doit permettre d'indiquer la nouvelle valeur de la données, et qu'elle doit faire des vérifications (sinon c'est pas la peine de laisser en privé ma variable membre).

                  Non, elle ne doit pas permettre d'indiquer la nouvelle valeur! Tout ce que l'utilisateur de ta classe doit savoir, c'est "dans quelle mesure" il veut faire varier les valeurs et ... dans quel sens (augmentation Vs diminution). 

                  Quant au nom donné à cette (ces??) fonctions, je ne peux dire qu'une chose : il doit clairement indiqué l'objectif poursuivi par la fonction ;)

                  • Partager sur Facebook
                  • Partager sur Twitter
                  Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait

                  Bonne pratique en POO

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