web-dev-qa-db-fra.com

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

Cette question est quelque peu indépendante du langage, mais pas complètement, car la programmation orientée objet (POO) est différente dans, par exemple, Java , qui n'a pas de fonctions de première classe, que dans - Python .

En d'autres termes, je me sens moins coupable d'avoir créé des classes inutiles dans un langage comme Java, mais j'ai l'impression qu'il pourrait y avoir un meilleur moyen dans les langages moins standardisés comme Python.

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

C'est pourquoi il doit également appeler beaucoup d'autres "sous-opérations" - tout mettre en une seule méthode énorme 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 n'a qu'une seule méthode publique et conserve l'état interne requis pour les sous-opérations dans ses variables d'instance.

Cela ressemblerait à quelque chose comme 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 qu'en interne, et il ne peut rien faire avec ses instances sauf 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 tas de fonctions sans classe et passez les différentes variables nécessaires en interne entre elles (comme arguments).

Le problème que je vois avec cela est que je dois passer beaucoup de variables entre les fonctions. De plus, 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 passer.

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

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

40
iCanLearn
  1. Créez une classe qui n'a qu'une seule méthode publique et conserve l'état interne requis pour les 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 qu'en interne et ne peut rien faire avec ses instances, sauf appeler leur seule méthode publique.

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

Si cela signifie que votre classe n'a qu'une seule méthode publique, qu'il en soit ainsi. Ce sera beaucoup plus facile à entretenir.

Dans OOP 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 tas de fonctions sans classe et passez les différentes variables nécessaires en interne entre elles (comme arguments).

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

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

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

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'elles entraînent. C'est pourquoi vous entendez tout le temps de vieux pets comme moi qui crient sur l'encapsulation.


L'option gagnante est # 1.

46
MetaFight

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()

Quels éléments de données la sous-opération1 utilise-t-elle? Collecte-t-il les données utilisées par la sous-opération2? Lorsque vous transmettez des données en les stockant sur vous-même, je ne peux pas dire comment les éléments de fonctionnalité sont liés. Quand je regarde moi-même, certains des attributs proviennent du constructeur, certains de l'appel à the_public_method, et certains ajoutés au hasard à d'autres endroits. À mon avis, c'est un gâchis.

Et le numéro 2? Eh bien, tout d'abord, regardons le deuxième problème avec:

De plus, 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 cela est que je dois passer beaucoup de variables entre les fonctions.

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

Habituellement, lorsque cette situation se présente, cela signifie que vous n'avez pas trouvé la bonne façon de décomposer votre problème. Vous trouvez 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 petit aperçu de ce que vous traitez ici:

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

Permettez-moi de choisir un exemple de quelque chose qui correspond à votre description, un installateur. Un programme d'installation doit copier un tas de fichiers, mais si vous l'annulez en cours de route, vous devez rembobiner tout le processus, y compris remettre tous les fichiers que vous avez remplacés. L'algorithme pour cela ressemble à ceci:

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)

Multipliez maintenant cette logique pour avoir à faire des paramètres de registre, des icônes de programme, etc., et vous avez un sacré bordel.

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 il masque la façon dont les différentes pièces communiquent.

L'approche n ° 2 ressemble à ceci:

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, c'est explicite comment les morceaux de données se déplacent entre les fonctions, mais c'est très gênant.

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 création de la copie temporaire et garde la trace des fichiers qui ont été copiés. En cas d'exception, il copie les fichiers originaux et en cas de succès, il supprime les copies temporaires.

Cela fonctionne parce que nous avons trouvé une décomposition plus naturelle de la tâche. Auparavant, nous mélangions la logique sur la façon d'installer l'application avec la logique sur la façon de récupérer une installation annulée. Maintenant, le journal des transactions gère tous les détails sur 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 plus simplement et avec élégance.

24
Winston Ewert

É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 est de pousser l'encapsulation un peu plus loin: utilisez une classe, mais aussi fournissez une fonction autonome, qui ne construit que 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 à votre code, et la classe que vous utilisez en interne ne devient vraiment qu'un détail d'implémentation de votre fonction publique. Le code appelant n'a jamais besoin de connaître votre classe interne.

D'une part, la question est en quelque sorte indépendante du langage; mais d'un autre côté, l'implémentation dépend du langage et de ses paradigmes. Dans ce cas, c'est Python, qui prend en charge plusieurs paradigmes.

En plus de vos solutions, il y a aussi la possibilité de faire les opérations complètes sans état de manière plus fonctionnelle, par ex.

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 OOP, cela viole la cohérence si soudainement certaines parties sont écrites dans un style (plus) fonctionnel.

4
Thomas Junk

Pourquoi concevoir quand je peux coder?

J'offre une vue contraire aux 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 la mécanique de codage. Mais c'est un problème de conception.

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

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


La conception orientée objet concerne la complexité

Le point de OO est de construire et de maintenir avec succès de grands systèmes complexes en encapsulant le code en termes du système lui-même. Une conception "correcte" dit que tout est dans une classe.

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

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

4
radarbob

Pourquoi ne pas comparer vos besoins à quelque chose qui existe dans la bibliothèque standard Python, 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 il y a la nouvelle déclaration nonlocal pour vous permettre de changer les variables dans votre fonction parent.

Vous pourriez toujours trouver utile d'avoir quelques classes privées simples à l'intérieur de votre fonction pour implémenter des abstractions et des opérations de rangement.

3
meuh