Dans Clean Code , page 35, il est dit
Cela implique que les blocs contenus dans les instructions if, les instructions else, les instructions while, etc. doivent être d'une ligne. Cette ligne devrait probablement être un appel de fonction. Non seulement cela réduit la fonction englobante, mais cela ajoute également de la valeur documentaire car la fonction appelée dans le bloc peut avoir un nom bien descriptif.
Je suis entièrement d'accord, cela a beaucoup de sens.
Plus tard, à la page 40, il est question des arguments de fonction
Le nombre idéal d'arguments pour une fonction est zéro (niladique). Vient ensuite un (monadique), suivi de près par deux (dyadiques). Trois arguments (triadiques) doivent être évités dans la mesure du possible. Plus de trois (polyadiques) nécessitent une justification très spéciale - et ne devraient donc pas être utilisés de toute façon. Les arguments sont difficiles. Ils prennent beaucoup de pouvoir conceptuel.
Je suis entièrement d'accord, cela a beaucoup de sens.
Cependant, assez souvent, je me retrouve à créer une liste à partir d'une autre liste et je devrai vivre avec l'un des deux maux.
Soit je tilisez deux lignes dans le bloc, une pour créer la chose, une pour l'ajouter au résultat:
public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
List<Flurp> flurps = new List<Flurp>();
foreach (BadaBoom badaBoom in badaBooms)
{
Flurp flurp = CreateFlurp(badaBoom);
flurps.Add(flurp);
}
return flurps;
}
Ou je ajouter un argument à la fonction pour la liste où la chose sera ajoutée, ce qui en fait "un argument pire".
public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
List<Flurp> flurps = new List<Flurp>();
foreach (BadaBoom badaBoom in badaBooms)
{
CreateFlurpInList(badaBoom, flurps);
}
return flurps;
}
Y a-t-il des (dés) avantages que je ne vois pas, qui rendent l'un d'entre eux préférable en général? Ou existe-t-il de tels avantages dans certaines situations; dans ce cas, que dois-je rechercher lors de la prise de décision?
Ces directives sont une boussole, pas une carte. Ils vous pointent dans un sens direction. Mais ils ne peuvent pas vraiment vous dire en termes absolus quelle solution est la "meilleure". À un moment donné, vous devez arrêter de marcher dans la direction vers laquelle votre boussole pointe, car vous êtes arrivé à destination.
Clean Code vous encourage à diviser votre code en très petits blocs évidents. C'est une bonne direction générale. Mais poussé à l'extrême (comme le suggère une interprétation littérale des conseils cités), vous aurez alors divisé votre code en petits morceaux inutiles. Rien ne fait vraiment rien, tout ne fait que déléguer. Il s'agit essentiellement d'un autre type d'obfuscation de code.
C'est votre travail d'équilibrer "plus petit c'est mieux" contre "trop petit est inutile". Demandez-vous quelle solution est la plus simple. Pour moi, c'est clairement la première solution car elle évidemment rassemble une liste. C'est un idiome bien compris. Il est possible de comprendre ce code sans avoir à regarder encore une autre fonction.
S'il est possible de faire mieux, c'est en notant que "transformer tous les éléments d'une liste en une autre liste" est un modèle courant qui peut souvent être abstrait, en utilisant une opération fonctionnelle map()
. En C #, je pense que ça s'appelle Select
. Quelque chose comme ça:
public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
return badaBooms.Select(BadaBoom => CreateFlurp(badaBoom)).ToList();
}
Le nombre idéal d'arguments pour une fonction est zéro (niladique)
Non! Le nombre idéal d'arguments pour une fonction est un. S'il est égal à zéro, vous garantissez que la fonction doit accéder à des informations externes pour pouvoir effectuer une action. "Oncle" Bob s'est trompé.
Concernant votre code, votre premier exemple n'a que deux lignes dans le bloc car vous créez une variable locale sur la première ligne. Supprimez cette affectation et vous respectez ces directives de code propre:
public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
List<Flurp> flurps = new List<Flurp>();
foreach (BadaBoom badaBoom in badaBooms)
{
flurps.Add(CreateFlurp(badaBoom));
}
return flurps;
}
Mais c'est un code très long (C #). Faites-le simplement comme:
IEnumerable<Flurp> CreateFlurps(IEnumerable<BadaBoom> badaBooms) =>
from badaBoom in babaBooms select CreateFlurp(badaBoom);
Le conseil "Clean Code" est complètement faux.
Utilisez deux lignes ou plus dans votre boucle. Masquer les deux mêmes lignes dans une fonction est logique lorsqu'il s'agit de calculs aléatoires qui nécessitent une description, mais cela ne fait rien lorsque les lignes sont déjà descriptives. "Créer" et "Ajouter"
La deuxième méthode que vous mentionnez n'a pas vraiment de sens, car vous n'êtes pas obligé d'ajouter un deuxième argument pour éviter les deux lignes.
public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
List<Flurp> flurps = new List<Flurp>();
foreach (BadaBoom badaBoom in badaBooms)
{
flurps.Add(badaBoom .CreateFlurp());
//or
badaBoom.AddToListAsFlurp(flurps);
//or
flurps.Add(new Flurp(badaBoom));
//or
//make flurps a member of the class
//use linq.Select()
//etc
}
return flurps;
}
ou
foreach(var flurp in ConvertToFlurps(badaBooms))...
Comme indiqué par d'autres, le conseil selon lequel la meilleure fonction est celle sans argument est biaisé à OOP au mieux et tout simplement mauvais conseil au pire
La seconde est certainement pire, car CreateFlurpInList
accepte la liste et modifie cette liste, rendant la fonction pas pure et plus difficile à raisonner. Rien dans le nom de la méthode ne suggère que la méthode ne fait qu'ajouter à la liste.
Et je propose la troisième meilleure option:
public List<Flurp> CreateFlurps(List<BadaBoom> badaBooms)
{
return badaBooms.Select(CreateFlurp).ToList();
}
Et l'enfer, vous pouvez incorporer cette méthode immédiatement s'il n'y a qu'un seul endroit où elle est utilisée, car le one-liner est clair par lui-même, il n'a donc pas besoin d'être encapsulé par la méthode pour lui donner un sens.
La version à un argument est meilleure, mais pas principalement en raison du nombre d'arguments.
La raison la plus importante pour laquelle il est préférable est qu'il a couplage inférieur, ce qui le rend plus utile, plus facile à raisonner, plus facile à tester et moins susceptible de se transformer en clones copiés + collés.
Si vous me fournissez une CreateFlurp(BadaBoom)
, je peux l'utiliser avec n'importe quel type de conteneur de collection: Simple Flurp[]
, List<Flurp>
, LinkedList<Flurp>
, Dictionary<Key, Flurp>
, Etc. Mais avec une CreateFlurpInList(BadaBoom, List<Flurp>)
, je reviens vers vous demain pour demander CreateFlurpInBindingList(BadaBoom, BindingList<Flurp>)
afin que mon viewmodel puisse recevoir la notification que la liste a changé. Beurk!
Comme avantage supplémentaire, la signature plus simple est plus susceptible de s'adapter aux API existantes. Vous dites que vous avez un problème récurrent
assez souvent je me retrouve à créer une liste à partir d'une autre liste
Il s'agit simplement d'utiliser les outils disponibles. La version la plus courte, la plus efficace et la meilleure est:
var Flurps = badaBooms.ConvertAll(CreateFlurp);
Non seulement ce code vous permet d'écrire et de tester moins, mais il est également plus rapide, car List<T>.ConvertAll()
est suffisamment intelligent pour savoir que le résultat aura le même nombre d'éléments que l'entrée et préallouera la liste des résultats à la bonne taille. Alors que votre code (les deux versions) nécessitait d'agrandir la liste.
Gardez à l'esprit l'objectif global: rendre le code facile à lire et à maintenir.
Souvent, il sera possible de regrouper plusieurs lignes en une seule fonction significative. Faites-le dans ces cas. Parfois, vous devrez reconsidérer votre approche générale.
Par exemple, dans votre cas, remplacer toute l'implémentation par var
flups = badaBooms.Select(bb => new Flurp(bb));
pourrait être une possibilité. Ou vous pourriez faire quelque chose comme
flups.Add(new Flurp(badaBoom))
Parfois, la solution la plus propre et la plus lisible ne tient tout simplement pas sur une seule ligne. Vous aurez donc deux lignes. Ne rendez pas le code plus difficile à comprendre, juste pour remplir une règle arbitraire.
Votre deuxième exemple est (à mon avis) beaucoup plus difficile à comprendre que le premier. Ce n'est pas seulement que vous avez un deuxième paramètre, c'est que le paramètre est modifié par la fonction. Recherchez ce que Clean Code a à dire à ce sujet. (Je n'ai pas le livre à portée de main en ce moment, mais je suis pratiquement sûr que c'est "ne fais pas ça si tu peux l'éviter").