Wie kann man eine Python-Gottklasse umgestalten?

10

Problem

Ich arbeite an einem Python-Projekt, dessen Hauptklasse ein bisschen " God Object " ist. Es gibt so verdammt viele Attribute und Methoden!

Ich möchte die Klasse umgestalten.

Bisher…

Für den ersten Schritt möchte ich etwas relativ Einfaches tun; Aber als ich den einfachsten Ansatz ausprobierte, wurden einige Tests und vorhandene Beispiele gebrochen.

Grundsätzlich hat die Klasse eine lange Liste von Attributen - aber ich kann sie mir klar ansehen und denken: "Diese 5 Attribute sind verwandt ... Diese 8 sind auch verwandt ... und dann gibt es den Rest."

getattr

Ich wollte im Grunde nur die verwandten Attribute in einer diktartigen Hilfsklasse gruppieren. Ich hatte das Gefühl __getattr__, ideal für den Job zu sein. Also habe ich die Attribute in eine separate Klasse verschoben und natürlich __getattr__seine Magie perfekt umgesetzt ...

Am ersten .

Aber dann habe ich versucht, eines der Beispiele auszuführen. Die Beispielunterklasse versucht, eines dieser Attribute direkt (auf Klassenebene) festzulegen . Da sich das Attribut jedoch nicht mehr "physisch" in der übergeordneten Klasse befand, wurde die Fehlermeldung angezeigt, dass das Attribut nicht vorhanden war.

@Eigentum

Ich habe dann über den @propertyDekorateur gelesen . Aber dann habe ich auch gelesen, dass es Probleme für Unterklassen schafft, die tun wollen, self.x = blahwenn xes sich um eine Eigenschaft der übergeordneten Klasse handelt.

Gewünscht

  • Lassen Sie den gesamten Client-Code weiterhin verwenden self.whatever, auch wenn sich die whateverEigenschaft des übergeordneten Elements nicht "physisch" in der Klasse (oder Instanz) selbst befindet.
  • Gruppieren Sie verwandte Attribute in diktartigen Containern.
  • Reduzieren Sie das extreme Rauschen des Codes in der Hauptklasse.

Zum Beispiel möchte ich das nicht einfach ändern:

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

Das mögen:

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

… Weil das immer noch laut ist. Obwohl dies ein einfaches Attribut erfolgreich zu etwas macht, das die Daten verwalten kann, hatte das Original 3 Variablen und die optimierte Version hat immer noch 3 Variablen.

Mit so etwas würde ich jedoch gut zurechtkommen:

stooges = Stooges()

Und wenn eine Suche nach self.larryfehlschlägt, prüft etwas, stoogesob larryes vorhanden ist. (Es muss aber auch funktionieren, wenn eine Unterklasse dies larry = 'blah'auf Klassenebene versucht .)

Zusammenfassung

  • Möchten Sie verwandte Gruppen von Attributen in einer übergeordneten Klasse durch ein einzelnes Attribut ersetzen, in dem alle Daten an anderer Stelle gespeichert sind
  • Möchten Sie mit vorhandenem Client-Code arbeiten, der (z. B.) larry = 'blah'auf Klassenebene verwendet
  • Sie möchten weiterhin zulassen, dass Unterklassen diese überarbeiteten Attribute erweitern, überschreiben und ändern, ohne zu wissen, dass sich etwas geändert hat


Ist das möglich? Oder belle ich den falschen Baum an?

Zearin
quelle
6
Sie vermissen die Hälfte der Vorteile, wenn Sie darauf bestehen, diese riesige gottähnliche Oberfläche weiterhin zu haben, selbst wenn Sie Teile der Implementierung trennen. Sie können Verknüpfungen bereitstellen, aber wenn Sie die Variablen nur in verschiedene Namespaces einfügen und vollständig zu diesen umleiten, erhalten Sie, wenn überhaupt, nur sehr wenig.
1
@delnan: Okay, was würdest du stattdessen empfehlen?
Zearin

Antworten:

9

