Pour des raisons d'argument, voici un exemple de fonction qui imprime le contenu d'un fichier donné ligne par ligne.
Version 1:
void printFile(const string & filePath) {
fstream file(filePath, ios::in);
string line;
while (std::getline(file, line)) {
cout << line << endl;
}
}
Je sais qu'il est recommandé que les fonctions fassent une chose à un niveau d'abstraction. Pour moi, bien que le code ci-dessus fasse à peu près une chose et soit assez atomique.
Certains livres (comme le Clean Code de Robert C. Martin) semblent suggérer de diviser le code ci-dessus en fonctions distinctes.
Version 2:
void printFile(const string & filePath) {
fstream file(filePath, ios::in);
printLines(file);
}
void printLines(fstream & file) {
string line;
while (std::getline(file, line)) {
printLine(line);
}
}
void printLine(const string & line) {
cout << line << endl;
}
Je comprends ce qu'ils veulent réaliser (ouvrir un fichier / lire des lignes / imprimer une ligne), mais n'est-ce pas un peu exagéré?
La version originale est simple et dans un certain sens fait déjà une chose - imprime un fichier.
La deuxième version entraînera un grand nombre de fonctions très petites qui peuvent être beaucoup moins lisibles que la première version.
Ne serait-il pas préférable, dans ce cas, d'avoir le code en un seul endroit?
À quel moment le paradigme "Do One Thing" devient-il nuisible?
printFile
,printLines
et enfinprintLine
.Réponses:
Bien sûr, cela pose simplement la question "Qu'est-ce qu'une chose?" Lire une ligne est-il une chose et écrire une ligne en est une autre? Ou la copie d'une ligne d'un flux à l'autre doit-elle être considérée comme une chose? Ou copier un fichier?
Il n'y a pas de réponse dure et objective à cela. C'est à vous. Tu peux décider. Vous devez décider. Le principal objectif du paradigme «faire une chose» est probablement de produire un code aussi facile à comprendre que possible, afin que vous puissiez l'utiliser comme ligne directrice. Malheureusement, ce n'est pas objectivement mesurable non plus, alors vous devez vous fier à votre intuition et au "WTF?" compter dans l'examen du code .
IMO, une fonction composée d'une seule ligne de code en vaut rarement la peine. Votre
printLine()
n'a aucun avantage sur l'utilisation directe destd::cout << line << '\n'
1 . Si je voisprintLine()
, je dois soit supposer qu'il fait ce que dit son nom, soit le rechercher et vérifier. Si je voisstd::cout << line << '\n'
, je sais immédiatement ce qu'il fait, car c'est la manière canonique de sortir le contenu d'une chaîne sous forme de lignestd::cout
.Cependant, un autre objectif important du paradigme est de permettre la réutilisation du code, et c'est une mesure beaucoup plus objective. Par exemple, dans votre 2ème version
printLines()
pourrait facilement être écrite de sorte que c'est un algorithme universellement utile qui copie des lignes d'un flux à un autre:Un tel algorithme pourrait également être réutilisé dans d'autres contextes.
Vous pouvez ensuite mettre tout ce qui est spécifique à ce cas d'utilisation dans une fonction qui appelle cet algorithme générique:
1 Notez que j'ai utilisé
'\n'
plutôt questd::endl
.'\n'
devrait être le choix par défaut pour la sortie d'une nouvelle ligne ,std::endl
est le cas étrange .la source
do_x_and_y()
en nommant la fonction à lado_everything()
place. Oui, c'est un exemple stupide, mais cela montre que cette règle ne parvient même pas à empêcher les exemples les plus extrêmes de mauvaise conception. L'OMI est une décision qui fait tout autant penser que celle dictée par les conventions. Sinon, si c'était objectif, vous pourriez trouver une métrique pour cela - ce que vous ne pouvez pas.printLine
etc est valide - chacun d'eux est une seule abstraction - mais cela ne signifie pas nécessaire.printFile
c'est déjà "une chose". Bien que vous puissiez décomposer cela en trois abstractions de niveau inférieur distinctes, vous n'avez pas à les décomposer à tous les niveaux d'abstraction possibles . Chaque fonction doit faire «une chose», mais toutes les «une chose» possibles ne doivent pas nécessairement être une fonction. Déplacer trop de complexité dans le graphique d'appel peut lui-même être un problème.Avoir une fonction ne faire qu'une "chose" est un moyen d'atteindre deux fins souhaitables, pas un commandement de Dieu:
Si votre fonction ne fait qu'une "chose", cela vous aidera à éviter la duplication de code et le gonflement de l'API, car vous serez en mesure de composer des fonctions pour faire des choses plus complexes au lieu d'avoir une explosion combinatoire de fonctions de niveau supérieur et moins composables .
Le fait d'avoir des fonctions ne fait qu'une "chose" peut rendre le code plus lisible. Cela dépend si vous gagnez plus de clarté et de facilité de raisonnement en découplant les choses que vous ne perdez face à la verbosité, à l'indirection et aux frais généraux conceptuels des constructions qui vous permettent de découpler les choses.
Par conséquent, "une chose" est inévitablement subjective et dépend du niveau d'abstraction pertinent pour votre programme. Si
printLines
est considérée comme une opération unique et fondamentale et la seule façon d'imprimer des lignes dont vous vous souciez ou envisagez de prendre soin, alors pour vos besoinsprintLines
ne fait qu'une chose. À moins que vous ne trouviez la deuxième version plus lisible (je ne le sais pas), la première version est très bien.Si vous commencez à avoir besoin de plus de contrôle sur les niveaux d'abstraction inférieurs et que vous vous retrouvez avec une duplication subtile et une explosion combinatoire (c'est-à-dire un
printLines
pour les noms de fichiers et un complètement séparéprintLines
pour lesfstream
objets, unprintLines
pour la console et unprintLines
pour les fichiers), alorsprintLines
faites plus d'une chose au niveau d'abstraction qui vous tient à cœur.la source
À cette échelle, cela n'a pas d'importance. L'implémentation à fonction unique est parfaitement évidente et compréhensible. Cependant, en ajoutant un peu plus de complexité, il est très intéressant de séparer l'itération de l'action. Par exemple, supposons que vous deviez imprimer des lignes à partir d'un ensemble de fichiers spécifié par un modèle comme "* .txt". Ensuite, je séparerais l'itération de l'action:
Maintenant, l'itération du fichier peut être testée séparément.
J'ai divisé les fonctions pour simplifier les tests ou pour améliorer la lisibilité. Si l'action effectuée sur chaque ligne de données était suffisamment complexe pour justifier un commentaire, je la diviserais certainement en une fonction distincte.
la source
Extrayez les méthodes lorsque vous ressentez le besoin d'un commentaire pour expliquer les choses.
Écrivez des méthodes qui ne font que ce que leur nom dit d'une manière évidente, ou racontez une histoire en appelant des méthodes intelligemment nommées.
la source
Même dans votre cas simple, il vous manque des détails que le principe de responsabilité unique vous aiderait à mieux gérer. Par exemple, que se passe-t-il en cas de problème lors de l'ouverture du fichier. L'ajout de la gestion des exceptions pour renforcer les cas de bord d'accès aux fichiers ajouterait 7 à 10 lignes de code à votre fonction.
Après avoir ouvert le fichier, vous n'êtes toujours pas en sécurité. Il pourrait vous être arraché (surtout s'il s'agit d'un fichier sur un réseau), vous pourriez manquer de mémoire, encore une fois, un certain nombre de cas extrêmes peuvent survenir contre lesquels vous voulez vous durcir et gonfler votre fonction monolithique.
La doublure imprimée semble assez inoffensive. Mais à mesure que de nouvelles fonctionnalités sont ajoutées à l'imprimante de fichiers (analyse et formatage du texte, rendu sur différents types d'écrans, etc.), elle se développera et vous vous en remercierez plus tard.
L'objectif de SRP est de vous permettre de penser à une seule tâche à la fois. C'est comme casser un gros bloc de texte en plusieurs paragraphes afin que le lecteur puisse comprendre le point que vous essayez de traverser. Il faut un peu plus de temps pour écrire du code qui respecte ces principes. Mais ce faisant, nous facilitons la lecture de ce code. Pensez à la façon dont votre futur sera heureux quand il devra retrouver un bogue dans le code et le trouver proprement partitionné.
la source
Personnellement, je préfère cette dernière approche, car elle vous évite de travailler dans le futur et force la mentalité "comment le faire de manière générique". Malgré cela, dans votre cas, la version 1 est meilleure que la version 2 - simplement parce que les problèmes résolus par la version 2 sont trop triviaux et spécifiques au flux. Je pense que cela devrait être fait de la manière suivante (y compris la correction de bogue proposée par Nawaz):
Fonctions utilitaires génériques:
Fonction spécifique au domaine:
Maintenant
printLines
etprintLine
peut fonctionner non seulement avecfstream
, mais avec n'importe quel flux.la source
printLine()
fonction n'a aucune valeur. Voir ma réponse .Chaque paradigme , (pas seulement nécessairement celui que vous avez cité) à suivre, requiert une certaine discipline, et donc - la réduction de la "liberté de parole" - entraîne un surcoût initial (au moins juste parce que vous devez l'apprendre!). En ce sens, chaque paradigme peut devenir nuisible lorsque le coût de ces frais généraux n'est pas surcompensé par l'avantage que le paradigme est conçu pour conserver avec lui-même.
La vraie réponse à la question requiert donc une bonne capacité à "prévoir" l'avenir, comme:
A
etB
A-
etB+
(c'est-à-dire quelque chose qui ressemble à A et B, mais juste un peu différent)?A*
ouA*-
?Si cette probabilité est relativement élevée, ce sera une bonne chance si -en pensant à A et B- je pense aussi à leurs variantes possibles, donc d'isoler les parties communes pour pouvoir les réutiliser.
Si cette probabilité est très faible (quelle que soit la variante autour, elle
A
n'est rien de plus queA
lui-même), étudiez comment décomposer A plus loin entraînera très probablement une perte de temps.À titre d'exemple, laissez-moi vous raconter cette histoire vraie:
Au cours de ma vie passée en tant que professeur, j'ai découvert que -Sur la plupart des projets- étudiant pratiquement tous d'entre eux fournissent leur propre fonction pour calculer la longueur d'une chaîne C .
Après quelques recherches, j'ai découvert que, étant un problème fréquent, tous les étudiants ont eu l'idée d'utiliser une fonction pour cela. Après leur avoir dit qu'il existe une fonction de bibliothèque pour cela (
strlen
), beaucoup d'entre eux ont répondu que puisque le problème était si simple et trivial, il était plus efficace pour eux d'écrire leur propre fonction (2 lignes de code) que de chercher le manuel de la bibliothèque C (c'était en 1984, j'ai oublié le WEB et google!) dans un ordre alphabétique strict pour voir s'il y avait une fonction prête pour cela.C'est un exemple où aussi le paradigme "ne pas réinventer la roue" peut devenir nuisible, sans un catalogue de roue efficace!
la source
Votre exemple est parfait pour être utilisé dans un outil à jeter qui a été nécessaire hier pour effectuer une tâche spécifique. Ou comme un outil d'administration directement contrôlé par un administrateur. Rendez-le maintenant robuste pour convenir à vos clients.
Ajoutez une gestion correcte des erreurs / exceptions avec des messages significatifs. Peut-être avez-vous besoin d'une vérification des paramètres, y compris des décisions qui doivent être prises, par exemple comment gérer les fichiers non existants. Ajoutez une fonctionnalité de journalisation, peut-être avec différents niveaux comme les informations et le débogage. Ajoutez des commentaires pour que vos collègues de l'équipe sachent ce qui s'y passe. Ajoutez toutes les parties qui sont généralement omises par souci de concision et laissées comme exercice au lecteur lors de la présentation d'exemples de code. N'oubliez pas vos tests unitaires.
Votre petite fonction agréable et assez linéaire se termine soudainement dans un gâchis complexe qui demande à être divisé en fonctions distinctes.
la source
OMI, il devient nuisible quand il va si loin qu'une fonction ne fait pratiquement rien du tout, mais délègue du travail à une autre fonction, car c'est un signe qu'il ne s'agit plus d'une abstraction de quoi que ce soit et que l'état d'esprit qui conduit à de telles fonctions est toujours en danger de faire des choses pires ...
De la poste d'origine
Si vous êtes assez pédant, vous remarquerez peut-être que printLine fait toujours deux choses: écrire la ligne dans cout et ajouter un caractère de "fin". Certaines personnes pourraient vouloir gérer cela en créant de nouvelles fonctions:
Oh non, maintenant nous avons encore aggravé le problème! Maintenant, il est même évident que printLine fait DEUX choses !!! 1! Il ne fait pas beaucoup de bêtise de créer les "solutions" les plus absurdes que l'on puisse imaginer juste pour se débarrasser de ce problème inévitable que l'impression d'une ligne consiste à imprimer la ligne elle-même et à ajouter un caractère de fin de ligne.
la source
Réponse courte ... cela dépend.
Pensez à ceci: que se passe-t-il si, à l'avenir, vous ne voudrez pas imprimer uniquement sur la sortie standard, mais sur un fichier.
Je sais ce qu'est YAGNI, mais je dis juste qu'il pourrait y avoir des cas où certaines implémentations sont connues pour être nécessaires, mais reportées. Alors peut-être que l'architecte ou n'importe qui sait que cette fonction doit également pouvoir imprimer dans un fichier, mais ne veut pas faire l'implémentation pour le moment. Il crée donc cette fonction supplémentaire, donc à l'avenir, vous n'aurez qu'à modifier la sortie à un seul endroit. Logique?
Cependant, si vous êtes certain de n'avoir besoin que d'une sortie dans la console, cela n'a pas vraiment de sens. L'écriture d'un «wrapper»
cout <<
semble inutile.la source
La raison pour laquelle il existe des livres consacrant des chapitres aux vertus de «faire une chose» est qu'il existe encore des développeurs qui écrivent des fonctions de 4 pages et imbriquent des conditions à 6 niveaux. Si votre code est simple et clair, vous l'avez bien fait.
la source
Comme d'autres affiches l'ont fait remarquer, faire une chose est une question d'échelle.
Je dirais également que l'idée One Thing est d'empêcher les gens de coder par effet secondaire. Ceci est illustré par le couplage séquentiel où les méthodes doivent être appelées dans un ordre particulier pour obtenir le «bon» résultat.
la source