Comment refactoriser une «classe de dieu» Python?

10

Problème

Je travaille sur un projet Python dont la classe principale est un peu " God Object ". Il y a tellement d'attributs et de méthodes!

Je veux refactoriser la classe.

Jusque là…

Pour la première étape, je veux faire quelque chose de relativement simple; mais quand j'ai essayé l'approche la plus simple, elle a cassé certains tests et exemples existants.

Fondamentalement, la classe a une longue liste d'attributs - mais je peux clairement les regarder et penser, "Ces 5 attributs sont liés ... Ces 8 sont également liés ... et puis il y a le reste."

getattr

Je voulais simplement regrouper les attributs associés dans une classe d'aide de type dict. J'avais le sentiment que ce __getattr__serait idéal pour le travail. J'ai donc déplacé les attributs dans une classe distincte et, bien sûr, j'ai __getattr__parfaitement bien fonctionné sa magie…

Au début .

Mais j'ai ensuite essayé d'exécuter l'un des exemples. L'exemple de sous-classe tente de définir directement l'un de ces attributs (au niveau de la classe ). Mais comme l'attribut n'était plus «physiquement localisé» dans la classe parente, j'ai eu une erreur en disant que l'attribut n'existait pas.

@propriété

J'ai ensuite lu sur le @propertydécorateur. Mais j'ai également lu que cela crée des problèmes pour les sous-classes qui veulent faire self.x = blahquand xest une propriété de la classe parente.

Voulu

  • Demandez à tout le code client de continuer à fonctionner self.whatever, même si la whateverpropriété du parent n'est pas «physiquement située» dans la classe (ou l'instance) elle-même.
  • Regroupez les attributs associés dans des conteneurs de type dict.
  • Réduisez l'extrême bruit du code dans la classe principale.

Par exemple, je ne veux pas simplement changer cela:

larry = 2
curly = 'abcd'
moe   = self.doh()

En cela:

larry = something_else('larry')
curly = something_else('curly')
moe   = yet_another_thing.moe()

… Parce que c'est encore bruyant. Bien que cela transforme avec succès un simple attribut en quelque chose qui peut gérer les données, l'original avait 3 variables et la version modifiée a toujours 3 variables.

Cependant, je serais bien avec quelque chose comme ça:

stooges = Stooges()

Et si une recherche self.larryéchoue, quelque chose vérifierait stoogeset verrait s'il larryexiste. (Mais cela doit aussi fonctionner si une sous-classe essaie de faire larry = 'blah'au niveau de la classe.)

Sommaire

  • Vous souhaitez remplacer des groupes d'attributs associés dans une classe parente par un seul attribut qui stocke toutes les données ailleurs
  • Vous voulez travailler avec du code client existant qui utilise (par exemple) larry = 'blah'au niveau de la classe
  • Vous souhaitez continuer à autoriser les sous-classes à étendre, remplacer et modifier ces attributs refactorisés sans savoir que quelque chose a changé


Est-ce possible? Ou suis-je en train d'aboyer le mauvais arbre?

Zéarine
la source
6
Vous manquez la moitié des avantages de si vous insistez pour conserver cette énorme interface semblable à un dieu, même si vous séparez des parties de la mise en œuvre. Vous pouvez fournir des raccourcis, mais le simple fait de placer les variables dans différents espaces de noms et de les rediriger entièrement vers celles-ci vous donne très peu, voire rien.
1
@delnan: D'accord, alors que recommanderiez-vous à la place?
Zearin

Réponses:

9

Après avoir écrit puis refactorisé un "objet divin" en python, je sympathise. Ce que j'ai fait est de diviser l'objet d'origine en sous-sections basées sur des méthodes. Par exemple, l'original ressemblait à ce pseudo-code:

method A():
    self.bla += 1

method B():
    self.bla += 1

do stuff():
    self.bla = 1
    method A()
    method B()
    print self.bla

La méthode des trucs est une «unité» de travail autonome. Je l'ai migré vers une nouvelle classe que l'original instancie. Cela a également retiré les propriétés nécessaires. Certains n'étaient utilisés que par la sous-classe et pouvaient se déplacer directement. D'autres ont été partagés et ont été transférés dans une classe partagée.

L '"objet Dieu" crée une nouvelle copie de la classe partagée au démarrage et chacune des nouvelles sous-classes accepte un pointeur dans le cadre de leur méthode init. Par exemple, voici une version dépouillée de l'expéditeur:

