¿Cuáles son los beneficios y las desventajas inherentes al uso de clases para encapsular algoritmos numéricos?

13

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.

Ben
fuente
2
Siempre es una mala señal cuando el nombre de tu clase es un adjetivo en lugar de un sustantivo.
David Ketcheson
3
Una clase podría servir como un espacio de nombres sin estado para organizar funciones con el fin de gestionar la complejidad, pero hay otras formas de gestionar la complejidad en los idiomas que proporcionan clases. (Los espacios de nombres en C ++ y los módulos en Python me vienen a la mente.)
Geoff Oxberry
@ GeoffOxberry No puedo hablar sobre si este es un uso bueno o malo, por eso pregunto en primer lugar, pero las clases, a diferencia de los espacios de nombres o módulos, también pueden administrar el "estado temporal", por ejemplo, la jerarquía de la cuadrícula en multigrid, que se descarta al completar el algoritmo.
Ben

Respuestas:

13

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.

Wolfgang Bangerth
fuente
16

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 MatMultfunció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.

Jed Brown
fuente
8

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:

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

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

tmarico
fuente
5

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 structs 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 resumen Matrixtipo y subtipos DenseMatrix, SparseMatrixy tener un método genérico do_something(a::Matrix, b::Matrix)con la especialización do_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 thissu 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/

Federico Poloni
fuente
1

Dos ventajas del enfoque OO podrían ser:

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

  • calculate_f()F(X,y,z)zset_z()zcalculate_f()z

ptomato
fuente