Como refatorar uma "classe divina" do Python?

10

Problema

Estou trabalhando em um projeto Python cuja classe principal é um pouco " God Object ". Não são tão friggin muitos atributos e métodos!

Eu quero refatorar a classe.

Tão longe…

Para o primeiro passo, quero fazer algo relativamente simples; mas quando tentei a abordagem mais direta, ela quebrou alguns testes e exemplos existentes.

Basicamente, a classe tem uma lista loooonga de atributos - mas eu posso examiná-los claramente e pensar: "Esses 5 atributos estão relacionados ... Esses 8 também estão relacionados ... e depois há o resto".

getattr

Basicamente, eu só queria agrupar os atributos relacionados em uma classe auxiliar do tipo ditado. Tive a sensação de __getattr__que seria ideal para o trabalho. Então, mudei os atributos para uma classe separada e, com certeza, __getattr__funcionou perfeitamente bem sua mágica…

No primeiro .

Mas então tentei executar um dos exemplos. A subclasse de exemplo tenta definir um desses atributos diretamente (no nível da classe ). Mas como o atributo não estava mais "fisicamente localizado" na classe pai, recebi um erro dizendo que o atributo não existia.

@propriedade

Eu então li sobre o @propertydecorador. Mas também li que isso cria problemas para as subclasses que desejam fazer self.x = blahquando xé uma propriedade da classe pai.

Desejado

  • Faça com que todo o código do cliente continue trabalhando self.whatever, mesmo que a whateverpropriedade do pai não esteja "fisicamente localizada" na própria classe (ou instância).
  • Agrupe atributos relacionados em contêineres semelhantes a dict.
  • Reduza o barulho extremo do código na classe principal.

Por exemplo, eu não quero simplesmente mudar isso:

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

Nisso:

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

... porque ainda é barulhento. Embora isso faça com que um atributo simplesmente seja algo que possa gerenciar os dados, o original tinha 3 variáveis ​​e a versão aprimorada ainda possui 3 variáveis.

No entanto, eu ficaria bem com algo assim:

stooges = Stooges()

E se uma pesquisa self.larryfalhar, algo verificará stoogesse larryhá. (Mas também deve funcionar se uma subclasse tentar fazer isso larry = 'blah'no nível da classe.)

Sumário

  • Deseja substituir grupos de atributos relacionados em uma classe pai por um único atributo que armazena todos os dados em outro local
  • Deseja trabalhar com o código do cliente existente que usa (por exemplo) larry = 'blah'no nível da classe
  • Deseja continuar permitindo que as subclasses estendam, substituam e modifiquem esses atributos refatorados sem saber que algo mudou


Isso é possível? Ou estou latindo na árvore errada?

Zearin
fonte
6
Você está perdendo metade dos benefícios se insistir em ainda ter essa enorme interface divina, mesmo que você separe partes da implementação. Você pode fornecer atalhos, mas apenas colocar as variáveis ​​em diferentes espaços para nome e redirecionar completamente para esses itens fornece muito pouco, se é que existe alguma coisa.
11
@ delnan: Ok, então o que você recomendaria?
Zearin 30/03

Respostas:

9

Depois de escrever e refatorar um python "objeto de Deus", eu simpatizo. O que fiz foi dividir o objeto original em subseções com base em métodos. Por exemplo, o original se parecia com este pseudo-código:

method A():
    self.bla += 1

method B():
    self.bla += 1

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

O método stuff é uma "unidade" de trabalho independente. Eu a migrei para uma nova classe que o original instancia. Isso retirou as propriedades necessárias também. Alguns eram usados ​​apenas pela subclasse e podiam atravessar diretamente. Outros foram compartilhados e foram transferidos para uma classe compartilhada.

O "objeto Deus" cria uma nova cópia da classe compartilhada na inicialização e cada uma das novas subclasses aceita um ponteiro como parte do método init. Por exemplo, aqui está uma versão simplificada da mala direta:

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

Ele é criado uma vez e compartilhado entre as diferentes classes que precisam de recursos de correspondência.

Então, para você, crie uma classe larrycom as propriedades e métodos que você precisa. Em todos os lugares que o cliente solicitar, larry = blahsubstitua-o larryObj.larry = blah. Isso migra as coisas para os subprojetos sem interromper a interface atual.

A única outra coisa a fazer é procurar por "unidades de trabalho". Se você iria transformar parte do "Objeto de Deus" em seu próprio método, faça-o . Mas, coloque o método fora dele. Isso força você a criar uma interface entre os componentes.

Estabelecer esse fundamento permite que todo o resto o siga. Por exemplo, uma parte do objeto auxiliar que demonstra como ele interage com a mala direta:

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

Concentre-se na menor unidade individual de trabalho possível e mova-a para fora. Isso é mais fácil de fazer e permite que você jogue com a configuração rapidamente. Não olhe as propriedades para mover coisas, elas são auxiliares das tarefas que estão sendo feitas com eles na maioria dos casos. O que sobrar depois que você lida com os métodos provavelmente deve permanecer no objeto original, pois faz parte do estado compartilhado.

Porém , os novos objetos agora devem aceitar as propriedades de que precisam como variáveis ​​de inicialização, sem tocar na propriedade dos objetos de chamada. Em seguida, eles retornam quaisquer valores necessários, que o chamador pode usar para atualizar as propriedades compartilhadas conforme necessário. Isso ajuda a dissociar os objetos e cria um sistema mais robusto.

Spencer Rathbun
fonte
11
Resposta fantástica, Spencer. Obrigado! Tenho algumas perguntas de acompanhamento que são de natureza muito específica para serem apropriadas aqui. Posso entrar em contato com você em particular para discutir isso?
Zearin 30/03/12
@Zearin, meu perfil tem meu endereço de e-mail. Este foi para um projeto da empresa, e não posso fornecer uma cópia completa do repositório por causa das coisas proprietárias nele. Com uma quantidade de tempo suficiente, eu poderia limpar os instantâneos antes / depois, mas não tenho certeza de quanto isso ajudaria.
Spencer Rathbun
Não consigo ver nenhum endereço de email no seu perfil. Há todo tipo de informação, mas não informações de contato. ☺ Como devo entrar em contato com você?
Zearin
Entendi. Cybermen: “Apague! Excluir! Excluir!"
Zearin