Dans Clean Code, l'auteur donne un exemple de
assertExpectedEqualsActual(expected, actual)
contre
assertEquals(expected, actual)
avec l’ancien prétendu être plus clair parce que cela supprime la nécessité de se rappeler où vont les arguments et les abus qui en découlent. Pourtant, je n'ai jamais vu un exemple du premier système de nommage dans un code et je vois ce dernier tout le temps. Pourquoi les codeurs n'adoptent-ils pas le premier si, comme le prétend l'auteur, il est plus clair que le dernier?
clean-code
Étudiant éternel
la source
la source
assertEquals()
que cette méthode est utilisée des centaines de fois dans une base de code, on peut donc s’attendre à ce que les lecteurs se familiarisent une fois avec la convention. Différents cadres ont différentes conventions (par exemple(actual, expected) or an agnostic
(gauche, droite) `), mais d'après mon expérience, c'est tout au plus une source de confusion mineure.assert(a).toEqual(b)
(même si, à l’OMI, elle reste inutilement prolixe) où vous pouvez enchaîner quelques affirmations connexes.assertExpectedValueEqualsActualValue
? Mais attendez, comment nous rappelons-nous s'il utilise==
ou.equals
ouObject.equals
? Cela devrait-il êtreassertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter
?Réponses:
Parce que c’est plus à taper et plus à lire
La raison la plus simple est que les gens aiment taper moins, et coder cette information signifie plus de dactylographie. En le lisant, je dois tout lire à chaque fois, même si je connais parfaitement l'ordre des arguments. Même si pas familier avec l'ordre des arguments ...
De nombreux développeurs utilisent des IDE
Les IDE fournissent souvent un mécanisme permettant d'afficher la documentation d'une méthode donnée en survolant ou via un raccourci clavier. Pour cette raison, les noms des paramètres sont toujours à portée de main.
Le codage des arguments introduit la duplication et le couplage
Les noms des paramètres devraient déjà documenter ce qu’ils sont. En écrivant les noms dans le nom de la méthode, nous dupliquons également ces informations dans la signature de la méthode. Nous créons également un couplage entre le nom de la méthode et les paramètres. Dites
expected
etactual
sont déroutants à nos utilisateurs. Aller deassertEquals(expected, actual)
àassertEquals(planned, real)
ne nécessite pas de changer le code client en utilisant la fonction. Passer deassertExpectedEqualsActual(expected, actual)
àassertPlannedEqualsReal(planned, real)
signifie un changement radical de l'API. Ou nous ne changeons pas le nom de la méthode, ce qui devient rapidement déroutant.Utilisez des types au lieu d'arguments ambigus
Le vrai problème est que nous avons des arguments ambigus qui sont facilement interchangeables car ils sont du même type. Nous pouvons plutôt utiliser notre système de types et notre compilateur pour imposer le bon ordre:
Cela peut ensuite être appliqué au niveau du compilateur et garantit que vous ne pouvez pas les récupérer à l'envers. En abordant le problème sous un angle différent, c'est essentiellement ce que la bibliothèque Hamcrest fait pour les tests.
la source
assertExpectedEqualsActual
"parce que c’est plus à taper et plus à lire", comment pouvez-vous plaiderassertEquals(Expected.is(10), Actual.is(x))
?assertExpectedEqualsActual
demande toujours au programmeur de spécifier les arguments dans le bon ordre. LaassertEquals(Expected<T> expected, Actual<T> actual)
signature utilise le compilateur pour imposer l'utilisation correcte, ce qui constitue une approche totalement différente. Vous pouvez optimiser cette approche par souci de concision, par exempleexpect(10).equalsActual(x)
, mais ce n'était pas la question…Vous demandez un débat de longue date dans la programmation. Combien de verbosité c'est bien? De manière générale, les développeurs ont constaté que la verbosité supplémentaire nommant les arguments n'en valait pas la peine.
Verbosité ne signifie pas toujours plus de clarté. Considérer
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
contre
copy(output, source)
Les deux contiennent le même bogue, mais avons-nous réellement rendu plus facile la recherche de ce bogue? En règle générale, la meilleure chose à faire pour le débogage est lorsque tout est extrêmement sommaire, à l'exception des quelques éléments qui présentent le bogue et qui sont suffisamment détaillés pour vous dire ce qui n'a pas fonctionné.
Il y a une longue histoire d'ajout de verbosité. Par exemple, il y a la " notation hongroise " généralement impopulaire qui nous a donné des noms merveilleux comme
lpszName
. Cela a généralement été mis de côté dans la foule des programmeurs. Cependant, l'ajout de caractères aux noms de variable de membre (commemName
oum_Name
ouname_
) continue d'avoir une certaine popularité dans certains cercles. D'autres ont tout laissé tomber. Il m'est arrivé de travailler sur une base de code de simulation physique dont les documents de style de codage exigent que toute fonction renvoyant un vecteur spécifie la trame du vecteur dans l'appel de fonction (getPositionECEF
).Certaines des langues rendues populaires par Apple pourraient vous intéresser. Objective-C inclut les noms d'arguments dans la signature de la fonction (la fonction
[atm withdrawFundsFrom: account usingPin: userProvidedPin]
est écrite dans la documentation en tant quewithdrawFundsFrom:usingPin:
. C'est le nom de la fonction). Swift a pris un ensemble similaire de décisions, vous obligeant à mettre les noms d'arguments dans les appels de fonction (greet(person: "Bob", day: "Tuesday")
).la source
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
étaient écritscopy_from_source_stream_to_destination_stream_without_blocking(file_stream_from_choose_preferred_output_dialog, heuristically_decided_source_file_handle)
. Vous voyez comme c'était plus facile ?! En effet, il est trop facile de rater de petits changements au milieu de cet humungousunbrokenwordsalad, et il faut plus de temps pour déterminer où se trouvent les limites de mots. Smashing confuse.withdrawFundsFrom: account usingPin: userProvidedPin
est en fait empruntée à SmallTalk.Addingunderscoresnakesthingseasiertoreadnotharderasyousee
manipule l'argument. La réponse ici a utilisé la capitalisation, que vous omettez.AddingCapitalizationMakesThingsEasyEnoughToReadAsYouCanSeeHere
. Deuxièmement, 9 fois sur 10, un nom ne devrait jamais dépasser[verb][adjective][noun]
(où chaque bloc est facultatif), un format qui est bien lisible avec une simple capitalisation:ReadSimpleName
L'auteur de "Clean Code" signale un problème légitime, mais sa solution suggérée est plutôt inélégante. Il existe généralement de meilleurs moyens d'améliorer les noms de méthodes peu clairs.
Il a raison de dire que
assertEquals
(à partir des bibliothèques de tests d'unités de style xUnit), il n'est pas précisé quel argument est attendu et quel est l'argument réel. Cela m'a aussi mordu! De nombreuses bibliothèques de tests unitaires ont noté le problème et ont introduit des syntaxes alternatives, telles que:Ou similaire. Ce qui est certainement beaucoup plus clair que
assertEquals
mais aussi beaucoup mieux queassertExpectedEqualsActual
. Et c'est aussi beaucoup plus composable.la source
fun(x)
soit égal à 5, alors qu'est-ce qui pourrait mal tourner si je renverse l'ordre -assert(fun(x), 5)
? Comment ça vous a mordu?expected
andactual
, donc les inverser peut entraîner un message inexact. Mais je suis d'accord pour dire que cela semble plus naturel :)assert(expected, observed)
ou nonassert(observed, expected)
. Un meilleur exemple serait quelque chose commelocateLatitudeLongitude
: si vous inversez les coordonnées, cela gâchera sérieusement.Vous essayez d’orienter votre chemin entre Scylla et Charybde vers la clarté, en essayant d’éviter une verbosité inutile (aussi connue sous le nom de balade sans but), ainsi qu’une brièveté excessive (également connue sous le nom de terseness cryptique).
Nous devons donc examiner l’interface que vous voulez évaluer, une façon de faire des assertions de débogage que deux objets sont égaux.
Non, le nom lui-même est assez clair.
Non, alors ignorons-les. Vous l'avez déjà fait? Bien.
Presque, en cas d'erreur, le message met chaque représentation d'arguments à sa place.
Voyons donc si cette petite différence est importante et n’est pas couverte par les conventions fortes existantes.
L'audience visée est-elle gênée si les arguments sont échangés involontairement?
Non, les développeurs reçoivent également une trace de pile et ils doivent quand même examiner le code source pour corriger le bogue.
Même sans trace de pile complète, la position des assertions résout cette question. Et même si cela manque et que le message n’est pas une évidence, il double tout au plus les possibilités.
Est-ce que l'ordre des arguments suit la convention?
Semble être le cas. Bien que cela semble au mieux une convention faible.
Ainsi, la différence semble assez insignifiante et l'ordre des arguments est couvert par une convention suffisamment forte pour que tout effort visant à le coder dans le nom de la fonction ait une utilité négative.
la source
expected
etactual
(au moins avec Strings)assertEquals("foo", "doo")
donne le message d'erreur estComparisonFailure: expected:<[f]oo> but was:<[d]oo>
... Permuter les valeurs inverserait le sens du message, cela me semble plus anti symétrique. Quoi qu'il en soit, comme vous l'avez dit, un développeur a d'autres indicateurs pour résoudre l'erreur, mais cela peut être trompeur à mon humble avis et prendre un peu plus de temps pour le débogage.Souvent, cela n’ajoute aucune clarté logique.
Comparez "Ajouter" à "AddFirstArgumentToSecondArgument".
Si vous avez besoin d'une surcharge, par exemple, ajoute trois valeurs. Qu'est-ce qui aurait plus de sens?
Un autre "Ajouter" avec trois arguments?
ou
"AddFirstAndSecondAndThirdArgument"?
Le nom de la méthode doit transmettre sa signification logique. Il devrait dire ce qu'il fait. Dire, au niveau micro, quelles sont les étapes à suivre ne facilite pas le lecteur. Les noms des arguments fourniront des détails supplémentaires si nécessaire. Si vous avez encore besoin de plus de détails, le code sera là pour vous.
la source
Add
suggère une opération commutative. Le PO s'intéresse aux situations dans lesquelles l'ordre est important.sum
est un verbe parfaitement cromulent . Il est particulièrement courant dans la phrase "résumer".J'aimerais ajouter quelque chose d'autre qui est suggéré par d'autres réponses, mais je ne pense pas que cela ait été mentionné explicitement:
@puck dit "Il n'y a toujours aucune garantie que le premier argument mentionné dans le nom de la fonction soit réellement le premier paramètre."
@cbojar dit "Utilisez des types au lieu d'arguments ambigus"
Le problème est que les langages de programmation ne comprennent pas les noms: ils sont simplement traités comme des symboles opaques et atomiques. Ainsi, à l'instar des commentaires de code, il n'y a pas nécessairement de corrélation entre le nom d'une fonction et son fonctionnement réel.
Comparez
assertExpectedEqualsActual(foo, bar)
avec certaines alternatives (à partir de cette page et ailleurs), comme:Celles-ci ont toutes plus de structure que le nom commenté, ce qui donne au langage un aspect non opaque à regarder. La définition et l'utilisation de la fonction dépendent également de cette structure. Elle ne peut donc pas être désynchronisée par rapport à ce que fait l'implémentation (comme un nom ou un commentaire).
Lorsque je rencontre ou prévois un problème de ce type, avant de crier sur mon ordinateur de frustration, je prends d'abord un moment pour demander s'il est «juste» de blâmer la machine. En d'autres termes, la machine a-t-elle reçu suffisamment d'informations pour distinguer ce que je voulais de ce que je demandais?
Un appel de ce type a
assertEqual(expected, actual)
tout autant de sens que nousassertEqual(actual, expected)
, il est donc facile pour nous de les mélanger et pour que la machine avance et fasse la mauvaise chose. Si nous avons utilisé à laassertExpectedEqualsActual
place, il pourrait faire nous moins susceptibles de faire une erreur, mais il ne donne pas plus d' informations à la machine (il ne peut pas comprendre l' anglais, et le choix du nom ne devrait pas affecter la sémantique).Ce qui rend les approches "structurées" plus préférables, telles que les arguments de mots clés, les champs libellés, les types distincts, etc., est que les informations supplémentaires sont également lisibles par machine , de sorte que la machine détecte les utilisations incorrectes et nous aide à bien faire les choses. Le
assertEqual
cas n’est pas trop grave, le seul problème étant des messages inexacts. Un exemple plus sinistre pourrait êtreString replace(String old, String new, String content)
, qui est facile à confondre avecString replace(String content, String old, String new)
qui a un sens très différent. Un remède simple serait de prendre une paire[old, new]
, ce qui ferait des erreurs déclencher une erreur immédiatement (même sans types).Notez que même avec les types, nous pouvons nous retrouver à ne pas "dire à la machine ce que nous voulons". Par exemple, l'anti-motif appelé "programmation très typée" traite toutes les données en tant que chaînes, ce qui permet de mélanger facilement les arguments (comme dans ce cas), d'oublier l'exécution d'une étape (par exemple l'échappement), de casser accidentellement des invariants (par exemple rendant JSON imparable), etc.
Ceci est également lié à la "cécité booléenne", où nous calculons un groupe de booléens (ou de nombres, etc.) dans une partie du code, mais lorsque vous essayez de les utiliser dans une autre, vous ne savez pas exactement ce qu'ils représentent, nous les avons mélangés, etc. Comparez ceci par exemple à des énumérations distinctes qui ont des noms descriptifs (par exemple
LOGGING_DISABLED
plutôt quefalse
) et qui provoquent un message d'erreur si nous les mélangeons.la source
Est-ce vraiment? Il n'y a toujours aucune garantie que le premier argument mentionné dans le nom de la fonction soit réellement le premier paramètre. Il est donc préférable de regarder (ou de laisser votre IDE le faire) et de conserver des noms raisonnables plutôt que de vous fier aveuglément à un nom assez stupide.
Si vous lisez le code, vous devriez facilement voir ce qui se passe lorsque les paramètres sont nommés comme il se doit.
copy(source, destination)
est beaucoup plus facile à comprendre que quelque chose commecopyFromTheFirstLocationToTheSecondLocation(placeA, placeB)
.Parce qu'il existe différents points de vue sur différents styles et que vous pouvez trouver x auteurs d'autres articles affirmant le contraire. Tu deviendrais fou en essayant de suivre tout ce que quelqu'un écrit quelque part ;-)
la source
Je conviens que le codage des noms de paramètres en noms de fonctions rend l'écriture et l'utilisation de fonctions plus intuitive.
Il est facile d'oublier l'ordre des arguments dans les fonctions et les commandes du shell et de nombreux programmeurs s'appuient sur les fonctionnalités IDE ou les références de fonctions pour cette raison. Avoir les arguments décrits dans le nom serait une solution éloquente à cette dépendance.
Cependant, une fois écrite, la description des arguments devient redondante pour le prochain programmeur qui doit lire l'instruction, car dans la plupart des cas, des variables nommées seront utilisées.
La brièveté de ceci va gagner la plupart des programmeurs et je trouve personnellement cela plus facile à lire.
EDIT: Comme l'a souligné @Blrfl, les paramètres de codage ne sont pas si intuitifs après tout, car vous devez vous rappeler le nom de la fonction. Cela nécessite de rechercher des références de fonction ou d'obtenir de l'aide d'un IDE qui fournira probablement des informations sur le classement des paramètres.
la source
copyFromSourceToDestination
oucopyToDestinationFromSource
, vos choix la trouveront par essais et erreurs ou en lisant le matériel de référence. Les IDE pouvant compléter des noms partiels ne sont qu'une version automatisée de cette dernière.copyFromSourceToDestination
est que si vous le pensezcopyToDestinationFromSource
, le compilateur trouvera votre bogue, mais s'il l'a appelécopy
, il ne le fera pas. Obtenir les paramètres d'une routine de copie dans le mauvais sens est facile, car strcpy, strcat, etc. créent un précédent. Et le résumé est-il plus facile à lire? MergeLists (listA, listB, listC) crée-t-il listA à partir de listB & listC, ou lit-t-il listA & listB et écrit-il listC?dir1.copy(dir2)
fonctionne? Aucune idée. Qu'en est- ildir1.copyTo(dir2)
?