Design Patterns
TBD refactor old et old corrigé
Les design patterns, patron de conception ou plus prosaïquement "façons de faire", sont pour ainsi dire de l'algorithmie objet : ils permettent de résoudre nombre de problèmes courants en développement et d'éviter les erreurs classiques, aussi appelées anti-pattern. Il existe 3 grands types de design pattern, on en donnera un exemple pour chaque :
- creational qui créent des objets.
- structural qui mettent les objets en relation.
- behavioural qui regroupent les objets ayant même comportement.
Le terme de design pattern a été initialement donné dans le livre Design Patterns: Elements of Reusable Object-Oriented Software pour le langage C++.
Ils permettent de résoudre de façon efficace des problèmes courants en programmation. Il est utile de connaître une liste actuelle de design patterns (certains de la liste originelle, comme singleton ne sont plus utilisés et d'autres, comme MVP sont apparus), car ils donnent souvent de bonnes idées pour résoudre les problèmes que l'on se pose.
À retenir
Les design pattern sont très utilisés par les bibliothèques (par exemple les design pattern de react), les reconnaître vous permettra 'aller vite dans la compréhension de celles-ci.
Le site suivant contient une liste actualisée de design patterns :
Il existe aussi leurs doubles maléfiques, les anti-patterns, qui sont des solutions évidentes -- et mauvaises -- à des problèmes courants. Là aussi, il est bon de connaître une liste actualisée d'anti-pattern, comme par exemple :
Nous allons, encore une fois, utiliser la classe Dé que l'on a utilisé intensivement dans la partie consacrée à la programmation objet et le desin-patterniser selon le problème posé.
On va partir du dé amélioré :
Les bases
On se rappelle comment bien commencer un projet, avec trois fichiers :
- le code
- les tests
- le programme
On commence avec les 3 fichiers du lien.
- copiez le dossier du code sur votre ordinateur
- ouvrez ce dossier dans vscode, cela sera votre projet
- créez vous y un environnement virtuel
- exécutez les tests et le programme principal pour s'assurer que tout fonctionne.
Method chaining
On aimerait bien pouvoir chaîner les lancers de dés pour pouvoir par exemple écrire le test :
# ...
def test_lancer():
dé = Dé()
assert Dé.MIN_VALEUR <= dé.lancer().valeur <= Dé.MAX_VALEUR
# ...
L'idée est que les méthodes modifiant l'objet le rendent ! Ceci ne change pas le code existant on ne fait juste rien du résultat de la méthode mais permet le chaînage
TBD écrire le code TBD écrire le test plus cours en exercice. TBD il faut que tout marche. Si une erreur au milieu foutu TBD ecriture python avec les (). exemple des monolignes avec le replace.
On commence avec les 3 fichiers ci-après. Exécutez le main et les tests. Les 3 tests doivent passer.
Fichier main.py :
from choice import Choice
d6 = Choice(list(range(1, 7)))
for etape in range(100):
print(d6.roll().get_position())
Fichier choices.py :
import random
class Choice:
def __init__(self, choices):
self.choices = choices
self.position = choices[0]
def get_position(self):
return self.position
def roll(self):
self.position = self.choices[random.randrange(len(self.choices))]
return self
Regardez le code de la méthode roll pourquoi rendre self ?
corrigé
corrigé
Pour pouvoir chaîner nos opérations. En rendant self, on peut écrire des choses du genre :
choice = Choices([1, 2, 3, 4])
print(choice.roll().get_position())
Fichier tests.py :
from choice import Choice
def test_init():
d1 = Choice([1])
assert d1.choices == [1]
def test_initial_position():
d1 = Choice([1, 2])
assert d1.get_position() == 1
def test_roll():
d1 = Choice([1])
d1.roll()
assert d1.get_position() == 1
attention aux effets de bord !
TBD mettre une liste de possibilités pour rendre le tout générique puis value object.
Choice prend l'objet choices et le garde. Cela pose plusieurs problèmes. En particulier :
- si on modifie l'objet passé en paramètre, cela change le comportement de choice.
- on expose l'attribut
choicespar un attribut public.
Pour résoudre ces soucis on va copier l'objet choices et rendre l'attribut non modifiable. Pour cela, on va écrire des tests pour mettre en lumière le problème puis corriger le code. Il faudra certainement changer d'autres tests dans le processus de réécriture du code.
On commence par écrire un petit test :
def test_copy_choices():
initial_list = ["a"]
d1 = Choice(initial_list)
initial_list[0] = "CHANGE"
d1.roll()
assert d1.get_position() == "a"
Regardez-le planter. Que s'est-il passé ? Puis proposez une solution.
Enfin, on peut tester la deuxième objection avec le test ci-après. Le but de ce test est de vérifier que l'attribut choices est non mutable. Il cherche à produire une erreur : modifier un élément non mutable.
import pytest
def test_no_modification():
d1 = Choice([1])
with pytest.raises(Exception):
d1.choices[0] = 2
La façon dont pytest gère les erreurs est décrite ici : le test précédent est vrai si le code plante et produit une erreur. essayer de comprendre la structure de ce test.
pattern factory
Le pattern factory en est un exemple, il permet de créer des objets via des méthodes presque sans paramètre et avec un nom adapté plutôt qu'avec un constructeur avec des milliers de paramètres
TBD: faire avec les fonctions et constantes dans le fichier en amélioration, mettre les constantes en attributs de classes (les valeurs des dés) et en méthodes de classes (les factory) Un dé 6, un dé 20 et un dé avec ce qu'on veut.
Premières expérimentations
Commencez par créer dans le fichier main.py un objet simulant 1 dé à six faces (dont les faces valent 1, 2, 3, 4, 5 ou 6). On ne va pas faire de test pour cela, car nos tests sont censés couvrir ce genre d'usage.
En revanche, on peut calculer les probabilités d'apparitions de chaque face :
- lancez N fois le dé (N = 1000)
- comparez les probabilités d'apparition de chaque face par rapport à la théorie (1/6)
Pour aller vite, on pourra utiliser la classe Counter du module collections de python (voir la définition, ou des exemples). Le module collections de python, c'est plein de bonnes choses.
N'hésitez pas à regarder les différents modules de la bibliothèque standard de python avant de recoder la roue...
On prendra soin de créer une constante N, pour éviter l'anti-pattern "magic number"
Refaite la même chose pour simuler la somme d'un lancer de 2 dés à 6 faces (2d6 si on jargonne) avec un seul objet Choice.
On place le tout dans le fichier de la classe
Une fois que vous êtes satisfait de vos fonctions, ajoutez les dans le fichier choices.py, ce qui rajoutera des fonctions de créations d'objets et modifiez votre main.py pour utiliser ces nouvelles fonctions.
Comme maintenant ce sont des fonctions de votre programme et non plus une utilisation de votre code, il faut ajouter des tests pour ces deux fonctions. Faites le.
Pour que vos tests ne soient pas trop fastidieux, vous pouvez vérifier que les possibilités correspondent aux comptes que vous avez effectués avec les objets Counter (du module collections).
Composite
TBD: https://fr.wikipedia.org/wiki/Composite_(patron_de_conception) enseignements/MPCI/programmation-algorithmes/annales/2022-2023/4_test_sujet_composition_agrégation_héritage/
Memento
TBD: https://refactoring.guru/fr/design-patterns/memento premiere partie /enseignements/MPCI/programmation-algorithmes/annales/2021-2022/5_test_sujet/ behavioural pattern un dé et une autre classe, la liste avec un save du test. . avec et sans
Adaptation des objets au pattern
Le pattern memento nécessite de pouvoir changer la valeur de nos objets. On va donc rajouter une méthode set_position à nos objets choice.
On sait maintenant comment faire :
- faites un test vérifiant que
set_positionexiste et fonctionne, - regardez le planter,
- ajouter une méthode
set_positionàChoice, - regardez le test réussir.
Pour l'instant, on fera une méthode set_position la plus simple possible car elle ne sera utilisée que pour le memento. En particulier, on ne vérifiera pas la validité de la valeur remise dans choice, ce n'est pas utile maintenant. Le coder serait du codage préventif et c'est YAGNI.
Attention cependant. Pour respecter le DRY la modification d'un attribut ne doit se faire qu'à un seul endroit : ici la méthode set_position. Il faut donc modifier la méthode roll pour qu'elle l'utilise.
Création d'un memento
Créez la classe Memento dans le fichier memento.py et ses tests dans le fichier :test_memento.py.
La classe Memento doit avoir :
- un objet comme paramètre du constructeur ayant les méthodes
get_positionetset_position. Ici unChoice. - une méthode
restore()qui permet à l'objet sauvé de reprendre la valeur qu'il avait à la création du memento.
Vous pouvez par exemple transformer le code ci-après en test(s) :
import choice
dice = choice.dice()
dice.set_position(2)
memento = Memento(dice)
dice.set_position(6)
memento.restore()
print(dice.get_position()) # doit valoir 2
Undo list
Nous pouvons maintenant créer une classe Undo (dans le fichier undo.py) qui va nous permettre de sauver des dés (et leurs valeurs) et de les restaurer à la demande. Cette classe doit pouvoir :
- sauver un dé avec la méthode :
save(dice)(unMementosera créé dans la méthodesaveexemple comme ça :Memento(dice)) - restaurer la dernière valeur sauvée avec la méthode
restore() - connaître le nombre d'items sauvegardés avec une méthode
nb_undos()
Bien sur vous créerez un fichier de tests test_undo.py qui testera les 3 fonctionnalités ci-dessus. Une façon d'utiliser les différents objets est décrite ci-après :
import choice
from undo import Undo
dice = choice.dice()
undo = Undo()
undo.save(dice)
dice.set_position(5)
print(dice.get_position()) # vaut 5
undo.save(dice)
dice.roll() # dès que l'on change la valeur (ici possiblement différent de 5)
undo.restore()
print(dice.get_position()) # vaut 5
Un undo dans dice
Pour ne pas toujours avoir à sauver le dé avant un roll, on pourra utiliser une classe fille de Choice dont le set_position sauve l'état dans un undo avant de modifier la position. L'objet undo devant être unique dans le programme, il faudrait que le code suivant fonctionne :
from undo import Undo
from choiceUndo import ChoiceUndo
undo = Undo()
d = ChoiceUndo(range(1, 7), undo)
d.set_position(1)
print(d.get_position()) # 1
d.set_position(4)
print(d.get_position()) # 4
undo.restore()
print(d.get_position()) # 1
Respectez le DRY ! Ne recodez que le minimum possible, c'est à dire une classe ChoiceUndo qui hérite de Choice et qui ne diffère de celle-ci que par la méthode set_position (et le constructeur bien sûr).
Faites le même essai avec 10 utilisations de la méthode roll().
Observer
TBD: base de la programmation évènementielle savoir lorsqu'un dé change. S'abonner à la valeur ? seconde partie /enseignements/MPCI/programmation-algorithmes/annales/2021-2022/5_test_sujet/ https://en.wikipedia.org/wiki/Observer_pattern