Code propre B : DRY, KISS, séparation des responsabilités, commentaires et refactoring

Objectifs de la leçon
  • Reconnaître du code dupliqué et le remplacer par une fonction ou une boucle (DRY)
  • Préférer une solution simple et lisible à une solution « astucieuse » (KISS)
  • Séparer l’entrée, la logique et la sortie (Separation of Concerns)
  • Savoir quand écrire un commentaire et quand le code doit s’expliquer de lui-même
  • Refactoriser par petites étapes en gardant le même comportement

Qu’allons-nous apprendre ?

  • DRY — Don’t Repeat Yourself (ne vous répétez pas).
  • KISS — Keep It Simple, Stupid (restez simple).
  • Separation of Concerns — la séparation des responsabilités entre les parties du programme.
  • Les commentaires — quand un commentaire aide et quand il nuit.
  • Le refactoring — améliorer la structure du code sans changer son comportement.

Pourquoi en avons-nous besoin ?

Dans le thème sur l’ordinateur et le système d’exploitation, nous avons écrit project_check.py avec quatre blocs presque identiques. Que se passe-t-il quand il faut vérifier 20 fichiers de plus ? Ou changer le message de OK en ✓ ? Il faut toucher 20 endroits, et à l’un d’eux vous oublierez forcément.

Les principes du jour répondent à la question : comment écrire un code facile à modifier ? En réalité, la majeure partie du temps d’un développeur est consacrée à modifier du code existant, pas à en écrire du nouveau.

Explication à partir des bases

DRY — Don’t Repeat Yourself

Chaque élément de connaissance ne doit apparaître qu’à un seul endroit. Quand la même logique apparaît à plusieurs endroits :

  • Une correction de bug doit être faite à tous les endroits, et on en oublie toujours un.
  • Le code est plus long, et il est difficile de voir ce qui est propre à chaque partie.

Les outils pour corriger : une fonction (une logique qui se répète), une boucle (une action sur de nombreux éléments), une constante (une valeur qui se répète).

Signe d’alerte : si vous faites un copier-coller de code et y changez un petit détail, il vous faut probablement une fonction avec un paramètre.

KISS — Keep It Simple

La solution la plus simple qui fonctionne est généralement la meilleure. Un code « astucieux » (une ligne compliquée, des astuces) est difficile à lire, à déboguer et à modifier. Le but du code est d’être compris, pas d’impressionner.

La séparation des responsabilités (Separation of Concerns)

Concern = un domaine de responsabilité. Chaque programme a quelques domaines différents :

  • L’entrée — d’où vient l’information (clavier, fichier, requête HTTP).
  • La logique — ce que nous en faisons (calculs, décisions, règles).
  • La sortie — où va l’information (écran, fichier, réponse HTTP).

Quand ils sont séparés, on peut en remplacer un sans toucher aux autres. C’est exactement la structure d’un serveur dans les thèmes backend et bases de données : la route (entrée/sortie HTTP) est séparée du code d’accès à la base de données (Database).

Le SRP (de la leçon précédente) parle d’une seule fonction. La Separation of Concerns parle de tout le programme : quelles parties existent, et comment elles sont séparées.

Les commentaires

Le code explique le « quoi » et le « comment ». Un commentaire explique le « pourquoi ».

Un bon commentaireUn commentaire inutile / nuisible
Explique pourquoi nous avons choisi une certaine façonRépète ce que le code dit déjà
Avertit d’un comportement inattenduCompense un mauvais nom
Renvoie à une source (une loi, une norme, un document)Du « code mort » transformé en commentaire
Un vieux commentaire qui n’est plus vrai, ce qui est le pire

Le refactoring

Le refactoring = changer la structure du code sans changer son comportement. On renomme, on découpe des fonctions, on supprime le code dupliqué. À la fin, le code fait exactement la même chose, seulement plus clairement.

La règle importante : de petites étapes, et une vérification après chaque étape. On change une chose, on exécute, on compare avec la sortie d’origine, et on continue.

Syntaxe

# DRY: repeated code  →  one function + loop
def check(item):
    ...

