Welche Vor- und Nachteile hat die Verwendung von Klassen zur Kapselung numerischer Algorithmen?


13

Viele Algorithmen, die beim wissenschaftlichen Rechnen verwendet werden, haben eine andere inhärente Struktur als Algorithmen, die üblicherweise in weniger mathematisch intensiven Formen des Software-Engineerings berücksichtigt werden. Insbesondere einzelne mathematische Algorithmen sind in der Regel hochkomplex und umfassen häufig Hunderte oder Tausende von Codezeilen, enthalten jedoch keinen Zustand (dh sie wirken nicht auf eine komplexe Datenstruktur ein) und können oftmals programmgesteuert zusammengefasst werden Schnittstelle - zu einer einzelnen Funktion, die auf ein Array (oder zwei) wirkt.

Dies legt nahe, dass eine Funktion und keine Klasse die natürliche Schnittstelle zu den meisten Algorithmen ist, die im wissenschaftlichen Rechnen anzutreffen sind. Dieses Argument gibt jedoch wenig Aufschluss darüber, wie die Implementierung komplexer mehrteiliger Algorithmen zu handhaben ist.

Während der traditionelle Ansatz darin bestand, nur eine Funktion zu haben, die eine Reihe anderer Funktionen aufruft und dabei die relevanten Argumente weitergibt, bietet OOP einen anderen Ansatz, bei dem Algorithmen als Klassen eingekapselt werden können. Der Klarheit halber meine ich durch Einkapseln eines Algorithmus in eine Klasse das Erstellen einer Klasse, in der die Algorithmeneingaben in den Klassenkonstruktor eingegeben werden, und dann wird eine öffentliche Methode aufgerufen, um den Algorithmus tatsächlich aufzurufen. Eine solche Implementierung von Multigrid in C ++ - Pseudocode könnte folgendermaßen aussehen:

class multigrid {
    private:
        x_, b_
        [grid structure]

        restrict(...)
        interpolate(...)
        relax(...)
    public:
        multigrid(x,b) : x_(x), b_(b) { }
        run()
}

multigrid::run() {
     [call restrict, interpolate, relax, etc.]
}

Meine Frage lautet dann wie folgt: Was sind die Vor- und Nachteile dieser Art von Praxis im Vergleich zu einem traditionelleren Ansatz ohne Unterricht? Gibt es Probleme mit der Erweiterbarkeit oder Wartbarkeit? Zweifellos möchte ich keine Meinung einholen, sondern die nachgelagerten Auswirkungen (dh diejenigen, die möglicherweise erst auftreten, wenn eine Codebasis ziemlich groß wird) einer solchen Codierungspraxis besser verstehen.


2
Es ist immer ein schlechtes Zeichen, wenn Ihr Klassenname eher ein Adjektiv als ein Substantiv ist.
David Ketcheson

3
Eine Klasse kann als zustandsloser Namespace zum Organisieren von Funktionen dienen, um die Komplexität zu verwalten. Es gibt jedoch auch andere Möglichkeiten, um die Komplexität in Sprachen zu verwalten, die Klassen bereitstellen. (Namespaces in C ++ und Module in Python kommen in den Sinn.)
Geoff Oxberry

@GeoffOxberry Ich kann nicht sagen, ob dies eine gute oder eine schlechte Verwendung ist - weshalb ich in erster Linie frage -, aber Klassen können im Gegensatz zu Namespaces oder Modulen auch einen "temporären Status" verwalten, z. B. die Rasterhierarchie In Multigrid wird dies nach Abschluss des Algorithmus verworfen.
Ben

Antworten:


13

