Soll ich eine Klasse erstellen, wenn meine Funktion komplex ist und viele Variablen enthält?

40

Diese Frage ist sprachunabhängig, aber nicht vollständig, da sich die objektorientierte Programmierung (Object Oriented Programming, OOP) beispielsweise in Java , das keine erstklassigen Funktionen hat, von der in Python unterscheidet .

Mit anderen Worten, ich fühle mich weniger schuldig, unnötige Klassen in einer Sprache wie Java zu erstellen, aber ich habe das Gefühl, dass es einen besseren Weg geben könnte, wenn Sprachen wie Python weniger gut geschrieben sind.

Mein Programm muss mehrmals eine relativ komplexe Operation ausführen. Dieser Vorgang erfordert viel "Buchhaltung", muss einige temporäre Dateien erstellen und löschen, usw.

Aus diesem Grund müssen auch viele andere "Unteroperationen" aufgerufen werden - alles in einer großen Methode zusammenzufassen ist nicht sehr schön, modular, lesbar usw.

Das sind Ansätze, die mir in den Sinn kommen:

1. Erstellen Sie eine Klasse, die nur über eine öffentliche Methode verfügt und den für die Unteroperationen erforderlichen internen Status in ihren Instanzvariablen beibehält.

Es würde ungefähr so ​​aussehen:

class Thing:

    def __init__(self, var1, var2):
        self.var1 = var1
        self.var2 = var2
        self.var3 = []

    def the_public_method(self, param1, param2):
        self.var4 = param1
        self.var5 = param2
        self.var6 = param1 + param2 * self.var1
        self.__suboperation1()
        self.__suboperation2()
        self.__suboperation3()


    def __suboperation1(self):
        # Do something with self.var1, self.var2, self.var6
        # Do something with the result and self.var3
        # self.var7 = something
        # ...
        self.__suboperation4()
        self.__suboperation5()
        # ...

    def suboperation2(self):
        # Uses self.var1 and self.var3

#    ...
#    etc.

Das Problem, das ich bei diesem Ansatz sehe, ist, dass der Status dieser Klasse nur intern Sinn macht und mit ihren Instanzen nichts tun kann, außer ihre einzige öffentliche Methode aufzurufen.

# Make a thing object
thing = Thing(1,2)

# Call the only method you can call
thing.the_public_method(3,4)

# You don't need thing anymore

2. Erstellen Sie eine Reihe von Funktionen ohne Klasse und übergeben Sie die verschiedenen intern benötigten Variablen (als Argumente) zwischen ihnen.

Das Problem, das ich dabei sehe, ist, dass ich viele Variablen zwischen Funktionen übergeben muss. Außerdem wären die Funktionen eng miteinander verwandt, würden jedoch nicht zu Gruppen zusammengefasst.

3. Wie 2., aber machen Sie die Statusvariablen global, anstatt sie zu übergeben.

Dies wäre überhaupt nicht gut, da ich die Operation mehr als einmal mit unterschiedlichen Eingaben durchführen muss.

Gibt es einen vierten, besseren Ansatz? Wenn nicht, welcher dieser Ansätze wäre besser und warum? Fehlt mir etwas?

Ich kann lernen
quelle
9
"Das Problem, das ich bei diesem Ansatz sehe, ist, dass der Status dieser Klasse nur intern Sinn macht und mit ihren Instanzen nichts tun kann, außer ihre einzige öffentliche Methode aufzurufen." Sie haben dies als Problem formuliert, aber nicht, warum Sie glauben, dass es das ist.
Patrick Maupin
@Patrick Maupin - Du hast recht. Und ich weiß nicht wirklich, das ist das Problem. Es fühlt sich einfach so an, als würde ich Klassen für eine Sache verwenden, für die etwas anderes verwendet werden sollte, und es gibt eine Menge Dinge in Python, die ich noch nicht erforscht habe, also dachte ich, vielleicht würde jemand etwas passenderes vorschlagen.
iCanLearn
Vielleicht geht es darum, klar zu machen, was ich zu tun versuche. Ich sehe zwar nichts Besonderes daran, gewöhnliche Klassen anstelle von Aufzählungen in Java zu verwenden, aber dennoch gibt es Dinge, für die Aufzählungen natürlicher sind. Bei dieser Frage geht es also wirklich darum, ob es eine natürlichere Herangehensweise an das gibt, was ich versuche zu tun.
iCanLearn,
1
Wenn Sie Ihren vorhandenen Code nur in Methoden- und Instanzvariablen aufteilen und nichts an seiner Struktur ändern, gewinnen Sie nichts, aber verlieren an Klarheit. (1) ist eine schlechte Idee.
USR