for item in items:
    check(item)

# Comment: explain WHY, not WHAT
# Tax authority requires rounding up to whole shekels (regulation 12b)
total = math.ceil(total)

# Docstring: a string right under def that describes the function
def calculate_shipping(weight_kg):
    """Return shipping cost in NIS for a package of the given weight."""
    ...

Une docstring est une chaîne entre triples guillemets ("""...""") qui apparaît sur la première ligne d’une fonction et décrit ce qu’elle fait. VS Code l’affiche quand on survole un appel à la fonction.

Premier exemple de code

DRY en action. Vous vous souvenez de project_check.py du thème sur l’ordinateur, le système d’exploitation et le réseau ? Avant :

import os

base = "../shop"

if os.path.exists(base + "/data"):
    print("data -> OK")
else:
    print("data -> MISSING")

if os.path.exists(base + "/logs"):
    print("logs -> OK")
else:
    print("logs -> MISSING")

if os.path.exists(base + "/app.py"):
    print("app.py -> OK")
else:
    print("app.py -> MISSING")

Après :

import os

PROJECT_DIR = "../shop"
REQUIRED_PATHS = ["data", "logs", "app.py", "data/products.txt"]

def check_path(relative_path):
    full_path = PROJECT_DIR + "/" + relative_path
    if os.path.exists(full_path):
        status = "OK"
    else:
        status = "MISSING"
    print(relative_path, "->", status)

for path in REQUIRED_PATHS:
    check_path(path)

Sortie (le même comportement) :

data -> OK
logs -> OK
app.py -> OK
data/products.txt -> OK

Analyse du code ligne par ligne

LigneExplication
PROJECT_DIR = "../shop"Le chemin est défini une seule fois. Si le projet change de dossier, on modifie une seule ligne.
REQUIRED_PATHS = [...]Les données (quoi vérifier) sont séparées de la logique (comment vérifier). Ajouter une vérification = ajouter un nom à la liste.
def check_path(relative_path):La logique qui se répétait quatre fois, écrite une seule fois. La partie qui changeait d’un bloc à l’autre (le nom du chemin) est devenue un paramètre.
status = ... puis un seul printAu lieu de deux print presque identiques, une variable avec l’état et un seul affichage. C’est aussi du DRY à petite échelle.
for path in REQUIRED_PATHS:La boucle exécute la vérification sur chaque élément.

Un autre exemple

KISS + séparation des responsabilités + commentaires. Un programme qui calcule un coût de livraison. Avant — « astucieux », tout mélangé et avec des commentaires inutiles :

w = float(input("w: "))  # get w
# calculate the price
print("Price:", (w <= 1) * 20 + (1 < w <= 5) * 35 + (w > 5) * (35 + (w - 5) * 4))  # print

Après :

BASE_PRICE_SMALL = 20
BASE_PRICE_MEDIUM = 35
PRICE_PER_EXTRA_KG = 4
SMALL_MAX_KG = 1
MEDIUM_MAX_KG = 5

def read_weight():
    return float(input("Package weight (kg): "))

def calculate_shipping(weight_kg):
    """Return the shipping price in NIS for a package of the given weight."""
    if weight_kg <= SMALL_MAX_KG:
        return BASE_PRICE_SMALL
    if weight_kg <= MEDIUM_MAX_KG:
        return BASE_PRICE_MEDIUM
    # Above the medium tier the courier charges per extra kg (price list 2026)
    extra_kg = weight_kg - MEDIUM_MAX_KG
    return BASE_PRICE_MEDIUM + extra_kg * PRICE_PER_EXTRA_KG

def show_price(price):
    print("Shipping price:", price, "NIS")

weight = read_weight()
price = calculate_shipping(weight)
show_price(price)

Exécution :

Package weight (kg): 7.5
Shipping price: 45.0 NIS

