Quels sont les avantages et les inconvénients inhérents à l'utilisation de classes pour encapsuler des algorithmes numériques?

13

De nombreux algorithmes utilisés dans le calcul scientifique ont une structure inhérente différente de celle des algorithmes couramment considérés dans les formes de génie logiciel moins gourmandes en mathématiques. En particulier, les algorithmes mathématiques individuels ont tendance à être très complexes, impliquant souvent des centaines ou des milliers de lignes de code, mais n'impliquent néanmoins aucun état (c'est-à-dire qu'ils n'agissent pas sur une structure de données complexe) et peuvent souvent être résumés - en termes de programmation interface - à une seule fonction agissant sur un tableau (ou deux).

Cela suggère qu'une fonction, et non une classe, est l'interface naturelle de la plupart des algorithmes rencontrés dans le calcul scientifique. Pourtant, cet argument offre peu de renseignements sur la façon dont la mise en œuvre d'algorithmes complexes en plusieurs parties doit être gérée.

Alors que l'approche traditionnelle consistait à avoir simplement une fonction qui appelle un certain nombre d'autres fonctions, en passant les arguments pertinents en cours de route, la POO propose une approche différente, dans laquelle les algorithmes peuvent être encapsulés en tant que classes. Pour plus de clarté, en encapsulant un algorithme dans une classe, je veux dire créer une classe dans laquelle les entrées de l'algorithme sont entrées dans le constructeur de classe, puis une méthode publique est appelée pour réellement invoquer l'algorithme. Une telle implémentation de multigrid en psuedocode C ++ pourrait ressembler à:

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.]
}

