Estamos comenzando un nuevo proyecto, desde cero. Alrededor de ocho desarrolladores, una docena de subsistemas, cada uno con cuatro o cinco archivos fuente.
¿Qué podemos hacer para prevenir el "infierno de encabezado", también conocido como "encabezados de espagueti"?
- ¿Un encabezado por archivo fuente?
- ¿Más uno por subsistema?
- ¿Separar Typdefs, estucts y enumeraciones de prototipos de funciones?
- ¿Separar el subsistema interno del subsistema externo?
- ¿Insiste en que cada archivo, ya sea el encabezado o la fuente, debe ser compilable de forma independiente?
No estoy pidiendo una "mejor" forma, sino un indicador de a qué vigilar y qué podría causar dolor, para que podamos tratar de evitarlo.
Este será un proyecto de C ++, pero C info ayudaría a futuros lectores.
Respuestas:
Método simple: un encabezado por archivo fuente. Si tiene un subsistema completo en el que no se espera que los usuarios conozcan los archivos de origen, tenga un encabezado para el subsistema que incluya todos los archivos de encabezado necesarios.
Cualquier archivo de encabezado debe ser compilable por sí mismo (o digamos que se debe compilar un archivo fuente que incluya cualquier encabezado). Es un dolor si encuentro qué archivo de encabezado contiene lo que quiero, y luego tengo que buscar los otros archivos de encabezado. Una forma simple de hacer cumplir esto es hacer que cada archivo fuente incluya primero su archivo de encabezado (gracias doug65536, creo que lo hago la mayor parte del tiempo sin siquiera darme cuenta).
Asegúrese de utilizar las herramientas disponibles para mantener bajos los tiempos de compilación: cada encabezado debe incluirse solo una vez, usar encabezados precompilados para mantener bajos los tiempos de compilación, usar módulos precompilados si es posible para mantener los tiempos de compilación más bajos.
fuente
Con mucho, el requisito más importante es reducir las dependencias entre sus archivos de origen. En C ++ es común usar un archivo fuente y un encabezado por clase. Por lo tanto, si tiene un buen diseño de clase, ni siquiera se acercará al infierno del encabezado.
También puede ver esto al revés: si ya tiene un infierno de encabezado en su proyecto, puede estar bastante seguro de que el diseño del software debe mejorarse.
Para responder a sus preguntas específicas:
fuente
Además de las otras recomendaciones, en la línea de reducción de dependencias (principalmente aplicable a C ++):
fuente
Un encabezado por archivo fuente, que define lo que implementa / exporta su archivo fuente.
Tantos archivos de encabezado como sea necesario, incluidos en cada archivo de origen (comenzando con su propio encabezado).
Evite incluir (minimizar la inclusión de) archivos de encabezado dentro de otros archivos de encabezado (para evitar dependencias circulares). Para más detalles, vea esta respuesta a "¿pueden dos clases verse entre sí usando C ++?"
Hay un libro completo sobre este tema, Diseño de software C ++ a gran escala por Lakos. Describe tener "capas" de software: las capas de alto nivel usan capas de nivel inferior, no al revés, lo que nuevamente evita dependencias circulares.
fuente
Yo diría que su pregunta es fundamentalmente sin respuesta, ya que hay dos tipos de infierno de encabezado:
La cuestión es que si intentas evitar lo primero, terminas, en cierta medida, con lo último, y viceversa.
También hay un tercer tipo de infierno, que son las dependencias circulares. Estos pueden aparecer si no tienes cuidado ... evitarlos no es súper complicado, pero debes tomarte el tiempo para pensar cómo hacerlo. Vea la charla de John Lakos sobre nivelación en CppCon 2016 (o solo las diapositivas ).
fuente
Desacoplamiento
En última instancia, se trata de desacoplarme al final del día en el nivel de diseño más fundamental sin los matices de las características de nuestros compiladores y enlazadores. Quiero decir que puede hacer cosas como hacer que cada encabezado defina solo una clase, use pimpls, envíe declaraciones a tipos que solo necesitan ser declarados, no definidos, tal vez incluso use encabezados que solo contengan declaraciones directas (ej .:)
<iosfwd>
, un encabezado por archivo fuente , organizar el sistema de manera coherente en función del tipo de cosa que se declara / define, etc.Técnicas para reducir las "dependencias en tiempo de compilación"
Y algunas de las técnicas pueden ayudar un poco, pero puede encontrarse agotando estas prácticas y aún así encontrar que su archivo fuente promedio en su sistema necesita un preámbulo de dos páginas de
#include
directivas para hacer algo levemente significativo con tiempos de compilación disparados si se enfoca demasiado en reducir las dependencias de tiempo de compilación en el nivel de encabezado sin reducir las dependencias lógicas en los diseños de su interfaz, y aunque eso no puede considerarse "encabezados de espagueti" estrictamente hablando, yo Todavía diría que se traduce en problemas perjudiciales similares a la productividad en la práctica. Al final del día, si sus unidades de compilación todavía requieren una gran cantidad de información para ser visible para hacer algo, entonces se traducirá en un aumento de los tiempos de construcción y multiplicará las razones por las que potencialmente tiene que regresar y cambiar las cosas mientras hace que los desarrolladores sienten que están golpeando el sistema simplemente tratando de terminar su codificación diaria. Eso'Puede, por ejemplo, hacer que cada subsistema proporcione una interfaz y un archivo de encabezado muy abstracto. Pero si los subsistemas no están desacoplados entre sí, entonces obtienes algo parecido al espagueti nuevamente con interfaces de subsistema que dependen de otras interfaces de subsistema con un gráfico de dependencia que parece un desastre para funcionar.
Declaraciones directas a tipos externos
De todas las técnicas que agoté para tratar de obtener una antigua base de código que tardó dos horas en compilarse, mientras que los desarrolladores a veces esperaban 2 días para su turno en CI en nuestros servidores de compilación (casi puedes imaginar esas máquinas de compilación como bestias de carga agotadas intentando frenéticamente mantenerse al día y fallar mientras los desarrolladores empujan sus cambios), lo más cuestionable para mí fue declarar los tipos definidos en otros encabezados. Y logré reducir esa base de código a 40 minutos más o menos después de años de hacer esto en pequeños pasos incrementales mientras intentaba reducir el "espagueti de encabezado", la práctica más cuestionable en retrospectiva (como en hacerme perder de vista la naturaleza fundamental de el diseño mientras el túnel se visualizaba en las interdependencias de los encabezados) declaraba los tipos definidos en otros encabezados.
Si imagina un
Foo.hpp
encabezado que tiene algo como:Y solo utiliza
Bar
en el encabezado una forma que requiere declaración, no definición. entonces puede parecer una obviedad declararclass Bar;
para evitar hacerBar
visible la definición de encabezado. Excepto en la práctica, a menudo encontrará que la mayoría de las unidades de compilación que usanFoo.hpp
todavía necesitanBar
definirse de todos modos con la carga adicional de tener que incluirseBar.hpp
encimaFoo.hpp
, o se encuentra con otro escenario donde eso realmente ayuda y 99 El% de sus unidades de compilación puede funcionar sin incluirBar.hpp
, excepto que plantea la pregunta de diseño más fundamental (o al menos creo que debería ser hoy en día) de por qué necesitan ver la declaraciónBar
y por quéFoo
incluso debe molestarse en saberlo si es irrelevante para la mayoría de los casos de uso (¿por qué cargar un diseño con dependencias a otro que casi nunca se usa?).Debido a que conceptualmente no hemos desacoplado
Foo
deBar
. Acabamos de hacerlo para que el encabezado deFoo
no necesite tanta información sobre el encabezado deBar
, y eso no es tan sustancial como un diseño que realmente los hace completamente independientes uno del otro.Scripting Embebido
Esto es realmente para bases de código de mayor escala, pero otra técnica que encuentro inmensamente útil es usar un lenguaje de script integrado para al menos las partes de más alto nivel de su sistema. Descubrí que podía incrustar Lua en un día y que podía llamar de manera uniforme a todos los comandos de nuestro sistema (afortunadamente, los comandos eran abstractos). Desafortunadamente, me encontré con un obstáculo en el que los desarrolladores desconfiaban de la introducción de otro idioma y, quizás lo más extraño, con el rendimiento como su mayor sospecha. Sin embargo, aunque podría entender otras preocupaciones, el rendimiento no debería ser un problema si solo utilizamos el script para invocar comandos cuando los usuarios hacen clic en botones, por ejemplo, que no realizan bucles fuertes propios (qué estamos tratando de hacer, preocuparse por las diferencias de nanosegundos en los tiempos de respuesta para un clic de botón?).
Ejemplo
Mientras tanto, la forma más efectiva que he presenciado después de agotar las técnicas para reducir los tiempos de compilación en grandes bases de código son arquitecturas que realmente reducen la cantidad de información requerida para que funcione cualquier cosa en el sistema, no solo desacoplar un encabezado de otro de un compilador perspectiva, pero exigiendo a los usuarios de estas interfaces que hagan lo que tienen que hacer mientras conocen (tanto desde el punto de vista humano como del compilador, un desacoplamiento verdadero que va más allá de las dependencias del compilador) lo mínimo.
El ECS es solo un ejemplo (y no estoy sugiriendo que use uno), pero al encontrarlo me demostró que puede tener algunas bases de código realmente épicas que aún se construyen sorprendentemente rápido mientras utilizan felizmente plantillas y muchas otras ventajas porque el ECS, por naturaleza, crea una arquitectura muy desacoplada donde los sistemas solo necesitan saber sobre la base de datos de ECS y, por lo general, solo un puñado de tipos de componentes (a veces solo uno) para hacer lo suyo:
Diseño, Diseño, Diseño
Y este tipo de diseños arquitectónicos desacoplados a nivel conceptual humano es más efectivo en términos de minimizar los tiempos de compilación que cualquiera de las técnicas que exploré anteriormente a medida que su base de código crece y crece y crece, porque ese crecimiento no se traduce en su promedio la unidad de compilación multiplica la cantidad de información requerida en la compilación y los tiempos de enlace para trabajar (cualquier sistema que requiera que su desarrollador promedio incluya una gran cantidad de cosas para hacer cualquier cosa también los requiere y no solo el compilador debe saber sobre una gran cantidad de información para hacer cualquier cosa ) También tiene más beneficios que tiempos de construcción reducidos y encabezados desenredados, ya que también significa que sus desarrolladores no necesitan saber mucho sobre el sistema más allá de lo que se requiere de inmediato para hacer algo con él.
Si, por ejemplo, puede contratar a un desarrollador de física experto para desarrollar un motor de física para su juego AAA que abarque millones de LOC, y puede comenzar muy rápidamente mientras conoce la información mínima absoluta en cuanto a tipos e interfaces disponibles así como los conceptos de su sistema, entonces eso naturalmente se traducirá en una cantidad reducida de información para que él y el compilador requieran construir su motor de física, y también se traducirá en una gran reducción en los tiempos de construcción, mientras que generalmente implica que no hay nada parecido a los espaguetis en cualquier parte del sistema. Y eso es lo que sugiero que priorice por encima de todas estas otras técnicas: cómo diseña sus sistemas. Agotar otras técnicas será la guinda en la parte superior si lo haces mientras que, de lo contrario,
fuente
Es una cuestión de opinión. Vea esta respuesta y esa . Y también depende mucho del tamaño del proyecto (si cree que tendrá millones de líneas de origen en su proyecto, no es lo mismo que tener unas pocas docenas de miles de ellas).
A diferencia de otras respuestas, recomiendo un encabezado público (bastante grande) por subsistema (que podría incluir encabezados "privados", quizás con archivos separados para implementaciones de muchas funciones en línea). Incluso podría considerar un encabezado que solo tenga varias
#include
directivas.No creo que se recomienden muchos archivos de encabezado. En particular, no recomiendo un archivo de encabezado por clase, o muchos archivos de encabezado pequeños de unas pocas docenas de líneas cada uno.
(Si tiene una gran cantidad de archivos pequeños, deberá incluir muchos de ellos en cada pequeña unidad de traducción , y el tiempo de construcción general podría verse afectado)
Lo que realmente desea es identificar, para cada subsistema y archivo, el desarrollador principal responsable de ello.
Por último, para un proyecto pequeño (por ejemplo, de menos de cien mil líneas de código fuente), no es muy importante. Durante el proyecto, podrá refactorizar el código con bastante facilidad y reorganizarlo en diferentes archivos. Simplemente copiará y pegará fragmentos de código en archivos nuevos (encabezados), no es un gran problema (lo que es más difícil es diseñar sabiamente cómo reorganizaría sus archivos, y eso es específico del proyecto).
(mi preferencia personal es evitar archivos demasiado grandes y demasiado pequeños; a menudo tengo archivos de origen de varios miles de líneas cada uno; y no tengo miedo de un archivo de encabezado, incluidas las definiciones de funciones en línea) de muchos cientos de líneas o incluso un par de miles de ellos)
Tenga en cuenta que si desea utilizar encabezados precompilados con GCC (que a veces es un enfoque sensato para reducir el tiempo de compilación), necesita un único archivo de encabezado (incluidos todos los demás y también los encabezados del sistema).
Observe que en C ++, los archivos de encabezado estándar extraen mucho código . Por ejemplo,
#include <vector>
está tirando más de diez mil líneas en mi GCC 6 en Linux (18100 líneas). Y se#include <map>
expande a casi 40KLOC. Por lo tanto, si tiene muchos archivos de encabezado pequeños, incluidos encabezados estándar, terminará volviendo a analizar muchos miles de líneas durante la compilación, y su tiempo de compilación se ve afectado. Es por eso que no me gusta tener muchas líneas pequeñas de fuente C ++ (de unos cientos de líneas como máximo), pero estoy a favor de tener menos archivos C ++ pero más grandes (de varios miles de líneas).(por lo que tener cientos de pequeños archivos C ++ que siempre incluyen, incluso indirectamente, varios archivos de encabezado estándar proporciona un tiempo de compilación enorme, lo que molesta a los desarrolladores)
En el código C, a menudo los archivos de encabezados se expanden a algo más pequeño, por lo que la compensación es diferente.
Busque también, en busca de inspiración, prácticas previas en proyectos de software libre existentes (por ejemplo, en github ).
Tenga en cuenta que las dependencias podrían tratarse con un buen sistema de automatización de compilación . Estudie la documentación de GNU make . Tenga en cuenta varios
-M
indicadores de preprocesador para GCC (útil para generar dependencias automáticamente).En otras palabras, su proyecto (con menos de cien archivos y una docena de desarrolladores) probablemente no sea lo suficientemente grande como para preocuparse realmente por el "infierno de encabezado", por lo que su preocupación no está justificada . Podría tener solo una docena de archivos de encabezado (o incluso mucho menos), podría elegir tener un archivo de encabezado por unidad de traducción, incluso podría elegir tener un solo archivo de encabezado, y lo que elija hacer no será un "cabecera del infierno" (y refactorizar y reorganizar sus archivos se mantendría razonablemente fácil, por lo que la elección inicial no es realmente importante ).
(No centre sus esfuerzos en el "infierno de encabezado" -que no es un problema para usted-, pero enfóquelos para diseñar una buena arquitectura)
fuente