La documentation en POO doit éviter de spécifier si un «getter» effectue un calcul ou non.


39

Le programme CS de mon école évite toute mention de la programmation orientée objet. Je me suis donc mis à lire moi-même pour la compléter, en particulier la Construction de logiciels orientés objet de Bertrand Meyer.

Meyer insiste à plusieurs reprises sur le fait que les classes doivent cacher autant d'informations que possible sur leur implémentation, ce qui est logique. En particulier, il soutient à plusieurs reprises que les attributs (c'est-à-dire les propriétés statiques, non calculées de classes) et les routines (propriétés de classes qui correspondent à des appels de fonction / procédure) ne devraient pas être distingués l'un de l'autre.

Par exemple, si une classe Persona l'attribut age, il affirme qu'il devrait être impossible de dire, à partir de la notation, si Person.agecorrespond en interne à quelque chose comme return current_year - self.birth_dateou simplement return self.age, où self.agea été défini en tant qu'attribut constant. Cela a du sens pour moi. Cependant, il continue en affirmant ce qui suit:

La documentation client standard d'une classe, appelée forme abrégée, sera conçue de manière à ne pas révéler si une caractéristique donnée est un attribut ou une fonction (dans les cas où elle pourrait l'être).

c'est-à-dire qu'il affirme que même la documentation de la classe devrait éviter de spécifier si un "getter" effectue ou non un calcul.

Ceci, je ne suis pas. La documentation n'est-elle pas le seul endroit où il serait important d'informer les utilisateurs de cette distinction? Si je devais concevoir une base de données contenant des Personobjets, ne serait-il pas important de savoir si l' Person.ageappel est coûteux ou non , afin que je puisse décider de mettre en œuvre ou non une sorte de cache pour celui-ci? Ai-je mal compris ce qu'il dit ou est-il simplement un exemple particulièrement extrême de la philosophie de conception de la programmation orientée objet?


1
Question interessante. J'ai récemment posé une question à propos de quelque chose de très similaire: comment concevrais-je une interface qui indique clairement quelles propriétés peuvent changer de valeur et lesquelles resteront constantes? . Et j’ai une bonne réponse qui pointe vers la documentation, c’est-à-dire ce contre quoi Bertrand Meyer semble s’opposer.
Stakx

Je n'ai pas lu le livre. Meyer donne-t-il des exemples du style de documentation qu'il recommande? J'ai du mal à imaginer ce que vous avez décrit comme travaillant pour n'importe quelle langue.
user16764

1
@ PatrickCollins, je vous suggère de lire «exécution dans le royaume des noms» et de passer derrière le concept des verbes et des noms ici. Deuxièmement, la POO ne concerne pas les getters et les setters, je suggère Alan Kay (inventeur de la POO): programmation et ampleur
AndreasScheinert

@AndreasScheinert - parlez-vous de cela ? Je ris au «tout pour le besoin d'un clou en fer à cheval», mais cela semble être un discours sur les maux de la programmation orientée objet.
Patrick Collins

1
@PatrickCollins oui ça: steve-yegge.blogspot.com/2006/03/… ! Cela donne quelques points à méditer, les autres sont les suivants: vous devez transformer vos objets en structures de données en utilisant (b) des setters.
AndreasScheinert

Réponses:


58

Je ne pense pas que le point de Meyer, c'est que vous ne devriez pas dire à l'utilisateur quand vous avez une opération coûteuse. Si votre fonction doit accéder à la base de données, ou faire une demande à un serveur Web, et passer plusieurs heures en calcul, il faudra que le code suivant le sache.

Mais le codeur utilisant votre classe n'a pas besoin de savoir si vous avez implémenté:

return currentAge;

ou:

return getCurrentYear() - yearBorn;

Les caractéristiques de performance entre ces deux approches sont tellement minimes que cela ne devrait pas avoir d'importance. Le codeur qui utilise votre classe ne devrait vraiment pas se soucier de ce que vous avez. C'est le point de mon yer.

Mais ce n'est pas toujours le cas, par exemple, supposons que vous ayez une méthode de taille sur un conteneur. Cela pourrait être mis en œuvre:

return size;

ou

return end_pointer - start_pointer;

ou ce pourrait être:

