Come rifattorizzare un Python "dio class"?

10

Problema

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

Voglio refactoring della classe.

So Far ...

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

Fondamentalmente, la classe ha una lista mooolto di attributi - ma posso chiaramente guardarli e pensare, "Questi 5 attributi sono correlati ... Questi 8 sono anche correlati ... e poi c'è il resto."

getattr

Fondamentalmente volevo raggruppare gli attributi correlati in una classe helper dict-like. Ho avuto la sensazione che __getattr__ sarebbe l'ideale per il lavoro. Così ho spostato gli attributi in una classe separata, e, abbastanza sicuro, __getattr__ ha funzionato perfettamente con la sua magia ...

A prima .

Ma poi ho provato a eseguire uno degli esempi. La sottoclasse di esempio tenta di impostare direttamente uno di questi attributi (al 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.

@property

Poi ho letto del decoratore @property . Ma poi ho anche letto che crea problemi per sottoclassi che vogliono fare self.x = blah quando x è una proprietà della classe genitore.

desiderata

  • Il codice cliente continua a funzionare utilizzando self.whatever , anche se la proprietà% gen_de% del genitore non è "localizzata fisicamente" nella classe (o nell'istanza) stessa.
  • Raggruppa gli attributi in contenitori dict-like.
  • Riduce l'estrema rumorosità del codice nella classe principale.

Ad esempio, I non semplicemente vuoi cambiare questo:

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

In this:

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

... perché è ancora rumoroso. Anche se questo rende con successo un semplice attributo in qualcosa che può gestire i dati, l'originale aveva 3 variabili e la versione ottimizzata ha ancora 3 variabili.

Tuttavia, mi andrebbe bene qualcosa del genere:

stooges = Stooges()

E se una ricerca per whatever fallisce, qualcosa controlla self.larry e verifica se stooges è presente. (Ma deve funzionare anche se una sottoclasse prova a fare larry a livello di classe.)

Sommario

  • Desideri sostituire i relativi gruppi di attributi in una classe genitore 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
  • Vuoi continuare a consentire alle sottoclassi di estendere, sovrascrivere e modificare questi attributi refactored senza sapere che qualcosa è cambiato

È possibile? O sto abbaiando dall'albero sbagliato?

    
posta Zearin 28.03.2012 - 18:45
fonte

1 risposta

8

Avendo scritto e poi rifatto un pitone "God object", sono solidale. Quello che ho fatto è spezzare 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 roba è una "unità" di lavoro autonoma. L'ho migrato verso una nuova classe che l'originale istanzia. Questo ha tirato fuori anche le proprietà necessarie. Alcuni erano usati solo dalla sottoclasse e potevano spostarsi dritto. Altri sono stati condivisi e sono stati spostati in una classe condivisa.

L'oggetto "Dio" 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="[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

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

Quindi per te, crea una classe larry con le proprietà e i metodi di cui hai bisogno. Ovunque il cliente dice larry = blah sostituirlo con larryObj.larry = blah . Questo migra le cose in sottoprogetti senza rompere l'interfaccia corrente.

L'unica altra cosa da fare è cercare "unità di lavoro". Se stavi per trasformare parte del "God Object" nel suo metodo, fai così . Ma, metti il metodo all'esterno . Questo ti obbliga a creare un'interfaccia tra i componenti.

Porre queste basi permette a tutto il resto di seguirlo. Ad esempio, un pezzo dell'oggetto helper che mostra 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 più piccola unità di lavoro possibile e spostala. Questo è più facile da fare e ti consente di giocare velocemente con l'installazione. Non guardare le proprietà di oggetti in movimento, sono accessorie ai compiti che vengono eseguiti con loro 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.

Ma , i nuovi oggetti dovrebbero ora accettare le proprietà di cui hanno bisogno come variabili init, non toccando la proprietà dell'oggetto chiamante. Quindi restituiscono tutti i valori necessari, che possono essere utilizzati dal chiamante per aggiornare le proprietà condivise in base alle esigenze. Questo aiuta a disaccoppiare gli oggetti e crea un sistema più robusto.

    
risposta data 28.03.2012 - 20:12
fonte

Leggi altre domande sui tag