Leistung versus Wiederverwendbarkeit

8

Wie kann ich Funktionen schreiben, die wiederverwendbar sind, ohne die Leistung zu beeinträchtigen? Ich stoße immer wieder auf die Situation, in der ich eine Funktion so schreiben möchte, dass sie wiederverwendbar ist (z. B. keine Annahmen über die Datenumgebung), aber den Gesamtfluss des Programms zu kennen, von dem ich weiß, dass es nicht die effizienteste ist Methode. Wenn ich beispielsweise eine Funktion schreiben möchte, die einen Bestandscode validiert, aber wiederverwendbar ist, kann ich nicht einfach davon ausgehen, dass das Recordset geöffnet ist. Wenn ich das Recordset jedoch jedes Mal öffne und schließe, wenn die Funktion aufgerufen wird, kann der Leistungseinbruch beim Durchlaufen von Tausenden von Zeilen sehr groß sein.

Für die Leistung könnte ich also haben:

Function IsValidStockRef(strStockRef, rstStockRecords)
    rstStockRecords.Find ("stockref='" & strStockRef & "'")
    IsValidStockRef = Not rstStockRecords.EOF
End Function

Aber für die Wiederverwendbarkeit würde ich ungefähr Folgendes benötigen:

Function IsValidStockRef(strStockRef)
    Dim rstStockRecords As ADODB.Recordset

    Set rstStockRecords = New ADODB.Recordset
    rstStockRecords.Open strTable, gconnADO

    rstStockRecords.Find ("stockref='" & strStockRef & "'")
    IsValidStockRef = Not rstStockRecords.EOF

    rstStockRecords.Close
    Set rstStockRecords = Nothing
End Function

Ich mache mir Sorgen, dass die Auswirkungen des Öffnens und Schließens dieses Datensatzes beim Aufrufen aus einer Schleife über Tausende von Zeilen / Datensätzen auf die Leistung schwerwiegend wären, aber die Verwendung der ersten Methode macht die Funktion weniger wiederverwendbar.

Was sollte ich tun?

Caltor
quelle

Antworten:

13

Sie sollten alles tun, was in dieser Situation den höheren Geschäftswert ergibt.

Das Schreiben von Software ist immer ein Kompromiss. Fast nie sind alle gültigen Ziele (Wartbarkeit, Leistung, Klarheit, Prägnanz, Sicherheit usw. usw.) vollständig aufeinander abgestimmt. Fallen Sie nicht in die Falle dieser kurzsichtigen Menschen, die eine dieser Dimensionen als vorrangig betrachten, und fordern Sie Sie auf, alles dafür zu opfern .

