Site original : Sam & Max: Python, Django, Git et du cul
En informatique, le vocabulaire c’est une bonne partie du travail. On a des tas de termes comme polymorphisme, récursivité, idempotent ou closure. Certains sont des termes mathématiques, d’autres sont des anglicismes, mais la majorité sont juste des mots compliqués pour décrire des choses simples.
Vous connaissez mon manque d’attrait pour ça, on va donc clarifier.
Pour comprendre cet article, il vous faut les pré-requis suivants :
Et pour une fois, on va mettre un morceau qui colle bien au blog :
Un proxy, en anglais, c’est un mandataire, c’est à dire grosso merdo un intermédiaire. Un objet proxy est donc tout simplement un objet qui fait l’intermédiaire, généralement entre un objet et un autre.
Exemple, on a des enseignants qui vont avoir besoin d’une autorisation de sortie pour des enfants dans leur classe. Un enfant ne peut pas décider de cela, ses parents décident de ce genre de chose :
class Enseignant(object): # la demande d'autorisation attend un objet enfant en paramètre def demande_autorisation_de_sortie(self, enfant): return enfant.peut_sortir() class Enfant(object): def __init__(self, age): self.age = age # mais un enfant ne peut pas donner cette information def peut_sortir(self): raise NotImplementedError('Un enfant ne peut decider de cela') class Parent(object): # le parent va faire proxy sur l'objet enfant def __init__(self, enfant): self.enfant = enfant # et propose la même méthode que l'objet enfant afin de pouvoir # être passé en paramètre à sa place à la méthode # demande_autorisation_de_sortie() def peut_sortir(self): return self.enfant.age > 10
Ici, si la classe Enseignant
appelle directement la méthode peut_sortir()
de la classe Enfant
, on obtient une erreur. :
>>> prof = Enseignant() >>> enfant = Enfant(11) >>> prof.demande_autorisation_de_sortie(enfant) Traceback (most recent call last): File "<ipython-input-23-6d509f016e40>", line 1, in <module> prof.demande_autorisation_de_sortie(enfant) File "<ipython-input-18-e8d34178df4c>", line 4, in demande_autorisation_de_sortie return enfant.peut_sortir() File "<ipython-input-18-e8d34178df4c>", line 13, in peut_sortir raise NotImplementedError('Un enfant ne peut decider de cela') NotImplementedError: Un enfant ne peut decider de cela
Nous avons créé un système où on est obligé de créer une instance de Parent
, qui va se placer entre la classe Enseignant
et la classe Enfant
, et qui va retourner la valeur pour cette méthode :
>>> parent = Parent(enfant) # on passe l'enfant en paramètre au parent >>> prof.demande_autorisation_de_sortie(parent) # et le parent remplace l'enfant True
Ceci est un objet proxy, c’est à dire un objet qui se place entre deux objets, et fait l’intermédiaire. Pour que cela marche, il faut que l’objet proxy, ici l’instance de Parent
, ait la même interface (les mêmes méthodes, avec les mêmes paramètres et noms) que l’objet derrière le proxy, ici l’instance de Enfant
.
Dans notre cas, c’est académique, ça ne sert pas à grand chose. Mais il y a des tas de cas utiles pour les objets proxy.
Ainsi, on veut parfois créer des objets dit “lazy”, littéralement “paresseux”, c’est à dire pas évalués tout de suite. C’est ce que fait Django dans le cadre de certaines chaînes de caractères traduites. En effet, en Django, il existe une fonction lazy_gettex()
que l’on peut appliquer sur les chaînes à traduire. Mais elle ne retourne pas une string, car on ne connait pas encore la langue dans laquelle il faut traduire la chaîne : la langue dépend de l’utilisateur qui viendra demander la page Web en question. lazy_gettex()
retourne un objet proxy, et quand, à l’appel de la page Web, on tente de manipuler cet objet comme une string pour l’afficher, le proxy va chercher la traduction, et retourne la bonne chaîne. Pour que ça marche, il faut que l’interface de l’objet lazy ressemble trait pour trait à celui d’une chaîne.
Dans d’autres cas on veut mettre un proxy pour gérer des permissions, et selon le niveau de permission, l’objet proxy donne accès ou non à l’objet derrière lui.
Le design pattern décorateur, le fameux @truc
en Python, n’est jamais qu’un objet fonction qui fait proxy vers une autre fonction qu’on a enrobé.
Mais la raison majeur pour laquelle on va utiliser un objet proxy est le design pattern “adapter”.
En informatique, on aime bien les traitements généralistes.
Par exemple, imaginez cet objet qui dit si on a le droit de se bourrer la gueule :
class Videur(object): def tu_peux_rentrer(self, personne): """ Cette fonction n'est pas réaliste, puisque bien entendu une mineure à gros nichons rentrerait, et un arabe en short lacoste non, mais heureusement l'informatique est plus simple que notre société. """ return personne.age >= 18 class Personne(object): def __init__(self, age): self.age = age
Et tant que vous avez la main sur le système, tout va bien dans le meilleur des mondes :
>>> import random >>> clients = [Personne(random.randint(12, 65)) for x in xrange(random.randint(0, 100))] >>> videur = Videur() >>> for client in clients: ... if videur.tu_peux_rentrer(client): ... print("Glouglou") ... Glouglou Glouglou Glouglou Glouglou Glouglou ...
Mais malheureusement la vie n’est pas toujours pleine de licornes et de bisounours, et votre collègue, lui, il a codé une lib en C qui a extrait les clients d’un XML, et quand vous vous mappez directement dessus avec ctype, vous obtenez des clients comme ça:
class Client(object): def __init__(self, majeur): self.majeur = majeur
Bon, vous vous dites que c’est pas grave. Après tout, c’est juste une condition à rajouter :
class Videur(object): def tu_peux_rentrer(self, personne): if hasattr(personne, 'age'): return personne.age >= 18 return personne.majeur
Ouai mais votre commercial arrive avec un super contrat avec pillierdebar.com, et il faut s’interfacer avec leur API SOAP de 1997, dont la deserialisation vous retourne :
class Prospect(object): def __init__(self, datetime): self.date_de_naissance = datetime
Les connards. Ah les connards.
Mais bon, c’est pas grave, c’est juste une petite condition à rajouter.
import datetime class Videur(object): def tu_peux_rentrer(self, personne): if hasattr(personne, 'age'): return personne.age >= 18 if hasattr(personne, 'date_de_naissance'): # TODO: prendre en compte les années bisextiles return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18 return personne.majeur
Bon, vous allez enfin pouvoir soufll…. Ahhhh ! Votre boss appelle, il a oublié de vous dire, le stagiaire travaille aussi sur une feature, et a fait son propre objet Prospect
, c’est comme l’objet Client
mais pas pareil. Non on ne peut pas changer. Oui il a fait de la merde mais si on change tout son code pête. Il se barre demain. Il n’a pas fait de test. Démerde toi.
class LeClient(object): def __init__(self, majeur_de_combien): self.__majeur_de_combien = majeur_de_combien def get_majeur_de_combien(self): return self.__majeur_de_combien
A ce stade là, le code est tellement moche de toute façon, vous vous en branlez complètement. Vous allez rentrer jouer Left4Dead avec un mod qui transforme les zombies pour qu’ils aient la tête de votre patron. Le reste n’est que détail d’implémentation.
class Videur(object): def tu_peux_rentrer(self, personne): if hasattr(personne, 'age'): return personne.age >= 18 if hasattr(personne, 'date_de_naissance'): # TODO: prendre en compte les années bisextiles return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18 if hasattr(personne, 'get_majeur_de_combien'): # retourne le nombre d'années après 18 ans ou -1. Don't ask... return personne.get_majeur_de_combien() > -1 return personne.majeur
Quelque moi(s) plus tard, les specs changent. Il faut maintenant pouvoir vérifier la carte d’identité des clients. Ou la carte de membre. Et garder l’ancien système compatible bien entendu. Ah, et il y a aussi des controleurs, c’est comme des videurs, mais qui doivent vérifier si les personnes dans le bar sont majeurs, sauf qu’il n’acceptent pas la carte de membre ou la date de naissance donnée à l’oral.
class CarteDeMembre(object): pass class CarteIdentité(object): def __init__(self, date_de_naissance): self.date_de_naissance = date_de_naissance class Client(object): def __init__(self, majeur=None, carte_identite=None, carte_membre=None): self.majeur = majeur self.carte_identite = carte_identite self.carte_membre = None class Videur(object): def tu_peux_rentrer(self, personne): if hasattr(personne, 'age') and personne.age is not None: return personne.age >= 18 if hasattr(personne, 'carte_membre'): return True if hasattr(personne, 'carte_identite') and personne.carte_identite is not None: now = datetime.datetime.now() return (now - personne.carte_identite.date_de_naissance) / 365 > 18 if hasattr(personne, 'date_de_naissance'): # TODO: prendre en compte les années bisextiles return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18 if hasattr(personne, 'get_majeur_de_combien'): # retourne le nombre d'années après 18 ans ou -1. Don't ask... return personne.get_majeur_de_combien() > -1 return personne.majeur class Controleur(object): def est_autorise(self, personne): if hasattr(personne, 'age') and personne.age is not None: return personne.age >= 18 if hasattr(personne, 'carte_identite') and personne.carte_identite is not None: now = datetime.datetime.now() return (now - personne.carte_identite.date_de_naissance) / 365 > 18 if hasattr(personne, 'date_de_naissance'): # TODO: prendre en compte les années bisextiles return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18 if hasattr(personne, 'get_majeur_de_combien'): # retourne le nombre d'années après 18 ans ou -1. Don't ask... return personne.get_majeur_de_combien() > -1 return personne.majeur
Bon, là c’est la merde. Vous le sentez, demain il va y avoir un portail électronique qui check uniquement les cartes de membres. Quand il va falloir faire une modif quelque part, ça va être galère. La maintenance va être un enfer. Le debuggage, bien relou. Il faut trouver un moyen de faire plus propre.
C’est ici qu’entre en jeu le pattern “adapter”. Un adapter, c’est comme les adaptateurs pour prises, mais pour les objets. C’est un proxy qu’on met devant pour que la prise ressemble à une autre.
Dans le domaine du code, ça veut dire qu’on va enrober chaque objet dans un autre pour qu’ils aient tous la même interface. Le traitement du coup sera de nouveau très simple, et la complexité sera répartie dans les adapters.
# on met le code commun dans un parent abstrait, notamment le check pour # savoir si un adapter convient a un objet donné class Adapteur(object): cls = None def __init__(self, objet): self.objet = objet def peut_adapter(self, objet): return self.cls == type(objet) # un adapter, c'est juste un objet proxy qui arrondit les angles class PersonneAdapteur(Adapteur): cls = Personne # on va réduire la complexité en faisant en sorte que tous les checks # soient cachés derrière une simple property @property def majeur(self): return self.objet >= 18 class ProspectAdapteur(Adapteur): cls = Prospect @property def majeur(self): # TODO: prendre en compte les années bisextiles return (datetime.datetime.now() - self.objet.date_de_naissance) / 365 > 18 class LeClientAdapteur(Adapteur): cls = LeClient @property def majeur(self): # retourne le nombre d'années après 18 ans ou -1. Don't ask... return self.objet.get_majeur_de_combien() > -1 class ClientAdapteur(Adapteur): cls = Client # les adapteurs peuvent être configurables def __init__(self, objet, accepte_carte_membre=True): self.objet = objet self.accepte_carte_membre = accepte_carte_membre @property def majeur(self): if self.accepte_carte_membre and self.objet.carte_membre is not None: return True if self.objet.majeur: return True if self.object.carte_identite is not None: now = datetime.datetime.now() # TODO: prendre en compte les années bisextiles return (now - personne.carte_identite.date_de_naissance) / 365 > 18 return False
Le controleur et le videur deviennent du coup beaucoup plus simples, maintenant que la logique est normalisée :
class VerificateurDeMajorite(object): def check_majorite(self, personne): for adapteur in self.adapteurs: if adapteur.peut_adapter(personne): return adapteur(personne).majeur # du coup un vérificateur, c'est très déclaratif class Videur(VerificateurDeMajorite): adapteurs = ( PersonneAdapteur(), ClientAdapteur(), ProspectAdapteur(), LeClientAdapteur(), ) def tu_peux_rentrer(self, personne): return self.check_majorite(personne) class Controleur(VerificateurDeMajorite): adapteurs = ( PersonneAdapteur(), ClientAdapteur(accepte_carte_membre=False), # ProspectAdapteur() LeClientAdapteur(), ) def est_autorise(self, personne): return self.check_majorite(personne)
Si il faut ajouter un autre format de client, il suffit de coder un adapteur. Si il faut ajouter une autre classe de vérification, il suffit de lui donner les bons adapteurs.
Et le traitement est aussi simple, car si vous avez une liste qui contient plein de clients différents :
>>> clients = [Client(majeur=True), Client(carte_de_membre=CarteDeMembre()), Client(carte_identite=CarteIdentité(datetime.datetime.now()), LeClient(-1), Personne(age=16)...]
Vous pouvez quand même vérifier tout ça sans y réfléchir:
>>> videur = Videur() >>> for client in clients: ... if videur.tu_peux_rentrer(client): ... print("Glouglou") ... Glouglou Glouglou ... >>> controlleur = Controleur() >>> for client in clients: ... if not controlleur.est_autorise(client): ... print("Ahhhh !") ... Ahhhh ! Ahhhh ! ...
Dans un cas comme celui de notre exemple, l’adapter est un peu overkill, on s’en tirerait aussi bien avec une liste de fonctions, et le code serait plus léger et lisible.
Mais quand les objets et la logique deviennent très complexes, tout ramener à une interface commune permet de simplifier énormément le code.