count = 0
for(Node * node = firstNode; node; node = node->next)
{
    count++
}
return count

La différence entre les deux premiers ne devrait vraiment pas avoir d'importance. Mais le dernier pourrait avoir de graves conséquences sur les performances. C'est pourquoi le TSL, par exemple, dit que .size()c'est le cas O(1). Il ne documente pas exactement comment la taille est calculée, mais il me donne les caractéristiques de performance.

Donc : documentez les problèmes de performance. Ne documentez pas les détails de la mise en œuvre. Je me fiche de savoir comment std :: sort trie mes données, tant que cela fonctionne correctement et efficacement. Votre classe ne devrait pas non plus documenter comment elle calcule les choses, mais si quelque chose a un profil de performance inattendu, documentez-le.


4
De plus: documentez d'abord la complexité spatio-temporelle, puis expliquez pourquoi une fonction possède ces propriétés. Par exemple:// O(n) Traverses the entire user list.
Jon Purdy

2
= (Quelque chose d'aussi trivial que Python lenéchoue à le faire ... (Dans au moins certaines situations, c'est ce O(n)que nous avons appris dans un projet à l'université lorsque j'ai suggéré de stocker la longueur au lieu de la recalculer à chaque itération de la boucle)
Izkata

@ Izkata, curieux. Vous souvenez-vous de quelle structure était O(n)?
Winston Ewert

@ WinstonEwert Malheureusement pas. C'était il y a plus de 4 ans dans un projet d'exploration de données, et je ne l'avais suggéré à mon ami que parce que je travaillais avec C dans une autre classe.
Izkata Le

1
@JonPurdy J'ajouterais qu'en code métier normal, il n'est probablement pas logique de spécifier la complexité big-O. Par exemple, un accès à la base de données O (1) sera probablement beaucoup plus lent que le parcours de liste en mémoire O (n). Documentez donc ce qui compte. Mais il y a certainement des cas où la documentation de la complexité est très importante (collections ou autre code exigeant beaucoup d'algorithmes).
svick

16

D'un point de vue académique ou puriste CS, il s'agit bien sûr d'un échec dans la documentation pour décrire quoi que ce soit sur les éléments internes de la mise en œuvre d'une fonctionnalité. En effet, l'utilisateur d'une classe ne devrait idéalement pas émettre d'hypothèses sur son implémentation interne. Si la mise en œuvre change, idéalement, aucun utilisateur ne le remarquera - la fonctionnalité crée une abstraction et les éléments internes doivent rester complètement masqués.

Cependant, la plupart des programmes du monde réel souffrent de la "loi des abstractions qui fuit" de Joel Spolsky , qui dit

"Toutes les abstractions non triviales, dans une certaine mesure, ont des fuites."

Cela signifie qu'il est pratiquement impossible de créer une abstraction complète des entités complexes dans une boîte noire. Et un symptôme typique de ceci est des problèmes de performances. Ainsi, dans le cas de programmes réels, il peut être très important de savoir quels appels coûtent cher ou non, et une bonne documentation devrait inclure cette information (ou indiquer où l'utilisateur d'une classe est autorisé à émettre des hypothèses sur les performances, et où il n'en est pas ainsi. ).

Mon conseil est donc le suivant: incluez les informations sur les appels potentiellement coûteux si vous écrivez des documents pour un programme réel, et excluez-les pour un programme que vous écrivez uniquement à des fins éducatives de votre cours CS, étant donné que toute considération de performance doit être conservée intentionnellement hors de portée.


+1, plus la majeure partie de la documentation créée est destinée au prochain programmeur qui gère votre projet, et non au prochain programmeur qui l' utilisera .
Jmoreno

12

Vous pouvez écrire si un appel donné est coûteux ou non. Mieux, utilisez une convention de dénomination comme getAgepour un accès rapide loadAgeou fetchAgepour une recherche coûteuse. Vous voulez absolument informer l'utilisateur si la méthode exécute des E / S.

Chaque détail que vous donnez dans la documentation est comme un contrat qui doit être honoré par la classe. Il devrait informer sur le comportement important. Souvent, vous verrez des indications de complexité avec la grande notation O. Mais vous voulez généralement être bref et concis.


1
+1 pour mentionner que la documentation fait autant partie du contrat d'une classe que son interface.
Bart van Ingen Schenau