Verstehen Sie stattdessen, welche Risiken und welche Vorteile jede Alternative bietet, quantifizieren Sie sie und entscheiden Sie sich für dasjenige, das das Ergebnis maximiert. (Sie müssen natürlich keine numerischen Schätzungen vornehmen. Es reicht aus, Faktoren wie "Wenn Sie diese Klasse verwenden, müssen Sie uns an diesen Hash-Algorithmus binden, aber da wir ihn nicht zum Schutz vor böswilligen Angriffen verwenden, nur für." Bequemlichkeit, diese ist gut genug, dass wir die 1: 1.000.000.000 Wahrscheinlichkeit einer versehentlichen Kollision einfach ignorieren können . ")

Das Wichtigste ist, sich daran zu erinnern, dass es sich um Kompromisse handelt. Kein einziges Prinzip rechtfertigt alles , um zu befriedigen, und keine einmal getroffene Entscheidung muss für immer bestehen bleiben . Möglicherweise müssen Sie im Nachhinein immer überarbeiten, wenn sich die Umstände auf eine Weise ändern, die Sie nicht vorausgesehen haben. Das ist ein Schmerz, aber bei weitem nicht so schmerzhaft wie die gleiche Entscheidung ohne Rückblick zu treffen .

Kilian Foth
quelle
2
Während das, was Sie sagen, im Allgemeinen zutrifft, sollte etwas zum Schreiben von Code gesagt werden, um sich vor möglichen Fällen zu schützen und um Voraussetzungen zu haben. Meiner bescheidenen Meinung nach ist es nichts Falsches, einfach zu erwarten, dass das Recordset bei ausreichender Dokumentation bereits geöffnet ist, wenn es aufgerufen wird. Wenn dies eine Methode in einer Bibliothek ist, führen Sie eine schnelle Überprüfung durch, ob sie geöffnet ist, und lösen Sie eine Ausnahme aus, wenn dies nicht der Fall ist. In keinem möglichen Szenario muss es "funktionieren".
Neil
6

Keines davon scheint wiederverwendbarer zu sein als das andere. Sie scheinen sich nur auf verschiedenen Abstraktionsebenen zu befinden . Der erste besteht darin, Code aufzurufen, der das Bestandsystem genau genug versteht, um zu wissen, dass das Validieren einer Bestandsreferenz das Durchsuchen einer Recordsetmit einer Art Abfrage bedeutet. Der zweite dient zum Aufrufen von Code, der nur wissen möchte, ob ein Bestandscode gültig ist oder nicht, und kein Interesse daran hat, wie Sie so etwas überprüfen würden.

Aber wie bei den meisten Abstraktionen ist diese "undicht". In diesem Fall verliert die Abstraktion ihre Leistung - der aufrufende Code kann die Implementierung der Validierung nicht vollständig ignorieren, da er diese Funktion möglicherweise tausendmal aufruft , wie Sie es beschrieben haben, und die Gesamtleistung erheblich beeinträchtigt.

Wenn Sie zwischen schlecht abstrahiertem Code und inakzeptabler Leistung wählen müssen, müssen Sie letztendlich den schlecht abstrahierten Code wählen. Aber zuerst sollten Sie nach einer besseren Lösung suchen - einem Kompromiss, der eine akzeptable Leistung beibehält und eine anständige (wenn nicht ideale) Abstraktion bietet. Leider kenne ich VBA nicht sehr gut, aber in einer OO-Sprache wäre mein erster Gedanke, aufrufendem Code eine Klasse mit Methoden wie:

BeginValidation()
IsValidStockRef(strStockRef)
EndValidation()

Hier führen Ihre Begin...und End...Methoden die einmalige Lebenszyklusverwaltung des Datensatzes durch, die IsValidStockRefmit Ihrer ersten Version übereinstimmt, verwendet jedoch diesen Datensatz, für den die Klasse selbst die Verantwortung übernommen hat, anstatt ihn übergeben zu lassen. Der aufrufende Code würde dann das Begin...und aufrufen End...Methoden außerhalb der Schleife und die Validierungsmethode innerhalb.

Hinweis: Dies ist nur ein sehr grobes anschauliches Beispiel und kann als erster Durchgang beim Refactoring angesehen werden. Die Namen könnten wahrscheinlich optimiert werden, und je nach Sprache sollte es eine sauberere oder idiomatischere Methode geben (C # könnte beispielsweise den Konstruktor zum Beginnen und Dispose()Beenden verwenden). Im Idealfall sollte Code, der nur überprüfen möchte, ob eine Bestandsreferenz gültig ist, selbst überhaupt kein Lebenszyklusmanagement durchführen müssen.

Dies stellt eine leichte Verschlechterung der Abstraktion dar, die wir präsentieren: Jetzt muss der aufrufende Code genug über die Validierung wissen, um zu verstehen, dass dies eine Art Setup und Teardown erfordert. Als Gegenleistung für diesen relativ bescheidenen Kompromiss verfügen wir jetzt über Methoden, die einfach durch Aufrufen von Code verwendet werden können, ohne unsere Leistung zu beeinträchtigen.

Ben Aaronson
quelle
Downvoter: Gibt es einen bestimmten Grund aus Interesse?
Ben Aaronson
Ich war nicht der Downvoter, aber ich werde raten. BeginValidation,, EndValidationund IsValidStockRefhaben eine besondere Beziehung zueinander. Das Wissen über diese Beziehung ist komplexer als das Wissen, das erforderlich wäre, um a direkt zu handhaben RecordSet. Und das Wissen, das erforderlich ist, um mit a umzugehen, RecordSetist breiter anwendbar.
Keen
@Cory Ich stimme bis zu einem gewissen Grad zu, und meine Hand wurde ein wenig durch mangelndes Wissen über vba gezwungen. Ich habe versucht, dies mit dem nächsten Satz herauszustellen, aber vielleicht war mein Wortlaut nicht klar oder stark genug. Ich habe eine Bearbeitung vorgenommen, um dies ein wenig klarer zu machen
Ben Aaronson
Ein interessanter Hinweis: In C # wird von Ihnen erwartet, dass Sie die usingAnweisung verwenden, um diesen Job auszuführen. In anderen Sprachen (die ohnehin Ausnahmen verwenden) müssen Sie die gleiche Arbeit usingverwenden try {} finally {}, um die ordnungsgemäße Entsorgung zu gewährleisten, und selbst dann ist es manchmal unmöglich, den gesamten Code korrekt zu verpacken throw. Dies ist ein potenzielles Problem bei allen hier genannten Lösungen, und ich bin mir auch nicht sicher, wie dies in VBA gelöst werden soll.
Keen
@Cory: Und in C ++ würden Sie einfach RAII verwenden.
Deduplikator
3

Ich habe lange Zeit ein kompliziertes Überprüfungssystem implementiert, um Datenbanktransaktionen verwenden zu können. Die Transaktionslogik lautet wie folgt: Öffnen Sie eine Transaktion, führen Sie die Datenbankoperationen aus, führen Sie ein Rollback bei einem Fehler durch oder schreiben Sie bei Erfolg fest. Die Komplikation ergibt sich aus dem, was passiert, wenn eine zusätzliche Operation innerhalb derselben Transaktion ausgeführt werden soll. Sie müssten entweder eine zweite Methode vollständig schreiben, die beide Operationen ausführt, oder Sie könnten Ihre ursprüngliche Methode von einer Sekunde an aufrufen, indem Sie eine Transaktion nur öffnen, wenn noch keine geöffnet wurde, und Änderungen nur dann festschreiben / zurücksetzen, wenn Sie die waren eine, um die Transaktion zu öffnen.

Zum Beispiel:

public void method1() {
    boolean selfOpened = false;
    if(!transaction.isOpen()) {
        selfOpened = true;
        transaction.open();
    }

    try {
        performDbOperations();
        method2();

        if(selfOpened) 
            transaction.commit();
    } catch (SQLException e) {
        if(selfOpened) 
            transaction.rollback();
        throw e;
    }
}

public void method2() {
    boolean selfOpened = false;
    if(!transaction.isOpen()) { 
        selfOpened = true;
        transaction.open();
    }

    try {
        performMoreDbOperations();

        if(selfOpened) 
            transaction.commit();
    } catch (SQLException e) {
        if(selfOpened) 
            transaction.rollback();
        throw e;
    }
}

Bitte beachten Sie, dass ich den obigen Code in keiner Weise befürworte. Dies sollte als Beispiel dafür dienen, was nicht zu tun ist!

Es schien albern, eine zweite Methode zu erstellen, um die gleiche Logik wie die erste und etwas Besonderes auszuführen, aber ich wollte in der Lage sein, den Datenbank-API-Abschnitt des Programms aufzurufen und dort Probleme abzudichten. Während dies mein Problem teilweise löste, umfasste jede Methode, die ich schrieb, das Hinzufügen dieser ausführlichen Logik zum Überprüfen, ob eine Transaktion bereits geöffnet ist, und das Festschreiben / Zurücksetzen von Änderungen, wenn meine Methode sie geöffnet hat.

Das Problem war konzeptionell. Ich hätte nicht versuchen sollen, jedes mögliche Szenario anzunehmen. Der richtige Ansatz bestand darin, die Transaktionslogik in einer einzigen Methode unterzubringen, wobei eine zweite Methode als Parameter verwendet wurde, der die eigentliche Datenbanklogik ausführen würde. Diese Logik setzt voraus, dass die Transaktion offen ist und führt nicht einmal eine Prüfung durch. Diese Methoden können in Kombination aufgerufen werden, damit diese Methoden nicht mit unnötiger Transaktionslogik überfüllt sind.

Der Grund, warum ich dies erwähne, ist, dass mein Fehler darin bestand anzunehmen, dass ich meine Methode in jeder Situation zum Laufen bringen musste. Dabei überprüfte meine aufgerufene Methode nicht nur, ob eine Transaktion geöffnet war, sondern auch die von ihr aufgerufenen. In diesem Fall handelt es sich nicht um einen großen Leistungseinbruch, aber wenn ich beispielsweise das Vorhandensein eines Datensatzes in der Datenbank überprüfen müsste, bevor ich fortfahre, würde ich nach jeder Methode suchen, die dies erfordert, wenn ich dies nur hätte annehmen sollen Der Anrufer sollte darauf hingewiesen werden, dass der Datensatz vorhanden sein sollte. Wenn die Methode trotzdem aufgerufen wird, ist dies ein undefiniertes Verhalten, und Sie müssen sich keine Gedanken darüber machen, was passiert.

Vielmehr sollten Sie ausreichend Dokumentation bereitstellen und schreiben, was Sie für wahr halten, bevor Ihre Methode aufgerufen wird. Wenn es wichtig genug ist, fügen Sie es als Kommentar vor Ihrer Methode hinzu, damit es keinen Fehler gibt (javadoc bietet eine gute Unterstützung für diese Art von Dingen in Java).

Ich hoffe das hilft!

Neil
quelle
2

Sie könnten zwei überladene Funktionen haben. Auf diese Weise können Sie beide je nach Situation verwenden.

Man kann nie (ich habe es noch nie gesehen) für alles optimieren, also muss man sich mit etwas zufrieden geben. Wählen Sie, was Ihrer Meinung nach wichtiger ist.

cauchy
quelle
Leider mache ich viel in VBA und Überladung ist keine Option. Ich könnte jedoch einen OptionalParameter verwenden, um einen ähnlichen Effekt zu erzielen.
Caltor
2

2 Funktionen: Man öffnet das Recordset und übergibt es an eine Datenanalysefunktion.

Die erste kann umgangen werden, wenn Sie bereits ein offenes Recordset haben. Der zweite kann davon ausgehen, dass ein offenes Recordset übergeben wird, wobei ignoriert wird, woher es stammt, und die Daten verarbeitet werden.

Sie haben dann sowohl Leistung als auch Wiederverwendbarkeit!

gbjbaanb
quelle
Ich denke nicht, dass es notwendig ist, das Recordset für den Anrufer zu öffnen, aber ansonsten stimme ich zu.
Neil
0

Die Optimierung (neben der Mikrooptimierung) steht in direktem Widerspruch zur Modularität.

Modularität isoliert Code vom globalen Kontext, während die Leistungsoptimierung den globalen Kontext ausnutzt, um die Aufgaben des Codes zu minimieren. Modularität ist der Vorteil einer niedrigen Kopplung, während (das Potenzial für) eine sehr hohe Leistung der Vorteil einer hohen Kopplung ist.

Die Antwort ist architektonisch. Betrachten Sie die Teile des Codes, die Sie wiederverwenden möchten. Vielleicht ist es die Preisberechnungskomponente oder die Konfigurationsvalidierungslogik.

Dann sollten Sie den Code schreiben, der mit dieser Komponente interagiert, um die Wiederverwendbarkeit zu gewährleisten. Innerhalb einer Komponente, in der Sie niemals nur einen Teil des Codes verwenden können, können Sie die Leistung optimieren, da Sie wissen, dass niemand anderes ihn verwenden wird.

Der Trick dabei ist zu bestimmen, was Ihre Komponenten sind.

tl; dr: zwischen Komponenten schreiben unter Berücksichtigung der Modularität, innerhalb von Komponenten unter Berücksichtigung der Leistung.

Schlitten
quelle
Modularität und Optimierung stehen nicht unbedingt im Widerspruch. Moderne Compiler können so ziemlich alles überall einbinden. Egal wie modular Sie schreiben, solange der Compiler es zu einer „nicht modularen ausführbaren Datei“ zusammenfügen kann, gibt es keinen Grund, warum es nicht so schnell sein kann wie geschriebener Code in erster Linie nicht modular. Natürlich können nicht alle Compiler dies sehr gut, aber ...
links um den
@leftaroundabout Nun, ich meinte auf der Quellcode-Ebene, aber Sie haben sehr Recht. Es gibt keinen Grund, warum ein ausreichend intelligenter Compiler Ihre Blasensortierung nicht durch eine schnelle Sortierung ersetzen könnte!
Schlitten