Qu’est-ce que le principe de responsabilité unique ?
Le principe de responsabilité unique dit qu’une classe ne doit avoir qu’une seule raison de changer. Une classe qui fait deux métiers casse chaque fois que l’un des deux évolue. Savoir repérer ces classes vous évite des modifications qui en cassent d’autres.
Une cafétéria édite des factures. Le calcul du total ne change que si les prix changent. La mise en page change dès que la direction veut un autre modèle.
À la fin de cette leçon
Remarque
Je peux repérer une classe qui a deux métiers ou qui est difficile à étendre, et dire comment la corriger.
Mots-clés
| Mot | Ce que ça veut dire |
|---|---|
| Principe de conception | une règle qui garde un programme facile à changer |
| Responsabilité | un métier précis qu’une classe assure |
| Cohésion | le fait que tout ce que contient une classe va ensemble |
| Ouvert à l’extension | on ajoute un cas en écrivant du code neuf |
| Fermé à la modification | on n’a pas à rouvrir le code qui marche |
| Refactorisation | réécrire le code sans changer ce qu’il fait |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Deux métiers, deux raisons de changer
La classe Facture ci-dessous garde les lignes, calcule le total et fabrique le texte à imprimer. Ce sont deux métiers : les chiffres et la présentation.
Si la direction veut les prix en euros avec une virgule, on rouvre une classe qui calcule aussi les totaux. Une faute dans l’un peut casser l’autre.
Une Facture qui calcule et affiche
class Facture:
def __init__(self):
self.lignes = []
def ajouter(self, nom, prix):
self.lignes.append([nom, prix])
def total(self):
somme = 0
for ligne in self.lignes:
somme = somme + ligne[1]
return somme
def texte(self):
sortie = ""
for ligne in self.lignes:
sortie = sortie + ligne[0] + " : " + str(ligne[1]) + " centimes\n"
return sortie + "Total : " + str(self.total()) + " centimes"
facture = Facture()
facture.ajouter("Café", 120)
facture.ajouter("Sandwich", 450)
print(facture.texte())
Séparer : une classe par métier
On sépare en deux classes. Facture garde les lignes et calcule le total. Afficheur ne garde rien : il reçoit une facture et rend son texte.
Chaque classe a maintenant une seule raison de changer. La mise en page évolue sans toucher aux calculs.
Deux classes, un métier chacune
class Facture:
def __init__(self):
self.lignes = []
def ajouter(self, nom, prix):
self.lignes.append([nom, prix])
def total(self):
somme = 0
for ligne in self.lignes:
somme = somme + ligne[1]
return somme
class Afficheur:
def texte(self, facture):
sortie = ""
for ligne in facture.lignes:
sortie = sortie + ligne[0] + " : " + str(ligne[1]) + " centimes\n"
return sortie + "Total : " + str(facture.total()) + " centimes"
facture = Facture()
facture.ajouter("Café", 120)
facture.ajouter("Sandwich", 450)
print(Afficheur().texte(facture))
Créer un dossier pour les essais
mkdir -p essaisAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la classe à deux métiers
printf 'class Facture:\n def __init__(self):\n self.lignes = []\n\n def ajouter(self, nom, prix):\n self.lignes.append([nom, prix])\n\n def total(self):\n somme = 0\n for ligne in self.lignes:\n somme = somme + ligne[1]\n return somme\n\n def texte(self):\n sortie = ""\n for ligne in self.lignes:\n sortie = sortie + ligne[0] + " : " + str(ligne[1]) + " centimes\\n"\n return sortie + "Total : " + str(self.total()) + " centimes"\n' > essais/facture.pyAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Imprimer la facture
PYTHONPATH=essais python3 -c "from facture import Facture; f = Facture(); f.ajouter('Cafe', 120); f.ajouter('Sandwich', 450); print(f.texte())"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire la version en deux classes
printf 'class Facture:\n def __init__(self):\n self.lignes = []\n\n def ajouter(self, nom, prix):\n self.lignes.append([nom, prix])\n\n def total(self):\n somme = 0\n for ligne in self.lignes:\n somme = somme + ligne[1]\n return somme\n\n\nclass Afficheur:\n def texte(self, facture):\n sortie = ""\n for ligne in facture.lignes:\n sortie = sortie + ligne[0] + " : " + str(ligne[1]) + " centimes\\n"\n return sortie + "Total : " + str(facture.total()) + " centimes"\n' > essais/facture2.pyAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Imprimer avec la version séparée
PYTHONPATH=essais python3 -c "from facture2 import Facture, Afficheur; f = Facture(); f.ajouter('Cafe', 120); f.ajouter('Sandwich', 450); print(Afficheur().texte(f))"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Compter les classes du fichier séparé
grep -c "^class " essais/facture2.pyAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ouvert à l’extension, fermé à la modification
Un second principe : ajouter un cas doit se faire en écrivant du code neuf, sans rouvrir le code qui marche. Une suite de if sur le type oblige à rouvrir la fonction à chaque nouveau cas.
Avec le polymorphisme, un nouveau cas est une nouvelle classe. La fonction qui s’en sert ne bouge pas.
Ajouter un tarif sans toucher la fonction
class TarifPlein:
def prix(self, base):
return base
class TarifEtudiant:
def prix(self, base):
return base - base * 20 // 100
# Un nouveau tarif : une classe de plus, la fonction ne change pas.
class TarifSenior:
def prix(self, base):
return base - base * 30 // 100
def prix_du_billet(tarif, base):
return tarif.prix(base)
print(prix_du_billet(TarifEtudiant(), 1000), prix_du_billet(TarifSenior(), 1000))
Erreur fréquente
Attention
Beaucoup croient qu’il faut une classe par méthode, ou qu’appliquer les principes veut dire couper partout. Séparez seulement ce qui change pour des raisons différentes. Deux classes minuscules qui changent toujours ensemble n’en font qu’une.
Séparer la classe des notes
Dans le devoir, une classe Rapport garde des notes, calcule la moyenne et fabrique un texte. Vous la séparez en deux classes.
Notes ajoute des notes et calcule la moyenne entière. Afficheur rend le texte d’un objet Notes. Notes ne doit connaître aucune mise en page.