J'aime vraiment le code propre et je veux toujours coder mon code de la meilleure façon possible. Mais il y avait toujours une chose, je n'ai pas vraiment compris:
Quand est-ce trop de "séparation des préoccupations" concernant les méthodes?
Disons que nous avons la méthode suivante:
def get_last_appearance_of_keyword(file, keyword):
with open(file, 'r') as file:
line_number = 0
for line in file:
if keyword in line:
line_number = line
return line_number
Je pense que cette méthode est bien bien comme elle est. C'est simple, facile à lire et cela fait clairement ce que dit le nom. Mais: ça ne fait pas vraiment "une seule chose". Cela ouvre effectivement le fichier, puis le trouve. Cela signifie que je pourrais la diviser encore plus loin (considérant également le "principe de responsabilité unique"):
Variation B (Eh bien, cela fait du sens. De cette façon, nous pouvons facilement réutiliser l'algorithme de trouver la dernière apparition d'un mot-clé dans un texte, mais il semble que "trop". Je ne peux pas expliquer pourquoi, mais je viens de "penser "C'est comme ça):
def get_last_appearance_of_keyword(file, keyword):
with open(file, 'r') as text_from_file:
line_number = find_last_appearance_of_keyword(text_from_file, keyword)
return line_number
def find_last_appearance_of_keyword(text, keyword):
line_number = 0
for line in text:
if keyword in line:
line_number = line
return line_number
Variation C (c'est juste absurde à mon avis. Nous encapsulons fondamentalement une doublure dans une autre méthode avec une seule ligne deux fois. Mais on pourrait discuter, que la manière d'ouvrir quelque chose puisse changer à l'avenir, à cause de demandes de fonctionnalités. , et comme nous ne voulons pas le changer plusieurs fois, mais juste une fois, nous venons de l'encapsuler et de séparer notre fonction principale encore plus loin):
def get_last_appearance_of_keyword(file, keyword):
text_from_file = get_text_from_file(file)
line_number = find_keyword_in_text(text_from_file, keyword)
return line_number
def get_text_from_file(file):
with open(file, 'r') as text:
return text
def find_last_appearance_of_keyword(text, keyword):
line_number = 0
for line in text:
if check_if_keyword_in_string(line, keyword):
line_number = line
return line_number
def check_if_keyword_in_string(text, keyword):
if keyword in string:
return true
return false
Donc, ma question maintenant: quelle est la bonne façon d'écrire ce code et pourquoi les autres approches sont-elles correctes ou fausses? J'ai toujours appris: la séparation, mais jamais quand c'est tout simplement trop. Et comment puis-je être sûr à l'avenir, que c'est "juste juste" et qu'il n'a pas besoin de plus de séparation lorsque je refuse à nouveau?
Vos différents exemples de division des préoccupations dans des fonctions distinctes ne souffrent que du même problème: vous êtes toujours en train de coder la dépendance du fichier dans get_last_appearance_of_keyword
. Cela rend cette fonction difficile à tester car elle doit maintenant répondre sur un fichier existant dans le système de fichiers lorsque le test est exécuté. Cela conduit à des tests fragiles.
Donc, je changerais simplement votre fonction d'origine à:
def get_last_appearance_of_keyword(text, keyword):
line_number = 0
for line in text:
if keyword in line:
line_number = line
return line_number
Maintenant, vous avez une fonction qui n'a qu'une seule responsabilité: trouver la dernière occurrence d'un mot-clé dans un texte. Si ce texte viendra d'un fichier, cela devient la responsabilité de l'appelant de traiter. Lors du test, vous pouvez alors simplement passer dans un bloc de texte. Lorsque vous l'utilisez avec code d'exécution, d'abord le fichier est lu, cette fonction est appelée. C'est une véritable séparation des préoccupations.
Le principe de responsabilité unique indique qu'une classe devrait s'occuper d'une seule pièce de fonctionnalité, et cette fonctionnalité doit être correctement encapsulée à l'intérieur.
Que fait exactement votre méthode? Il obtient la dernière apparition d'un mot-clé. Chaque ligne à l'intérieur de la méthode fonctionne vers cela et ce n'est pas lié à rien d'autre, et le résultat final n'est qu'un seul et un seul. En d'autres termes: vous n'avez pas besoin de diviser cette méthode en autre chose.
L'idée principale derrière le principe est que vous ne devriez pas faire plus d'une chose à la fin. Peut-être que vous ouvrez le fichier et laissez-le aussi que d'autres méthodes peuvent l'utiliser, vous ferez deux choses. Ou si vous deviez persister les données relatives à cette méthode, encore une fois, deux choses.
Maintenant, vous pouvez extraire la ligne "Fichier ouvert" et rendre la méthode recevoir un objet de fichier à utiliser, mais c'est plus un refactorisation technique que d'essayer de respecter le SRP.
C'est un bon exemple de l'ingénierie. Ne pensez pas trop ou vous vous retrouverez avec un tas de méthodes de ligne.
Ma prise sur elle: ça dépend :-)
À mon avis d'opinion, le code devrait répondre à ces objectifs, commandé par la priorité:
Pour moi, votre exemple original transmet tous ces objectifs (sauf peut-être que la correction est la correction à cause de la line_number = line
chose déjà mentionnée dans les commentaires , mais ce n'est pas le point ici).
La chose est que SRP n'est pas le seul principe à suivre. Il y a aussi vous n'en aurez pas besoin (Yagni) (parmi beaucoup d'autres). Lorsque les principes entrent en collision, vous devez les équilibrer.
Votre premier exemple est parfaitement lisible, facile à refacteur lorsque vous devez, mais pourrait ne pas suivre SRP autant que possible.
Chaque méthode de votre troisième exemple est également parfaitement lisible, mais tout cela n'est plus aussi facile à comprendre car vous devez brancher toutes les pièces dans votre esprit. Cela suit cependant SRP.
Comme vous ne recevez rien en ce moment de fractionnez votre méthode, ne le faites pas, car vous avez une alternative plus facile à comprendre.
Lorsque vos exigences changent, vous pouvez refroidir la méthode en conséquence. En fait, le "tout en une chose" pourrait être plus facile au refacteur: Imaginez que vous souhaitiez trouver la dernière ligne correspondant à un critère arbitraire. Maintenant, il vous suffit de passer une fonction de prédicat Lambda pour évaluer si une ligne correspond au critère ou non.
def get_last_match(file, predicate):
with open(file, 'r') as file:
line_number = 0
for line in file:
if predicate matches line:
line_number = line
return line_number
Dans votre dernier exemple, vous devez transmettre le prédicat 3 niveaux profond, c'est-à-dire modifier 3 méthodes pour modifier le comportement du dernier.
Notez que même diviser la lecture du fichier (un refactoring qui semble généralement trouver utile, y compris de moi) pourraient avoir des conséquences inattendues: vous devez lire le fichier entier à la mémoire pour la transmettre en tant que chaîne à votre mode. Si les fichiers sont grands qui pourraient ne pas être ce que vous voulez.
En bout de ligne: les principes ne doivent jamais être suivis à l'extrême sans prendre un pas en compte et en tenant compte de tous les autres facteurs.
Peut-être que "fractionnement prématuré des méthodes" pourrait être considéré comme un cas particulier de optimisation prématurée ? ;-)