Nachdem ich 15 Jahre lang numerische Software entwickelt habe, kann ich Folgendes eindeutig angeben:

  • Kapselung ist wichtig. Sie möchten keine Zeiger auf Daten weitergeben (wie Sie vorschlagen), da hierdurch das Datenspeicherschema verfügbar gemacht wird. Wenn Sie das Speicherschema freigeben, können Sie es nie wieder ändern, da Sie im gesamten Programm auf die Daten zugreifen. Die einzige Möglichkeit, dies zu vermeiden, besteht darin, die Daten in private Membervariablen einer Klasse zu kapseln und nur Memberfunktionen darauf einwirken zu lassen. Wenn ich Ihre Frage lese, denken Sie an eine Funktion, die die Eigenwerte einer Matrix als zustandslos berechnet, einen Zeiger auf die Matrixeinträge als Argument nimmt und die Eigenwerte auf irgendeine Weise zurückgibt. Ich denke, das ist der falsche Weg, darüber nachzudenken. Meiner Ansicht nach sollte diese Funktion eine "const" -Mitgliedsfunktion einer Klasse sein - nicht, weil sie die Matrix ändert, sondern weil sie mit den Daten arbeitet.

  • In den meisten OO-Programmiersprachen können Sie private Member-Funktionen verwenden. Auf diese Weise können Sie einen großen Algorithmus in einen kleineren zerlegen. Zum Beispiel arbeiten die verschiedenen Hilfsfunktionen, die Sie für die Eigenwertberechnung benötigen, immer noch mit der Matrix und wären daher natürlich private Elementfunktionen einer Matrixklasse.

  • Im Vergleich zu vielen anderen Softwaresystemen sind Klassenhierarchien häufig weniger wichtig als beispielsweise in grafischen Benutzeroberflächen. Es gibt sicherlich Stellen in numerischer Software, an denen sie im Vordergrund stehen - Jed umreißt eine andere Antwort auf diesen Thread, nämlich die vielen Möglichkeiten, eine Matrix (oder allgemeiner einen linearen Operator auf einem endlichen dimensionalen Vektorraum) darzustellen. PETSc tut dies sehr konsequent mit virtuellen Funktionen für alle Operationen, die auf Matrizen wirken (sie nennen es nicht "virtuelle Funktionen", aber so ist es). Es gibt andere Bereiche in typischen Finite-Elemente-Codes, in denen dieses Konstruktionsprinzip von OO-Software verwendet wird. Diejenigen, die in den Sinn kommen, sind die vielen Arten von Quadraturformeln und die vielen Arten von finiten Elementen, alle von denen sind natürlich als eine Schnittstelle / viele Implementierungen dargestellt. Materialgesetzliche Beschreibungen würden ebenfalls in diese Gruppe fallen. Es ist jedoch möglich, dass dies der Fall ist und der Rest eines Finite-Elemente-Codes die Vererbung nicht so häufig verwendet, wie dies beispielsweise in GUIs der Fall ist.

Anhand dieser drei Punkte sollte klar sein, dass die objektorientierte Programmierung auf jeden Fall auch auf numerische Codes anwendbar ist und dass es dumm wäre, die vielen Vorteile dieses Stils zu ignorieren. Es mag richtig sein, dass BLAS / LAPACK dieses Paradigma nicht verwendet (und das übliche Interface von MATLAB auch nicht), aber ich würde die Vermutung wagen, dass jede erfolgreiche numerische Software, die in den letzten 10 Jahren geschrieben wurde, tatsächlich objektorientierten.


16

Die Kapselung und das Verbergen von Daten sind für erweiterbare Bibliotheken im wissenschaftlichen Rechnen äußerst wichtig . Betrachten Sie Matrizen und lineare Löser als zwei Beispiele. Ein Benutzer muss nur wissen, dass ein Operator linear ist, er kann jedoch eine interne Struktur aufweisen, z. B. Sparsity, einen Kernel, eine hierarchische Darstellung, ein Tensorprodukt oder ein Schur-Komplement. In allen Fällen hängen die Krylov-Methoden nicht von den Details des Operators ab, sondern nur von der Wirkung der MatMultFunktion (und möglicherweise von ihrem Nebeneffekt). Ebenso kümmert sich der Benutzer einer linearen Löserschnittstelle (z. B. eines nichtlinearen Lösers) nur darum, dass das lineare Problem gelöst wird, und sollte den verwendeten Algorithmus nicht benötigen oder spezifizieren wollen. In der Tat würde das Spezifizieren solcher Dinge die Fähigkeit des nichtlinearen Lösers (oder einer anderen äußeren Schnittstelle) behindern.