Antworten:

47
  1. Erstellen Sie eine Klasse, die nur über eine öffentliche Methode verfügt und den für die Unteroperationen erforderlichen internen Status in ihren Instanzvariablen beibehält.

Das Problem, das ich bei diesem Ansatz sehe, ist, dass der Status dieser Klasse nur intern Sinn macht und mit ihren Instanzen nichts tun kann, außer ihre einzige öffentliche Methode aufzurufen.

Option 1 ist ein gutes Beispiel für die korrekte Verwendung der Kapselung . Sie möchten, dass der interne Status vor externem Code verborgen bleibt.

Wenn das bedeutet, dass Ihre Klasse nur eine öffentliche Methode hat, dann soll es so sein. Es wird so viel einfacher zu warten sein.

Wenn Sie in OOP eine Klasse haben, die genau eins macht, eine kleine öffentliche Oberfläche hat und ihren gesamten internen Zustand verbirgt, dann sind Sie (wie Charlie Sheen sagen würde) GEWINNEN .

  1. Machen Sie eine Reihe von Funktionen ohne eine Klasse und übergeben Sie die verschiedenen intern benötigten Variablen (als Argumente) zwischen ihnen.

Das Problem, das ich dabei sehe, ist, dass ich viele Variablen zwischen Funktionen übergeben muss. Außerdem wären die Funktionen eng miteinander verwandt, würden aber nicht zusammen gruppiert.

Option 2 weist eine geringe Kohäsion auf . Dies erschwert die Wartung.

  1. Wie 2. aber machen Sie die Zustandsvariablen global, anstatt sie zu übergeben.

Option 3 leidet wie Option 2 unter einer geringen Kohäsion, aber viel stärker!

Die Geschichte hat gezeigt, dass die Bequemlichkeit globaler Variablen durch die damit verbundenen hohen Wartungskosten aufgewogen wird. Deshalb hört man alte Fürze wie mich die ganze Zeit über die Einkapselung schimpfen.


Die gewinnende Wahl ist # 1 .

MetaFight
quelle
6
# 1 ist jedoch eine ziemlich hässliche API. Wenn ich mich dazu entschließen würde, eine Klasse dafür zu erstellen, würde ich wahrscheinlich eine einzelne öffentliche Funktion erstellen, die an die Klasse delegiert und die gesamte Klasse privat macht. ThingDoer(var1, var2).do_it()vs. do_thing(var1, var2).
user2357112
7
Während Option 1 ein klarer Gewinner ist, würde ich noch einen Schritt weiter gehen. Verwenden Sie anstelle des internen Objektstatus lokale Variablen und Methodenparameter. Dies macht die öffentliche Funktion wiedereintrittsfähig, was sicherer ist.
4
Eine zusätzliche Möglichkeit zur Erweiterung von Option 1: Schützen Sie die resultierende Klasse (indem Sie einen Unterstrich vor dem Namen einfügen) und definieren Sie dann eine Funktion def do_thing(var1, var2): return _ThingDoer(var1, var2).run()auf Modulebene , um die externe API etwas hübscher zu gestalten.
Sjoerd Job Postmus
4
Ich folge Ihrer Argumentation für 1 nicht. Der interne Zustand ist bereits verborgen. Das Hinzufügen einer Klasse ändert daran nichts. Ich verstehe daher nicht, warum Sie (1) empfehlen. Tatsächlich ist es nicht notwendig, die Existenz der Klasse überhaupt aufzudecken.
USR
3
Jede Klasse, die Sie instanziieren und dann sofort eine einzelne Methode aufrufen und dann nie wieder verwenden, ist für mich ein wichtiger Codegeruch. Es ist isomorph zu einer einfachen Funktion, nur mit verdecktem Datenfluss (wie die Ausgebrochenen der Implementierung kommunizieren, indem sie Variablen auf der Wegwerfinstanz zuweisen, anstatt Parameter zu übergeben). Wenn Ihre internen Funktionen so viele Parameter verwenden, dass die Aufrufe komplex und unhandlich sind, wird der Code nicht weniger kompliziert, wenn Sie diesen Datenfluss verbergen!
Ben
23

