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.
la source
Réponses:
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.
la source
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
MatMult
fonction (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.
la source
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:
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
la source
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 à
struct
s 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éMatrix
type et sous - typesDenseMatrix
,SparseMatrix
et une méthode génériquedo_something(a::Matrix, b::Matrix)
avec une spécialisationdo_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
this
comme 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/
la source
Deux avantages de l'approche OO pourraient être:
calculate_alpha()
calculate_beta()
calculate_alpha()
calculate_f()
set_z()
calculate_f()
nouveau, seule la partie du calcul qui dépend dela source