Analyse :

  • KISS : l’astuce (w <= 1) * 20 fonctionne (parce que True compte pour 1 et False pour 0), mais personne ne la comprendra d’un coup d’œil. Une simple suite de if se lit immédiatement.
  • Séparation des responsabilités : read_weight (entrée), calculate_shipping (logique), show_price (sortie). Demain le poids vient d’une requête HTTP ? On remplace seulement read_weight. La logique ne change pas.
  • Commentaires : # get w et # print ont été supprimés, car ils répétaient le code. Il reste un commentaire qui explique pourquoi (la grille tarifaire du transporteur), une information qu’on ne peut pas comprendre à partir du code lui-même.
  • La docstring décrit ce que renvoie la fonction et dans quelle unité (NIS, le shekel).

Erreur courante

Erreur 1 : un commentaire à la place d’un bon nom.

d = 30  # number of days until the subscription expires

Mieux : days_until_expiry = 30, sans aucun commentaire. Si vous avez besoin d’un commentaire pour expliquer une variable, le nom n’est pas assez bon.

Erreur 2 : un commentaire qui ment.

# Discount is 10%
price = price * 0.85

Quelqu’un a mis à jour le code et a oublié le commentaire. Maintenant, le lecteur ne sait plus quoi croire. Les commentaires ne sont pas vérifiés par Python, ils « pourrissent » donc. C’est une raison de plus d’écrire peu de commentaires, et de bons.

Erreur 3 : DRY trop tôt. Deux morceaux de code qui se ressemblent par hasard, mais qui représentent des règles métier différentes, ne doivent pas forcément être fusionnés. Par exemple, « 17 % de TVA » et « 17 % de commission » sont une coïncidence, pas la même connaissance. Si on les fusionne en une seule constante, modifier l’une cassera l’autre.

Erreur 4 : un « gros » refactoring d’un coup. Vous changez 10 choses, vous exécutez, quelque chose casse, et vous n’avez aucune idée de quoi. Une étape, exécuter, comparer, étape suivante.

Exercices

Les exercices n’utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d’abord seul, et n’ouvrez la solution qu’ensuite.

Exercice 1: DRY — un ticket de caisse sans copier-coller

print("Pen", 5, "x", 4, "=", 5 * 4)
print("Notebook", 12, "x", 2, "=", 12 * 2)
print("Bag", 60, "x", 1, "=", 60 * 1)
print("Total:", 5 * 4 + 12 * 2 + 60 * 1)

Réécrivez le code pour que les données soient conservées dans des listes, et qu’une seule fonction avec une boucle affiche le ticket. La sortie doit rester :

Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104
Afficher la solution
def line_total(price, quantity):
    return price * quantity


def print_receipt(names, prices, quantities):
    total = 0
    for i in range(len(names)):
        amount = line_total(prices[i], quantities[i])
        print(names[i], prices[i], "x", quantities[i], "=", amount)
        total = total + amount
    print("Total:", total)


print_receipt(["Pen", "Notebook", "Bag"], [5, 12, 60], [4, 2, 1])
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104

Chaque calcul n’apparaît plus qu’une seule fois. Un quatrième produit = une valeur de plus dans les listes, sans ligne de code supplémentaire.

Exercice 2: KISS — un code trop « astucieux »

def parity(n):
    return (n % 2 == 0) * "even" + (n % 2 == 1) * "odd"

print(parity(4))
print(parity(7))

Le code fonctionne, mais il est difficile à comprendre. Réécrivez parity de la façon la plus simple et la plus lisible.

Résultat attendu :

even
odd
Afficher la solution
def parity(n):
    if n % 2 == 0:
        return "even"
    return "odd"


print(parity(4))
print(parity(7))
even
odd

KISS — Keep It Simple. Un if ordinaire se lit immédiatement, sans réfléchir à la façon dont fonctionne la multiplication de True par une chaîne.

Exercice 3: Les commentaires — pourquoi, pas quoi

Pour chaque commentaire — est-il bon, inutile ou nuisible ? Expliquez.

1.  count = count + 1   # add 1 to count
2.  # 30 days: the refund policy of the shop
    REFUND_DAYS = 30
