Oui, bientôt sans doute :P
Version imprimable
Pour ma part, je vois surtout les propriétés comme une manière propre et élégante d'avoir des attributs vus comme des variables sans devoir les déclarer publics et/ou risquer l'incohérence en cas "d'oubli" d'appel d'un accesseur... Cela permet également d'avoir des attributs réellement write/read only, et cela centralise à l'endroit où l'on définit la propriété sa valeur par défaut (au lieu d'en avoir un bout dans le constructeur), bref c'est nettement plus propre et lisible. Tout comme le With du Pascal continue de me manquer sévèrement en C/C++ lors de la manipulation de structures / classes un peu complexes, même après plus de 10 ans de C/C++ intensif...
Après, une "vraie" propriété est normalement très contrainte au niveau des prototypes, on ne peut donc pas mettre n'importe quoi dessus. Mais c'est extrêmement agréable de pouvoir utiliser les "attributs" directement, comme des variables, en se contrefichant de savoir si oui ou non il y a une fonction à appeler derrière. En cas d'évolution (plus ou moins de contrôles requis par exemple, verrouillage d'un sens d'accès, changement d'implémentation), la syntaxe dans le programme appelant reste la même et, surtout, elle reste bien plus lisible qu'avec trois tonnes de fonctions s'appellant les unes les autres...
Entre un Object1.Attribute1 += Object2.Attribute2 ; et un Object1.SetAttribute1(Object1.GetAttribute1()+Object2.GetAttribute2()) ;, perso, je sais quelle écriture est la plus lisible... ;)
Pour ma part, quand j'ai la possibilité d'utiliser des propriétés, je déclare directement les propriétés correspondant aux attributs... Quitte à les laisser en read-only.
Certes, je suis "marqué" par les propriétés sous Delphi, mais elles ont été implémentées également sous BCB, de la même façon, et ça n'a pas trop l'air de poser problème jusqu'à présent.
Super, c'est exactement pour toutes ces raison que j'aime bien les propriétés.
Le fait est que, en étant à peine un peu exhaustif, tu ne devrais pas voir à écrire un code proche de
ni un code proche deCode:Object1.Attribute1 += Object2.Attribute2 ;
En effet, ces codes devraient, en toute bonne logique, être intégrés dans un comportement que l'on souhaite autoriser (et qui doit donc être pris en compte au moment de la conception) pour la classe en question.Code:Object1.SetAttribute1(Object1.GetAttribute1()+Object2.GetAttribute2()) ;
Cela pourrait, pour le premier, prendre la forme de
et, pour le second etre proche deCode:
1
2
3
4 void MaClass::Add(Maclass const & rhs) { Attribute1+=rhs.Attribute1; }
car les approches que je présente permettent de *réellement* encapsuler Attribute1 et de respecter demeter.Code:
1
2
3
4 void MaClass::synchronize(MaClass const & rhs) { Attribute1=rhs.Attribute1; }
J'ai, encore une fois, l'impression que vous avez des approches, si pas fragmentaires, en tout cas restrictives ou erronées de ce que peuvent représenter l'encapsulation, la loi demeter, ou le concept orienté objet lui-même.
J'admets que, dans certains cas, un accesseur puisse s'avérer intéressant, mais, très souvent, il subira l'énorme inconvénient de fournir un acces "global" (dans le sens de disponible dans n'importe quelle portion de code) à quelque chose dont l'acces devrait être particulièrement restreint.
Et, comme de bien entendu, j'émets ces critiques de manière encore plus virulente vis à vis... des mutateurs...
Je ne suis pas un ferme partisan de la loi de Demeter, je dois dire, pour des raisons d'efficacité principalement. Dans mon domaine (embarqué temps réel), c'est un élément crucial et non-négociable.
Toutefois, je ne vois vraiment pas en quoi une propriété viole cette règle, dans le sens qu'elle ne permet ni plus, ni moins de choses qu'un accesseur... Après, si effectivement tu publies par propriété/accesseur un attribut d'une sous-classe, tu t'exposes à un paquet d'emm.... gros comme ça, mais là encore, le concept de propriété n'est en rien responsable de ça.
Au contraire : pour respecter Demeter, tu vas souvent devoir écrire un (trop) grand nombre de wrappers, chose lourde et franchement pesante. Les propriétés permettent de faire ceci de façon nettement plus élégante qu'une tripaille d'accesseurs/mutateurs/foncteurs, et n'interdisent absolument pas la création de méthodes de "service macroscopique" avec propagation dudit service aux sous-classes...
Par rapport aux exemple précités : c'est pourtant un cas tout à fait légitime d'additionner deux attributs différents de deux objets différents... D'une part, deux objets peuvent parfaitement décrire un système, mais de deux points de vue différents, et ne pas se connaître ni de près, ni de loin. D'autre part, l'attribut de l'un peut refléter l'attribut de l'autre. De plus, tu es parti du principe que cela représentait deux instance de la même classe : et pourquoi donc devrait-ce être le cas ? ;)
Reformulé, on pourrait écrire : StatisticObject.TotalNumberOfCommands += aTempCommandObject.Size ; : cela a un sens, et permet d'éviter de créer une fonction spécifique d'ajout (imposant de connaître les types de classes impliqués) dans une fonction qui n'a de sens que pour le programme appelant, ou de créer une classe spécifique pour centraliser ces deux sous-classes... Tu ne peux pas créer des prototypes pour toutes les références croisées possibles et imaginables, c'est une évidence. Tu ne peux pas non plus prévoir toutes les utilisations possibles et imaginables de ta classe si elle est un tant soit peu générique / bas niveau. Tu vas donc reporter ce problème vers l'appelant en l'obligeant à définir un paquet de classes d'adaptation.
Tu vas me dire "Quel intérêt de faire ça ?", peut-être : ben c'est d'un intérêt évident quand tu écris beaucoup de code-passerelle, c'est à dire du code de transfert entre deux mondes. Or, beaucoup de mes librairies sont justement utilisées par un code de ce type. Actuellement, en C++, je n'ai pas d'autre choix que de fournir des accesseurs, ou directement l'attribut dans certains cas... Performances et simplicité d'utilisation obligent.
Le déport dans le code appelant au travers des propriétés (ou des accesseurs) est, justement, une partie de la solution à ce problème. Certes, dans du code 100% métier, ce cas de figure apparaît rarement. Quand tu fais des librairies génériques dans 99% des cas, comme moi, tu apprends vite à savoir déléguer à l'appelant et/ou à imposer des contrats. De plus, je dois avouer que la "pureté" du code source et le respect de Grands Principes (les majuscules sont volontaires) sont le cadet de mes soucis : mon but premier, à la création d'une classe, est d'avoir une API claire, concise et pratique à utiliser. Le reste, c'est ma tripaille interne et ma sauce, je n'ai pas à exposer à l'utilisateur mes contraintes de code et/ou mes évolutions d'interface.
Or, justement, les propriétés me permettent de pas mal blinder à ce niveau sans rendre le code appelant infect... J'ai tendance à pleurer à chaque fois que je vois un code généré C++ de lecture XML, où l'on finit par penser que le concepteur du générateur a été payé à la parenthèse produite ! ;) Les propriétés simplifieraient grandement ce type de code, sans modifier en quoi que ce soit les fonctionnalités ou les restrictions d'accès.
Question de point de vue, donc...
Une propriété fait ce que tu veux qu'elle fasse. Tu peux l'utiliser pour n'importe quelle donnée, et définir le niveau d'accès que tu souhaites avoir (R, W, RW).
J'ai également beaucoup de mal à voir en quoi restreindre l'accès à une donnée comme la cardinalité d'un container, ou l'accès aux éléments unitaires, pose le moindre problème.
Typiquement, une propriété peut remplacer les opérateurs () et [] de façon tout à fait confortable, en n'autorisant que la lecture et non pas l'écriture.
Ce que je vois surtout, c'est que le programme appelant n'est pas à modifier lorsque l'on transforme un attribut "simple" (= une variable de classe uniquement, donc) en un attribut "complexe" (= sa modification déclenche un trigger interne). Typiquement, tu peux transformer une classe relativement figée et statique en quelque chose de dynamique réagissant "en direct-live" à ses modifications.
Après, je te concède que le niveau de visibilité published peut avoir son importance dans une implémentation correcte des propriétés, tout comme des concepts tels que le inherited ou le deprecated...
N.B. : les liens de ce paragraphe renvoient vers des cours/posts Delphi.
C'est quand même un cas concret dans le développement en situation réelle : on implémente une première version d'un composant, qui va ensuite évoluer en quelque chose de plus complexe. Au moins, le code source appelant n'a pas à être modifié, et cela a évité de créer initialement des tonnes d'accesseurs plus ou moins inutiles (et coûteux en temps de développement / tests !!).
D'un point de vue strictement pratique, j'ai rarement vu quelqu'un dire que la VCL Borland soit mal foutue côté architecture... Elle fait pourtant un usage plus qu'intensif et constant des propriétés, et c'est aussi pour ça qu'elle est simple et pratique d'utilisation... :mrgreen:
la VCL est mal foutue (il fallait bien quelqu'un pour le dire...)
les objets melangent plusieurs cmportements (beaucoup, en fait) parce qu'apparemment quand on fait de l'IHM on ne connait rien a la composition, on ne connait que l'héritage (c'est un trait très commun qui se retrouve partout de toutes facons)
=> classes bloatées qui ont 43 rôles différents.
un exemple simple : si je veux une image qui se comporte comme un bouton, comment je fais ?
dériver de bouton et redéfinir la fonction d'image?
dériver d'image et redéfinir la fonction de clic ?
dériver des deux a la fois ?
si le comportement (ce qui se passe lorsque je clique...) n'avait pas été integré au rendu (comment déssiner le composant) alors ca serait très simple; il y aurait un comportement "bouton" et un widget "image", il suffit de coller ces deux la ensemble et on a une image cliquable.
MacLak tu parles de la lisibilité, je trouve que
est particulièrement crade si l'un des deux a un effet de bord quelconque, et très illisible. si ce ne sont que des attributs publics (ce qui ne change rien a l'affaire)on est sur qu'il n'y a pas d'effet de bord. Si cette ligne se transforme en appel de fonctions, moi je râle; je ne veux pas aller voir ce que la propriété machin fait réellement.Code:StatisticObject.TotalNumberOfCommands += aTempCommandObject.Size ;
lorsqu'il y a une propriété il peut y avoir mensonge de l'interface; par exemple, une propriété "size" qui calcule la taille en O(n) comme pour une liste chaînée, si c'est une méthode j'irai faire attention. si c'est un attribut, c'est simplement stocké.
et si c'est une propriété, ca ressemble a un attribut mais en fait ca mobilise le processeur.
Mais si c'est un attribut, tu veux aussi interdire la modification directe depuis l'extérieur de ta classe Liste, donc on est d'accord que ça passe obligatoirement par une méthode dans les deux cas.
Si tu fais un size() ou un count(), tu n'as pas réellement d'indication non plus sur ce qui est fait sous le rideau. Cela pourrait être un simple "return this->size".
En fait ce qui te dérange c'est plutôt la confusion possible entre une variable membre et la propriété du point de vue de la syntaxe d'écriture? Je pense que c'est une question d'habitude et de documentation.
Pas forcément...
C'est là que peut entrer en jeu l'amitié qui, si elle est correctement utilisée, renforcera encore l'encapsulation ;)
Rien ne t'empêche, en t'aidant de l'amitié, de créer une classe dont la responsabilité sera d'assurer la conversion de ces deux types qui ne "se connaissent absolument pas" ;)Citation:
Par rapport aux exemple précités : c'est pourtant un cas tout à fait légitime d'additionner deux attributs différents de deux objets différents... D'une part, deux objets peuvent parfaitement décrire un système, mais de deux points de vue différents, et ne pas se connaître ni de près, ni de loin. D'autre part, l'attribut de l'un peut refléter l'attribut de l'autre. De plus, tu es parti du principe que cela représentait deux instance de la même classe : et pourquoi donc devrait-ce être le cas ? ;)
Reformulé, on pourrait écrire : StatisticObject.TotalNumberOfCommands += aTempCommandObject.Size ; : cela a un sens, et permet d'éviter de créer une fonction spécifique d'ajout (imposant de connaître les types de classes impliqués) dans une fonction qui n'a de sens que pour le programme appelant, ou de créer une classe spécifique pour centraliser ces deux sous-classes...
Avec les template, tu peux en arriver à n'écrire qu'une fois les choses ;)Citation:
Tu ne peux pas créer des prototypes pour toutes les références croisées possibles et imaginables, c'est une évidence. Tu ne peux pas non plus prévoir toutes les utilisations possibles et imaginables de ta classe si elle est un tant soit peu générique / bas niveau. Tu vas donc reporter ce problème vers l'appelant en l'obligeant à définir un paquet de classes d'adaptation.
tu devrais peut être relire cette entrée de la FAQ en ce qui concerne l'amité ;)Citation:
Tu vas me dire "Quel intérêt de faire ça ?", peut-être : ben c'est d'un intérêt évident quand tu écris beaucoup de code-passerelle, c'est à dire du code de transfert entre deux mondes. Or, beaucoup de mes librairies sont justement utilisées par un code de ce type. Actuellement, en C++, je n'ai pas d'autre choix que de fournir des accesseurs, ou directement l'attribut dans certains cas... Performances et simplicité d'utilisation obligent.
Les deux ne sont absolument pas antinomiques, sans passer par les propriétés ;)Citation:
Quand tu fais des librairies génériques dans 99% des cas, comme moi, tu apprends vite à savoir déléguer à l'appelant et/ou à imposer des contrats. De plus, je dois avouer que la "pureté" du code source et le respect de Grands Principes (les majuscules sont volontaires) sont le cadet de mes soucis : mon but premier, à la création d'une classe, est d'avoir une API claire, concise et pratique à utiliser. Le reste, c'est ma tripaille interne et ma sauce, je n'ai pas à exposer à l'utilisateur mes contraintes de code et/ou mes évolutions d'interface.
J'ai bien compris qu'un propriété peut être, au choix, un accesseur, un mutateur ou le couple des eux...Citation:
Une propriété fait ce que tu veux qu'elle fasse. Tu peux l'utiliser pour n'importe quelle donnée, et définir le niveau d'accès que tu souhaites avoir (R, W, RW).
Il n'empêche que cela renvoi un "détail d'implémentation" qui n'a, normalement, souvent pas lieu d'être exposé...
La cardinalité d'un containeur, si la classe agit comme tel, cela ne me dérange pas si on l'expose, mais je ne vois pas en quoi écrire monContainer.size est plus facile que d'écrire monContainer.size()...Citation:
J'ai également beaucoup de mal à voir en quoi restreindre l'accès à une donnée comme la cardinalité d'un container, ou l'accès aux éléments unitaires, pose le moindre problème.
D'autant plus que nous avons, comme le fait remarquer screetch, qu'au moins, nous sommes en mesure, dans le deuxième cas, de nous rendre plus facilement compte qu'il s'agit, effectivement, d'une fonction
[/QUOTE]Le problème de lecture et / ou de l'écriture n'en est pas un, étant donné que nous convenons tous les deux que rien n'empêche de prévoir les restrictions utiles, à la manière de ce que le sucre syntaxique remplace.Citation:
Typiquement, une propriété peut remplacer les opérateurs () et [] de façon tout à fait confortable, en n'autorisant que la lecture et non pas l'écriture.
Le problème est, surtout pour l'opérateur [], que tu expose un détail d'implémentation qu'il serait bien plus intéressant d'exposer sous la forme d'un itérateur (parce que, si tu dois renvoyer l'objet contenu dans une liste, l'opérateur [] n'a strictement plus lieur d'être...)
Ce que je vois surtout, c'est que le programme appelant n'est pas à modifier lorsque l'on transforme un attribut "simple" (= une variable de classe uniquement, donc) en un attribut "complexe" (= sa modification déclenche un trigger interne). Typiquement, tu peux transformer une classe relativement figée et statique en quelque chose de dynamique réagissant "en direct-live" à ses modifications.
On en revient toujours au même problème, qui est que les mutateurs et ou les accesseurs sont, par nature, à éviter...Citation:
C'est quand même un cas concret dans le développement en situation réelle : on implémente une première version d'un composant, qui va ensuite évoluer en quelque chose de plus complexe. Au moins, le code source appelant n'a pas à être modifié, et cela a évité de créer initialement des tonnes d'accesseurs plus ou moins inutiles (et coûteux en temps de développement / tests !!).
Une fois que tu as compris cela ( c'est ce que je répète depuis de nombreux posts), on comprend pourquoi les propriétés sont, elles aussi, à éviter.
Si, pour une raison ou une autre, une modification doit être apportée, il est bien plus intéressant d'invoquer une fonction dont le nom correspond au comportement attendu et de lui fournir les arguments nécessaires, sans que cette fonction ne s'appelle setMachin, d'autant plus que l'on n'a normalement pas à s'inquiéter, en invoquant cette fonction, de la valeur de l'attribut avant la modification: C'est à la fonction de vérifier s'il faut apporter ou non la modification demandée.
Evidemment, lorsqu'il s'agit d'un container, le fait de savoir de combien d'objet il est composé fait partie des services que l'on attend de sa part...
Mais je ne vois pas en quoi cela simplifie les chose d'avoir une propriété size au lieu de la fonction membre constante size.
Tellement mal foutue que Lazarus cherche absolument à la reproduire à 100%, au lieu d'utiliser quelque chose d'autre... :mrgreen:
Tu vas avoir du mal de dériver des deux à la fois... Delphi ne supporte pas l'héritage multiple.
Quant au faux problème du comportement, c'est très simple pourtant : tu définis un bouton (comportement le plus complexe, donc celui que l'on ne "refait" pas), et tu écris sa méthode Paint... Éventuellement en appelant le Paint d'une image non stockée sur la fiche, d'ailleurs. Tu as très exactement le même problème avec les MFC ou l'API Win32, et c'est d'ailleurs tout à fait normal : tout est canvas et window, les variantes de comportement ne sont justement que des variantes.
Autre solution, nettement plus élégante : tu prends un TImageButton et tu règles ses propriétés correctement... :mrgreen:
VCL = Visual Component Library, pour mémoire et information. C'est une bibliothèque RAD, et non pas une base élémentaire comme pourraient l'être les MFC (où le "F" signifie Foundation, je te rappelle, et non pas F**king Very High Level Interface).
En VCL, tu rends une image cliquable très simplement, la différence étant que, justement, tu n'as PAS besoin de séparer le comportement du rendu dans 99.99% des cas... Les composants sont donc adaptés au cas nominal, interdire ou augmenter ce comportement nominal prendra du temps. Réciproquement, en MFC, c'est plutôt "tout ce qui n'est pas codé est inexistant" : ça fait des exécutables plus compacts, certes, mais dans 90% des cas tu passes ton temps à redéfinir 20 fois les mêmes choses.
Mais vouloir un comportement "roots" avec des outils RAD (ou le contraire, d'ailleurs), tu n'as pas l'impression de t'être planté de librairie ? ;)
Arrêtes un peu la mauvaise foi : c'est exactement pareil quand tu appelles une méthode qui te renvoie soit un attribut directement, soit qui effectue un comportement complexe. Tu n'as PAS à savoir ce que ça fait, et tu as des contrats si l'état initial / final ont une importance quelconque (= des effets de bord). Tu as aussi une documentation de la classe, et avec des avertissements sur les coûts CPU anormaux si elle n'a pas été écrite par un incapable fini.
Les problèmes que tu décris sont plus liés à une documentation minable et une conception faite pendant un abrutissement devant la Star'Ac qu'à l'utilisation de propriétés à la place d'accesseurs (ou vice-versa, d'ailleurs).
C'est exactement la même chose pour les méthodes : ce que tu décris, c'est simplement du code de goret, peu importe la manière dont il est appelé. Obtenir la taille d'une liste chaînée en la parcourant à chaque fois, faut ne pas être fini et/ou n'avoir absolument aucune conscience des performances...
Une bonne propriété size renvoie un attribut interne à la lecture, redimensionne le container à l'écriture, et l'attribut est mis à jour à chaque modification du container. Bref, un size() + resize() en un seul point d'accès logique, au lieu de devoir retenir deux fonctions, et le tout prévu pour un minimum syndical de performances.
le RAD est hors sujet ici
les propriétés sont un détail, avoir des parenthèses ou pas ca n'a aucune incidence
permet d'ecrireCode:
1
2
3
4
5
6
7 class AvecAccesseur { int m_value; public: int& value() { return m_value; } const int& value() const { return m_value; } };
pas de get, pas de set. c'est vraiment un détail que ca soit possible de retirer les parenthèses en D et pas en C++, franchement.Code:avecAccesseur.value() += avecAutreAccesseur.value();
ce qui est interessant avec les propriétés c'est surtout la sérialisation et 'introspection, le reste est plutôt anecdotique. c'est comme dire que "struct" est un meilleur mot-lé que "class" ou l'inverse, ca n'apporte rien au débat.
Je te conseille de regarder un peu WPF. C'est usine çà gaz par moment, mais ils ont une manière de gérer ce genre de situation assez différente, qui me semble bien plus intéressante. En gros, un bouton peut contenir d'autres contrôles, par exemple, une image, u texte, une listbox... D'ailleurs, un élément d'une listbox peut elle aussi contenir d'autres contrôles...
L'amitié impose de pouvoir modifier la classe ciblée, ce qui n'est pas toujours ni possible, ni souhaitable.
Ce qui impose l'écriture d'une classe de plus... Ce que justement, je cherche à éviter comme la peste. Une classe wrapper, c'est des lignes de code en plus, donc une source de bug potentielle. Autant limiter ça au code appelant : si tu veux généraliser le principe (et rendre ta classe réutilisable), ta classe-wrapper devra ne pas introduire de comportements spécifiques au code métier appelant, donc ne réellement définir QUE du wrapping... Ce qui fait que tu vas, au final, écrire du code proche (sur le fond) de celui des propriétés bien que différent dans la forme.
Si, par contre, elle introduit du code métier, alors elle n'est plus générique, devient spécifique au code métier appelant, et n'a alors plus aucune raison d'être car elle introduit une couche supplémentaire inutile car non réutilisable.
Encore faut-il que ce soit possible, et c'est rarement le cas. Pour te donner un exemple, tu vas avoir du mal à définir un comportement commun (et donc une interface "unique") entre un bus CAN et des lignes analogiques... Pourtant, il peut être utile de connaître le nombre total de données acquises/transmises, peu importe le médium. Un template va t'imposer, à certains moments, l'écriture d'une interface "commune" qui pourrait très bien ne pas avoir réellement de sens pour l'une des classes, et n'en avoir que pour l'appelant. Tu vas donc complexifier une classe avec des informations "inutiles" pour respecter une interface commune : OK, mais franchement, je trouve que c'est du boulot inutile et pas forcément réutilisable (donc pas nécessaire de se taper une interface from scratch pour ça).
Pas plus qu'un accesseur et/ou un attribut public, y compris "public" via un friend.
Le fait que size() n'est pas affectable, contrairement à la propriété homonyme, donc tu devras écrire une méthode resize() supplémentaire.
Pour augmenter ta capacité de 10 éléments, tu devras donc écrire monContainer.resize(monContainer.size()+10) au lieu de monContainer.size+=10... Ou développer une troisième méthode "relative_resize()" qui prends un delta.
Cela commence à faire beaucoup de méthodes et des lignes d'appel bien longues, je trouve !! ;)
Et j'y réponds la même chose : utilises des classes mieux documentées et/ou ne les fais pas faire par des incapables.
En quoi est-ce un détail d'implémentation ??? Que tu appelles l'obtention d'une cardinalité "property size", "getSize()", "count()" ou "size()", c'est la même chose derrière. Simplement, certaines formes d'écriture sont plus pratiques pour l'appelant. Toutes ces formes exposent autant (ou aussi peu, je dirais) les détails d'implémentation réels derrière.
Ce qui va rendre l'écriture de containers un minimum performants assez difficile... :twisted:
Que tu appelles ta méthode de lecture getElementAt(i) ou [ i ], quelle est la différence réelle ? Pour moi, les deux sont des accesseurs... Le premier est simplement plus lourd et pénible à utiliser que le second.
De plus, comme les deux formes sont (sémantiquement parlant) équivalentes, il n'y a pas plus de problèmes à définir un itérateur avec une propriété qu'avec des méthodes.
Ce qui revient pour moi à la même chose : tu n'as finalement fait que renommer ton accesseur/mutateur mais il remplit exactement le même rôle. Un accesseur n'est pas caractérisé par un "get" ou "set" en préfixe, j'espère que l'on est d'accord sur ce point...
Cf. exemple d'utilisation ci-dessus, avec le resize.
Si c'est sans importance, pourquoi tu combats autant le fait de les enlever ? Faudrait savoir, ou être cohérent... :mrgreen:
Désolé, mais là, c'était trop tentant.
Parce que tu as pris l'habitude de te taper ça. Moi, j'ai pris l'habitude d'avoir plus simple et, surtout, d'avoir quelque chose de nettement plus proche du raisonnement humain tout en restant à un niveau d'efficacité proche de l'optimal. Via des propriétés, la taille (pour continuer cet exemple) est alors une entité à part entière, UNIQUE, que tu manipules en fonction de tes besoins et non pas quelque chose de "diffus" accédé au travers de 3 ou 4 fonctions plus ou moins cohérentes.
Via des méthodes classiques, la seule façon d'arriver à ce niveau de centralisation serait de définir une sous-classe "Size", avec quelques primitives, qui permettrait d'avoir un point central d'accès aux fonctions liées à la taille du container.
Ce qui alourdit très nettement l'écriture et l'utilisation dudit container, sans parler des risques de pertes de performances... Inconvénients que n'ont pas les propriétés. La "tripaille" interne reste tout aussi "diffuse" avec les propriétés (plusieurs méthodes privées nécessaires pour réaliser les actions atomiques), donc pas de changements majeurs pour le développeur, mais l'interface publique (celle de l'utilisateur) est naturellement centralisée.
Parce que tu appelles ça documenter ???? Pour toi, documenter, c'est simplement donner un nom à peu près correct à une méthode, et point barre ???
Ben ils sont vachement tolérants, dans ta boîte... Si un de mes dévs me fait ce coup-là sur une doc client, je le crucifie.
Ce qui pour moi est rédhibitoire pour faire des IHMs. C'est certes bien mieux que les MFC, mais c'est loin d'être parfait ou du niveau d'un vrai RAD. Et il se pose le problème (plus ou moins épineux) de la connexion avec des DLL natives : autant certains cas de figure sont très simples, autant d'autres sont franchement galères vu l'impossibilité de franchir (via debug) la barrière entre le natif et le managé.
Si ce n'esxt que l'amitié permet de n'exposer quelque chose qu'à ce qui en a réellement besoin.
Par nature, toute fonction publique (qu'il s'agisse d'un accesseur, d'un mutateur, d'une fonction normale) est susceptible d'être appelée de "n'importe où dans le code", dés le moment où le contenu de l'objet est connu.
Or, je suis désolé, mais il y a quantité de chose qui doivent n'être connues que d'un minimum d'autres choses extérieures.
De ce point de vue, l'amitié est, définitivement, la meilleure manière de travailler, si l'on excepte l'héritage (qui n'est pas toujours cohérent)
Le principe des propriétés, encore une fois, c'est que cela facilite le fait d'exposer des choses qui... auraient en tout casdu être exposées de manière beaucoup plus restrictive.
Au contraire, si une même fonction a -- en dehors du polymorphisme, qui garde quand même la sémantique de la fonction -- deux comportement clairement opposés, c'est plutôt le signe d'une mauvaise compréhension de ce que l'on attend de cette fonction..Citation:
Le fait que size() n'est pas affectable, contrairement à la propriété homonyme, donc tu devras écrire une méthode resize() supplémentaire.
Pour augmenter ta capacité de 10 éléments, tu devras donc écrireTu sembles oublier que, lorsque tu crées un conteneur, il ne suffit pas de modifier la valeur d'un attribut pour, effectivement, redimensionner ce container...Citation:
monContainer.resize(monContainer.size()+10) au lieu de monContainer.size+=10... Ou développer une troisième méthode "relative_resize()" qui prends un delta.
De plus, pour redimensionner un container, tu as classiquement trois souhaits possibles:Dans le premier cas, il s'agit de redimensionner le conteneur sur base de la taille d'un autre conteneur (pas forcément compatible), d'un compteur ou d'une information récupérée par ailleurs.
- le redimensionner à une valeur déterminée, indépendante de la taille d'origine
- Réduire sa taille de un (ou de plusieurs) élément(s)
- augmenter sa taille de un (ou de plusieurs) élément(s)
Dans le deuxième cas, on peut estimer que cette diminution de la taille correspond... au retrait d'élément du conteneur
Et, dans le troisième cas, on peut, à l'inverse, estimer que cette augmentation de taille correspond... à l'ajout d'élément dans le contenur.
Nous avons donc bel et bien affaire à... trois comportements différents qui...méritent d'être clairement différenciés.
Maintenant, si la fonction d'ajout ou de retrait d'élément utilise resize, ma fois, pourquoi pas :question:
Après tout, un simple
le fait très bien, quand ce n'est pas, encore mieux unCode:
1
2
3
4
5
6 void MaClass::add(/*...*/) { resize(m_size+1); /* suite de la logique, dépedant du conteneur mis en oeuvre }
Code:
1
2
3
4 void MaClass::add(/*...*/) { m_items.add(/*...*/) }
Beaucoup de méthodes... faut pas exagérer...Citation:
Cela commence à faire beaucoup de méthodes et des lignes d'appel bien longues, je trouve !! ;)
Dans le cadre de la gestion de la taille, tu as, au total quatre fonctions à créer, qui, au moins, définissent clairement ce qu'elles font:
- size() : renvoi du nombre d'élément
- resize(int) : redéfini le nombre d'éléments en fonction du nombre donné en paramètre
- add( type) : ajoute un objet
- remove(iterateur) : retire un objet
Bon, allez, je t'accorde que, pour add et remove, on peut aussi envisager une version prenant un itérateur de début et un itérateur de fin
et, en terme de ligne de code, ca ne fait pas grand chose non plus.
Par contre, cela permet d'avoir des comportement clairement définis, qui ne risquent pas de prêter à confusion
Et j'y réponds la même chose : utilises des classes mieux documentées et/ou ne les fais pas faire par des incapables.
En quoi est-ce un détail d'implémentation ??? Que tu appelles l'obtention d'une cardinalité "property size", "getSize()", "count()" ou "size()", c'est la même chose derrière. Simplement, certaines formes d'écriture sont plus pratiques pour l'appelant. Toutes ces formes exposent autant (ou aussi peu, je dirais) les détails d'implémentation réels derrière.
Ce qui va rendre l'écriture de containers un minimum performants assez difficile... :twisted:
La sémantique que l'on accorde généralement à l'opérateur ()...Citation:
Que tu appelles ta méthode de lecture getElementAt(i) ou [ i ], quelle est la différence réelle ?
Cela n'a l'air de rien, mais, modifier la sémantique d'un opérateur ou d'une fonction est la pire des choses qui soient.
Ainsi, l'opérateur [] véhicule une sémantique d'acces direct, aléatoire.
C'est très bien lorsque la collection, effectivement, basée sur un tableau, mais cela perd tout son sens lorsque la collection est basée sur... une liste ou n'importe quoi d'autre...
C'est la raison pour laquelle, plutôt que de renvoyer une référence (constante ou non)sur l'élément de la collection, je suis clairement partisan de... définir un itérateur imbriqué et d'utiliser de préférence celui-ci comme valeur de retour, si tant est, toujours, qu'il soit opportun de permettre "à l'extérieur" d'accéder au contenu de la collection.
De cette manière, tu arrives même à éviter, au minimum dans une certaine mesure, la réécriture des fonctions qui utilisent l'itérateur si, un jour, tu décide de modifier la manière dont la collection est effectivement gérée en interne.
Comme je l'ai dit plus haut, tu fait l'impasse sur la sémantique, et c'est bien dommage ;)Citation:
Pour moi, les deux sont des accesseurs... Le premier est simplement plus lourd et pénible à utiliser que le second.
Le fait est que la sémantique que tu vas donner au nom de ta fonction importe tout autant que le comportement qu'elle met en oeuvre.Citation:
Ce qui revient pour moi à la même chose : tu n'as finalement fait que renommer ton accesseur/mutateur mais il remplit exactement le même rôle. Un accesseur n'est pas caractérisé par un "get" ou "set" en préfixe, j'espère que l'on est d'accord sur ce point...
Nous sommes, effectivement, d'accord sur le fait qu'un accesseur n'est pas caractérisé par la présence du préfixe "get" ou "set", mais, là où nous ne sommes visiblement pas d'accord c'est:
- sur le fait qu'une fonction doit avoir un comportement déterminé, et non un comportement susceptible de varier selon les circonstances
- sur le fait que le meilleur moyen pour quelqu'un qui lit le code le comprenne facilement, c'est que le nom de chaque fonction représente clairement le comportement qu'elle met en oeuvre
- sur le fait qu'il faut autant que faire se peut éviter de changer la sémantique généralement accordée à des fonctions ou des opérateurs couremment utilisés par ailleurs
Et, au passage, de manière générale, un accesseur non constant est, pour moi, une aberration... Surtout s'il renvoie une référence sur l'attribut (cf effective C++, entre autres)
Ma priorité n'est pas que je puisse avoir le meilleur design mais que ce design puisse changer, et si possible rapidement. Je ne me pose pas la question si je dois faire un accesseur, un mutateur, etc... tant que c'est documenté.
Les propriétés, quand elles sont bien faites, permettent de rajouter comme un espèce de wrapper autour des fonctionnalités offertes par une classe. Si il y a une modif à faire, hop, on change un champ en méthode mais le contrat et le code de l'appelant reste le même.
Ce qui était vraiment bien en Delphi, c'est que ce wrapper ne coûtait rien en performance vu qu'on pouvait mapper une propriété sur un champ. Donc on pouvait les utiliser tout le temps sans que ca fasse plein d'appels de méthode en plus.
En fait je pense qu'il faut avoir codé sous Delphi pour comprendre cet intérêt (que MacLak a bien expliqué) et combien ca accélère les choses.
Renvoyer une reférence sur un objet en C++, ce n'est pas pareil, ca ne permet pas de transformer un champ en méthode de manière transparente.
Je ne vois pas pourquoi enlever ces parenthèses est si grave que cela. C'est au contraire un moyen d'abstraction intéressant je trouves.
Pour le reste, les variadic templates et le fait de pouvoir faire des templates sur du int, float ou même char[], c'est quand même le bonheur. Ça n'a pas été cités parmis les top features manquantes au C++. Pourtant, c'est parmis celles dont je me sers le plus.
certes, si on pouvait passer a autre chose que les property maintenant...
Ou de complexifier inutilement quelque chose qui ne doit pas l'être... Chaque médaille a son revers, tu le sais aussi bien que moi.
Et alors ? Si c'est en lecture seule, ça n'a aucune importance, sauf celui de te permettre d'avoir du code d'introspection.
Si c'est en écriture, tu as des tonnes de mécanismes permettant de blinder cet aspect (sections critiques, enregistrement via par exemple le TLS, encapsulation et/ou private implementation, singletons, pointeurs avec compteur de référence, et j'en passe).
On est bien d'accord. Je te rappelle qu'une propriété est forcément publique, donc remplace purement et simplement la publication des accesseurs (qui deviennent/restent privés), OU permet une centralisation d'un concept sur un seul point d'accès (typiquement, la gestion de la taille / capacité d'un container, justement).
Rien ne t'oblige à publier 100% de tes attributs dans des propriétés RW, ni à remplacer le concept d'attribut par celui de propriété.
Une propriété est une interface de la classe : si quelque chose ne doit pas être manipulé de l'extérieur, pourquoi diable voudrais-tu en faire une propriété ???
Pas plus qu'un accesseur... C'est très exactement la même chose, seul l'utilisateur voit une différence réelle entre les deux.
Avec une propriété, si... ;) Du moins, c'est l'impression qu'aura l'utilisateur.
Tu me reparles de multiples fonctions de redimensionnement, moi je te parle d'un concept unique de taille (ou capacité, si tu préfères). Cela n'empêche absolument pas d'utiliser une méthode Add() pour ajouter un élément, wrappant au besoin un sous-container. Quand aux fonctions de modification de taille, il est souhaitable qu'elles existent de façon unitaire (erase(), resize(), incsize(), decsize(), que sais-je encore...) et qu'elles soient utilisées au sein de l'interface "Write" de la propriété, en fonction de la valeur passée en paramètre.
Bref, c'est à toi de t'assurer que tu respectes ton contrat, c'est à dire que ta taille a été augmentée d'une unité après un ajout... Le formalisme utilisé (méthodes ou propriété) n'a absolument aucune importance, sauf pour l'utilisateur final qui y gagne en simplicité.
Je trouve plus confusant d'avoir N points d'accès à un concept donné (ex : la capacité d'un container) au lieu d'un seul et unique, dont la valeur ou le cadre d'utilisation (L/R-Value) est proche de la représentation "humaine" dudit concept. Une taille/capacité se lit, se détermine, peut augmenter, diminuer, être réduite à néant. C'est une formalisation assez proche de ce que l'on trouve fréquemment dans les spécifications de besoin, en fait.
[mode bash]
Ah, la force de l'inertie... ;)
[/mode bash]
Blague à part : taper dans une map de la STL se fait également via [], simplement tu n'as pas toute la finesse de contrôle qui est possible via find/insert. Et alors ? Les exceptions existent pour ça, ce sont effectivement des comportements non-nominaux de tenter de récupérer un élément inexistant et/ou d'écraser un existant, mais ça peut parfaitement convenir à la plupart des besoins.
Pourquoi s'emm.... avec un itérateur et des méthodes find/insert, alors que l'on contrôle en amont ce qui est lu/écrit dans la map, et que les exceptions (cas anormal et rarissime) conviennent parfaitement à la gestion des erreurs ? L'opérateur [] fait parfaitement l'affaire, est cent fois plus simple à utiliser, et répond au besoin. Utiliser autre chose, c'est simplement aimer se compliquer la vie pour rien.
Pour toi, peut-être. Pour moi, c'est un index, ni plus, ni moins, avec des limites hautes et basses et (par défaut) pas de "trous" dans son domaine. Son caractère aléatoire et/ou direct m'importe peu.
Je le vois comme "méthode pratique et basique d'accès", ni plus, ni moins. Je ne pars pas du principe que c'est l'accès le plus performant (même si c'est souvent le cas), je pars du principe que c'est le plus PRATIQUE. C'est très différent.
Notamment pour les maps, surtout quand je les utilise façon tableau associatif des langages interprétés : c'est nettement plus pratique pour la configuration lue de façon "rare" dans le programme, la configuration lue "intensivement" étant de toutes façons convertie dans un format plus efficace.
A titre personnel, j'ai récemment écrit une classe qui possède deux opérateurs [] : le premier prends un entier en paramètre, est est effectivement LA méthode la plus efficace pour accéder à un élément donné (accès direct, aléatoire et par référence).
L'opérateur est surchargé pour accepter, en paramètre, une chaîne de caractères. Dans ce cas précis, la recherche est séquentielle (O(N)), et sert à retrouver un élément via un critère de haut niveau, demandé de façon rare mais qui évite à l'utilisateur de se rappeler des numéros plus ou moins abstraits pour accéder aux données.
Et moi, ça m'évite de faire une map / classe d'association dédiée à ça alors que c'est une fonction accessoire, pour ne pas dire une scorie...
Heu... On ne doit pas avoir le même sens pour sémantique (="le fond"), que j'associe en général au mot "syntaxe" (="la forme").
La sémantique, c'est l'opération à effectuer (récupérer un élément, avoir la taille du container, etc.). La syntaxe, c'est la forme que cela prends (propriété, méthode, accesseur, service réseau, glutz vénusien...).
Sûr... Va dire ça à la méthode connect() d'une socket, suivant si le câble réseau est branché ou pas, ou si le DHCP a filé une passerelle ou pas, ou si ça répond ou pas, etc.
Encore heureux que les fonctions s'adaptent aux circonstances : comment voudrais-tu faire de la gestion d'erreurs sans ça ???
Sans même parler des surcharges de méthodes...
Je pense que tu as voulu dire autre chose que ce que je suis en train de lire : peux-tu reformuler, stp ?
Ce qui est le cas d'une propriété, clairement définie comme telle bien entendu, qui regroupe via un seul identifiant un concept complet telle que la taille / capacité d'un container, au lieu de disperser cette information sur N entités diverses.
Donc, un seul point à documenter / assimiler, et tu as l'accès à toutes les fonctions liées à un concept. Difficile de faire plus simple, plus concis et plus compréhensible, je trouve.
Il faut surtout éviter des présuppositions qui sont, à mon sens, dangereuses... Comme croire qu'un accès par [] est forcément direct et aléatoire, alors que la STL te prouve le contraire sur ses propres containers. Ou encore croire qu'un container nommé "Liste" est forcément lent, séquentiel et tutti-quanti, qu'une map est forcément une association complexe (une LUT n'est qu'un cas particulier de map), etc.
Tu as aussi d'autre présuppositions malheureuses, comme de présupposer du rôle d'une fonction size() : c'est quoi, le couple (size,count), ou le couple (capacity,size) ?? En fonction des frameworks / langages / habitudes, c'est l'un, ou l'autre... Pour moi, c'est en général le premier cas par exemple, je récupère habituellement la cardinalité via count plutôt que size, si je n'ai pas de contraintes spécifiques.
Des concepts comme les propriétés permettent de rendre les classes plus "intelligentes", sans pour autant complexifier leur interface publique, ni les rendre spécialement moins performantes, ni spécialement plus longues à écrire. Pour ma part, je trouve que ça va dans le bon sens, étant donné que développeur comme utilisateur y gagnent.
Bon, désolé du pavé, mais t'es au moins "aussi pire" que moi au niveau longueur des posts... ;)
il ne s'agit pas de documenter un bidule, il s'agit que le bidule soit auto-documenté, grâce a un nom non pas "a peu près" correct mais _exact_
resize() fait exactement ce que son nom indique.
size non, et a besoin d'une documentation.
operator[] n'est pas clair non plus. la preuve ? tu t'es trompé dans sa description.
insert() est très clair lui.