Journal Si j'ai bien tout compris

Posté par  .
Étiquettes : aucune
0
28
nov.
2002
Je suis dans le bouquin de Meyer en ce moment dans http://www.eyrolles.com/php.informatique/Ouvrages/9782212091113.php3

J'y trouve une utilisation un peu strange de l'héritage multiple : au lieu de composer un objet par un autre, Meyer fait hériter 2 classes pour avoir les services de l'une (dont il vire l'interface) et l'interface de l'autre (qui n'offrait de toute façon rien de concret).

Son exemple est ARRAYED_STACK[G], qui est une pile générique implantée par un tableau. Il fait donc hériter sa nouvelle classe de STACK[G], définition abstraite d'une pile et de ARRAY[G], tableau de G.
indexing
description: "Piles représentées par des tableaux"
class ARRAYED_STACK[G] inherit
STACK[G]
redefine change_top end
ARRAY[G]
rename
count as capacity,
put as array_put,
make as array_make
export
{NONE} all -- tout ce qui est hérité de ARRAY[G] est inacessible
end
feature
... tout le bordel
end

Cette méthode est largement défendue par l'auteur car effectivement, il est impossible d'acceder directement aux éléments du tableau par l'interface. Et c'est bien une pile.

Cepandant, au sens des types, une ARRAYED_STACK[G] reste toujours un ARRAY[G] en particulier, si une variable s est de type ARRAY[G], et que a contient une instance de ARRAYED_STACK[G],
s:=a
est valide et toute l'interface de ARRAY[G] sera disponible sur s, par exemple,
s.put(2, unG)
est valide transperçant d'un coup tout le bel encapsulage qui était de rigueur dans la situation.
Alors qu'une classique composition avec transmission des messages aurait préservé lencapsulation, au prix d'une écriture beaucoup plus fastidieuse.

Meyer étant loin d'être un con, j'ai l'impression d'avoir raté quelquechose, quelqu'un voit où est mon erreur ?
  • # Re: Si j'ai bien tout compris

    Posté par  . Évalué à 1.

    Il faudrait que je m'installe un chtit compilo eiffel pour tester mais je doute que ton s puisse être passé en array avec des méthodes redéfinies et surtout un export none du reste ... en tout cas que ça passe sans casse.
    Si mes souvenirs sont bons les redéfinitions de méthodes ne sont pas shuntées en tentant de remonter l'arbre d'héritage donc put n'aura pas le bon profil (pour une stack put est du genre put(item : like G)).
    • [^] # Re: Si j'ai bien tout compris

      Posté par  . Évalué à 1.

      Aucune des méthodes héritées de le classe ARRAY[G] n'est redéfinie.
      Même si elles l'avaient été, le contrat hérité n'aurait pas pu être allégé, il aurrait été impossible de casser le jouet.
      La clause export ne concerne que le type statique ARRAYED_STACK[G], elle est est indépendante de la hiérarchie (particularité assez déroutante au début, car on peut pas planquer une méthode avec certitude, un des descendants peut la sortir du placard). N'importe quel niveau de hiréarchie peut exporter à qui il veut n'importe quelle fonction de son interface, mais ceci ne sert que statiquement.

      Et "put" appellé sur une variable dont le type est ARRAY[G] sera bien celui que j'ai indiqué, il est impossible de faire n'importe quoi dans les descendant (normalement).
      • [^] # Re: Si j'ai bien tout compris

        Posté par  . Évalué à 1.

        Après réflexion c'est vrai que le type est forcé en ARRAY[G] ... il doit y avoir un truc, ça m'étonnerait que ça soit innocent.
        s est déclarée comme STACK[G], ça se fait comme ça :

        plop is
        local
        s : STACK[G]
        do
        !ARRAYED_STACK[G]!s.make
        -- ou autre syntaxe avec create
        end -- plop

        L'arrayed stack est un détail d'implantation, c'est sensé être utilisé avec une sémantique STACK et quand on code en eiffel on pose pour les variables un type "générique", plus comportemental qu'orienté implantation.
        Dans ce cas, je ne peux pas ramener s à un ARRAY[WHATEVER]. Peut-être avec des typage dégueux à base de ?= (c'est ça, non ?) mais là c'est du gruïkage donc bon, c'est normal de tout faire foirer.

        Bref, ça ne me viendrais pas à l'idée de déclarer une variable en tant qu'ARRAYED_STACK en dehors d'ARRAYED_STACK.
        • [^] # Re: Si j'ai bien tout compris

          Posté par  . Évalué à 1.

          J'ai pas tout compris.
          Effectivement, dans ton code, tu peux forcer à créer une instance d'une sous-classe.
          Le problème que je soulevais est celui qu'on retrouve dans les langages typés uniquement à l'exécution (genre smalltalk) : tu risques de te retrouver avec une vache dans une collection de strings sans que le compilo dise rien, évidement, quand tu va vouloir l passer dans un parser par ex. ça va faire un grumeau. Tout ça car tu as débranché une sécurité (la vérification de type).

          Ici, suite à un bug, tu peux te retrouver à manipuler directement les éléments d'une stack sans t'appercevoir qu'il y a un problème. Donc au lieu de te planter à la compilation, il va te faire un comportement douteux, avec débuggage, perte de cheveux etc.
          • [^] # Re: Si j'ai bien tout compris

            Posté par  . Évalué à 1.

            Ah non pas du tout, si le type mis entre !! n'est pas ramenable à STACK, ça ne fonctionne pas, et à l'exécution ça ne sera manipulable que comme une stack (un bourrin pourra tenter de forcer en arrayed_stack mais c'est Mal®). Les assertions seront vérifiées, etc. comme d'hab.
            Si s est de type STACK[OURS], je ne peux pas instancier s comme !ARRAYED_STACK[VACHE]! ou !ARRAYED_STACK[ANY]!, juste comme !ARRAYED_STACK[G -> OURS]! (si on peut dire).
            La vérification de type n'est pas supprimée du tout, au contraire. En déclarant s comme ARRAYED_STACK, par contre, on veut montrer que s sera accéder en tant que ARRAYED_STACK, pas comme STACK du coup il est nromal d'avoir accès à la structure tableau. Si on ne veut pas ça, on déclare s comme STACK et puis c'est tout, le type non abstrait utilisé n'entre pas en ligne de compte dans la vérif de type, l'interface est celle d'une STACK et rien de plus.

            Bref, l'exemple correspond aussi au style de porgrammation préconisé dans Eiffel, the language et autres de ses bouquins :

            s : ARRAYED_STACK[PLOP]
            do
            create s.make
            -- Mal®

            s : STACK[PLOP]
            do
            !ARRAYED_STACK[PLOP]!s.make
            -- Bien©

            Juste mes 2.10-2 EUR (et mon cours de poo de d.colnet).
            • [^] # Re: Si j'ai bien tout compris

              Posté par  . Évalué à 1.

              mais qu'est-ce qu'il dit lui ?
              Seuls tes 2 bouts de codes ont à voir avec ce que je disais.
              Avec le bout de code MAL, il est possible de foutre un ARRAY_STACK[G] dans, par ex. un collection d'ARRAY[G], et de pêter tout (car la référence sera partagée), sans downcaster violement dans la hiérarchie comme tu le proposes. Une variable référencera l'instance avec l'interface ARRAY[G] et l'autre avec ARRAY_STACK[G].

              Ta dernière proposition fait avancer le débat, se serait donc aux utilisateurs de la classe de se démerder avec les risques posés par son concepteur ? Et si des services spécifiques à la classe rendaient celle-ci atrayante pour elle-même (et non pour son interface héritée), du style la possibilité de jouer sur l'espace disponible dans le ARRAY[G] ?

              J'arrive pas à savoir si je suis borné dans mon idée, si c'est réellement une connerie le "mariage de raison", ou s'il existe quelquechose que j'ai pas vu pour limiter le risque.
              • [^] # Re: Si j'ai bien tout compris

                Posté par  . Évalué à 1.

                Bon, en fait je me relis et je ne me comprends pas, c'est mauvais signe :)

                Le bout de code Mal correspond à un code où l'on désire explicitement que s soit accessible en tant qu'array et que stack sous la forme d'une arrayed_stack. Avec le bout de code Bien, on montre explicitement que l'on désire que s soit une pile, accessoirement une pile gérée dans un tableau _mais_ c'est un détail qui ne doit pas transparaitre à l'extérieur.

                En fait, c'est au programmeur de savoir ce qu'il veut.
                Pour limiter la remontée dans l'arbre d'héritage il faut fixer le type de la variable, c'est tout. À moins de tricher (ce qui n'est pas toujours possible) il n'est pas possible de remonter l'arbre comme ça.
                Pour se protéger encore plus, il faut passer par un intermédiaire, c'est tout (pattern proxy).

                Bon, sinon je ne vois pas pourquoi foutre un ARRAY_STACK[G] dans un COLLECTION[ARRAY[G]] mettrais le souk ?
                s : ARRAYED_STACK[G]
                a : ARRAY[G]
                c : COLLECTION[ARRAY[G]]
                --
                a := s
                c.put(a)
                --
                Et donc
                c.item est de type ARRAY[G]
                s est de type ARRAYED_STACK[G]

                Où est le problème ? En posant le type de s à ARRAYED_STACK, on demande explicitement à ce que la pile soit accessible des deux manières, c'est ainsi. En modifiant c on modifie la structure de la pile s, et alors ? C'est voulu dans le code ! Normalement, c devra quand même respecter les assertions (l'invariant notamment) de s donc en théorie il ne devrait même pas y avoir de casse ...
                Bon, il faut absolument que je déterre un compilo eiffel pour vérifier ... grmbl.
                • [^] # Re: Si j'ai bien tout compris

                  Posté par  . Évalué à 1.

                  Je pensais qu'on ce comprenait mais apparement non

                  C'est voulu dans le code

                  Non, ce qu'il veut c'est implanter une pile utilisable avec comme conteneur un tableau. Mais c'est un flemmard.

                  ! Normalement, c devra quand même respecter les assertions (l'invariant notamment) de s donc en théorie il ne devrait même pas y avoir de casse ...
                  Bon, il faut absolument que je déterre un compilo eiffel pour vérifier ... grmbl.


                  reprenons depuis le début, dans une pile, la seule manière de midifier une valeur est de la dépiler et de rempiler la nouvelle. Or là, on peut modifier une valeur par le passage par l'interface ARRAY[G], qui n'est là qu'accidentellement. Bien entendu, les invariants serons vérifiés, mais la clause qui dit que tu retrouves ce que tu y a mis n'est pas exprimable simplement en Eiffel, en son absence, tout devrait lamentablement chier.
                  • [^] # Re: Si j'ai bien tout compris

                    Posté par  . Évalué à 1.

                    Oui une pile avec comme conteneur un tableau, aucun problème.
                    Donc si tu t'en fous d'accéder à la partie "tableau" tu déclares la variable comme une pile et tu y met une instance de ARRAYED_STACK. Et voilou.
                    Le fait que si la variable a comme type statique ARRAYED_STACK on puisse violer la sémantique de STACK avec array_put, etc. est normal, c'est le contrat. Une ARRAYED_STACK n'est pas une STACK ni un ARRAY, c'est une ARRAYED_STACK. Pour avoir une vraie STACK dont le contenu est géré en tableau, il faut soit un type statique STACK soit en faire rouler une avec un ARRAY comme conteneur privé si tu veux être sûr.
                    En tous cas, ce type de construction (ARRAYED_STACK) ne me choque pas du tout, la plupart de mes cours cours de POO étaient autour de structures comme celle-ci (bon, en autres profs j'avais D. Colnet, un des auteurs de SmallEiffel, donc forcément ayant appris comme ça ne me choque pas).

                    La logique du truc c'est que si tu veux une ARRAYED_STACK ayant un comportement uniquement de STACK vis à vis d'autres (notamment du cas dont tu parles) il faut lui foutre comme type statique STACK et comme type dynamique ARRAYED_STACK en instanciant avec !ARRAYED_STACK[G]!. C'est la méthode classique utilisée très souvent dans des libs Eiffel.
  • # Re: Si j'ai bien tout compris

    Posté par  . Évalué à 1.

    Meyer étant loin d'être un con, j'ai l'impression d'avoir raté quelquechose, quelqu'un voit où est mon erreur ?

    Il est peut-être simplement feignant, comme tout bon programmeur.
    • [^] # Re: Si j'ai bien tout compris

      Posté par  . Évalué à 1.

      d'un autre côté, j'aimerais pas me tapper le compilo Eiffel qu'il a développé, la grammaire est simple mais la sémantique .... plutôt évoluée.
      • [^] # Re: Si j'ai bien tout compris

        Posté par  . Évalué à 1.

        On s'y fait. ISE eiffel est super bien foutu (le compilo en ligne de commande, pas l'IDE). Ce qui est chiant c'est que je n'ai pas pratiqué depuis le mois de mars et que les petits détails partent vite :(
  • # Une explication ?

    Posté par  . Évalué à 1.

    Suite à un petit googling, je suis tombé sur cette discussion... A mon avis, la réponse se trouve dans la future norme du langage (la 5, OOSC étant basé sur la 3). Meyer introduit "l'héritage non-conforme" dont la sémantique est d'hériter de l'implantation et non du type. Ceci introduit donc partiellement dans Eiffel la différence entre type et classe (une implentation n'existant pas sans type...). A mon avis, ce que veut montrer Meyer va dans ce sens : j'hérite de l'implantation. Le fait que j'hérite du type est un dommage colatéral, auquel l'introduction d'héritage non-conforme répond. J'interprète donc sa recommandation comme : pour implanter un comportement, il vaut mieux hériter que de passer par une relation client/fournisseur, l'écriture fastidieuse que tu évoques étant quelque chose qu'il faut à tout prix éviter pour minimiser les bugs... Précision, je n'ai toujours pas lu OOSC, des subtilités peuvent donc m'échapper... Thomas.
  • # Re: Si j'ai bien tout compris

    Posté par  . Évalué à 1.


    Meyer étant loin d'être un con, j'ai l'impression d'avoir raté quelquechose, quelqu'un voit où est mon erreur ?


    Tu ne dois pas être non plus un con. Je viens de tester ton exemple sous linux avec ISE EiffelStudio 5.2.

    En effet, en faisant l'affectation s := a que tu décris, on peut placer des éléments dans la pile en utilisant la routine de la classe ARRAY [G], put (item: G, i: INTEGER) appliquée sur l'objet s. La même chose est impossible en passant par l'objet a,qui est en mémoire la même référence pointant donc sur le même objet.

    Donc on manipule s comme un ARRAY [G] et a comme un ARRAYED_STACK [G], de la sorte un a.remove est valide et un s.remove provoque une erreur de compilation. Plus : s est un ARRAY et a est un ARRAYED_STACK dont les données sont malencontreusement accessibles par d'autres moyens que ceux prévus par sa classe.

    La méthode suggérée par Jean-Yves Burlett de déclarer STACK comme le type statique de a élimine le problème, mais il faut s'en être aperçu.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.