¿Qué tan grandes bases de código no OO se administran?

27

Siempre veo que la abstracción es una característica muy útil que OO proporciona para administrar la base de código. Pero, ¿cómo se gestionan las grandes bases de código no OO? ¿O eventualmente se convierten en una " Gran Bola de Barro "?

Actualización:
Parecía que todo el mundo estaba pensando que la 'abstracción' era solo modularización u ocultación de datos. Pero en mi humilde opinión, también significa el uso de 'clases abstractas' o 'interfaces', que es imprescindible para la inyección de dependencia y, por lo tanto, para las pruebas. ¿Cómo las bases de código no OO manejan esto? Y además, aparte de la abstracción, la encapsulación también ayuda mucho a administrar bases de código grandes, ya que define y restringe la relación entre datos y funciones.

Con C, es muy posible escribir código pseudo-OO. No sé mucho sobre otros idiomas que no sean OO. Entonces, ¿es LA manera de administrar grandes bases de código C?

Gulshan
fuente
66
En un lenguaje agnóstico, por favor describa un objeto. ¿Qué es, cómo se modifica, qué debe heredar y qué debe proporcionar? El kernel de Linux está lleno de estructuras asignadas con muchos ayudantes y punteros de función, pero eso probablemente no satisfaría la definición de objetos orientados para la mayoría. Sin embargo, es uno de los mejores ejemplos de una base de código muy bien mantenida. ¿Por qué? Porque cada mantenedor del subsistema sabe qué hay en su área de responsabilidad.
Tim Post
De manera independiente del lenguaje, describa cómo ve que se manejan las bases de código y qué tiene que ver OO con esto.
David Thornley
@Tim Post Estoy interesado en la gestión del código fuente del kernel de Linux. ¿Podría describir más el sistema? ¿Quizás como respuesta con un ejemplo?
Gulshan
77
En los viejos tiempos, utilizamos enlaces separados para simulacros y trozos para pruebas unitarias. La inyección de dependencia es solo una técnica entre varias. La compilación condicional es otra.
Macneil
Creo que es una exageración referirse a bases de código grandes (OO o de otro tipo) como "administradas". Sería bueno tener una mejor definición del término central en su pregunta.
tottinge

Respuestas:

43

Parece pensar que la POO es el único medio para lograr la abstracción.

Si bien OOP es muy bueno para hacerlo, de ninguna manera es la única forma. Los grandes proyectos también pueden mantenerse manejables mediante una modularización intransigente (solo mire Perl o Python, los cuales han sobresalido en eso, y también lo hacen los lenguajes funcionales como ML y Haskell), y mediante el uso de mecanismos como plantillas (en C ++).

Konrad Rudolph
fuente
27
+1 Además, es posible escribir una "Big Ball of Mud" usando OOP si no sabes lo que estás haciendo.
Larry Coleman el
¿Qué pasa con las bases de código C?
Gulshan el
66
@Gulshan: Muchas bases de código C grandes son OOP. El hecho de que C no tenga clases no significa que la POO no se pueda lograr con un poco de esfuerzo. Además, C permite una buena modularización utilizando encabezados y el lenguaje PIMPL. No es tan cómodo o poderoso como los módulos en lenguajes modernos, pero una vez más es lo suficientemente bueno.
Konrad Rudolph el
99
C permite la modularización a nivel de archivo. La interfaz va en el archivo .h, las funciones disponibles públicamente en el archivo .c, y las variables y funciones privadas obtienen el staticmodificador de acceso adjunto.
David Thornley
1
@Konrad: si bien estoy de acuerdo con que OOP no es la única forma de hacerlo, creo que OP probablemente tenía en mente estrictamente C, que no es un lenguaje funcional ni dinámico. Así que dudo que mencionar a Perl y Haskell le sea de alguna utilidad. De hecho, considero que su comentario es más relevante y útil para OP ( no significa que no se pueda lograr OOP con un poco de esfuerzo ); puede considerar agregarlo como una respuesta separada con detalles adicionales, tal vez compatible con un fragmento de código o un par de enlaces. Al menos ganaría mi voto, y posiblemente OP. :)
Groo
11

Módulos, funciones (externas / internas), subrutinas ...

