Injection de dépendance

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je travaille actuellement sur un projet en Python 3.5 et je m’interroge sur la manière d’organiser mon code.

Ma bibliothèque permet de gérer des projets, où un projet est simplement un ensemble de documents à éditer (des fichier odf par exemple) par le responsable de projet et permettant de générer des documents de rendu (des fichiers pdf, par exemple). Il est alors possible de :

  • Télécharger des modèles de documents, à compléter ;
  • Lire les documents (par exemple, avec LibreOffice) ;
  • Mettre des documents (de sortie) en ligne (sur Google Drive par exemple) pour les sauvegarder et/ou les partager.

J’ai donc créé trois classes abstraites ainsi que des filles plus fonctionnelles :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class DocumentReader: pass
class DocumentDownloader: pass
class DocumentUploader: pass

# Mes documents sont au format ODF
class LibreOfficeDocumentReader(DocumentReader): pass
# Mes modèles sont sur un Drive
class DriveDocumentDownloader(DocumentDownloader): pass
# Je partage mes documents sur un Drive
class DriveDocumentUploader(DocumentUploader): pass

Un projet (classe Project) est en particulier un DocumentReader et possiblement un DocumentDownloader et/ou DocumentUploadder. Et là, j’ignore quelle API fournir, vu que je crois avoir plusieurs possibilités :

Héritage multiple

1
class MyProject(LibreOfficeDocumentReader, DriveDocumentDownloader, DriveDocumentUploader): pass

Seulement, j’obtiens un conflit d’attributs. Par exemple, DriveDocumentUploader et DriveDocumentDownloader ont toutes les deux un attribut drive permettant de faire des requêtes à l’API GDrive sous un compte particulier. Mais la valeur diffère, parce que je ne télécharge pas les modèles à partir du même compte que celui sur lequel je sauvegarde les documents.

Bref, cette méthode d’héritage multiple me semble compromise.

Injection de dépendance

Une autre méthode consisterait à passer en paramètre de mon projet mes reader, downloader et uploader :

1
2
class Project:
    def __init__(self, name, reader, downloader=None, uploader=None): pass

Seulement, cela signifie que je ne peux pas faire my_project.my_reader_method() (mais my_project.reader.my_reader_method()), à moins d’ajouter dynamiquement à my_project les méthodes du reader. Là encore, je risque de tomber sur des conflits au niveau des noms des méthodes du reader, downloader et uploader (en l’occurrence, il n’y en a pas, mais c’est plausible).

Conclusion

Après rélfexion, la seconde méthode dans sa première variante (my_project.reader.my_reader_method()) me semble préférable, mais j’ignore si elle est pythonique, ou même juste judicieuse. Auriez-vous des conseils à ce sujet ?

Merci !

+0 -0

Je serais toi je laisserais tomber complètement l’idée de gérer ca par héritage et je partirais sur l’idée simple de ta conclusion.

Le meilleur moyen de faire un code non-pythonique c’est de le démarrer en faisant de la conception objet pure et dure. Ici en particulier je ne vois pas l’intérêt de commencer par ce style de modélisation abstraite. Code d’abord tes fonctionnalités, tu pourras toujours refactoriser quand l’articulation du programme sera plus claire.

+3 -0

J’ignore si ça s’applique aussi bien à python car je ne suis de loin pas aussi expérimenté qu’en Java ou C++, mais une des règles de bonnes pratiques OO dit qu’il vaut mieux privilégier la composition à l’héritage (Prefer composition over inheritance).

En fait il n’y a pas besoin d’aller très loin pour s’apercevoir que la version héritage est mauvaise. En tout cas de mon point de vue, je suppose que ta classe Project va décrire un processus: télécharger le document, le lire, le traiter, puis l’uploader par exemple. Si on admet que tu as:

  • deux DocumentReader: un pour open office et l’autre pour MS Office
  • Trois downloaders: GDrive, Dropbox et OneDrive
  • Trois uploaders: idem

Au final tu vas potentiellement créer 2*3*3=18 classes qui décriront à peu de chose près exactement le même processus de base: télécharger, ouvrir, traiter, uploader.

De l’autre côté avec la version composition, tu ne créeras q’une seule classe Project qui décrira ce processus en particulier. C’est à la construction, ou via de la configuration si tu décides d’aller plus loin, que tu lui passeras les bonnes briques pour qu’il puisse effectuer son processus.