3.  total = price * 2   # multiply price by 3
4.  # retry 3 times — the payment API sometimes fails on the first call
Afficher la solution
  1. Inutile — il répète ce que le code dit déjà.
  2. Bon — il explique pourquoi 30, une information qui n’apparaît pas dans le code.
  3. Nuisible — il ment (le code multiplie par 2). Un commentaire non mis à jour est pire qu’aucun commentaire.
  4. Bon — il explique pourquoi le code fait quelque chose qui semble étrange.

La règle : le code dit quoi, le commentaire dit pourquoi.

Exercice 4: Séparation des responsabilités — le calcul face à l’entrée et la sortie

def bmi():
    weight = float(input("Weight: "))
    height = float(input("Height: "))
    print("BMI:", round(weight / (height * height), 1))

La fonction fait trois choses : elle lit l’entrée, calcule et affiche. On ne peut donc pas utiliser le calcul sans clavier. Découpez-la en trois fonctions, une par responsabilité :

  1. read_number(message) — ne fait que lire un nombre saisi par l’utilisateur et le renvoyer.
  2. calculate_bmi(weight, height) — ne fait que calculer et renvoyer, sans input ni print.
  3. print_bmi(bmi) — ne fait qu’afficher un résultat déjà calculé.

Testez le calcul et l’affichage sans clavier :

bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)

Résultat attendu :

BMI: 22.9
Afficher la solution
def read_number(message):
    return float(input(message))


def calculate_bmi(weight, height):
    return round(weight / (height * height), 1)


def print_bmi(bmi):
    print("BMI:", bmi)


bmi = calculate_bmi(70, 1.75)
print_bmi(bmi)
BMI: 22.9

Dans le vrai programme, on relie les trois fonctions, et chaque ligne exécute une seule responsabilité :

weight = read_number("Weight: ")
height = read_number("Height: ")
bmi = calculate_bmi(weight, height)
print_bmi(bmi)
  • input se trouve seulement dans read_number, et print seulement dans print_bmi. calculate_bmi n’a ni entrée ni sortie : on peut donc l’appeler avec des données venant de n’importe où — d’une saisie, d’un fichier ou d’un test.
  • print("BMI:", calculate_bmi(70, 1.75)) est plus court, mais l’affichage reste collé au calcul sur la même ligne, et aucune fonction n’est responsable de l’affichage. Vous voulez afficher le résultat autrement ? Avec la séparation, on remplace seulement print_bmi.

Exercice 5: Refactoring par étapes

t = 12
if t < 15:
    print(12, "cold")
elif t < 28:
    print(12, "pleasant")
else:
    print(12, "hot")
t = 24
if t < 15:
    print(24, "cold")
elif t < 28:
    print(24, "pleasant")
else:
    print(24, "hot")

Refactorisez-le : une fonction classify(temperature) qui renvoie la classification, des constantes pour les limites, et une boucle sur la liste de températures [12, 24, 33].

Résultat attendu :

12 cold
24 pleasant
33 hot
Afficher la solution
COLD_LIMIT = 15
HOT_LIMIT = 28


def classify(temperature):
    if temperature < COLD_LIMIT:
        return "cold"
    if temperature < HOT_LIMIT:
        return "pleasant"
    return "hot"


for temperature in [12, 24, 33]:
    print(temperature, classify(temperature))
12 cold
24 pleasant
33 hot

Les étapes : (1) extraire une fonction, (2) déplacer 15 et 28 dans des constantes, (3) remplacer le code dupliqué par une boucle — et exécuter après chaque étape pour s’assurer que la sortie n’a pas changé.

Résumé

  • DRY — chaque élément de connaissance à un seul endroit. Du code qui se répète → une fonction, une boucle ou une constante.
  • KISS — simple et lisible vaut mieux qu’« astucieux ».
  • Séparation des responsabilités — l’entrée, la logique et la sortie dans des parties séparées. C’est la structure de tout serveur.
  • Les commentaires expliquent pourquoi. Le code lui-même (avec de bons noms) explique quoi. Un commentaire qui ment est pire qu’aucun commentaire.
  • Le refactoring — améliorer la structure sans changer le comportement, par petites étapes avec une exécution après chaque étape.

Documentation officielle