Como dijo Konrad, OOP no es la única forma de administrar grandes bases de código. De hecho, se escribió una gran cantidad de software antes (antes de C ++ *).

Torre
fuente
* Y sí, sé que C ++ no es el único que admite OOP, pero de alguna manera fue cuando ese enfoque comenzó a tomar inercia.
Torre
8

El principio de modularidad no se limita a los lenguajes orientados a objetos.

Paul Nathan
fuente
6

Siendo realistas cambios poco frecuentes (piense en los cálculos de jubilación de la Seguridad Social) y / o conocimiento profundamente arraigado porque las personas que mantienen tal sistema lo han estado haciendo por un tiempo (la toma cínica es seguridad laboral).

Las mejores soluciones son la validación repetible, por lo que me refiero a la prueba automatizada (por ejemplo, pruebas unitarias) y pruebas en humanos que siguen los pasos proscritos (por ejemplo, pruebas de regresión) "en lugar de hacer clic y ver qué se rompe".

Para comenzar a avanzar hacia algún tipo de prueba automatizada con una base de código existente, recomiendo leer Michael Feather's Working Effectively with Legacy Code , que detalla los enfoques para llevar las bases de código existentes hasta algún tipo de marco de prueba repetible OO o no. Esto lleva al tipo de ideas que otros han respondido, como la modularización, pero el libro describe el enfoque correcto para hacerlo sin romper las cosas.

semillas de naranja
fuente
+1 para el libro de Michael Feather. Cuando te sientas deprimido por una gran base de código feo, (re) léelo :)
Matthieu
5

