Plus facile à lire avec un fond crème, une teinte, plus de contraste ou un texte plus grand ? Choisissez vos couleurs et votre texte, une fois pour toutes les pages.

Leçon gratuite

Principes de conception orientée objet

Après cette leçon: Je peux repérer une classe qui a deux métiers ou qui est difficile à étendre, et dire comment la corriger.

Leçon de 60 min6 commandesLab Linux du quotidien : la trentaine de commandes que les ingénieurs tapent de tête

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

MotCe que ça veut dire
Principe de conceptionune règle qui garde un programme facile à changer
Responsabilitéun métier précis qu’une classe assure
Cohésionle fait que tout ce que contient une classe va ensemble
Ouvert à l’extensionon ajoute un cas en écrivant du code neuf
Fermé à la modificationon n’a pas à rouvrir le code qui marche
Refactorisationréé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

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

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

Commande
mkdir -p essais

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 classe à deux métiers

Commande
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.py

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.

Imprimer la facture

Commande
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

Commande
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.py

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.

Imprimer avec la version séparée

Commande
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é

Commande
grep -c "^class " essais/facture2.py

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.

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

Code: python
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.

À propos de ce labo et de sa source: Lab Linux du quotidien : la trentaine de commandes que les ingénieurs tapent de tête

Vous enseignez à une classe ? Proposez ce module à vos étudiants avec un guide de l'enseignant, des devoirs corrigés et un laboratoire pour chaque étudiant.