Devrais-je créer une classe si ma fonction est complexe et comporte beaucoup de variables?

40

Cette question est un peu agnostique au langage, mais pas complètement, car la programmation orientée objet (OOP) est différente, par exemple, en Java , qui n'a pas de fonctions de première classe, que ce n'est le cas en Python .

En d’autres termes, je me sens moins coupable de créer des classes inutiles dans un langage comme Java, mais j’ai l’impression qu’il existe peut-être une meilleure solution dans les langages moins cinglés tels que Python.

Mon programme doit effectuer plusieurs fois une opération relativement complexe. Cette opération nécessite beaucoup de "comptabilité", doit créer et supprimer des fichiers temporaires, etc.

C'est pourquoi il doit également faire appel à beaucoup d'autres "sous-opérations" - mettre tout en une seule méthode n'est pas très agréable, modulaire, lisible, etc.

Maintenant ce sont des approches qui me viennent à l'esprit:

1. Créez une classe qui ne possède qu'une méthode publique et conserve l'état interne nécessaire aux sous-opérations dans ses variables d'instance.

Cela ressemblerait à ceci:

class Thing:

    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2
        self.var3 = []

    def the_public_method(self, param1, param2):
        self.var4 = param1
        self.var5 = param2
        self.var6 = param1 + param2 * self.var1
        self.__suboperation1()
        self.__suboperation2()
        self.__suboperation3()


    def __suboperation1(self):
        # Do something with self.var1, self.var2, self.var6
        # Do something with the result and self.var3
        # self.var7 = something
        # ...
        self.__suboperation4()
        self.__suboperation5()
        # ...

    def suboperation2(self):
        # Uses self.var1 and self.var3

#    ...
#    etc.

Le problème que je vois avec cette approche est que l'état de cette classe n'a de sens que de manière interne et qu'il ne peut rien faire avec ses instances, à l'exception d'appeler leur seule méthode publique.

# Make a thing object
thing = Thing(1,2)

# Call the only method you can call
thing.the_public_method(3,4)

# You don't need thing anymore