Je soutiens ceci. De plus, en général, nous essayons de minimiser le besoin de getters en proposant des méthodes comportementales.
sevenforce

9

Si je devais concevoir une base de données remplie d'objets Person, ne serait-il pas important de savoir si Person.age est un appel coûteux?

Oui.

C'est pourquoi j'utilise parfois des Find()fonctions pour indiquer que l'appel peut durer un certain temps. C'est plus une convention qu'autre chose. Le temps qu'il faut pour une fonction ou à un retour ne fait aucune différence au programme (bien qu'il pourrait à l'utilisateur), bien que parmi les programmeurs il est une attente, si elle est déclarée comme un attribut, le coût de l' appeler devrait être faible.

Dans tous les cas, le code lui-même devrait contenir suffisamment d'informations pour en déduire qu'il s'agit d'une fonction ou d'un attribut. Par conséquent, je ne vois pas vraiment la nécessité de le mentionner dans la documentation.


4
+1: cette convention est idiomatique à plusieurs endroits. En outre, la documentation doit être réalisée au niveau de l'interface. À ce stade, vous ne savez pas comment Person.Age est implémenté.
Telastyn

@Telastyn: Je n'ai jamais pensé à la documentation de cette manière. c'est-à-dire que cela devrait être fait au niveau de l'interface. Cela semble évident maintenant. +1 pour ce précieux commentaire.
Stakx

J'aime beaucoup cette réponse. Un exemple parfait de ce que vous décrivez selon lequel les performances ne concernent pas le programme lui-même serait le cas si Person était une entité extraite d'un service RESTful. GET est inhérent, mais il n’est pas évident si cela sera bon marché ou coûteux. Bien sûr, ce n'est pas nécessairement la POO, mais le point est le même.
maple_shaft

+1 pour utiliser des Getméthodes sur des attributs pour indiquer une opération plus lourde. J'ai vu suffisamment de code pour que les développeurs supposent qu'une propriété est simplement un accesseur et l'utilisent plusieurs fois au lieu d'enregistrer la valeur dans une variable locale, et exécutent ainsi plusieurs fois un algorithme très complexe. S'il n'y a pas de convention pour ne pas implémenter de telles propriétés et que la documentation n'indique pas la complexité, alors je souhaite à quiconque de maintenir une telle application bonne chance.
enzi

D'où vient cette convention? En pensant à Java, je l’attendrais dans l’inverse: la getméthode étant équivalente à un accès attributaire et donc pas chère.
sevenforce

3

Il est important de noter que la première édition de ce livre a été écrite en 1988, au début de la programmation orientée objet. Ces personnes travaillaient avec des langages plus purement orientés objet qui sont largement utilisés aujourd'hui. Nos langages OO les plus populaires aujourd'hui - C ++, C # et Java - présentent des différences assez significatives par rapport au fonctionnement des langages anciens, plus purement OO.

Dans un langage tel que C ++ et Java, vous devez faire la distinction entre l'accès à un attribut et un appel de méthode. Il y a un monde de différence entre instance.getter_methodet instance.getter_method(). L'un obtient réellement votre valeur et l'autre pas.

Lorsque vous travaillez avec un langage plus purement OO, de persuasion Smalltalk ou Ruby (ce qui semble être le langage Eiffel utilisé dans ce livre), il devient un conseil parfaitement valide. Ces langages appellent implicitement des méthodes pour vous. Il ne devient plus aucune différence entre instance.attributeet instance.getter_method.

Je ne voudrais pas transpirer ce point ou le prendre trop dogmatiquement. L'intention est bonne - vous ne voulez pas que les utilisateurs de votre classe s'inquiètent de détails d'implémentation non pertinents - mais cela ne correspond pas parfaitement à la syntaxe de nombreuses langues modernes.


1
Point très important à propos de l'année dans laquelle la suggestion a été faite. Nit: Smalltalk et Simula datent des années 60 et 70, ce qui fait que 88 n’est pas du "tout petit âge".
Luser droog

2

En tant qu'utilisateur, vous n'avez pas besoin de savoir comment quelque chose est implémenté.

Si les performances sont un problème, il faut faire quelque chose dans l'implémentation de la classe, pas autour. Par conséquent, l’action correcte consiste à corriger l’implémentation de la classe ou à signaler un bogue au responsable.