Nachdem ich ein Python "God Object" geschrieben und dann überarbeitet habe, sympathisiere ich. Ich habe das ursprüngliche Objekt anhand von Methoden in Unterabschnitte unterteilt. Zum Beispiel sah das Original wie folgt aus:

method A():
    self.bla += 1

method B():
    self.bla += 1

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

Die Stuff-Methode ist eine in sich geschlossene "Arbeitseinheit". Ich habe es in eine neue Klasse migriert, die das Original instanziiert. Dies zog auch die notwendigen Eigenschaften heraus. Einige wurden nur von der Unterklasse benutzt und konnten sich geradeaus bewegen. Andere wurden geteilt und in eine gemeinsame Klasse versetzt.

Das "God-Objekt" erstellt beim Start eine neue Kopie der gemeinsam genutzten Klasse, und jede der neuen Unterklassen akzeptiert einen Zeiger als Teil ihrer init-Methode. Hier ist zum Beispiel eine abgespeckte Version des Mailers:

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

Es wird einmal erstellt und von den verschiedenen Klassen gemeinsam genutzt, die Mailing-Funktionen benötigen.

Erstellen Sie für Sie eine Klasse larrymit den Eigenschaften und Methoden, die Sie benötigen. Überall, wo der Kunde sagt, larry = blahersetzen Sie es durchlarryObj.larry = blah . Dadurch werden Dinge in Unterprojekte migriert, ohne die aktuelle Schnittstelle zu beschädigen.

Das einzige andere, was zu tun ist, ist nach "Arbeitseinheiten" zu suchen. Wenn Sie einen Teil des "Gott-Objekts" in eine eigene Methode verwandeln möchten, tun Sie dies . Aber stellen Sie die Methode außerhalb . Dadurch müssen Sie eine Schnittstelle zwischen den Komponenten erstellen.

Wenn Sie diese Grundlagen schaffen, kann alles andere ihr folgen. Ein Teil des Hilfsobjekts zeigt beispielsweise, wie es mit dem Mailer verbunden ist:

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

Konzentrieren Sie sich auf die kleinstmögliche Arbeitseinheit und bewegen Sie sie heraus. Dies ist einfacher und ermöglicht es Ihnen, schnell mit dem Setup zu spielen. Sehen Sie sich keine Eigenschaften zum Verschieben von Gegenständen an, sie sind in den meisten Fällen eine Ergänzung zu den Aufgaben , die mit ihnen ausgeführt werden. Was übrig bleibt, nachdem Sie sich mit den Methoden befasst haben, sollte wahrscheinlich im ursprünglichen Objekt bleiben , da es Teil des gemeinsam genutzten Status ist.

Aber sollten die neuen Objekte akzeptieren nun die Eigenschaften , die sie als init Variablen benötigen, den Anrufer nicht berühren Eigenschaft Objekte. Sie geben dann alle erforderlichen Werte zurück, mit denen der Aufrufer die freigegebenen Eigenschaften nach Bedarf aktualisieren kann. Dies hilft, die Objekte zu entkoppeln und sorgt für ein robusteres System.

Spencer Rathbun
quelle
1
Fantastische Antwort, Spencer. Vielen Dank! Ich habe einige Anschlussfragen, die zu spezifisch sind, um hier angemessen zu sein. Darf ich Sie privat kontaktieren, um diese zu besprechen?
Zearin
@ Zearin sicher, mein Profil hat meine E-Mail-Adresse. Dies war jedoch für ein Unternehmensprojekt gedacht, und ich kann Ihnen aufgrund der darin enthaltenen proprietären Inhalte keine vollständige Kopie des Repositorys geben. Bei ausreichender Zeit könnte ich vor / nach Schnappschüssen aufräumen, bin mir aber nicht sicher, wie viel Ihnen das helfen würde.
Spencer Rathbun
Ich kann keine E-Mail-Adresse in Ihrem Profil sehen. Es gibt alle Arten von Informationen, aber keine Kontaktinformationen. ☺ Wie soll ich Sie kontaktieren?
Zearin
Verstanden. Cybermen: „Löschen! Löschen! Löschen!"
Zearin