# Code propre B : DRY, KISS, séparation des responsabilités, commentaires et refactoring<no value>

## 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 commentaire | Un commentaire inutile / nuisible |
|---|---|
| Explique **pourquoi** nous avons choisi une certaine façon | Répète ce que le code dit déjà |
| Avertit d'un comportement inattendu | Compense 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

```python
# 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 :**

```python
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 :**

```python
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) :**

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

## Analyse du code ligne par ligne

| Ligne | Explication |
|---|---|
| `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 `print` | Au 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 :

```python
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 :**

```python
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 :**

```text
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.**

```python
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.**

```python
# 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.**

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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

```python
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 :

```text
Pen 5 x 4 = 20
Notebook 12 x 2 = 24
Bag 60 x 1 = 60
Total: 104
```

{{< solution >}}
```python
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])
```

```text
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.
{{< /solution >}}

### Exercice 2: KISS — un code trop « astucieux »

```python
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 :**

```text
even
odd
```

{{< solution >}}
```python
def parity(n):
    if n % 2 == 0:
        return "even"
    return "odd"


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

```text
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.
{{< /solution >}}

### Exercice 3: Les commentaires — pourquoi, pas quoi

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

```text
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
```

{{< 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**.
{{< /solution >}}

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

```python
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 :

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

**Résultat attendu :**

```text
BMI: 22.9
```

{{< solution >}}
```python
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)
```

```text
BMI: 22.9
```

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

```py
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`.
{{< /solution >}}

### Exercice 5: Refactoring par étapes

```python
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 :**

```text
12 cold
24 pleasant
33 hot
```

{{< solution >}}
```python
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))
```

```text
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é.
{{< /solution >}}

<!-- exercises:end -->

## 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.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [PEP 20 — The Zen of Python](https://peps.python.org/pep-0020/)
- [PEP 8 — Comments](https://peps.python.org/pep-0008/#comments)
- [Python Tutorial — Defining Functions](https://docs.python.org/3/tutorial/controlflow.html#defining-functions)

<!-- docs:end -->
