[JOUR 20] II. Python, the right way : Les préceptes du Clean Code
Explorons les principes énoncés dans le fameux livre d'Uncle Bob
Sommaire :
I. Principes de base
II. Astuces pour l’intelligibilité
III. Règles de nommage
IV. Règles relatives aux fonctions
V. Structure du code source
VI. Objets et structures de données
Article précédent :
Cet article se concentre sur des principes de software engineering pour écrire du code propre (c’est-à-dire, comme vu précédemment, lisible, maintenable, extensible et testable). La plupart des concepts suivants sont tirés du livre Clean Code de Robert C. Martin, alias Uncle Bob.
Uncle Bob est une star dans le monde du développement ; il a popularisé des concepts comme le clean code, la clean architecture et le software craftsmanship.
Bien que le livre ait été à l'origine écrit avec des exemples en Java, les principes énoncés sont applicables à n’importe quel langage de programmation. Je vous ai mis des exemples en Python dans la suite pour vous les illustrer.
J’ai également opéré une sélection et ne vous ai pas mis tous les principes du livre, mais une sélection personnelle des plus importants selon moi.
I. Principes de base
YAGNI (You Ain't Gonna Need It)
YAGNI encourage les développeurs à se concentrer uniquement sur les fonctionnalités essentielles et immédiates. Les points clés sont :
Ne pas implémenter de fonctionnalités qui ne sont pas immédiatement nécessaires.
Éviter d'anticiper des besoins futurs hypothétiques.
Réduire la complexité du code en évitant l'ajout de fonctionnalités superflues.
Favoriser une approche itérative du développement.
KISS (Keep It Simple, Stupid)
KISS met l'accent sur la simplicité dans la conception et l'implémentation du code. Les avantages de ce principe sont :
Meilleure compréhension du code par les développeurs.
Diminution des erreurs et des bugs.
Code plus évolutif et facile à maintenir.
Réduction du temps de développement et du budget.
DRY (Don't Repeat Yourself)
DRY vise à éliminer la duplication de code et de logique dans un programme. Les objectifs de ce principe sont :
Améliorer la maintenabilité du code.
Augmenter la lisibilité et la compréhension du programme.
Réduire les risques d'erreurs liés à la duplication.
Optimiser l'efficacité de l'application.
En appliquant ces trois principes, les développeurs peuvent créer un code plus clair, plus efficace et plus facile à maintenir, ce qui contribue à la qualité globale du logiciel et à la réduction des coûts de développement à long terme.
La règle du boyscout
Laissez le camp plus propre que vous ne l’avez trouvé. L’idée est d’améliorer petit à petit le code à chacune de vos interventions.
Si vous voyez quelque chose de mal fait dans le code, corrigez-le ou, à minima, signalez-le (si le changement demande beaucoup de temps).
La règle du boyscout est connexe à la théorie de la fenêtre cassée (ou broken window theory en anglais). Si vous avez des standards de qualité élevés, alors l’équipe suivra ; à l’inverse, si vous laissez des déchets dans votre code, les autres développeurs seront plus enclins à en laisser également. Pour les curieux j’avais écrit un post linkedin il y a un moment sur le sujet.
II. Astuces pour l’intelligibilité
Soyez cohérent dans les règles que vous adoptez :
Mauvais :
get_user_info()
fetchCustomerData()
retrieve_product_details()
Bon :
get_user_info()
get_customer_data()
get_product_details()
Utilisez des noms de variables explicites (plus sur le nommage dans la partie suivante) :
Mauvais :
d = calculate(a, b)
Bon :
total_price = calculate_total(item_price, quantity)
=> Là on comprend de quoi on parle !
Encapsulez les conditions limites :
Mauvais :
if page_number >= 1 and page_number <= total_pages:
# Logique de pagination
Bon, plus simple à comprendre :
def is_valid_page(page_number, total_pages):
return 1 <= page_number <= total_pages
if is_valid_page(page_number, total_pages):
# Logique de pagination
Évitez les conditions négatives :
Mauvais :
if not is_invalid_user:
Bien :
if is_valid_user:
III. Règles de nommage
Dites-vous bien que le code est comme du texte dans un livre. Si vous le lisez de haut en bas, il doit vous raconter une histoire, et cette histoire doit être simple à comprendre. L’idéal est de ne pas avoir à chercher ailleurs ce que veut dire tel mot (dans notre cas les variables, fonctions et classes) et de ne pas s’arracher les cheveux pour comprendre l’histoire.
Choisissez des noms descriptifs et sans ambiguïté :
Mauvais :
getData()
Bon :
fetchUserProfile()
Faites des distinctions qui ont du sens :
Mauvais :
userInfo
etuserData
(peu de distinction, le sens est similaire)Bon :
userProfile
etuserPreferences
(distinction claire)
Utilisez des noms prononçables :
Mauvais :
genymdhms
(exemple de fonction pour générer une date/heure)Bon :
generateTimestamp
ougenerateDate
Utilisez des noms recherchables :
Mauvais : Des noms trop génériques comme var, temp, x
Bon :
seconds_per_day
, clair et facilement recherchable
Remplacez les nombres magiques par des constantes bien nommées :
Mauvais :
if (employee.daysInactive > 60)
Bon :
MAX_INACTIVE_DAYS = 60
if (employee.daysInactive > MAX_INACTIVE_DAYS):
# Fait quelque chose
Évitez d'ajouter des préfixes ou des informations sur les types :
Mauvais :
strName
,intAge
,arrItems
Bon :
name
,age
,items
Un bon moyen simple de préciser qu’une variable est une collection est de mettre le nom de la variable au pluriel ! :
item_name
⇒ nom d’un item
item_names
⇒ liste de noms d’items
IV. Règles relatives aux fonctions
Courtes, idéalement pas plus de 25 lignes. N’hésitez pas à découper une fonction en plusieurs petites pour alléger votre code :
Mauvais :
def process_data(data):
# 50 lignes de code avec plusieurs responsabilités
Bon :
def process_data(data):
cleaned_data = clean_data(data)
return analyze_data(cleaned_data)
Une fonction ne fait qu'une chose et le fait bien :
Mauvais :
def save_and_send_email(user):
# Sauvegarde l'utilisateur
# Envoie un email
Bon :
def save_user(user):
# Sauvegarde l'utilisateur
def send_welcome_email(user):
# Envoie un email
Utilisez des noms descriptifs :
Pas clair :
def process(x, y):
Clair :
def calculate_average_score(total_score, number_of_tests):
Préférez des fonctions avec le moins d'arguments possible, idéalement pas plus de 3 :
Mauvais :
def create_user(name, email, age, country, language, role):
Bon :
def create_user(user_info):
# user_info est un dictionnaire ou un objet contenant toutes les informations nécessaires
Un autre concept avancé qui peut être utile pour réduire le nombre de paramètres dans vos fonctions est les fonctions partielles.
Les fonctions partielles vous permettent de fixer un certain nombre d'arguments d'une fonction et de générer une nouvelle fonction. Pour utiliser les fonctions partielles :
Importez
partial
depuis le modulefunctools
Créez une nouvelle fonction en utilisant
partial()
, en spécifiant la fonction originale et les arguments fixés
Exemple :
from functools import partial
def multiply(x, y):
return x * y
double = partial(multiply, 2) # multiply est appelée avec x = 2 mais y non précisé
result = double(5) # Sortie : 10
Sans effet de bord si possible. Dans l’idéal, on passe tout ce dont a besoin une fonction en paramètres, et la fonction retourne un résultat obtenu seulement à partir des paramètres d’entrée. Pourquoi privilégier ce type de fonction ? Car elles sont plus prédictibles et facilement testables. Dans la programmation, on adore tout ce qui est déterministe et on déteste ce qui est imprévisible. Prenez les exemples suivants :
Mauvais :
total = 0
def add_to_total(value):
global total
total += value
return total
Comment savoir que total
n’a pas été modifié à un autre endroit ? Ainsi, la sortie de la fonction est imprévisible.
Dans l’exemple suivant, le résultat est totalement prédictible en fonction des paramètres d’entrée :
def add_to_total(current_total, value):
return current_total + value
N'utilisez pas de flag ; écrivez plutôt plusieurs méthodes sans ce type d'argument :
Mauvais :
def process_payment(amount, is_refund=False):
if is_refund:
# Logique de remboursement
else:
# Logique de paiement
Plus clair :
def process_payment(amount):
# Logique de paiement
def process_refund(amount):
# Logique de remboursement
V. Structure du code source
Séparez les concepts verticalement :
# Imports
import os
import sys
# Constantes
MAX_USERS = 100
DEFAULT_TIMEOUT = 30
# Classes
class User:
# ...
class Database:
# ...
# Fonctions principales
def main():
# ...
# Exécution du programme
if __name__ == "__main__":
main()
Le code lié logiquement devrait apparaître dense verticalement, c’est-à-dire sans saut de ligne :
def calculate_total_price(items):
total = 0
for item in items:
price = item.price
quantity = item.quantity
total += price * quantity
return total
Déclarez les variables à proximité de leurs usages :
def process_order(order):
tax_rate = 0.08 # Déclaré juste avant son utilisation
subtotal = calculate_subtotal(order)
tax = subtotal * tax_rate
total = subtotal + tax
return total
Les fonctions dépendantes les unes des autres devraient être à proximité :
def authenticate_user(user):
if validate_user(user):
# ...
def login_user(user):
if authenticate_user(user):
# ...
def validate_user(user):
# ...
Les fonctions similaires devraient être à proximité les unes des autres :
def save_user(user):
# ...
def update_user(user):
# ...
def delete_user(user):
# ...
Placez les fonctions dans la direction descendante :
def main():
data = load_data()
processed_data = process_data(data)
result = analyze_data(processed_data)
display_results(result)
def load_data():
# ...
def process_data(data):
# ...
def analyze_data(data):
# ...
def display_results(result):
# ...
Gardez les lignes courtes :
# Mauvais
long_variable_name = some_long_function_name(argument1, argument2, argument3, argument4, argument5)
# Bon
long_variable_name = some_long_function_name(
argument1,
argument2,
argument3,
argument4,
argument5
)
N'alignez rien horizontalement :
# Mauvais
x = 5
foo = 'bar'
y = 10
# Bon
x = 5
foo = 'bar'
y = 10
Utilisez des espaces pour associer des choses liées et dissocier des choses liées faiblement :
def complex_function():
do_first_thing()
do_second_thing()
prepare_for_next_step()
do_next_step()
finalize()
VI. Objets et structures de données
Cachez les structures internes : Ce principe met en avant l'encapsulation, recommandant de masquer les détails internes des classes ou objets et de n'exposer que les interfaces essentielles et claires. Cela diminue les dépendances et simplifie les modifications internes sans perturber le fonctionnement du code externe.
Les classes devraient être petites :
# Mauvais
class SuperUser:
def __init__(self):
# 20+ méthodes et attributs
# Bon
class User:
def __init__(self):
self.name = ''
self.email = ''
class AdminUser(User):
def __init__(self):
super().__init__()
self.admin_level = 0
Et ne faire qu'une chose :
# Mauvais
class OrderProcessor:
def process(self, order):
# Valide la commande
# Calcule le total
# Effectue le paiement
# Envoie un email de confirmation
# Bon
class OrderValidator:
def validate(self, order):
# Valide la commande
class OrderCalculator:
def
calculate_total(self, order): # Calcule le total
class PaymentProcessor:
def process_payment(self, order):
# Effectue le paiement
class EmailSender:
def send_confirmation(order):
# Envoie un email de confirmation
4. Elles possèdent un petit nombre d’attributs :
# Mauvais
class Customer:
def __init__(self):
self.name = ''
self.email = ''
self.phone = ''
self.address = ''
self.city = ''
self.country = ''
self.postal_code = ''
# ... et beaucoup d'autres attributs
# Bon
class Customer:
def __init__(self):
self.name = ''
self.email = ''
self.contact_info = ContactInfo()
self.address = Address()
class ContactInfo:
def __init__(self):
self.phone = ''
class Address:
def __init__(self):
self.street = ''
self.city = ''
self.country = ''
self.postal_code = ''
5. Une classe de base ne devrait rien connaître de ses classes dérivées :
# Mauvais
class Animal:
def make_sound(self):
if isinstance(self, Dog):
return "Woof"
elif isinstance(self, Cat):
return "Meow"
# Bon
class Animal:
def make_sound(self):
raise NotImplementedError("Subclass must implement abstract method")
class Dog(Animal):
def make_sound(self):
return "Woof"
class Cat(Animal):
def make_sound(self):
return "Meow"
Ces bonnes pratiques concernant les objets font écho aux principes SOLID que nous verrons dans l’article suivant.
Merci de votre lecture !