2010-05-03 25 views
9

caso de una biblioteca de software matriz de tener una clase de raíz (por ejemplo, MatrixBase) de la que más especializado (o más limitados) clases de matriz (por ejemplo, SparseMatrix, UpperTriangluarMatrix, etc.) derivar? En caso afirmativo, ¿deberían derivarse las clases derivadas públicamente/de forma protectora/privada? Si no, ¿deberían estar compuestos con una clase de implementación que encapsule la funcionalidad común y que de otro modo no esté relacionada? ¿Algo más?jerarquía de clases C++ Matrix

Estaba conversando sobre esto con un colega desarrollador de software (no soy per se) que mencionó que es un error común de diseño de programación derivar una clase más restringida de una más general (por ejemplo, utilizó el ejemplo de cómo no fue una buena idea derivar una clase Circle de una clase Ellipse como similar al problema de diseño de la matriz) incluso cuando es cierto que un SparseMatrix "ES A" MatrixBase. La interfaz presentada por las clases base y derivada debe ser la misma para las operaciones básicas; para operaciones especializadas, una clase derivada tendría funcionalidad adicional que podría no ser posible implementar para un objeto arbitrario MatrixBase. Por ejemplo, podemos calcular la descomposición de Cholesky solo para un objeto de clase PositiveDefiniteMatrix; sin embargo, la multiplicación por un escalar debería funcionar de la misma manera tanto para la base como para las clases derivadas. Además, incluso si la implementación de almacenamiento de datos subyacente difiere, el operator()(int,int) debería funcionar como se espera para cualquier tipo de clase de matriz.

He comenzado a buscar en algunas bibliotecas de matriz de código abierto y parece que esto es una especie de mezcla (o tal vez estoy buscando en una mezcla de bibliotecas). Estoy planeando ayudar con una refacturación de una biblioteca matemática donde este ha sido un punto de discusión y me gustaría tener opiniones (a menos que realmente haya una respuesta correcta a esta pregunta) en cuanto a qué diseño la filosofía sería lo mejor y cuáles son los pros y los contras de cualquier enfoque razonable.

Respuesta

0

¿Sería útil la posibilidad de tener una clase base Matrix que tenga métodos que le permitan construir la matriz específica? Por ejemplo algo como (un ejemplo muy simple):

MatrixClass m; 
m.buildRotationMatrix(/*params*/) 
// Now m is a rotation matrix 

Esto se utiliza en el marco OpenSceneGraph y funciona bien para nuestros propósitos. Sin embargo, los métodos de compilación son simplemente rotación o inversa y similares. Pero creo que te permitiría evitar el problema de derivar muchas subclases de matriz.

4

El problema con la subclase Circle de una elipse (o una subclase cuadrada de un rectángulo) se produce cuando puede modificar una dimensión por interfaz Ellipse, de modo que el círculo ya no es un círculo (y el cuadrado ya no es cuadrado)

Si solo permite matrices no modificables, entonces está seguro y puede estructurar su jerarquía de tipos de forma natural.

+1

+1 para "Circle is-a Ellipse only when al observar". – avakar

+0

¿Es esto un "voto" para usar datos inmutables? – bpw1621

+0

Sí, prefiero datos inmutables, aunque sé que eso no es tan común en C++. – starblue

1

hehehe. Al principio leí que tu amigo decía que un Círculo debería ser una Elipse y escribí una larga diatriba sobre por qué estaban llenos.

Debes escuchar a tu amigo, excepto que espero que no estén diciendo que una SparseMatrix "es-una" MatrixBase. El término significa cosas diferentes en el mundo real frente al mundo de modelado. En el mundo de la modelación, "is-a" significa seguir el Principio de sustitución de Liskov (¡búscalo!). Alternativamente, significa que SparseMatrix debe cumplir el contrato de MatrixBase en el que las funciones de los miembros no deben exigir condiciones previas adicionales y no deben cumplir ninguna condición posterior.

No sé exactamente cómo se aplica esto a la cuestión de la matriz, pero si nos fijamos en los términos que usé en el párrafo anterior (LSP y Diseño por contrato), entonces debería estar en camino de aprender la respuesta a tu problema.

Una forma en que podría aplicarse en su caso es tomar las diversas características comunes en su jerarquía y convertirlas en interfaces abstractas. A continuación, herede de estas interfaces en aquellas clases que responden a ellas correctamente. Esto le permitiría escribir funciones que deberían permitir el uso común y aún así conservar la separación cuando hay demasiada variación.

+0

Gracias por los consejos a las referencias. No me di cuenta de que había todo un artículo de wikipedia sobre este punto: http://en.wikipedia.org/wiki/Circle-ellipse_problem – bpw1621

1

Esta es una buena pregunta, pero todavía no estoy seguro de cuáles son las métricas por las que desea evaluar esto.

Por lo que vale la pena, la biblioteca de una matriz que uso actualmente es el más Armadillo tiene un Base objeto común utilizando el patrón de remplate curiosamente recurrente. Creo que Eigen (otra biblioteca Matrix reciente y con muchas plantillas) hace lo mismo.