3
Est-il toujours vrai qu'une méthode coûteuse en calcul est un bogue? Comme exemple trivial, supposons que je sois préoccupé par la somme des longueurs d'un tableau de chaînes. En interne, je ne sais pas si les chaînes de ma langue sont de style Pascal ou de style C. Dans le premier cas, puisque les chaînes "connaissent" leur longueur, je peux m'attendre à ce que ma boucle de somme de longueur prenne du temps linéaire en fonction du nombre de chaînes. Je devrais également savoir que les opérations qui changent la longueur des chaînes seront associées à une surcharge car string.lengthelles seront recalculées chaque fois que cela change.
Patrick Collins

3
Dans ce dernier cas, comme la chaîne ne "connaît" pas sa longueur, je peux m'attendre à ce que ma boucle de somme de longueur prenne un temps quadratique (qui dépend à la fois du nombre de chaînes et de leur longueur), mais des opérations qui modifient la longueur. des chaînes sera moins cher. Aucune de ces implémentations n'est fausse, et aucun ne mériterait un rapport de bogue, mais elles appellent des styles de codage légèrement différents afin d'éviter des ratés inattendus. Ne serait-il pas plus facile si l'utilisateur avait au moins une vague idée de ce qui se passait?
Patrick Collins

Donc, si vous savez que la classe string implémente le style C, vous choisirez une méthode de codage tenant compte de ce fait. Mais que se passe-t-il si la prochaine version de la classe string implémente la nouvelle représentation de style Foo? Allez-vous changer votre code en conséquence ou accepterez-vous la perte de performance causée par de fausses hypothèses dans votre code?
mouviciel

Je vois. Ainsi, la réponse de OO à "Comment puis-je tirer quelques performances supplémentaires de mon code, en s’appuyant sur une implémentation spécifique?" est "Vous ne pouvez pas." Et la réponse à "Mon code est plus lent que ce à quoi je m'attendais, pourquoi?" est "Il doit être réécrit." Est-ce plus ou moins l'idée?
Patrick Collins

2
@PatrickCollins La réponse de OO est basée sur des interfaces et non des implémentations. N'utilisez pas une interface qui n'inclut pas de garanties de performances dans la définition de l'interface (comme dans l'exemple de C ++ 11 List.size dont la garantie est O (1)). Il n'est pas nécessaire d'inclure des détails d'implémentation dans la définition de l'interface. Si votre code est plus lent que vous ne le souhaiteriez, existe-t-il une autre réponse que celle que vous devrez changer pour être plus rapide (après l'avoir profilée pour déterminer les goulots d'étranglement)?
Stonemetal

2

Toute documentation orientée programmeur qui ne renseigne pas les programmeurs sur le coût en complexité des routines / méthodes est erronée.

  • Nous cherchons à produire des méthodes sans effets secondaires.

  • Si l'exécution d'une méthode a une complexité d'exécution et / ou de mémoire autre que O(1), dans les environnements soumis à une mémoire ou à une contrainte de temps, des effets secondaires peuvent être considérés .

  • Le principe de la moindre surprise est violé si une méthode effectue quelque chose de complètement inattendu - dans ce cas, monopoliser de la mémoire ou perdre du temps processeur.


1

Je pense que vous l'avez bien compris, mais je pense aussi que vous avez un bon argument. Si Person.ageest implémenté avec un calcul coûteux, alors je pense que j'aimerais aussi voir cela dans la documentation. Cela peut faire la différence entre l'appeler de manière répétée (s'il s'agit d'une opération peu coûteuse) ou l'appeler une fois et mettre en cache la valeur (si elle est chère). Je ne sais pas avec certitude, mais je pense que dans ce cas, Meyer pourrait convenir qu'un avertissement dans la documentation devrait être inclus.

Une autre façon de gérer cela consiste à introduire un nouvel attribut dont le nom implique un long calcul (tel que Person.ageCalculatedFromDB) et à Person.agerenvoyer ensuite une valeur mise en cache dans la classe, mais cela peut ne pas toujours être approprié et sembler trop compliqué. les choses, à mon avis.


