Dans Clean Code l'auteur donne un exemple de
assertExpectedEqualsActual(expected, actual)
contre
assertEquals(expected, actual)
avec le premier a prétendu être plus clair car il supprime la nécessité de se rappeler où vont les arguments et les abus potentiels qui en découlent. Pourtant, je n'ai jamais vu d'exemple de l'ancien schéma de nommage dans aucun code et je vois ce dernier tout le temps. Pourquoi les codeurs n'adoptent-ils pas le premier s'il est, comme l'auteur l'affirme, plus clair que le second?
La raison la plus simple est que les gens aiment moins taper et encoder ces informations signifie plus de frappe. En le lisant, à chaque fois je dois lire le tout même si je connais bien l'ordre des arguments. Même si vous n'êtes pas familier avec l'ordre des arguments ...
Les IDE fournissent souvent un mécanisme pour voir 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.
Les noms des paramètres doivent 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. Disons que expected
et actual
sont source de confusion pour nos utilisateurs. Passer de assertEquals(expected, actual)
à assertEquals(planned, real)
ne nécessite pas de changer le code client à l'aide de la fonction. Passer de assertExpectedEqualsActual(expected, actual)
à assertPlannedEqualsReal(planned, real)
signifie une modification radicale de l'API. Ou nous ne changeons pas le nom de la méthode, ce qui devient rapidement déroutant.
Le vrai problème est que nous avons des arguments ambigus qui sont facilement commutables car ils sont du même type. Nous pouvons plutôt utiliser notre système de type et notre compilateur pour appliquer l'ordre correct:
class Expected<T> {
private T value;
Expected(T value) { this.value = value; }
static Expected<T> is(T value) { return new Expected<T>(value); }
}
class Actual<T> {
private T value;
Actual(T value) { this.value = value; }
static Actual<T> is(T value) { return new Actual<T>(value); }
}
static assertEquals(Expected<T> expected, Actual<T> actual) { /* ... */ }
// How it is used
assertEquals(Expected.is(10), Actual.is(x));
Cela peut ensuite être appliqué au niveau du compilateur et garantit que vous ne pouvez pas les récupérer en arrière. En approchant sous un angle différent, c'est essentiellement ce que fait la bibliothèque Hamcrest pour les tests.
Vous demandez un débat de longue date sur la programmation. Quelle verbosité est bonne? En règle générale, les développeurs ont constaté que la verbosité supplémentaire nommant les arguments n'en valait pas la peine.
La verbosité ne signifie pas toujours plus de clarté. Considérer
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
versus
copy(output, source)
Les deux contiennent le même bogue, mais avons-nous réellement facilité la recherche de ce bogue? En règle générale, la chose la plus simple à déboguer est lorsque tout est au maximum concis, à l'exception des quelques éléments qui ont le bogue, et ceux-ci sont suffisamment verbeux pour vous dire ce qui s'est mal passé.
Il y a une longue histoire d'ajout de verbosité. Par exemple, il y a le " notation hongroise " généralement impopulaire qui nous a donné des noms merveilleux comme lpszName
. Cela a généralement été abandonné dans la population générale des programmeurs. Cependant, l'ajout de caractères aux noms de variables membres (comme mName
ou m_Name
Ou name_
) Continue de gagner en popularité dans certains cercles. D'autres l'ont complètement abandonné. Il se trouve que je travaille sur une base de code de simulation physique dont les documents de style de codage exigent que toute fonction qui renvoie un vecteur doit spécifier le cadre du vecteur dans l'appel de fonction (getPositionECEF
).
Vous pourriez être intéressé par certaines des langues rendues populaires par Apple. Objective-C inclut les noms d'arguments dans le cadre de la signature de la fonction (la fonction [atm withdrawFundsFrom: account usingPin: userProvidedPin]
Est écrite dans la documentation sous la forme withdrawFundsFrom:usingPin:
. C'est le nom de la fonction). Swift a pris un ensemble de décisions similaire, vous obligeant à mettre les noms d'arguments dans les appels de fonction (greet(person: "Bob", day: "Tuesday")
).
L'auteur de "Clean Code" souligne un problème légitime, mais sa solution suggérée est plutôt inélégante. Il existe généralement de meilleures façons d'améliorer les noms de méthode peu clairs.
Il a raison de dire que assertEquals
(des bibliothèques de tests unitaires de style xUnit) ne précise pas quel argument est attendu et quel est le 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, comme:
actual.Should().Be(expected);
Ou similaire. Ce qui est certainement beaucoup plus clair que assertEquals
mais aussi beaucoup mieux que assertExpectedEqualsActual
. Et c'est aussi beaucoup plus composable.
Vous essayez d'orienter votre chemin entre Scylla et Charybdis vers la clarté, en essayant d'éviter la verbosité inutile (également connue sous le nom de randonnée sans but) ainsi que la brièveté excessive (également connue sous le nom de ténacité cryptique).
Donc, nous devons regarder l'interface que vous souhaitez évaluer, une façon de faire des affirmations de débogage que deux objets sont égaux.
Voyons donc si cette petite différence est importante et non couverte par les conventions fortes existantes.
Le public visé est-il incommodé si les arguments sont involontairement échangés?
Non, les développeurs obtiennent également une trace de pile et ils doivent quand même scruter le code source pour corriger le bogue.
Même sans trace de pile complète, la position des assertions résout cette question. Et si même cela manque et ce n'est pas évident d'après le message qui est lequel, cela double tout au plus les possibilités.
L'ordre des arguments suit-il 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 pour le coder dans le nom de la fonction ait une utilité négative.
Souvent, cela n'ajoute aucune clarté logique.
Comparez "Ajouter" à "AddFirstArgumentToSecondArgument".
Si vous avez besoin d'une surcharge qui, disons, 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. Raconter, à un micro-niveau, les étapes à suivre ne facilite pas la tâche du 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.
Je voudrais ajouter autre chose qui est suggérée 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 vraiment 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 atomiques opaques. Par conséquent, comme pour les 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 quelques alternatives (à partir de cette page et ailleurs), comme:
# Putting the arguments in a labelled structure
assertEquals({expected: foo, actual: bar})
# Using a keyword arguments language feature
assertEquals(expected=foo, actual=bar)
# Giving the arguments different types, forcing us to wrap them
assertEquals(Expected(foo), Actual(bar))
# Breaking the symmetry and attaching the code to one of the arguments
bar.Should().Be(foo)
Ceux-ci ont tous plus structure que le nom verbeux, ce qui donne à la langue quelque chose de non opaque à regarder. La définition et l'utilisation de la fonction aussi dépend de cette structure, donc elle ne peut pas être désynchronisée avec ce que fait l'implémentation (comme un nom ou un commentaire peut).
Lorsque je rencontre ou prévois un problème comme celui-ci, 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 j'ai demandé?
Un appel comme assertEqual(expected, actual)
a autant de sens que assertEqual(actual, expected)
, il est donc facile pour nous de les mélanger et pour que la machine avance et fasse le mauvaise chose. Si nous avons utilisé assertExpectedEqualsActual
à la place, cela pourrait rendre s moins susceptible de faire une erreur, mais cela ne donne plus d'informations à la machine (elle ne comprend pas 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, comme les arguments de mots clés, les champs étiquetés, les types distincts, etc., c'est que les informations supplémentaires sont également lisibles par la machine, de sorte que nous pouvons avoir la machine à repérer les utilisations incorrectes et l'aide nous faisons les choses correctement. Le cas assertEqual
n'est pas trop mal, car le seul problème serait les messages inexacts. Un exemple plus sinistre pourrait être String replace(String old, String new, String content)
, qui est facile à confondre avec String replace(String content, String old, String new)
qui a une signification très différente. Un remède simple serait de prendre une paire [old, new]
, Ce qui ferait que les erreurs déclenchent une erreur immédiatement (même sans types).
Notez que même avec des types, nous pouvons ne pas "dire à la machine ce que nous voulons". Par exemple, l'anti-modèle appelé "programmation typée en chaîne" traite toutes les données comme des chaînes, ce qui facilite la confusion des arguments (comme dans ce cas), l'oublie d'effectuer une étape (par exemple, l'échappement), pour casser accidentellement les invariants (par exemple, rendant JSON insaisissable), etc.
Ceci est également lié à la "cécité booléenne", où nous calculons un tas de booléens (ou nombres, etc.) dans une partie du code, mais lorsque vous essayez de les utiliser dans une autre, il n'est pas clair ce qu'ils représentent réellement, que ce soit nous les avons mélangés, etc. des énumérations distinctes qui ont des noms descriptifs (par exemple LOGGING_DISABLED
plutôt que false
) et qui provoquent un message d'erreur si nous les confondons.
car il supprime le besoin de se rappeler où vont les arguments
Est-ce vraiment le cas? Il n'y a toujours aucune garantie que le premier argument mentionné dans le nom de la fonction est vraiment le premier paramètre. Il vaut donc mieux le chercher (ou laissez votre IDE faire cela) et rester avec des noms raisonnables plutôt que de vous fier aveuglément à un nom assez idiot.
Si vous lisez le code, vous devriez facilement voir ce qui se passe lorsque les paramètres sont nommés tels qu'ils devraient être. copy(source, destination)
est beaucoup plus facile à comprendre que quelque chose comme copyFromTheFirstLocationToTheSecondLocation(placeA, placeB)
.
Pourquoi les codeurs n'adoptent-ils pas le premier s'il est, comme l'auteur l'affirme, plus clair que le second?
Parce qu'il existe différents points de vue sur différents styles et que vous pouvez trouver x auteurs d'autres articles qui disent le contraire. Vous deviendriez fou en essayant de suivre tout ce que quelqu'un écrit quelque part ;-)
Je suis d'accord que le codage des noms de paramètres en noms de fonction rend l'écriture et l'utilisation des fonctions plus intuitives.
copyFromSourceToDestination( // "...ahh yes, the source directory goes first"
Il est facile d'oublier l'ordre des arguments dans les fonctions et les commandes Shell et de nombreux programmeurs s'appuient sur IDE fonctionnalités ou références de fonction 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 superflue pour le prochain programmeur qui doit lire l'instruction, car dans la plupart des cas, des variables nommées seront utilisées.
copy(sourceDir, destinationDir); // "...makes sense"
La justesse de cela gagnera la plupart des programmeurs et je trouve personnellement plus facile à lire.
EDIT: Comme l'a souligné @Blrfl, les paramètres d'encodage ne sont pas si "intuitifs" après tout, car vous devez vous rappeler le nom de la fonction en premier lieu. 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 la commande des paramètres de toute façon.