Ich denke # 1 ist eigentlich eine schlechte Option.

Betrachten wir Ihre Funktion:

def the_public_method(self, param1, param2):
    self.var4 = param1
    self.var5 = param2 
    self.var6 = param1 + param2 * self.var1
    self.__suboperation1()
    self.__suboperation2()
    self.__suboperation3()

Welche Daten verwendet die Unteroperation1? Sammelt es Daten, die von Suboperation2 verwendet werden? Wenn Sie Daten weitergeben, indem Sie sie auf sich selbst speichern, kann ich nicht sagen, wie die Teile der Funktionalität zusammenhängen. Wenn ich mich selbst betrachte, stammen einige der Attribute aus dem Konstruktor, einige aus dem Aufruf von the_public_method und einige wurden zufällig an anderen Stellen hinzugefügt. Meiner Meinung nach ist es ein Chaos.

Was ist mit Nummer 2? Schauen wir uns zunächst das zweite Problem an:

Außerdem wären die Funktionen eng miteinander verwandt, würden aber nicht zusammen gruppiert.

Sie würden in einem Modul zusammen sein, also würden sie total zusammen gruppiert.

Das Problem, das ich dabei sehe, ist, dass ich viele Variablen zwischen Funktionen übergeben muss.

Meiner Meinung nach ist das gut. Dies macht die Datenabhängigkeiten in Ihrem Algorithmus explizit. Indem Sie sie in globalen Variablen oder in einem Selbst speichern, verbergen Sie die Abhängigkeiten und lassen sie weniger schlimm erscheinen, aber sie sind immer noch da.

In der Regel bedeutet dies, dass Sie nicht den richtigen Weg gefunden haben, um Ihr Problem zu lösen. Sie finden es umständlich, mehrere Funktionen aufzuteilen, weil Sie versuchen, sie falsch aufzuteilen.

Natürlich ist es schwer zu erraten, was ein guter Vorschlag wäre, ohne Ihre tatsächliche Funktion zu sehen. Aber Sie geben hier einen kleinen Hinweis darauf, womit Sie es zu tun haben:

Mein Programm muss mehrmals eine relativ komplexe Operation ausführen. Dieser Vorgang erfordert viel "Buchhaltung", muss einige temporäre Dateien usw. erstellen und löschen.

Lassen Sie mich ein Beispiel für etwas auswählen, das Ihrer Beschreibung entspricht, einen Installateur. Ein Installationsprogramm muss eine Reihe von Dateien kopieren. Wenn Sie den Vorgang jedoch vorzeitig abbrechen, müssen Sie den gesamten Vorgang zurückspulen, einschließlich des Zurücksetzens aller Dateien, die Sie ersetzt haben. Der Algorithmus dafür sieht ungefähr so ​​aus:

def install_program():
    copied_files = []
    try:
        for filename in FILES_TO_COPY:
           temporary_file = create_temporary_file()
           copy(target_filename(filename), temporary_file)
           copied_files = [target_filename(filename), temporary_file)
           copy(source_filename(filename), target_filename(filename))
     except CancelledException:
        for source_file, temp_file in copied_files:
            copy(temp_file, source_file)
     else:
        for source_file, temp_file in copied_files:
            delete(temp_file)

Multiplizieren Sie nun diese Logik, um Registrierungseinstellungen, Programmsymbole usw. vornehmen zu müssen, und Sie haben ein ziemliches Durcheinander.