3
On pourrait aussi faire valoir que si vous avez besoin de connaître l' ageun Person, vous devez appeler la méthode pour l' obtenir indépendamment. Si les appelants commencent à faire des calculs trop ingénieux pour éviter de faire le calcul, ils risquent de voir leur implémentation ne pas fonctionner correctement car ils ont franchi une limite d'anniversaire. Les implémentations coûteuses de la classe se traduiront par des problèmes de performances qui peuvent être résolus par le profilage et des améliorations telles que la mise en cache peuvent être effectuées en classe, où tous les appelants verront les avantages (et les résultats corrects).
Blrfl

1
@Blrfl: eh bien oui, la mise en cache devrait être faite dans la Personclasse, mais je pense que la question était destinée à être plus générale et ce Person.agen'était qu'un exemple. Il y a probablement des cas où le choix de l'appelant serait plus judicieux - peut-être l'appelé a-t-il deux algorithmes différents pour calculer la même valeur: un rapide mais inexact, un beaucoup plus lent mais plus précis où cela peut se produire), et la documentation doit le mentionner.
FrustratedWithFormsDesigner

Deux méthodes offrant des résultats différents constituent un cas d'utilisation différent de celui auquel vous attendez la même réponse à chaque fois.
Blrfl

0

La documentation pour les classes orientées objet implique souvent un compromis entre donner aux responsables de la classe une flexibilité pour changer de conception et permettre aux consommateurs de la classe d'exploiter pleinement son potentiel. Si une classe immuable aura un certain nombre de propriétés qui ont une certaine exacte relation les uns avec les autres (par exemple le Left, RightetWidthles propriétés d’un rectangle à coordonnées entières alignées sur la grille), on peut concevoir que la classe stocke toute combinaison de deux propriétés et calcule la troisième, ou l’utiliser pour stocker les trois. Si rien dans l'interface n'indique clairement quelles propriétés sont stockées, le programmeur de la classe pourra peut-être modifier la conception dans le cas où cela s'avérerait utile pour une raison quelconque. En revanche, si, par exemple, deux des propriétés sont exposées en tant que finalchamps et que la troisième ne l'est pas, les versions futures de la classe devront toujours utiliser les deux mêmes propriétés en tant que "base".

Si les propriétés n'ont pas une relation exacte (par exemple parce qu'elles sont floatou doubleplutôt que int), il peut être nécessaire de documenter quelles propriétés "définissent" la valeur d'une classe. Par exemple, même si Leftplus Widthest supposé être égal Right, les calculs en virgule flottante sont souvent inexacts. Par exemple, supposons que a Rectanglequi utilise le type Floataccepte Leftet Widthque les paramètres du constructeur soient construits avec Leftdonnes comme 1234567fet Widthcomme 1.1f. La meilleure floatreprésentation de la somme est 1234568.125 [pouvant afficher 1234568.13]; le prochain plus petit floatserait 1234568.0. Si la classe stocke réellement LeftetWidth, il peut signaler la valeur de la largeur telle qu’elle a été spécifiée. Si, toutefois, le constructeur calculait en Rightfonction de l'entrée Leftet Width, et plus tard, en Widthfonction de Leftet Right, il indiquerait la largeur comme 1.25fplutôt que comme entrée 1.1f.

Avec les classes mutables, les choses peuvent être encore plus intéressantes, puisqu'un changement à l'une des valeurs interdépendantes impliquera un changement à au moins une autre, mais on ne sait pas toujours laquelle. Dans certains cas, il peut être préférable d'éviter d' avoir des méthodes qui « ensemble » une seule propriété en tant que telle, mais soit avoir des méthodes pour par exemple SetLeftAndWidthou SetLeftAndRight, ou bien préciser ce que les propriétés sont spécifiées et qui changent (par exemple MoveRightEdgeToSetWidth, ChangeWidthToSetLeftEdgeou MoveShapeToSetRightEdge) .