#!/usr/bin/env python
# -*- coding: ascii -*-
'''Functions for emailing with dirMon.'''

from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Utils import COMMASPACE, formatdate
from email import Encoders
import os
import smtplib
import datetime
import logging

class mailer:
    def __init__(self,SERVER="mail.server.com",FROM="[email protected]"):
        self.server = SERVER
        self.send_from = FROM
        self.logger = logging.getLogger('dirMon.mailer')

    def send_mail(self, send_to, subject, text, files=[]):
        assert type(send_to)==list
        assert type(files)==list
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug(' '.join(("Sending email to:",' '.join(send_to))))
            self.logger.debug(' '.join(("Subject:",subject)))
            self.logger.debug(' '.join(("Text:",text)))
            self.logger.debug(' '.join(("Files:",' '.join(files))))
        msg = MIMEMultipart()
        msg['From'] = self.send_from
        msg['To'] = COMMASPACE.join(send_to)
        msg['Date'] = formatdate(localtime=True)
        msg['Subject'] = subject
        msg.attach( MIMEText(text) )
        for f in files:
            part = MIMEBase('application', "octet-stream")
            part.set_payload( open(f,"rb").read() )
            Encoders.encode_base64(part)
            part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(f))
            msg.attach(part)
        smtp = smtplib.SMTP(self.server)
        mydict = smtp.sendmail(self.send_from, send_to, msg.as_string())
        if self.logger.isEnabledFor(logging.DEBUG):
            self.logger.debug("Email Successfully Sent!")
        smtp.close()
        return mydict

Il est créé une fois et partagé entre les différentes classes qui ont besoin de capacités de diffusion.

Donc pour vous, créez une classe larryavec les propriétés et les méthodes dont vous avez besoin. Partout où le client dit de le larry = blahremplacer par larryObj.larry = blah. Cela migre les choses vers des sous-projets sans casser l'interface actuelle.

La seule autre chose à faire est de rechercher des "unités de travail". Si vous deviez transformer une partie de "l'Objet divin" en sa propre méthode, faites-le . Mais, mettez la méthode en dehors . Cela vous oblige à créer une interface entre les composants.

Poser ces bases permet à tout le reste de les suivre. Par exemple, un morceau de l'objet d'assistance montrant comment il s'interface avec le mailer:

#!/usr/bin/env python
'''This module holds a class to spawn various subprocesses'''
import logging, os, subprocess, time, dateAdditionLib, datetime, re

class spawner:
    def __init__(self, mailer):
        self.logger = logging.getLogger('dirMon.spawner')
        self.myMailer = mailer

Concentrez-vous sur la plus petite unité de travail individuelle possible et déplacez-la. Ceci est plus facile à faire et vous permet de jouer rapidement avec la configuration. Ne regardez pas les propriétés pour déplacer des objets, elles sont accessoires aux tâches qui sont effectuées avec eux dans la plupart des cas. Tout ce qui reste après avoir traité les méthodes devrait probablement rester dans l'objet d'origine, car il fait partie de l'état partagé.

Mais , les nouveaux objets doivent maintenant accepter les propriétés dont ils ont besoin en tant que variables init, sans toucher à la propriété des objets appelants. Ils renvoient ensuite toutes les valeurs nécessaires, que l'appelant peut utiliser pour mettre à jour les propriétés partagées si nécessaire. Cela permet de découpler les objets et rend le système plus robuste.

Spencer Rathbun
la source
1
Fantastique réponse, Spencer. Je vous remercie! J'ai quelques questions complémentaires qui sont de nature trop spécifique pour être appropriées ici. Puis-je vous contacter en privé pour en discuter?
Zearin
@Zearin bien sûr, mon profil a mon adresse e-mail. C'était pour un projet d'entreprise cependant, et je ne peux pas vous donner une copie complète du référentiel à cause des trucs propriétaires là-dedans. Étant donné un laps de temps suffisant, je pourrais nettoyer avant / après les instantanés, mais je ne sais pas dans quelle mesure cela pourrait vous aider.
Spencer Rathbun
Je ne vois aucune adresse e-mail sur votre profil. Il y a toutes sortes d'informations, mais pas d'informations de contact. ☺ Comment dois-je vous contacter?
Zearin
Je l'ai. Cybermen: «Supprimer! Supprimer! Supprimer!"
Zearin