Ich denke, dass Ihre Lösung Nummer 1 so aussieht:

class Installer:
    def install(self):
        try:
            self.copy_new_files()
        except CancellationError:
            self.restore_original_files()
        else:
            self.remove_temp_files()

Dies macht den gesamten Algorithmus klarer, verbirgt jedoch die Art und Weise, wie die verschiedenen Teile kommunizieren.

Ansatz 2 sieht ungefähr so ​​aus:

def install_program():
    try:
       temp_files = copy_new_files()
    except CancellationError as error:
       restore_old_files(error.files_that_were_copied)
    else:
       remove_temp_files(temp_files)

Nun ist es explizit, wie sich Teile der Daten zwischen Funktionen bewegen, aber es ist sehr umständlich.

Wie soll diese Funktion geschrieben werden?

def install_program():
    with FileTransactionLog() as file_transaction_log:
         copy_new_files(file_transaction_log)

Das FileTransactionLog-Objekt ist ein Kontextmanager. Wenn copy_new_files eine Datei kopiert, geschieht dies über das FileTransactionLog, das die temporäre Kopie erstellt und festhält, welche Dateien kopiert wurden. Im Ausnahmefall werden die Originaldateien zurückkopiert, und im Erfolgsfall werden die temporären Kopien gelöscht.

Dies funktioniert, weil wir eine natürlichere Zerlegung der Aufgabe gefunden haben. Zuvor mischten wir die Logik zum Installieren der Anwendung mit der Logik zum Wiederherstellen einer abgebrochenen Installation. Jetzt verarbeitet das Transaktionsprotokoll alle Details zu temporären Dateien und zur Buchhaltung, und die Funktion kann sich auf den grundlegenden Algorithmus konzentrieren.

Ich vermute, dass sich Ihr Fall im selben Boot befindet. Sie müssen die Buchhaltungselemente in eine Art Objekt extrahieren, damit Ihre komplexe Aufgabe einfacher und eleganter ausgedrückt werden kann.

Winston Ewert
quelle
9

Da der einzige offensichtliche Nachteil von Methode 1 sein suboptimales Verwendungsmuster ist, denke ich, besteht die beste Lösung darin, die Kapselung einen Schritt weiter zu treiben: Verwenden Sie eine Klasse, stellen Sie aber auch eine freistehende Funktion bereit, die nur das Objekt erstellt, die Methode aufruft und zurückgibt :

def publicFunction(var1, var2, param1, param2)
    thing = Thing(var1, var2)
    thing.theMethod(param1, param2)

Damit haben Sie die kleinstmögliche Schnittstelle zu Ihrem Code, und die Klasse, die Sie intern verwenden, wird tatsächlich nur zu einem Implementierungsdetail Ihrer öffentlichen Funktion. Das Aufrufen von Code muss nie über Ihre interne Klasse informiert sein.

cmaster
quelle
4

Einerseits ist die Frage irgendwie sprachunabhängig; Andererseits hängt die Implementierung von der Sprache und ihren Paradigmen ab. In diesem Fall ist es Python, das mehrere Paradigmen unterstützt.

Neben Ihren Lösungen gibt es auch die Möglichkeit, die Operationen auf eine funktionalere Weise zustandslos abzuschließen , z

def outer(param1, param2):
    def inner1(param1, param2, param3):
        pass
    def inner2(param1, param2):
        pass
    return inner2(inner1(param1),param2,param3)

Es kommt alles darauf an

  • Lesbarkeit
  • Konsistenz
  • Wartbarkeit

-Wenn Ihre Codebasis OOP ist, verletzt dies die Konsistenz, wenn plötzlich einige Teile in einem (mehr) funktionalen Stil geschrieben werden.

Thomas Junk
quelle
4

Warum gestalten, wenn ich codieren kann?

Ich biete den gelesenen Antworten eine konträre Sichtweise. Aus dieser Perspektive konzentrieren sich alle Antworten und offen gesagt sogar die Fragen auf die Codierungsmechanik. Dies ist jedoch ein Designproblem.