Schnittstellen sind gut. Je nach Implementierung ist schlecht. Ob Sie dies mit C ++ - Klassen, C-Objekten, Haskell-Typenklassen oder einer anderen Sprachfunktion erreichen, spielt keine Rolle. In wissenschaftlichen Bibliotheken kommt es auf die Fähigkeit, Robustheit und Erweiterbarkeit einer Schnittstelle an.


8

Klassen sollten nur verwendet werden, wenn die Struktur des Codes hierarchisch ist. Da Sie Algorithmen erwähnen, ist ihre natürliche Struktur ein Flussdiagramm, keine Hierarchie von Objekten.

Im Fall von OpenFOAM wird der algorithmische Teil in Form von generischen Operatoren (div, grad, curl usw.) implementiert, die im Grunde genommen abstrakte Funktionen sind, die mit verschiedenen Tensortypen unter Verwendung verschiedener Arten von numerischen Schemata arbeiten. Dieser Teil des Codes basiert im Wesentlichen auf einer Vielzahl von generischen Algorithmen, die mit Klassen arbeiten. Auf diese Weise kann der Client Folgendes schreiben:

solve(ddt(U) + div(phi, U)  == rho*g + ...);

Hierarchien wie Transportmodelle, Turbulenzmodelle, Differenzierungsschemata, Gradientenschemata, Randbedingungen usw. werden in Form von C ++ - Klassen implementiert (wiederum generisch für die Tensorgrößen).

Ich habe eine ähnliche Struktur in der CGAL-Bibliothek festgestellt, in der die verschiedenen Algorithmen als Gruppen von Funktionsobjekten gepackt sind, die mit geometrischen Informationen gebündelt sind, um geometrische Kerne (Klassen) zu bilden ein Gesicht, von einem Punktdatentyp).

Hierarchische Struktur ==> Klassen

Prozedurales Flussdiagramm ==> Algorithmen


5

Auch wenn dies eine alte Frage ist, denke ich, dass es wert ist, Julias spezielle Lösung zu erwähnen . Was diese Sprache tut, ist "OOP ohne Klassen": Die Hauptkonstrukte sind Typen, dh zusammengesetzte Datenobjekte, die structs in C ähneln und für die eine Vererbungsbeziehung definiert ist. Die Typen haben keine "Mitgliedsfunktionen", aber jede Funktion hat eine Typensignatur und akzeptiert Untertypen. Zum Beispiel könnten Sie eine abstrakte haben MatrixArt und Subtypen DenseMatrix, SparseMatrixund eine generische Methode haben do_something(a::Matrix, b::Matrix)mit Spezialisierung do_something(a::SparseMatrix, b::SparseMatrix). Mehrfachversand wird verwendet, um die am besten geeignete Version für den Aufruf auszuwählen.

Dieser Ansatz ist leistungsfähiger als klassenbasiertes OOP, das nur dann für die Vererbung des ersten Arguments äquivalent ist, wenn Sie die Konvention übernehmen, dass "eine Methode eine Funktion mit thisihrem ersten Parameter ist" (üblich z. B. in Python). Einige Formen des Mehrfachversands können beispielsweise in C ++ emuliert werden, jedoch mit erheblichen Abweichungen .

Der Hauptunterschied besteht darin, dass Methoden nicht zu Klassen gehören, sondern als separate Entitäten existieren und die Vererbung für alle Parameter erfolgen kann.

Einige Referenzen:

http://docs.julialang.org/en/release-0.4/manual/methods/

http://assoc.tumblr.com/post/71454527084/cool-things-you-can-do-in-julia

https://thenewphalls.wordpress.com/2014/03/06/understanding-object-oriented-programming-in-julia-inheritance-part-2/


1

Zwei Vorteile des OO-Ansatzes könnten sein:

  • βαcalculate_alpha()αcalculate_beta()calculate_alpha()α

  • calculate_f()f(x,y,z)zset_z()zcalculate_f()z

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.