Maintenant, entre la proposition 2.1 et la 2.2, je pense que si depuis la classe Project tu dois accéder à une méthode spécifique d’une sous-classe (p.ex. une méthode de OpenOfficeDocumentReader), alors c’est que ta conception est mauvaise. JE sais qu’en python ça n’existe pas vraiment, mais en Java ou C++, DocumentReader, DocumentDownloader et DocumentUploader seraient sans l’ombre d’un doute des interfaces. Si tu veux vraiment tirer parti de l’injection de dépendance, alors il faut que la classe Project se contrefiche de savoir si elle a reçu un GDriveDocumentUploader ou un DropboxDocumentUploader, du moment que c’est bien un DocumentUploader. A toi de bien abstraire ce qui est commun pour que les détails de chaque implémentation spécifique ne transparaissent pas dans la classe Project.

Je conclus avec cette autre règle de conception OO qui est aussi souvent évoquée, quoi que plus critiquée aussi: écrire project.sous_objet.methode() suppose une connaissance des propriétés internes de la classe Project; connaissance nécessaire qu’on devrait essayer de limiter au maximum, car si un jour tu décides de changer la composition interne de la classe Project, alors tu devras modifier le code utilisateur de cette classe. Ce que tu évites en proposant une méthode supplémentaire dans la classe Project. C’est plein d’appels à modifier un peu partout contre une seule méthode à modifier une fois pour toute. Ce n’est pas grave si la méthode fait 2 lignes.

+0 -0

Salut,

Je donne mon humble avis. A moins que je n’aie pas très bien compris ce que représente la classe Projet, tu dis:

un projet est simplement un ensemble de documents à éditer[…]

Donc à mon sens, la classe projet n’est pas un DocumentReader ou quoique ce soit d’autre. Un Projet est un conteneur de Documents. Il a aussi des méthodes tels que:

  • read(filename)
  • download(url)
  • upload(url)

Les arguments ne sont que des exemples, bien entendu. Tout comme dans ton deuxième exemple, tu injectes les dépendance à la construction. Tu pourrais même les définir plus tard et les changer dynamiquement en ré-affectant l’objet attaché à l’attribut reader, downloader et uploader. La méthode read ne ferait qu’utiliser l’objet reader qu’elle a reçu.

1
2
3
4
5
class Project:
    def read(self, filename):
        document = self.reader.read(filename)
        self.documents.append(document)
        return document

Bien entendu, ceci présuppose que tous les DocumentReader ont une API commune, avec par exemple la méthode read(filename).

Merci à tous pour vos explications, c’est plus clair pour moi.

Ici en particulier je ne vois pas l’intérêt de commencer par ce style de modélisation abstraite. Code d’abord tes fonctionnalités, tu pourras toujours refactoriser quand l’articulation du programme sera plus claire.

En fait, j’avais déjà commencé de cette manière. A l’origine, mon programme était un programme PyQt et la manipulation des projets ne passait pas par une classe, mais se faisait directement dans les widgets Qt. Quand j’ai voulu passer à une UI avec Flask, je me suis rendu compte que mon code était totalement dépendant de l’interface, rendant laborieux la conversion. C’est pourquoi je suis parti sur cette architecture en classes et en suis venu à me poser la question de l’architecture du programme.

Maintenant, entre la proposition 2.1 et la 2.2, je pense que si depuis la classe Project tu dois accéder à une méthode spécifique d’une sous-classe (p.ex. une méthode de OpenOfficeDocumentReader), alors c’est que ta conception est mauvaise.

Nop, je n’ai pas à accéder à une méthode spécifique, seulement à une définie dans la classe abstraite parente (DocumentReader) par exemple. :)

Bien entendu, ceci présuppose que tous les DocumentReader ont une API commune, avec par exemple la méthode read(filename).

Avoir une API commune, c’est la raison pour laquelle j’ai justement créé ces classes abstraites. ^^

+0 -0

En fait, j’avais déjà commencé de cette manière. A l’origine, mon programme était un programme PyQt et la manipulation des projets ne passait pas par une classe, mais se faisait directement dans les widgets Qt. Quand j’ai voulu passer à une UI avec Flask, je me suis rendu compte que mon code était totalement dépendant de l’interface, rendant laborieux la conversion. C’est pourquoi je suis parti sur cette architecture en classes et en suis venu à me poser la question de l’architecture du programme.

Pour éviter ce phénomène, réalise ta première implémentation en ligne de commande. Ça aide énormément à découpler.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte