Jak refaktoryzować „klasę boga” Pythona?

10

Problem

Pracuję nad projektem w języku Python, którego główną klasą jest nieco „ Boski obiekt ”. Jest tak cholernie wiele atrybutów i metod!

Chcę refaktoryzować klasę.

Jak dotąd…

W pierwszym kroku chcę zrobić coś stosunkowo prostego; ale kiedy wypróbowałem najprostsze podejście, przełamało kilka testów i istniejących przykładów.

Zasadniczo klasa ma długą listę atrybutów - ale mogę je przejrzeć i pomyśleć: „Te 5 atrybutów jest powiązanych… Te 8 są również powiązane… a potem jest cała reszta”.

getattr

Zasadniczo chciałem po prostu zgrupować powiązane atrybuty w klasę pomocnika przypominającą dykt. Miałem wrażenie, __getattr__że byłby idealny do tej pracy. Przeniosłem więc atrybuty do osobnej klasy i, oczywiście, __getattr__działałem doskonale magicznie…

Na pierwszy .

Ale potem spróbowałem uruchomić jeden z przykładów. Przykładowa podklasa próbuje ustawić jeden z tych atrybutów bezpośrednio (na poziomie klasy ). Ale ponieważ atrybut nie był już „fizycznie umiejscowiony” w klasie nadrzędnej, wystąpił błąd informujący, że atrybut nie istnieje.

@własność

Potem przeczytałem o @propertydekoratorze. Ale potem przeczytałem również, że stwarza to problemy dla podklas, które chcą zrobić, self.x = blahgdy xsą własnością klasy nadrzędnej.

Pożądany

  • Niech cały kod klienta nadal działa self.whatever, nawet jeśli kod nadrzędnywhatever nie jest „fizycznie zlokalizowana” w samej klasie (lub instancji).
  • Grupuj powiązane atrybuty w pojemniki podobne do dict.
  • Zmniejsz ekstremalną głośność kodu w klasie głównej.

Na przykład nie chcę po prostu tego zmieniać:

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

Zaangażowany w to:

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

… Ponieważ wciąż jest głośno. Chociaż z powodzeniem przekształca to po prostu atrybut w coś, co może zarządzać danymi, oryginał miał 3 zmienne, a poprawiona wersja wciąż ma 3 zmienne.

Byłbym jednak w porządku z czymś takim:

stooges = Stooges()

A jeśli wyszukiwanie self.larrynie powiedzie się, coś sprawdzi stoogesi zobaczy, czy larryjest. (Ale musi także działać, jeśli podklasa próbuje to zrobić larry = 'blah'na poziomie klasy).

Podsumowanie

  • Chcesz zastąpić pokrewne grupy atrybutów w klasie nadrzędnej jednym atrybutem, który przechowuje wszystkie dane w innym miejscu
  • Chcesz pracować z istniejącym kodem klienta, który używa (np.) larry = 'blah'Na poziomie klasy
  • Chcesz nadal zezwalać na rozszerzanie, zastępowanie i modyfikowanie tych podklas atrybutów podklas bez wiedzy, że coś się zmieniło


czy to możliwe? A może szczekam niewłaściwe drzewo?

Zearin
źródło
6
Tracisz połowę korzyści, jeśli nalegasz, aby nadal mieć ten ogromny boski interfejs, nawet jeśli oddzielisz części wdrożenia. Możesz podać skróty, ale po prostu umieszczenie zmiennych w różnych przestrzeniach nazw i pełne przekierowanie do nich daje bardzo niewiele, jeśli w ogóle.
1
@delnan: OK, więc co byś polecił?
Zearin

Odpowiedzi:

9

Po napisaniu, a następnie przeredagowaniu pythonowego „obiektu Boga”, współczuję. To, co zrobiłem, to podzielenie oryginalnego obiektu na podsekcje w oparciu o metody. Na przykład oryginał wyglądał jak ten pseudo kod:

method A():
    self.bla += 1

method B():
    self.bla += 1

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

Metoda rzeczy jest samodzielną „jednostką” pracy. Przeprowadziłem migrację do nowej klasy, którą tworzy instancja oryginalna. Wyciągnęło to również niezbędne właściwości. Niektóre były używane tylko przez podklasę i mogły poruszać się prosto. Inni zostali udostępnieni i zostali przeniesieni do wspólnej klasy.

„Boski obiekt” tworzy nową kopię klasy współdzielonej podczas uruchamiania, a każda z nowych podklas akceptuje wskaźnik jako część ich metody init. Na przykład, oto pozbawiona wersji wersja programu pocztowego:

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

Jest on tworzony raz i dzielony między różne klasy, które potrzebują możliwości wysyłania wiadomości.

Więc utwórz klasę larryz właściwościami i metodami, których potrzebujesz. Wszędzie, gdzie mówi klient, larry = blahzamień go nalarryObj.larry = blah . To migruje rzeczy do podprojektów bez przerywania bieżącego interfejsu.

Jedyne, co należy zrobić, to poszukać „jednostek pracy”. Jeśli chcesz zamienić część „Boskiego obiektu” na własną metodę, zrób to . Ale odłóż metodę na bok . Wymusza to utworzenie interfejsu między komponentami.

Ułożenie fundamentów pozwala podążać za nim wszystkim innym. Na przykład fragment obiektu pomocnika pokazujący, w jaki sposób łączy się on z programem pocztowym:

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

Skoncentruj się na najmniejszej możliwej jednostce pracy i przenieś ją. Jest to łatwiejsze do zrobienia i pozwala szybko rozpocząć konfigurację. Nie patrz na właściwości przenoszenia rzeczy, w większości przypadków są one pomocnicze w stosunku do zadań, które są z nimi wykonywane. Cokolwiek pozostanie po tym, jak poradzisz sobie z metodami, prawdopodobnie powinno zostać w oryginalnym obiekcie, ponieważ jest on częścią stanu wspólnego.

Ale nowe obiekty powinny teraz akceptować właściwości, których potrzebują, jako zmienne początkowe, nie dotykając właściwości obiektów wywołujących. Następnie zwracają wszelkie niezbędne wartości, które mogą zostać użyte przez osobę dzwoniącą do zaktualizowania wspólnych właściwości w razie potrzeby. Pomaga to rozdzielić obiekty i stworzyć bardziej niezawodny system.

Spencer Rathbun
źródło
1
Fantastyczna odpowiedź, Spencer. Dziękuję Ci! Mam kilka pytań uzupełniających, które są zbyt szczegółowe, aby były odpowiednie. Czy mogę skontaktować się z tobą prywatnie w celu omówienia tych kwestii?
Zearin
@ Z pewnością, mój profil ma mój adres e-mail. To było jednak dla projektu firmowego i nie mogę dać ci pełnej kopii repozytorium z powodu zastrzeżonych tam rzeczy. Biorąc pod uwagę wystarczającą ilość czasu, mógłbym posprzątać przed / po migawkach, ale nie jestem pewien, ile to by ci pomogło.
Spencer Rathbun
Nie widzę żadnego adresu e-mail w twoim profilu. Jest wiele informacji, ale nie dane kontaktowe. ☺ Jak mam się z tobą skontaktować?
Zearin,
Rozumiem. Cybermen: „Usuń! Usunąć! Usunąć!"
Zearin,