Muchos algoritmos utilizados en computación científica tienen una estructura inherente diferente a los algoritmos comúnmente considerados en formas menos intensivas en matemáticas de ingeniería de software. En particular, los algoritmos matemáticos individuales tienden a ser muy complejos, a menudo involucran cientos o miles de líneas de código, sin embargo, no involucran ningún estado (es decir, no actúan sobre una estructura de datos compleja) y a menudo pueden reducirse, en términos de programación. interfaz: a una sola función que actúa sobre una matriz (o dos).
Esto sugiere que una función, y no una clase, es la interfaz natural para la mayoría de los algoritmos encontrados en la informática científica. Sin embargo, este argumento ofrece poca información sobre cómo debe manejarse la implementación de algoritmos complejos de múltiples partes.
Si bien el enfoque tradicional ha sido simplemente tener una función que llame a varias otras funciones, pasando los argumentos relevantes en el camino, OOP ofrece un enfoque diferente, en el que los algoritmos se pueden encapsular como clases. Para mayor claridad, al encapsular un algoritmo en una clase, me refiero a crear una clase en la que las entradas del algoritmo se ingresen en el constructor de la clase, y luego se llama a un método público para invocar el algoritmo. Tal implementación de multigrid en psuedocode C ++ podría verse así:
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.]
}
Mi pregunta es la siguiente: ¿cuáles son los beneficios y los inconvenientes de este tipo de práctica en comparación con un enfoque más tradicional sin clases? ¿Hay problemas de extensibilidad o mantenibilidad? Para ser claros, no tengo la intención de solicitar una opinión, sino más bien de comprender mejor los efectos posteriores (es decir, los que podrían no surgir hasta que una base de código se vuelva bastante grande) de adoptar una práctica de codificación de este tipo.
fuente
Respuestas:
Después de haber realizado software numérico durante 15 años, puedo afirmar sin ambigüedades lo siguiente:
La encapsulación es importante. No desea pasar punteros a los datos (como sugiere) ya que expone el esquema de almacenamiento de datos. Si expone el esquema de almacenamiento, nunca podrá volver a cambiarlo porque accederá a los datos en todo el programa. La única forma de evitar esto es encapsular los datos en variables miembro privadas de una clase y dejar que solo las funciones miembro actúen sobre ella. Si leo su pregunta, piensa en una función que computa los valores propios de una matriz como sin estado, tomando un puntero a las entradas de la matriz como argumento y devolviendo los valores propios de alguna manera. Creo que esta es la forma incorrecta de pensarlo. En mi opinión, esta función debería ser una función miembro "constante" de una clase, no porque cambie la matriz, sino porque es una que opera con los datos.
La mayoría de los lenguajes de programación OO le permiten tener funciones de miembro privadas. Esta es su forma de separar un algoritmo grande en uno más pequeño. Por ejemplo, las diversas funciones auxiliares que necesita para el cálculo del valor propio todavía operan en la matriz, por lo que, naturalmente, serían funciones miembro privadas de una clase de matriz.
En comparación con muchos otros sistemas de software, puede ser cierto que las jerarquías de clases a menudo son menos importantes que, por ejemplo, en las interfaces gráficas de usuario. Ciertamente, hay lugares en el software numérico donde son prominentes: Jed describe una respuesta diferente a este hilo, a saber, las muchas formas en que uno puede representar una matriz (o, más generalmente, un operador lineal en un espacio vectorial de dimensiones finitas). PETSc hace esto de manera muy consistente, con funciones virtuales para todas las operaciones que actúan sobre matrices (no lo llaman "funciones virtuales", pero eso es lo que es). Hay otras áreas en los códigos típicos de elementos finitos donde uno usa este principio de diseño del software OO. Los que vienen a la mente son los muchos tipos de fórmulas de cuadratura y los muchos tipos de elementos finitos, todos los cuales están naturalmente representados como una interfaz / muchas implementaciones. Las descripciones de la ley material también se incluirían en este grupo. Pero puede ser cierto que se trata de eso y que el resto de un código de elementos finitos no usa la herencia de manera tan generalizada como se puede usar, digamos, en las GUI.
A partir de estos tres puntos, debe quedar claro que la programación orientada a objetos también es definitivamente aplicable a los códigos numéricos, y que sería una tontería ignorar los muchos beneficios de este estilo. Puede ser cierto que BLAS / LAPACK no use este paradigma (y que la interfaz habitual expuesta por MATLAB tampoco), pero me atrevería a suponer que todo software numérico exitoso escrito en los últimos 10 años es, de hecho, orientado a objetos.
fuente
La encapsulación y la ocultación de datos son extremadamente importantes para las bibliotecas extensibles en informática científica. Considere las matrices y los solucionadores lineales como dos ejemplos. Un usuario solo necesita saber que un operador es lineal, pero puede tener una estructura interna como la dispersión, un núcleo, una representación jerárquica, un producto tensor o un complemento de Schur. En todos los casos, los métodos de Krylov no dependen de los detalles del operador, solo dependen de la acción de la
MatMult
función (y quizás de su adjunto). Del mismo modo, el usuario de una interfaz de solucionador lineal (por ejemplo, un solucionador no lineal) solo se preocupa de que el problema lineal esté resuelto, y no debe necesitar o querer especificar el algoritmo que se utiliza. De hecho, especificar tales cosas impediría la capacidad del solucionador no lineal (u otra interfaz externa).Las interfaces son buenas. Dependiendo de una implementación es malo. Ya sea que logre esto usando clases C ++, objetos C, clases de tipo Haskell o alguna otra característica del lenguaje es intrascendente. La capacidad, robustez y extensibilidad de una interfaz es lo que importa en las bibliotecas científicas.
fuente
Las clases deben usarse solo si la estructura del código es jerárquica. Como está mencionando Algoritmos, su estructura natural es un diagrama de flujo, no una jerarquía de objetos.
En el caso de OpenFOAM, la parte algorítmica se implementa en términos de operadores genéricos (div, grad, curl, etc.) que son básicamente funciones abstractas que operan en diferentes tipos de tensores, utilizando diferentes tipos de esquemas numéricos. Esta parte del código se construye básicamente a partir de muchos algoritmos genéricos que operan en clases. Esto le permite al cliente escribir algo como:
Las jerarquías, como los modelos de transporte, los modelos de turbulencia, los esquemas de diferenciación, los esquemas de gradiente, las condiciones de contorno, etc., se implementan en términos de clases C ++ (nuevamente, genérico en las cantidades de tensor).
Noté una estructura similar en la biblioteca CGAL, donde los diversos algoritmos se agrupan como grupos de objetos de función agrupados con información geométrica para formar núcleos geométricos (clases), pero esto se hace nuevamente para separar las operaciones de la geometría (eliminación de puntos de una cara, desde un tipo de datos de punto).
Estructura jerárquica ==> clases
Procedimiento, diagrama de flujo ==> algoritmos
fuente
Incluso si esta es una vieja pregunta, creo que vale la pena mencionar la solución particular de Julia . Lo que hace este lenguaje es "OOP sin clase": las construcciones principales son tipos, es decir, objetos de datos compuestos similares a
struct
s en C, en los que se define una relación de herencia. Los tipos no tienen "funciones miembro", pero cada función tiene una firma de tipo y acepta subtipos. Por ejemplo, usted podría tener un resumenMatrix
tipo y subtiposDenseMatrix
,SparseMatrix
y tener un método genéricodo_something(a::Matrix, b::Matrix)
con la especializacióndo_something(a::SparseMatrix, b::SparseMatrix)
. El despacho múltiple se utiliza para seleccionar la versión más apropiada para llamar.Este enfoque es más poderoso que la OOP basada en clases, que es equivalente al despacho basado en la herencia solo en el primer argumento, si adopta la convención de que "un método es una función con
this
su primer parámetro" (común, por ejemplo, en Python). Se puede emular alguna forma de envío múltiple en, por ejemplo, C ++, pero con contorsiones considerables .La principal distinción es que los métodos no pertenecen a clases, pero existen como entidades separadas y la herencia puede ocurrir en todos los parámetros.
Algunas referencias:
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/
fuente
Dos ventajas del enfoque OO podrían ser:
calculate_alpha()
calculate_beta()
calculate_alpha()
calculate_f()
set_z()
calculate_f()
fuente