Aunque la inyección de dependencia basada en interfaces o clases abstractas es una muy buena manera de hacer pruebas, no es necesaria. No olvide que casi cualquier lenguaje tiene un puntero de función o una evaluación, que puede hacer cualquier cosa que pueda hacer con una interfaz o clase abstracta (el problema es que pueden hacer más , incluidas muchas cosas malas, y que no lo hacen ' t en sí mismos proporcionan metadatos). Tal programa realmente puede lograr la inyección de dependencia con estos mecanismos.

Me ha resultado muy útil ser riguroso con los metadatos. En los lenguajes OO, las relaciones entre los bits de código están definidas (hasta cierto punto) por la estructura de clase, de una manera lo suficientemente estandarizada como para tener cosas como una API de reflexión. En lenguajes de procedimiento, puede ser útil inventarlos usted mismo.

También he encontrado que la generación de código es mucho más útil en un lenguaje de procedimiento (en comparación con un lenguaje orientado a objetos). Esto garantiza que los metadatos estén sincronizados con el código (ya que se usa para generarlo) y le da algo parecido a los puntos de corte de la programación orientada a aspectos: un lugar donde puede inyectar código cuando lo necesite. A veces es la única forma de hacer programación DRY en un entorno que puedo entender.

psr
fuente
3

En realidad, como ha descubierto recientemente , las funciones de primer orden son todo lo que necesita para la inversión de dependencia.

C admite funciones de primer orden e incluso cierres hasta cierto punto . Y las macros C son una característica poderosa para la programación genérica, si se manejan con el cuidado necesario.

Todo esta ahí. SGLIB es un buen ejemplo de cómo se puede usar C para escribir código altamente reutilizable. Y creo que hay mucho más por ahí.

back2dos
fuente
2

Incluso sin abstracción, la mayoría de los programas se dividen en secciones de algún tipo. Esas secciones generalmente se relacionan con tareas o actividades específicas y usted trabaja en ellas de la misma manera que trabajaría en los bits más específicos de los programas abstraídos.

En proyectos pequeños a medianos, esto es realmente más fácil de hacer con una implementación OO purista a veces.

Cuenta
fuente
2

La abstracción, las clases abstractas, la inyección de dependencias, la encapsulación, las interfaces, etc., no son la única forma de controlar grandes bases de código; Esta es una forma justa y orientada a objetos.

El secreto principal es evitar pensar OOP al codificar sin OOP.

La modularidad es la clave en los idiomas que no son OO. En C esto se logra tal como David Thornley acaba de mencionar en un comentario:

La interfaz va en el archivo .h, las funciones disponibles públicamente en el archivo .c, y las variables y funciones privadas obtienen el modificador de acceso estático adjunto.

Mouviciel
fuente
1

Una forma de administrar el código es descomponerlo en los siguientes tipos de código, siguiendo las líneas de la arquitectura MVC (modelo-vista-controlador).

  • Controladores de entrada: este código trata con dispositivos de entrada como mouse, teclado, puerto de red o abstracciones de nivel superior, como eventos del sistema.
  • Controladores de salida: este código se ocupa del uso de datos para manipular dispositivos externos como monitores, luces, puertos de red, etc.
  • Modelos: este código trata de declarar la estructura de sus datos persistentes, reglas para validar datos persistentes y guardar datos persistentes en el disco (u otro dispositivo de datos persistentes).
  • Vistas: este código se ocupa de formatear datos para cumplir con los requisitos de varios métodos de visualización, como navegadores web (HTML / CSS), GUI, línea de comandos, formatos de datos de protocolo de comunicación (por ejemplo, JSON, XML, ASN.1, etc.).
  • Algoritmos: este código transforma repetidamente un conjunto de datos de entrada en un conjunto de datos de salida lo más rápido posible.
  • Controladores: este código toma entradas a través de los manejadores de entrada, analiza las entradas usando algoritmos y luego transforma los datos con otros algoritmos combinando opcionalmente las entradas con datos persistentes o simplemente transformando las entradas, y luego opcionalmente guardando los datos transformados en persistente a través del modelo software y, opcionalmente, transformar los datos a través del software de visualización para representar en un dispositivo de salida.

Este método de organización del código funciona bien para el software escrito en cualquier lenguaje OO o no OO porque los patrones de diseño comunes a menudo son comunes a cada una de las áreas. Además, este tipo de límites de código son a menudo los más débilmente acoplados, excepto los algoritmos porque vinculan los formatos de datos de las entradas al modelo y luego a las salidas.

La evolución del sistema a menudo toma la forma de que su software maneje más tipos de entradas, o más tipos de salidas, pero los modelos y las vistas son las mismas y los controladores se comportan de manera muy similar. O, con el tiempo, un sistema puede necesitar admitir más y más tipos diferentes de salidas, aunque las entradas, los modelos y los algoritmos sean los mismos, y los controladores y las vistas sean similares. O se puede aumentar un sistema para agregar nuevos modelos y algoritmos para el mismo conjunto de entradas, salidas similares y vistas similares.

Una forma en que la programación OO dificulta la organización del código es porque algunas clases están profundamente ligadas a las estructuras de datos persistentes, y otras no. Si las estructuras de datos persistentes están íntimamente relacionadas con cosas tales como relaciones en cascada 1: N o relaciones m: n, es muy difícil decidir los límites de clase hasta que haya codificado una parte significativa y significativa de su sistema antes de saber que lo hizo bien . Cualquier clase vinculada a las estructuras de datos persistentes será difícil de evolucionar cuando cambie el esquema de los datos persistentes. Las clases que manejan algoritmos, formateo y análisis tienen menos probabilidades de ser vulnerables a los cambios en el esquema de las estructuras de datos persistentes. El uso de un tipo de organización de código MVC aísla mejor los cambios de código más desordenados en el código del modelo.

Jay Godse
fuente
0

Cuando se trabaja en idiomas que carecen de estructura incorporada y características de organización (por ejemplo, si no tiene espacios de nombres, paquetes, ensamblajes, etc.) o donde estos son insuficientes para mantener bajo control una base de código de ese tamaño, la respuesta natural es desarrollar nuestras propias estrategias para organizar el código.

Esta estrategia de organización probablemente incluye estándares relacionados con dónde se deben guardar los diferentes archivos, cosas que deben suceder antes / después de ciertos tipos de operaciones, y convenciones de nombres y otros estándares de codificación, así como mucho "así es como está configurado - ¡No te metas con eso! " escriba comentarios, que son válidos siempre que expliquen por qué.

Debido a que lo más probable es que la estrategia se adapte a las necesidades específicas del proyecto (personas, tecnologías, entorno, etc.) es difícil dar una solución única para la administración de bases de código grandes.

Por lo tanto, creo que el mejor consejo es adoptar la estrategia específica del proyecto y hacer que la gestión sea una prioridad clave: documentar la estructura, por qué es así, los procesos para realizar cambios, auditarla para asegurarse de que se cumpla, y crucial: cámbielo cuando necesite cambiar.

La mayoría de las veces estamos familiarizados con las clases y métodos de refactorización, pero con una gran base de código en dicho lenguaje, es la estrategia de organización en sí misma (completa con la documentación) la que necesita ser refactorizada cuando sea necesario.

El razonamiento es el mismo que para la refactorización: desarrollará un bloqueo mental para trabajar en pequeñas partes del sistema si siente que la organización general es un desastre y eventualmente permitirá que se deteriore (al menos esa es mi opinión sobre eso).

Las advertencias también son las mismas: use la prueba de regresión, asegúrese de que puede revertir fácilmente si la refactorización sale mal, y diseñe para facilitar la refactorización en primer lugar (¡o simplemente no lo hará!).

Estoy de acuerdo en que es mucho más complicado que refactorizar el código directo, y es más difícil validar / ocultar el tiempo de los gerentes / clientes que pueden no entender por qué debe hacerse, pero estos también son los tipos de proyectos más propensos a la descomposición del software causado por diseños inflexibles de alto nivel ...

Andy tiene
fuente
0

Si está preguntando sobre la administración de una base de código grande, está preguntando cómo mantener su base de código bien estructurada en un nivel relativamente grueso (bibliotecas / módulos / construcción de subsistemas / uso de espacios de nombres / tener los documentos correctos en los lugares correctos etc.) Los principios OO, especialmente 'clases abstractas' o 'interfaces', son principios para mantener su código limpio internamente, en un nivel muy detallado. Por lo tanto, las técnicas para mantener manejable una base de código grande no difieren para el código OO o no OO.

Doc Brown
fuente
0

La forma en que se maneja es que descubres los bordes de los elementos que usas. Por ejemplo, los siguientes elementos en C ++ tienen un borde claro y cualquier dependencia fuera del borde debe pensarse cuidadosamente:

  1. función libre
  2. función miembro
  3. clase
  4. objeto
  5. interfaz
  6. expresión
  7. llamada del constructor / creación de objetos
  8. Llamada de función
  9. tipo de parámetro de plantilla

Combinando estos elementos y reconociendo sus bordes, puede crear casi cualquier estilo de programación que desee dentro de c ++.

Un ejemplo de esto es que una función sería reconocer que es malo llamar a otras funciones desde una función, porque causa dependencia, en su lugar, solo debe llamar a las funciones miembro de los parámetros de la función original.

revs tp1
fuente
-1

El mayor desafío técnico es el problema del espacio de nombres. La vinculación parcial se puede utilizar para solucionar esto. El mejor enfoque es diseñar utilizando estándares de codificación. De lo contrario, todos los símbolos se convierten en un desastre.

Jonathan Cline IEEE
fuente
-2

Emacs es un buen ejemplo de esto:

Arquitectura Emacs

Componentes Emacs

Las pruebas de Emacs Lisp usan skip-unlessy let-bindpara hacer funciones de detección y prueba de accesorios:

A veces, no tiene sentido ejecutar una prueba debido a la falta de condiciones previas. Es posible que no se compile una característica requerida de Emacs, la función que se probará podría llamar a un binario externo que podría no estar disponible en la máquina de prueba, lo que sea. En este caso, la macro skip-unlesspodría usarse para omitir la prueba:

 (ert-deftest test-dbus ()
   "A test that checks D-BUS functionality."
   (skip-unless (featurep 'dbusbind))
   ...)

El resultado de ejecutar una prueba no debe depender del estado actual del entorno, y cada prueba debe dejar su entorno en el mismo estado en el que se encuentra. En particular, una prueba no debe depender de ninguna variable o gancho de personalización de Emacs, y si tiene que hacer algún cambio en el estado de Emacs o en un estado externo a Emacs (como el sistema de archivos), debe deshacer estos cambios antes de que regrese, independientemente de si se aprobó o no.

Las pruebas no deben depender del entorno, ya que cualquiera de estas dependencias puede hacer que la prueba sea frágil o provocar fallas que ocurren solo bajo ciertas circunstancias y son difíciles de reproducir. Por supuesto, el código bajo prueba puede tener configuraciones que afectan su comportamiento. En ese caso, es mejor hacer la prueba con let-bindtodas esas variables de configuración para establecer una configuración específica durante la duración de la prueba. La prueba también puede configurar varias configuraciones diferentes y ejecutar el código bajo prueba con cada una.

Como es SQLite. Aquí está su diseño:

  1. sqlite3_open () → Abra una conexión a una base de datos SQLite nueva o existente. El constructor para sqlite3.

  2. sqlite3 → El objeto de conexión de la base de datos. Creado por sqlite3_open () y destruido por sqlite3_close ().

  3. sqlite3_stmt → El objeto de declaración preparado. Creado por sqlite3_prepare () y destruido por sqlite3_finalize ().

  4. sqlite3_prepare () → Compila texto SQL en código de bytes que hará el trabajo de consultar o actualizar la base de datos. El constructor para sqlite3_stmt.

  5. sqlite3_bind () → Almacenar datos de la aplicación en parámetros del SQL original.

  6. sqlite3_step () → Avanzar un sqlite3_stmt a la siguiente fila de resultados o hasta completarlo.

  7. sqlite3_column () → Valores de columna en la fila de resultados actual para un sqlite3_stmt.

  8. sqlite3_finalize () → Destructor para sqlite3_stmt.

  9. sqlite3_exec () → Una función de contenedor que hace sqlite3_prepare (), sqlite3_step (), sqlite3_column () y sqlite3_finalize () para una cadena de una o más instrucciones SQL.

  10. sqlite3_close () → Destructor para sqlite3.

arquitectura sqlite3

Los componentes Tokenizer, Parser y Code Generator se utilizan para procesar instrucciones SQL y convertirlas en programas ejecutables en un lenguaje de máquina virtual o código de bytes. En términos generales, estas tres capas superiores implementan sqlite3_prepare_v2 () . El código de bytes generado por las tres capas superiores es una declaración preparada. El módulo de máquina virtual es responsable de ejecutar el código de bytes de la declaración SQL. El módulo B-Tree organiza un archivo de base de datos en múltiples almacenes de clave / valor con claves ordenadas y rendimiento logarítmico. El módulo Pager es responsable de cargar páginas del archivo de la base de datos en la memoria, de implementar y controlar transacciones, y de crear y mantener los archivos de diario que evitan la corrupción de la base de datos luego de un bloqueo o falla de energía. La interfaz del sistema operativo es una abstracción delgada que proporciona un conjunto común de rutinas para adaptar SQLite para que se ejecute en diferentes sistemas operativos. En términos generales, las cuatro capas inferiores implementan sqlite3_step () .

tabla virtual sqlite3

Una tabla virtual es un objeto que está registrado con una conexión de base de datos SQLite abierta. Desde la perspectiva de una instrucción SQL, el objeto de la tabla virtual se parece a cualquier otra tabla o vista. Pero detrás de escena, las consultas y actualizaciones en una tabla virtual invocan métodos de devolución de llamada del objeto de tabla virtual en lugar de leer y escribir en el archivo de la base de datos.

Una tabla virtual puede representar estructuras de datos en memoria. O podría representar una vista de datos en el disco que no está en formato SQLite. O la aplicación podría calcular el contenido de la tabla virtual a pedido.

Estos son algunos usos existentes y postulados para tablas virtuales:

Una interfaz de búsqueda de texto completo.
Índices espaciales usando R-Trees
Examinar el contenido del disco de un archivo de base de datos SQLite (la tabla virtual dbstat)
Leer y / o escribir el contenido de un archivo de valores separados por comas (CSV)
Acceda al sistema de archivos de la computadora host como si fuera una tabla de base de datos
Habilitar la manipulación SQL de datos en paquetes de estadísticas como R

SQLite utiliza una variedad de técnicas de prueba que incluyen:

Tres arneses de prueba desarrollados independientemente
100% de cobertura de prueba de sucursal en una configuración implementada
Millones y millones de casos de prueba.
Pruebas sin memoria
Pruebas de error de E / S
Pruebas de choque y pérdida de potencia.
Pruebas de fuzz
Pruebas de valor límite
Pruebas de optimización desactivadas
Pruebas de regresión
Pruebas de base de datos mal formadas
Uso extenso de afirmación () y verificaciones en tiempo de ejecución
Análisis de Valgrind
Comprobaciones de comportamiento indefinidas
Listas de verificación

Referencias

Paul Sweatte
fuente