+0

Las métricas serían las habituales para el software en general (corrección, eficiencia, facilidad de mantenimiento) , etc.) y sin información adicional para proporcionar suponer en cualquier orden que clasifique usted mismo para una biblioteca de matriz. – bpw1621

+0

Entonces, ¿cómo funciona el CRTP exactamente aquí? ¿No es efectivamente un patrón de downcasting (que generalmente se llama un olor de código en SO)? Por ejemplo, plantilla clase MatrixBase {void interface() {static_cast (this) -> implementation(); }}; UpperTriangularMatrix: MatrixBase {void implementation(); }; Entonces, si la interfaz y la implementación se refieren a la multiplicación de matrices, ¿la idea es que la llamada de interfaz delegue a la llamada de implementación correcta en la clase derivada? Uso, ¿cliente solo llama a la interfaz? – bpw1621

+0

¿Hay alguna forma de formatear comentarios como si hubiera mensajes? El backtick no funcionó en el comentario anterior. – bpw1621

0

Si hay suficientes métodos y miembros comunes para garantizar una clase base, entonces debe haber uno y herencia. No utilizaría una clase base como un tipo común para todas las matrices, sino más bien como un contenedor de métodos y miembros comunes (proteger a los constructores).

A diferencia de Java, no todas las clases o estructuras necesitan una clase base. Recuerde la simplicidad; la complejidad hace que los proyectos sean más largos, más difíciles de administrar y más difíciles de corregir.

1

El problema principal a tener en cuenta con diseños heredados como este es SLICING.

Digamos que MatrixBase define un operador de asignación no virtual. Copia todos los miembros de datos comunes a todas las subclases de matriz. Su clase SparseMatrix define miembros de datos adicionales. Ahora que pasa cuando escribimos esto?

SparseMatrix sm(...); 
MatrixBase& bm = sm; 
bm = some_dense_matrix; 

Este código no tiene mucho sentido (tratando de asignar un DenseMatrix a una matriz dispersa directamente a través de un operador definido en la clase base) y es propenso a todo tipo de comportamiento rebanar desagradable, sin embargo, es un aspecto vulnerable de dicho código y hay una gran posibilidad de que esto ocurra en algún momento si proporciona operadores de asignación accesibles a través de MatrixBase */MatrixBase &. Incluso cuando tenemos esto:

SparseMatrix sm(...); 
MatrixBase& bm = sm; 
bm = some_other_sparase_matrix; 

... todavía tenemos problemas de corte debido a que el operador de asignación no es virtual. Sin la herencia de una clase base común, podemos proporcionarles a los operadores de asignación una copia significativa de una matriz densa a una matriz dispersa, pero tratar de hacer esto a través de una clase base común es propensa a todo tipo de problemas.

¡En general, se deben evitar los operadores de asignación para las clases base! Imagine un caso en el que Dog y Cat heredan de Mammal and Mammal proporcionó un operador de asignación, virtual o no. Implicaría que podemos asignar Dogs to Cats, lo cual no tiene sentido e incluso si el operador fuera virtual, sería difícil proporcionar algún tipo de comportamiento significativo para asignar mamíferos a otros mamíferos.

Digamos que tratamos de mejorar la situación implementando el operador de asignación en Dog para que solo se le puedan asignar otros perros. Ahora, ¿qué sucede cuando heredamos de Dog para crear Chihuahua y Doberman?No deberíamos poder asignar Chihuahuas a Dobermans, por lo que el caso original se repite recursivamente hasta que estés seguro de que has llegado a los nodos hoja de una jerarquía de herencia (es una pena que C++ no tenga una palabra clave final para evitar herencia adicional).

El mismo problema es evidente con el ejemplo común de Circle hereda Ellipse. El círculo puede requerir que coincidan el ancho y el alto: eso es una constante que la clase quiere mantener, sin embargo, cualquiera puede simplemente obtener un puntero base (Elipse *) apuntando a un objeto Circle y violar esa regla.

En caso de duda, evite la herencia, ya que es una de las características más utilizadas de C++ y de cualquier lenguaje que admita la programación orientada a objetos en general. Puede intentar solucionar el problema proporcionando mecanismos de tiempo de ejecución para determinar el tipo de subclase asignada a otra subclase y solo permitir tipos coincidentes, pero ahora está haciendo un montón de trabajo adicional y incurriendo en gastos generales de tiempo de ejecución. Es mejor evitar los operadores de asignación todos juntos para jerarquías de herencia y confiar en métodos como clonar para producir copias (patrón de prototipo).

Por lo tanto, si elige hacer una jerarquía de herencia de sus clases de matriz, debe pensar cuidadosamente si las ventajas (más probable a corto plazo) de heredar superan las desventajas a largo plazo. También debe asegurarse de evitar todos los casos potenciales en los que se puede cortar, lo que puede ser muy difícil de hacer para una biblioteca de matriz sin comprometer su usabilidad y eficiencia.