Come riformattare una "classe divina" di Python?


10

Problema

Sto lavorando a un progetto Python la cui classe principale è un po '" God Object ". Ci sono così tanti e svariati attributi e metodi!

Voglio riformattare la classe.

Finora…

Per il primo passo, voglio fare qualcosa di relativamente semplice; ma quando ho provato l'approccio più semplice, ho rotto alcuni test ed esempi esistenti.

Fondamentalmente, la classe ha un elenco di attributi minuzioso, ma posso chiaramente guardarli e pensare: "Questi 5 attributi sono correlati ... Questi 8 sono anche correlati ... e poi c'è il resto."

getattr

In pratica, volevo solo raggruppare gli attributi correlati in una classe di supporto simile a una dettatura. Ho avuto la sensazione che __getattr__sarebbe l'ideale per il lavoro. Quindi ho spostato gli attributi in una classe separata e, abbastanza sicuro, ha __getattr__funzionato perfettamente la sua magia ...

Alla prima .

Ma poi ho provato a eseguire uno degli esempi. La sottoclasse di esempio tenta di impostare direttamente uno di questi attributi (a livello di classe ). Ma poiché l'attributo non era più "localizzato fisicamente" nella classe genitore, ho ricevuto un errore che diceva che l'attributo non esisteva.

@proprietà

Poi ho letto del @propertydecoratore. Ma poi ho anche letto che crea problemi per le sottoclassi che vogliono fare self.x = blahquando xè una proprietà della classe genitore.

desiderata

  • Fai in modo che tutto il codice client continui a funzionare self.whatever, anche se la whateverproprietà del genitore non si trova "fisicamente" nella classe (o istanza) stessa.
  • Raggruppare gli attributi correlati in contenitori simili a dict.
  • Ridurre l'estrema rumorosità del codice nella classe principale.

Ad esempio, non voglio semplicemente cambiare questo:

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

In questo:

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

... Perché è ancora rumoroso. Sebbene ciò trasformi con successo un attributo semplice in qualcosa che può gestire i dati, l'originale aveva 3 variabili e la versione ottimizzata ha ancora 3 variabili.

Tuttavia, starei bene con qualcosa del genere:

stooges = Stooges()

E se una ricerca self.larrynon riesce, qualcosa verificherebbe stoogese vedrebbe se larryc'è. (Ma deve anche funzionare se una sottoclasse cerca di farlo larry = 'blah'a livello di classe.)

Sommario

  • Desideri sostituire i gruppi di attributi correlati in una classe padre con un singolo attributo che memorizza tutti i dati altrove
  • Vuoi lavorare con il codice client esistente che utilizza (ad esempio) larry = 'blah'a livello di classe
  • Desideri continuare a consentire alle sottoclassi di estendere, sovrascrivere e modificare questi attributi refactored senza sapere che nulla è cambiato


È possibile? O sto abbaiando sull'albero sbagliato?


6
Ti stai perdendo la metà dei vantaggi di se insisti nel mantenere questa enorme interfaccia divina, anche se separi parti dell'implementazione. Puoi fornire scorciatoie, ma semplicemente mettere le variabili in spazi dei nomi diversi e reindirizzare completamente a quelli ti dà molto poco, se non altro.

1
@delnan: Okay, cosa consiglieresti invece?
Zearin,

Risposte:


9

Avendo scritto e poi rifatturato un "oggetto Dio" in pitone, mi compiaccio. Quello che ho fatto è stato suddividere l'oggetto originale in sottosezioni basate su metodi. Ad esempio, l'originale assomigliava a questo pseudo codice:

method A():
    self.bla += 1

method B():
    self.bla += 1

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

Il metodo stuff è una "unità" autonoma di lavoro. L'ho migrato in una nuova classe che l'originale crea. Ciò ha tirato fuori anche le proprietà necessarie. Alcuni erano usati solo dalla sottoclasse e potevano spostarsi dritti. Altri sono stati condivisi e sono stati trasferiti in una classe condivisa.

L '"oggetto God" crea una nuova copia della classe condivisa all'avvio e ciascuna delle nuove sottoclassi accetta un puntatore come parte del loro metodo init. Ad esempio, ecco una versione ridotta del mailer:

#!/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="support@server.com"):
        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

Viene creato una volta e condiviso tra le diverse classi che richiedono funzionalità di mailing.

Quindi, per te, crea una classe larrycon le proprietà e i metodi di cui hai bisogno. Ovunque il client dice larry = blahsostituirlo con larryObj.larry = blah. Questo migra le cose verso i progetti secondari senza interrompere l'interfaccia corrente.

L'unica altra cosa da fare è cercare "unità di lavoro". Se avessi intenzione di trasformare parte dell '"Oggetto Dio" nel suo metodo, fallo . Ma metti il ​​metodo fuori di esso. Questo ti costringe a creare un'interfaccia tra i componenti.

Porre le basi per consentire a tutto il resto di seguirlo. Ad esempio, un pezzo dell'oggetto helper che dimostra come si interfaccia con il 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

Concentrati sulla singola unità di lavoro più piccola possibile e spostala fuori. Questo è più facile da fare e ti consente di giocare rapidamente con l'installazione. Non guardare le proprietà per spostare oggetti, sono accessori alle attività svolte nella maggior parte dei casi. Qualunque cosa rimanga dopo aver trattato i metodi dovrebbe probabilmente rimanere nell'oggetto originale, poiché fa parte dello stato condiviso.

Tuttavia , i nuovi oggetti dovrebbero ora accettare le proprietà di cui hanno bisogno come variabili init, senza toccare la proprietà degli oggetti chiamante. Restituiscono quindi tutti i valori necessari, che il chiamante può utilizzare per aggiornare le proprietà condivise, se necessario. Questo aiuta a disaccoppiare gli oggetti e rende un sistema più robusto.


1
Risposta fantastica, Spencer. Grazie! Ho alcune domande di follow-up di natura troppo specifica per essere appropriate qui. Posso contattarti privatamente per discuterne?
Zearin,

@Zearin certo, il mio profilo ha il mio indirizzo email. Questo era per un progetto aziendale, e non posso darti una copia completa del repository a causa delle cose proprietarie lì dentro. Dato un tempo sufficiente, potrei ripulire le istantanee prima / dopo, ma non sono sicuro di quanto ti possa aiutare.
Spencer Rathbun,

Non riesco a vedere alcun indirizzo e-mail sul tuo profilo. Ci sono tutti i tipi di informazioni, ma non le informazioni di contatto. ☺ Come devo contattarti?
Zearin,

Fatto. Cybermen: “Elimina! Elimina! Elimina!"
Zearin
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.