Soll ich eine Klasse erstellen, wenn meine Funktion komplex ist und viele Variablen enthält?

Ja, da es für Ihr Design Sinn macht. Es kann eine Klasse für sich selbst oder ein Teil einer anderen Klasse sein oder sein Verhalten kann auf Klassen verteilt sein.


Bei objektorientiertem Design geht es um Komplexität

Der Sinn von OO besteht darin, große, komplexe Systeme erfolgreich zu erstellen und zu warten, indem der Code in Bezug auf das System selbst eingekapselt wird. "Richtiges" Design sagt, dass alles in einer Klasse ist.

OO-Design bewältigt Komplexität in erster Linie über fokussierte Klassen, die dem Prinzip der Einzelverantwortung folgen. Diese Klassen verleihen dem gesamten Atemzug und der Tiefe des Systems Struktur und Funktionalität, interagieren und integrieren diese Dimensionen.

Angesichts dessen wird oft behauptet, dass Funktionen, die an dem System hängen - der allzu allgegenwärtigen allgemeinen Dienstprogrammklasse -, ein Code-Geruch sind, der auf ein unzureichendes Design hindeutet. Ich neige dazu, zuzustimmen.

Radarbob
quelle
OO-Design schafft Komplexität auf natürliche Weise OOP bringt auf natürliche Weise unnötige Komplexität mit sich.
Miles Rout
"Unnötige Komplexität" erinnert mich an wertvolle Kommentare von Mitarbeitern wie "Oh, das sind zu viele Klassen." Die Erfahrung zeigt mir, dass der Saft den Druck wert ist. Am besten sehe ich praktisch ganze Klassen mit Methoden, die 1 bis 3 Zeilen lang sind, und wenn jede Klasse einen Teil ihrer Arbeit erledigt, wird die Komplexität einer bestimmten Methode minimiert: Ein einziges kurzes LOC, das zwei Sammlungen vergleicht und die Duplikate zurückgibt - natürlich steckt Code dahinter. Verwechseln Sie nicht "viel Code" mit komplexem Code. In jedem Fall ist nichts frei.
Radarbob
1
Viel "einfacher" Code, der auf komplizierte Weise interagiert, ist viel schlimmer als eine kleine Menge komplexer Codes.
Miles Rout
1
Die geringe Menge an komplexem Code enthielt Komplexität . Es gibt Komplexität, aber nur dort. Es läuft nicht aus. Wenn Sie viele einzelne einfache Teile haben, die auf sehr komplexe und schwer verständliche Weise zusammenarbeiten, ist das viel verwirrender, da der Komplexität keine Grenzen oder Mauern gesetzt sind.
Miles Rout
3

Vergleichen Sie Ihre Anforderungen mit denen der Standard-Python-Bibliothek und sehen Sie, wie diese implementiert werden.

Wenn Sie kein Objekt benötigen, können Sie dennoch Funktionen innerhalb von Funktionen definieren. In Python 3 gibt es eine neue nonlocalDeklaration, mit der Sie Variablen in Ihrer übergeordneten Funktion ändern können.

Möglicherweise ist es dennoch nützlich, einige einfache private Klassen in Ihrer Funktion zu haben, um Abstraktionen zu implementieren und Vorgänge aufzuräumen.

meuh
quelle
Vielen Dank. Und wissen Sie etwas in der Standardbibliothek, das sich so anhört, wie ich es in der Frage habe?
iCanLearn
Ich finde es schwierig, etwas mit verschachtelten Funktionen zu zitieren, tatsächlich finde ich nonlocalin meinen derzeit installierten Python-Bibliotheken nichts . Vielleicht können Sie sich ein Herz nehmen, textwrap.pydas eine TextWrapperKlasse hat, aber auch eine def wrap(text)Funktion, die einfach eine TextWrapper Instanz erstellt, die .wrap()Methode darauf aufruft und zurückgibt. Verwenden Sie also eine Klasse, aber fügen Sie einige praktische Funktionen hinzu.
Mittwoch,