2. Créez un ensemble de fonctions sans classe et transmettez entre elles les différentes variables nécessaires en interne (sous forme d'arguments).

Le problème que je vois avec ceci est que je dois passer beaucoup de variables entre les fonctions. En outre, les fonctions seraient étroitement liées les unes aux autres, mais elles ne seraient pas regroupées.

3. Comme 2. mais rendez les variables d'état globales au lieu de les transmettre.

Ce ne serait pas bon du tout, car je dois faire l'opération plus d'une fois, avec des entrées différentes.

Y a-t-il une quatrième meilleure approche? Si non, laquelle de ces approches serait la meilleure et pourquoi? Y a-t-il quelque chose qui me manque?

je peux apprendre
la source
9
"Le problème que je vois avec cette approche est que l'état de cette classe n'a de sens que de manière interne et ne peut rien faire avec ses instances, à moins d'appeler leur seule méthode publique." Vous avez dit que c'était un problème, mais vous ne compreniez pas pourquoi.
Patrick Maupin
@ Patrick Maupin - Vous avez raison. Et je ne sais pas vraiment, c'est le problème. C'est comme si j'utilisais des cours pour une chose pour laquelle autre chose devrait être utilisée, et il y a beaucoup de choses dans Python que je n'ai pas encore explorées, alors j'ai pensé que quelqu'un suggérerait peut-être quelque chose de plus approprié.
iCanLearn
Peut-être s'agit-il de bien comprendre ce que j'essaie de faire. Par exemple, je ne vois rien de mal à utiliser des classes ordinaires à la place d'énums en Java, mais il y a tout de même des choses pour lesquelles les enums sont plus naturels. Donc, cette question est vraiment de savoir s'il existe une approche plus naturelle de ce que j'essaie de faire.
iCanLearn
1
Si vous ne faites que disperser votre code existant dans des méthodes et des variables d'instance et que vous ne changez rien à sa structure, vous ne gagnez rien, mais vous perdez en clarté. (1) est une mauvaise idée.
usr

Réponses:

47
  1. Créez une classe qui n'a qu'une méthode publique et conserve l'état interne nécessaire aux sous-opérations dans ses variables d'instance.

Le problème que je vois avec cette approche est que l'état de cette classe n'a de sens que de manière interne et ne peut rien faire avec ses instances, sauf l'appel de leur seule méthode publique.

L'option 1 est un bon exemple d' encapsulation utilisée correctement. Vous voulez que l'état interne soit caché du code extérieur.

Si cela signifie que votre classe n'a qu'une méthode publique, ainsi soit-il. Ce sera tellement plus facile à maintenir.

En POO, si vous avez une classe qui fait exactement 1 chose, a une petite surface publique et garde tout son état interne caché, alors vous êtes (comme dirait Charlie Sheen) GAGNANT .

  1. Créez un ensemble de fonctions sans classe et transmettez entre elles (sous forme d'arguments) les différentes variables nécessaires en interne.

Le problème que je vois avec ceci est que je dois passer beaucoup de variables entre les fonctions. En outre, les fonctions seraient étroitement liées les unes aux autres, mais ne seraient pas regroupées.

L'option 2 souffre d'une faible cohésion . Cela rendra la maintenance plus difficile.

  1. Comme 2. mais rendez les variables d'état globales au lieu de les transmettre.

L'option 3, comme l'option 2, souffre d'une faible cohésion, mais beaucoup plus sévèrement!

L’histoire a montré que la commodité des variables globales est compensée par le coût brutal de maintenance qu’elle entraîne. C'est pourquoi vous entendez de vieux pets comme moi parler sans cesse de l'encapsulation.


L'option gagnante est n ° 1 .

MetaFight
la source
6
N ° 1 est une API assez moche, cependant. Si je décidais de faire une classe pour cela, je créerais probablement une fonction publique unique qui déléguerait la classe et rendrait la classe entière privée. ThingDoer(var1, var2).do_it()par rapport à do_thing(var1, var2).
user2357112 prend en charge Monica le
7
Bien que l'option 1 soit clairement gagnante, j'irais un peu plus loin. Au lieu d'utiliser l'état interne de l'objet, utilisez des variables locales et des paramètres de méthode. Cela rend la fonction publique réentrante, ce qui est plus sûr.
4
Une chose supplémentaire que vous pouvez faire dans l’extension de l’option 1: protégez la classe résultante (en ajoutant un trait de soulignement devant le nom), puis définissez une fonction au niveau du module def do_thing(var1, var2): return _ThingDoer(var1, var2).run()pour rendre l’API externe un peu plus jolie.
Sjoerd Job Postmus le
4
Je ne suis pas votre raisonnement pour 1. L'état interne est déjà caché. Ajouter une classe ne change rien à cela. Je ne vois donc pas pourquoi vous recommandez (1). En fait, il n'est pas du tout nécessaire d'exposer l'existence de la classe.
usr
3
Toute classe que vous instanciez puis appelez immédiatement une méthode unique et que vous n'utilisez plus jamais constitue une odeur majeure de code, pour moi. C'est isomorphe à une fonction simple, uniquement avec un flux de données masqué (car les parties fragmentées de l'implémentation communiquent en affectant des variables sur l'instance non interceptable au lieu de transmettre des paramètres). Si vos fonctions internes prennent tellement de paramètres que les appels sont complexes et difficiles à manier, le code n'en devient pas moins compliqué lorsque vous masquez ce flux de données!
Ben
23

Je pense que # 1 est en fait une mauvaise option.

Considérons votre fonction:

def the_public_method(self, param1, param2):
    self.var4 = param1
    self.var5 = param2 
    self.var6 = param1 + param2 * self.var1
    self.__suboperation1()
    self.__suboperation2()
    self.__suboperation3()

Quelles sont les données utilisées par suboperation1? Recueille-t-il les données utilisées par suboperation2? Lorsque vous transmettez des données en les stockant sur votre ordinateur, je ne peux pas dire comment les éléments de fonctionnalité sont liés. Lorsque je regarde moi-même, certains attributs proviennent du constructeur, d'autres de l'appel à the_public_method et d'autres, ajoutés de manière aléatoire à d'autres endroits. À mon avis, c'est un gâchis.

Qu'en est-il du numéro 2? Eh bien, premièrement, examinons le deuxième problème avec celui-ci:

En outre, les fonctions seraient étroitement liées les unes aux autres, mais ne seraient pas regroupées.

Ils seraient dans un module ensemble, donc ils seraient totalement regroupés.

Le problème que je vois avec ceci est que je dois passer beaucoup de variables entre les fonctions.

À mon avis, c'est bien. Cela rend explicites les dépendances de données dans votre algorithme. En les stockant, que ce soit dans des variables globales ou sur un individu, vous masquez les dépendances et les rendez moins pires, mais elles sont toujours là.

Habituellement, lorsque cette situation se présente, cela signifie que vous n’avez pas trouvé le bon moyen de décomposer votre problème. Vous trouvez qu'il est difficile de diviser en plusieurs fonctions parce que vous essayez de le diviser dans le mauvais sens.

Bien sûr, sans voir votre fonction réelle, il est difficile de deviner ce qui serait une bonne suggestion. Mais vous donnez un aperçu de ce dont vous avez affaire ici:

Mon programme doit effectuer plusieurs fois une opération relativement complexe. Cette opération nécessite beaucoup de "comptabilité", doit créer et supprimer des fichiers temporaires, etc.

Permettez-moi de choisir un exemple de quelque chose qui correspond en quelque sorte à votre description, un installateur. Un programme d’installation doit copier un grand nombre de fichiers, mais si vous l’annulez à mi-chemin, vous devez revenir en arrière dans l’ensemble du processus, y compris la remise en place des fichiers que vous avez remplacés. L'algorithme pour cela ressemble à quelque chose comme:

def install_program():
    copied_files = []
    try:
        for filename in FILES_TO_COPY:
           temporary_file = create_temporary_file()
           copy(target_filename(filename), temporary_file)
           copied_files = [target_filename(filename), temporary_file)
           copy(source_filename(filename), target_filename(filename))
     except CancelledException:
        for source_file, temp_file in copied_files:
            copy(temp_file, source_file)
     else:
        for source_file, temp_file in copied_files:
            delete(temp_file)

Maintenant, multipliez cette logique pour avoir à définir les paramètres du registre, les icônes de programme, etc., et vous aurez un désordre total.

Je pense que votre solution n ° 1 ressemble à:

class Installer:
    def install(self):
        try:
            self.copy_new_files()
        except CancellationError:
            self.restore_original_files()
        else:
            self.remove_temp_files()

Cela rend l'algorithme global plus clair, mais cache la façon dont les différentes pièces communiquent.

L'approche n ° 2 ressemble à quelque chose comme:

def install_program():
    try:
       temp_files = copy_new_files()
    except CancellationError as error:
       restore_old_files(error.files_that_were_copied)
    else:
       remove_temp_files(temp_files)

Maintenant, il est explicite de savoir comment les éléments de données se déplacent entre les fonctions, mais c’est très délicat.

Alors, comment cette fonction devrait-elle être écrite?

def install_program():
    with FileTransactionLog() as file_transaction_log:
         copy_new_files(file_transaction_log)

L'objet FileTransactionLog est un gestionnaire de contexte. Lorsque copy_new_files copie un fichier, il le fait via le FileTransactionLog qui gère la copie temporaire et garde la trace des fichiers copiés. En cas d'exception, il copie les fichiers d'origine et en cas de succès, il supprime les copies temporaires.

Cela fonctionne car nous avons trouvé une décomposition plus naturelle de la tâche. Auparavant, nous mêlions la logique d'installation de l'application à la logique de récupération d'une installation annulée. Maintenant, le journal des transactions gère tous les détails concernant les fichiers temporaires et la comptabilité, et la fonction peut se concentrer sur l'algorithme de base.

Je soupçonne que votre cas est dans le même bateau. Vous devez extraire les éléments de comptabilité dans une sorte d’objet afin que votre tâche complexe puisse s’exprimer de manière plus simple et élégante.

Winston Ewert
la source
9

Étant donné que le seul inconvénient apparent de la méthode 1 est son modèle d'utilisation sous-optimal, je pense que la meilleure solution consiste à conduire l'encapsulation un peu plus loin: Utilisez une classe, mais fournissez également une fonction autonome, qui construit uniquement l'objet, appelle la méthode et renvoie :

def publicFunction(var1, var2, param1, param2)
    thing = Thing(var1, var2)
    thing.theMethod(param1, param2)

Avec cela, vous avez la plus petite interface possible avec votre code, et la classe que vous utilisez en interne devient vraiment un détail d'implémentation de votre fonction publique. Le code d'appel n'a jamais besoin de connaître votre classe interne.

cmaster
la source
4

D'une part, la question est en quelque sorte agnostique. mais d'autre part, la mise en œuvre dépend du langage et de ses paradigmes. Dans ce cas, il s’agit de Python, qui prend en charge plusieurs paradigmes.

Outre vos solutions, il est également possible d'effectuer des opérations complètes sans état de manière plus fonctionnelle, par exemple:

def outer(param1, param2):
    def inner1(param1, param2, param3):
        pass
    def inner2(param1, param2):
        pass
    return inner2(inner1(param1),param2,param3)

Tout se résume à

  • lisibilité
  • cohérence
  • maintenabilité

Mais -Si votre base de code est en POO, cela enfreint la cohérence si soudainement certaines parties sont écrites dans un style (plus) fonctionnel.

Thomas Junk
la source
4

Pourquoi concevoir quand je peux coder?

J'offre une vision à contre-courant des réponses que j'ai lues. De ce point de vue, toutes les réponses et franchement même la question elle-même se concentrent sur les mécanismes de codage. Mais c'est un problème de conception.

Devrais-je créer une classe si ma fonction est complexe et comporte beaucoup de variables?

Oui, comme cela a du sens pour votre conception. Ce peut être une classe en soi ou une partie d'une autre classe ou son comportement peut être réparti entre les classes.


La conception orientée objet concerne la complexité

L'objectif de OO est de créer et de gérer avec succès des systèmes complexes et volumineux en encapsulant le code en termes de système lui-même. "Propre" conception dit que tout est dans une classe.

OO Design gère naturellement la complexité principalement via des classes ciblées qui respectent le principe de responsabilité unique. Ces classes donnent structure et fonctionnalité tout au long du souffle et de la profondeur du système, interagissent et intègrent ces dimensions.

Cela dit, on dit souvent que les fonctions qui pendent du système - la classe d’utilité générale trop omniprésente - constituent une odeur de code suggérant une conception insuffisante. J'ai tendance à être d'accord.

radarbob
la source
La conception OO gère naturellement la complexité La POO introduit naturellement une complexité inutile.
Miles Rout
"Une complexité inutile" me rappelle des commentaires sans prix de collègues tels que "oh, c'est trop de classes." L’expérience me dit que le jus vaut la peine d’être pressé. Dans le meilleur des cas, je vois des classes pratiquement entières avec des méthodes de 1 à 3 lignes et chaque classe faisant partie de la méthode, la complexité d'une méthode donnée est minimisée: un seul LOC court comparant deux collections et renvoyant les doublons - bien sûr, il y a du code derrière. Ne confondez pas "beaucoup de code" avec un code complexe. En tout cas, rien n'est gratuit.
radarbob
1
Beaucoup de code "simple" qui interagit de manière complexe est bien pire qu'une petite quantité de code complexe.
Miles Rout
1
La petite quantité de code complexe a contenu la complexité . Il y a de la complexité, mais seulement là. Ça ne coule pas. Lorsque vous avez beaucoup d'éléments simples individuellement qui fonctionnent ensemble de manière vraiment complexe et difficile à comprendre, c'est beaucoup plus déroutant parce qu'il n'y a pas de frontières ni de murs à la complexité.
Miles Rout
3

Pourquoi ne pas comparer vos besoins à quelque chose qui existe dans la bibliothèque Python standard, puis voir comment cela est implémenté?

Notez que si vous n'avez pas besoin d'un objet, vous pouvez toujours définir des fonctions dans les fonctions. Avec Python 3, il y a la nouvelle nonlocaldéclaration qui vous permet de changer les variables dans votre fonction parent.

Vous pouvez toujours trouver utile de disposer de simples classes privées dans votre fonction pour implémenter des abstractions et des opérations de rangement.

meuh
la source
Merci. Et savez-vous quelque chose dans la bibliothèque standard qui ressemble à ce que j'ai dans la question?
iCanLearn
Je trouverais difficile de citer quelque chose en utilisant des fonctions imbriquées, en effet je ne trouve nonlocalnulle part dans mes bibliothèques python actuellement installées. Vous pouvez peut-être choisir textwrap.pyune TextWrapperclasse, mais aussi une def wrap(text)fonction qui crée simplement une TextWrapper instance, appelle la .wrap()méthode dessus et retourne. Utilisez donc une classe, mais ajoutez des fonctions pratiques.
meuh