¿Cómo refactorizar una "clase de dios" de Python?

10

Problema

Estoy trabajando en un proyecto de Python cuya clase principal es un poco " God Object ". ¡Hay tantos malditos atributos y métodos!

Quiero refactorizar la clase.

Hasta aquí…

Para el primer paso, quiero hacer algo relativamente simple; pero cuando probé el enfoque más directo, rompió algunas pruebas y ejemplos existentes.

Básicamente, la clase tiene una lista muuuucha larga de atributos, pero puedo verlos claramente y pensar: "Estos 5 atributos están relacionados ... Estos 8 también están relacionados ... y luego está el resto".

getattr

Básicamente, solo quería agrupar los atributos relacionados en una clase auxiliar tipo dict. Tenía la sensación de __getattr__que sería ideal para el trabajo. Así que moví los atributos a una clase separada y, efectivamente, __getattr__trabajé su magia perfectamente bien ...

En primer lugar .

Pero luego intenté ejecutar uno de los ejemplos. La subclase de ejemplo intenta establecer uno de estos atributos directamente (a nivel de clase ). Pero como el atributo ya no estaba "físicamente ubicado" en la clase principal, recibí un error que decía que el atributo no existía.

@propiedad

Luego leí sobre el @propertydecorador. Pero también leí que crea problemas para las subclases que quieren hacer self.x = blahcuando xes una propiedad de la clase principal.

Deseado

  • Haga que todo el código del cliente continúe funcionando self.whatever, incluso si la whateverpropiedad del padre no está "físicamente ubicada" en la clase (o instancia) en sí misma.
  • Agrupe atributos relacionados en contenedores tipo dict.
  • Reduzca el ruido extremo del código en la clase principal.

Por ejemplo, no quiero simplemente cambiar esto:

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

Dentro de esto:

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

... porque eso sigue siendo ruidoso. Aunque eso convierte con éxito un atributo simple en algo que puede administrar los datos, el original tenía 3 variables y la versión modificada todavía tiene 3 variables.

Sin embargo, estaría bien con algo como esto:

stooges = Stooges()

Y si una búsqueda self.larryfalla, algo verificaría stoogesy vería si larryestá allí. (Pero también debe funcionar si una subclase intenta hacerlo larry = 'blah'a nivel de clase).

Resumen

  • Desea reemplazar grupos de atributos relacionados en una clase principal con un solo atributo que almacena todos los datos en otro lugar
  • Quiere trabajar con el código de cliente existente que usa (por ejemplo) larry = 'blah'a nivel de clase
  • Desea continuar permitiendo que las subclases extiendan, anulen y modifiquen estos atributos refactorizados sin saber que algo ha cambiado.


es posible? ¿O estoy ladrando el árbol equivocado?

Zearin
fuente
66
Te estás perdiendo la mitad de los beneficios si insistes en seguir teniendo esta enorme interfaz divina, incluso si separas partes de la implementación. Puede proporcionar accesos directos, pero simplemente colocando las variables en diferentes espacios de nombres y redirigiendo completamente a ellas le da muy poco, si es que algo.
1
@delnan: Bien, ¿qué recomendarías en su lugar?
Zearin

Respuestas:

9

Habiendo escrito y luego refactorizado un "objeto de Dios" de Python, simpatizo. Lo que hice fue dividir el objeto original en subsecciones basadas en métodos. Por ejemplo, el original se parecía a este pseudocódigo:

method A():
    self.bla += 1

method B():
    self.bla += 1

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

El método de relleno es una "unidad" de trabajo autónoma. Lo migré a una nueva clase que la instancia original. Esto sacó las propiedades necesarias también. Algunos fueron utilizados solo por la subclase y podían moverse en línea recta. Otros fueron compartidos y se trasladaron a una clase compartida.

El "objeto de Dios" crea una nueva copia de la clase compartida al inicio, y cada una de las nuevas subclases acepta un puntero como parte de su método init. Por ejemplo, aquí hay una versión despojada del anuncio publicitario:

#!/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

Se crea una vez y se comparte entre las diferentes clases que necesitan capacidades de correo.

Entonces, para usted, cree una clase larrycon las propiedades y métodos que necesita. En todas partes el cliente dice larry = blahreemplazarlo con larryObj.larry = blah. Esto migra cosas a subproyectos sin romper la interfaz actual.

La única otra cosa que hacer es buscar "unidades de trabajo". Si iba a convertir parte del "Objeto de Dios" en su propio método, hágalo . Pero, pon el método fuera de él. Esto te obliga a crear una interfaz entre los componentes.

Colocar esa base permite que todo lo demás lo siga. Por ejemplo, una parte del objeto auxiliar que demuestra cómo interactúa con el programa de correo:

#!/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

Concéntrese en la unidad individual de trabajo más pequeña posible y muévala. Esto es más fácil de hacer y te permite jugar con la configuración rápidamente. No mire las propiedades para mover cosas, son auxiliares de las tareas que se realizan con ellos en la mayoría de los casos. Lo que quede después de que haya tratado con los métodos probablemente debería permanecer en el objeto original, ya que es parte del estado compartido.

Pero , los nuevos objetos ahora deberían aceptar las propiedades que necesitan como variables de inicio, sin tocar la propiedad de los objetos que llaman. Luego devuelven los valores necesarios, que la persona que llama puede usar para actualizar las propiedades compartidas según sea necesario. Esto ayuda a desacoplar los objetos y crea un sistema más robusto.

Spencer Rathbun
fuente
1
Fantástica respuesta, Spencer. ¡Gracias! Tengo algunas preguntas de seguimiento que son de naturaleza demasiado específica para ser apropiadas aquí. ¿Puedo contactarlo en privado para discutir esto?
Zearin
@Zearin seguro, mi perfil tiene mi dirección de correo electrónico. Sin embargo, esto fue para un proyecto de la compañía, y no puedo darle una copia completa del repositorio debido a las cosas de propiedad allí. Dado un tiempo suficiente, podría limpiar antes / después de las instantáneas, pero no estoy seguro de cuánto te ayudaría.
Spencer Rathbun
No puedo ver ninguna dirección de correo electrónico en su perfil. Hay todo tipo de información, pero no información de contacto. ☺ ¿Cómo debo contactarlo?
Zearin
Entendido. Cybermen: “¡Eliminar! ¡Eliminar! ¡Eliminar!"
Zearin