Ma question est alors la suivante: quels sont les avantages et les inconvénients de ce type de pratique par rapport à une approche plus traditionnelle sans cours? Y a-t-il des problèmes d'extensibilité ou de maintenabilité? Pour être clair, je n'ai pas l'intention de solliciter l'opinion, mais plutôt de mieux comprendre les effets en aval (c'est-à-dire ceux qui pourraient ne pas se produire jusqu'à ce qu'une base de code devienne assez grande) d'adopter une telle pratique de codage.

Ben
la source
2
C'est toujours un mauvais signe lorsque le nom de votre classe est un adjectif plutôt qu'un nom.
David Ketcheson
3
Une classe peut servir d'espace de noms sans état pour organiser des fonctions afin de gérer la complexité, mais il existe d'autres façons de gérer la complexité dans les langages qui fournissent des classes. (Les espaces de noms en C ++ et les modules en Python me viennent à l'esprit.)
Geoff Oxberry
@GeoffOxberry Je ne peux pas dire si c'est une bonne ou une mauvaise utilisation - c'est pourquoi je demande en premier lieu - mais les classes, contrairement aux espaces de noms ou aux modules, peuvent également gérer "l'état temporaire", par exemple la hiérarchie de la grille en multigrille, qui est jeté à la fin de l'algorithme.
Ben

Réponses:

13

Ayant fait du logiciel numérique pendant 15 ans, je peux affirmer sans ambiguïté ce qui suit:

  • L'encapsulation est importante. Vous ne voulez pas faire circuler les pointeurs vers les données (comme vous le suggérez) car cela expose le schéma de stockage des données. Si vous exposez le schéma de stockage, vous ne pourrez plus jamais le modifier car vous accéderez aux données sur l'ensemble du programme. La seule façon d'éviter cela est d'encapsuler les données dans des variables membres privées d'une classe et de ne laisser agir que les fonctions membres. Si je lis votre question, vous pensez à une fonction qui calcule les valeurs propres d'une matrice comme étant sans état, prenant un pointeur vers les entrées de la matrice comme argument et renvoyant les valeurs propres d'une manière ou d'une autre. Je pense que c'est la mauvaise façon d'y penser. À mon avis, cette fonction devrait être une fonction membre "const" d'une classe - non pas parce qu'elle modifie la matrice, mais parce qu'elle est celle qui fonctionne avec les données.

  • La plupart des langages de programmation OO vous permettent d'avoir des fonctions membres privées. C'est votre façon de séparer un grand algorithme en un plus petit. Par exemple, les diverses fonctions d'assistance dont vous avez besoin pour le calcul des valeurs propres fonctionnent toujours sur la matrice, et seraient donc naturellement des fonctions membres privées d'une classe matricielle.

  • Par rapport à de nombreux autres systèmes logiciels, il peut être vrai que les hiérarchies de classes sont souvent moins importantes que, disons, dans les interfaces utilisateur graphiques. Il y a certainement des endroits dans le logiciel numérique où ils sont importants - Jed décrit une dans une autre réponse à ce fil, à savoir les nombreuses façons dont on peut représenter une matrice (ou, plus généralement, un opérateur linéaire sur un espace vectoriel de dimension finie). PETSc le fait de manière très cohérente, avec des fonctions virtuelles pour toutes les opérations qui agissent sur des matrices (ils ne l'appellent pas "fonctions virtuelles", mais c'est ce que c'est). Il existe d'autres domaines dans les codes d'éléments finis typiques où l'on utilise ce principe de conception du logiciel OO. Ceux qui me viennent à l'esprit sont les nombreux types de formules de quadrature et les nombreux types d'éléments finis, qui sont tous naturellement représentés comme une seule interface / plusieurs implémentations. Les descriptions de droit matériel relèveraient également de ce groupe. Mais il peut être vrai que c'est à peu près tout et que le reste d'un code d'éléments finis n'utilise pas l'héritage de manière aussi omniprésente que l'on peut l'utiliser, par exemple, dans les interfaces graphiques.

À partir de ces trois points seulement, il devrait être clair que la programmation orientée objet est également très certainement applicable aux codes numériques, et qu'il serait stupide d'ignorer les nombreux avantages de ce style. Il est peut-être vrai que BLAS / LAPACK n'utilise pas ce paradigme (et que l'interface habituelle exposée par MATLAB non plus), mais je me risquerais à penser que chaque logiciel numérique réussi écrit au cours des 10 dernières années est, en fait, orienté objet.

Wolfgang Bangerth
la source
16

L'encapsulation et le masquage des données sont extrêmement importants pour les bibliothèques extensibles dans le calcul scientifique. Considérez les matrices et les solveurs linéaires comme deux exemples. Un utilisateur a juste besoin de savoir qu'un opérateur est linéaire, mais il peut avoir une structure interne telle que la rareté, un noyau, une représentation hiérarchique, un produit tensoriel ou un complément Schur. Dans tous les cas, les méthodes de Krylov ne dépendent pas des détails de l'opérateur, elles ne dépendent que de l'action de la MatMultfonction (et peut-être de son adjoint). De même, l'utilisateur d'une interface de solveur linéaire (par exemple un solveur non linéaire) se soucie seulement que le problème linéaire est résolu, et ne devrait pas avoir besoin ou vouloir spécifier l'algorithme qui est utilisé. En effet, spécifier de telles choses entraverait la capacité du solveur non linéaire (ou d'une autre interface externe).

Les interfaces sont bonnes. Dépendre d'une implémentation est mauvais. Que vous réalisiez cela en utilisant des classes C ++, des objets C, des classes de types Haskell ou une autre fonctionnalité de langage est sans conséquence. La capacité, la robustesse et l'extensibilité d'une interface sont ce qui compte dans les bibliothèques scientifiques.

Jed Brown
la source
8

Les classes ne doivent être utilisées que si la structure du code est hiérarchique. Puisque vous parlez d'algorithmes, leur structure naturelle est un organigramme, pas une hiérarchie d'objets.

Dans le cas d'OpenFOAM, la partie algorithmique est implémentée en termes d'opérateurs génériques (div, grad, curl, etc.) qui sont essentiellement des fonctions abstraites opérant sur différents types de tenseurs, utilisant différents types de schémas numériques. Cette partie du code est essentiellement construite à partir de nombreux algorithmes génériques opérant sur des classes. Cela permet au client d'écrire quelque chose comme:

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

Les hiérarchies telles que les modèles de transport, les modèles de turbulence, les schémas de différenciation, les schémas de gradient, les conditions aux limites, etc. sont implémentées en termes de classes C ++ (là encore, génériques sur les quantités de tenseur).

J'ai remarqué une structure similaire dans la bibliothèque CGAL, où les différents algorithmes sont regroupés sous la forme de groupes d'objets de fonction regroupés avec des informations géométriques pour former des noyaux géométriques (classes), mais cela est à nouveau effectué pour séparer les opérations de la géométrie (suppression de points de une face, à partir d'un type de données ponctuelles).

Structure hiérarchique ==> classes

Organigramme procédural ==> algorithmes

tmaric
la source
5

Même si c'est une vieille question, je pense qu'il vaut la peine de mentionner la solution particulière de Julia . Ce que fait ce langage est un "POO sans classe": les principales constructions sont des types, c'est-à-dire des objets de données composites semblables à structs dans C, sur lesquels une relation d'héritage est définie. Les types n'ont pas de "fonctions membres", mais chaque fonction a une signature de type et accepte des sous-types. Par exemple, vous pourriez avoir un résumé Matrixtype et sous - types DenseMatrix, SparseMatrixet une méthode générique do_something(a::Matrix, b::Matrix)avec une spécialisation do_something(a::SparseMatrix, b::SparseMatrix). La répartition multiple est utilisée pour sélectionner la version la plus appropriée à appeler.

Cette approche est plus puissante que la POO basée sur les classes, qui est équivalente à la répartition basée sur l'héritage sur le premier argument uniquement, si vous adoptez la convention selon laquelle "une méthode est une fonction avec thiscomme premier paramètre" (commun par exemple en Python). Une certaine forme de répartition multiple peut être émulée, disons, en C ++, mais avec des contorsions considérables .

La principale distinction est que les méthodes n'appartiennent pas à des classes, mais elles existent en tant qu'entités distinctes et l'héritage peut se produire sur tous les paramètres.

Quelques références:

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/

Federico Poloni
la source
1

Deux avantages de l'approche OO pourraient être:

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

  • calculate_f()F(X,y,z)zset_z()zLe paramètre est marqué en interne comme «sale», de sorte que lorsque vous appelez à calculate_f()nouveau, seule la partie du calcul qui dépend dez est refaite.

ptomato
la source