Parfois, il peut être utile d’avoir une classe qui garde en mémoire les valeurs des propriétés spécifiées et celles calculées par d’autres. Par exemple, une classe "moment dans le temps" peut inclure une heure absolue, une heure locale et un décalage de fuseau horaire. Comme avec beaucoup de ces types, étant donné deux informations quelconques, on peut calculer la troisième. Sachant lequelDes informations ont été calculées, cependant, peuvent parfois être importantes. Par exemple, supposons qu’un événement soit enregistré comme s’il s’est produit à "17h00 UTC, fuseau horaire -5, heure locale 12h00", et on découvre plus tard que le fuseau horaire aurait dû être -6. Si l’on sait que l’heure UTC a été enregistrée sur un serveur, l’enregistrement doit être corrigé comme suit: "18 h 00 UTC, fuseau horaire -6, heure locale 12 h 00"; si quelqu'un tape l'heure locale sur une horloge, il devrait s'agir de "17:00 UTC, fuseau horaire -6, heure locale 11:00 am". Sans savoir si l'heure locale ou globale doit être considérée comme "plus crédible", il est toutefois impossible de savoir quelle correction doit être appliquée. Si, toutefois, l'enregistrement garde trace de l'heure spécifiée, des modifications du fuseau horaire peuvent laisser celui-ci seul pendant le changement de l'autre.


0

Toutes ces règles sur la manière de cacher des informations dans des classes sont parfaitement logiques dans l’hypothèse où il est nécessaire de se protéger contre cette personne parmi les utilisateurs de la classe qui commettra l’erreur de créer une dépendance vis-à-vis de la mise en oeuvre interne.

C'est bien d'intégrer une telle protection, si la classe a un tel public. Mais lorsque l'utilisateur écrit un appel vers une fonction de votre classe, il vous fait confiance avec son compte bancaire au moment de l'exécution.

Voici le genre de chose que je vois beaucoup:

  1. Les objets ont un bit "modifié" indiquant s'ils sont, dans un certain sens, obsolètes. C'est assez simple, mais comme ils ont des objets subordonnés, il est donc simple de laisser «modifié» être une fonction qui additionne tous les objets subordonnés. Ensuite, s’il existe plusieurs couches d’objets subordonnés (partageant parfois plusieurs fois le même objet), les simples "Get" de la propriété "modifiée" peuvent finir par prendre une fraction saine du temps d’exécution.

  2. Lorsqu'un objet est modifié d'une manière ou d'une autre, il est supposé que les autres objets dispersés dans le logiciel doivent être "notifiés". Cela peut se produire sur plusieurs couches de structure de données, fenêtres, etc. écrites par différents programmeurs et se répétant parfois dans des récursions infinies contre lesquelles il faut se protéger. Même si tous les rédacteurs de ces gestionnaires de notifications veillent raisonnablement à ne pas perdre de temps, toute l’interaction composite peut aboutir à une fraction du temps d’exécution imprévue et douloureusement longue, et l’hypothèse selon laquelle elle est simplement "nécessaire" est prise allègrement.

SO, j'aime voir des classes qui présentent une belle interface abstraite avec le monde extérieur, mais j'aime bien avoir une idée de leur fonctionnement, ne serait-ce que pour comprendre le travail qu'elles me font économiser. Mais au-delà de ça, j'ai tendance à penser que "moins, c'est plus". Les gens sont tellement épris de la structure de données qu'ils croient que plus c'est mieux, et quand je règle les performances, la raison majeure et universelle des problèmes de performance est une adhésion servile aux structures de données gonflées qui sont construites de la même manière que les gens.

Alors allez comprendre.


0

L'ajout de détails d'implémentation tels que "calculer ou non" ou "informations de performance" rend plus difficile la synchronisation du code et de la documentation .

Exemple:

Si vous avez une méthode "coûteuse en performances", voulez-vous documenter "cher" également à toutes les classes qui l'utilisent? Et si vous changiez l'implémentation pour qu'elle ne soit plus chère? Voulez-vous mettre à jour cette information à tous les consommateurs?

Bien sûr, il est agréable pour un mainteneur de code d’obtenir toutes les informations importantes de la documentation du code, mais je n’aime pas la documentation qui réclame quelque chose qui n’est plus valide (non synchronisée avec le code)


0

Alors que la réponse acceptée arrive à la conclusion:

Donc: documentez les problèmes de performance.

et le code auto-documenté est considéré comme meilleur que la documentation, il s'ensuit que le nom de la méthode doit indiquer tout résultat de performance inhabituel.

Donc encore Person.agepour return current_year - self.birth_datemais si la méthode utilise une boucle pour calculer l'âge (oui):Person.calculateAge()

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.