Nous commençons un nouveau projet, à partir de zéro. Environ huit développeurs, une dizaine de sous-systèmes, chacun contenant quatre ou cinq fichiers sources.
Que pouvons-nous faire pour empêcher “l'en-tête”, AKA “en-têtes de spaghetti”?
- Un en-tête par fichier source?
- Plus un par sous-système?
- Séparez typdefs, stucts & enums des prototypes de fonctions?
- Séparez le matériel interne du sous-système externe du sous-système?
- Insister sur le fait que chaque fichier, qu’il soit en-tête ou source, doit être compilable de manière autonome?
Je ne demande pas un «meilleur» moyen, mais simplement un indicateur de ce qui doit être surveillé et de ce qui pourrait causer du chagrin, afin que nous puissions essayer de l'éviter.
Ce sera un projet C ++, mais C info aiderait les futurs lecteurs.
Réponses:
Méthode simple: un en-tête par fichier source. Si vous disposez d'un sous-système complet dans lequel les utilisateurs ne sont pas censés connaître les fichiers source, créez un en-tête pour le sous-système, y compris tous les fichiers d'en-tête requis.
Tout fichier d'en-tête doit être compilable seul (ou supposons qu'un fichier source incluant tout en-tête compile). C'est pénible si je trouve quel fichier d'en-tête contient ce que je veux, puis je dois traquer les autres fichiers d'en-tête. Un moyen simple de le faire est d’inclure tout d’abord le fichier d’en-tête dans chaque fichier source (merci, doug65536, je pense que je le fais la plupart du temps sans même m'en rendre compte).
Assurez-vous d’utiliser les outils disponibles pour réduire les temps de compilation - chaque en-tête ne doit être inclus qu’une seule fois, utilisez des en-têtes précompilés pour réduire les temps de compilation, utilisez si possible des modules précompilés pour réduire les temps de compilation.
la source
De loin, l'exigence la plus importante est de réduire les dépendances entre vos fichiers source. En C ++, il est courant d'utiliser un fichier source et un en-tête par classe. Par conséquent, si vous avez un bon design de classe, vous ne vous approcherez même pas de l'en-tête.
Vous pouvez également voir l’inverse: si vous avez déjà un en-tête dans votre projet, vous pouvez être certain que la conception du logiciel doit être améliorée.
Pour répondre à vos questions spécifiques:
la source
Outre les autres recommandations, dans le sens de la réduction des dépendances (principalement applicables au C ++):
la source
Un en-tête par fichier source, qui définit ce que son fichier source implémente / exporte.
Autant de fichiers d'en-tête que nécessaire, inclus dans chaque fichier source (en commençant par son propre en-tête).
Évitez d'inclure (minimisez l'inclusion de) les fichiers d'en-tête dans les autres fichiers d'en-tête (pour éviter les dépendances circulaires). Pour plus de détails, voir cette réponse à "deux classes peuvent-elles se voir en utilisant C ++?"
Il existe tout un livre sur ce sujet, Conception de logiciels à grande échelle C ++ par Lakos. Il décrit le fait de disposer de "couches" de logiciels: les couches de niveau supérieur utilisent des couches de niveau inférieur et non l'inverse, ce qui évite également les dépendances circulaires.
la source
Je dirais que votre question est fondamentalement sans réponse, car il existe deux types d'en-tête:
Le problème est que si vous essayez d'éviter le premier, vous vous retrouvez, dans une certaine mesure, avec le dernier et vice-versa.
Il y a aussi un troisième type d'enfer, qui est les dépendances circulaires. Celles-ci peuvent apparaître si vous ne faites pas attention ... les éviter n'est pas compliqué, mais vous devez prendre le temps de réfléchir à la façon de le faire. Voir John Lakos talk sur en nivellement CppCon 2016 (ou tout simplement les diapositives ).
la source
Découplage
Il s’agit en fin de compte de me dissocier en fin de journée au niveau de conception le plus fondamental, dépourvu de la nuance des caractéristiques de nos compilateurs et de nos lieurs. Je veux dire que vous pouvez faire des choses comme faire en sorte que chaque en-tête définisse une seule classe, utiliser pimpls, transmettre des déclarations à des types qui doivent seulement être déclarés, non définis, peut-être même utiliser des en-têtes ne contenant que des déclarations en aval (ex:)
<iosfwd>
, un en-tête par fichier source , organisez le système de manière cohérente en fonction du type de chose déclarée / définie, etc.Techniques pour réduire les "dépendances au moment de la compilation"
Certaines techniques peuvent aider, mais vous pouvez épuiser ces pratiques tout en trouvant que votre fichier source moyen dans votre système nécessite un préambule de deux pages.
#include
Si vous vous concentrez trop sur la réduction des dépendances au moment de la compilation au niveau de l'en-tête sans réduire les dépendances logiques dans la conception de vos interfaces, les directives ne font rien de vraiment significatif avec des temps de construction en flèche, et si cela peut ne pas être considéré à proprement parler comme un "en-tête de spaghetti", je Je dirais toujours que cela se traduit par des problèmes préjudiciables similaires à la productivité dans la pratique. À la fin de la journée, si vos unités de compilation requièrent toujours une batterie d'informations visibles pour pouvoir faire quoi que ce soit, elles se traduiront par des temps de construction de plus en plus longs et multiplieront les raisons pour lesquelles vous devez potentiellement revenir en arrière et devoir changer les choses tout en faisant en sorte que les développeurs ils ont l’impression d’être en train d’amorcer le système en essayant de terminer leur codage quotidien. Il'Vous pouvez, par exemple, faire en sorte que chaque sous-système fournisse un fichier d'en-tête et une interface très abstraits. Mais si les sous-systèmes ne sont pas découplés les uns des autres, vous obtenez à nouveau quelque chose qui ressemble à spaghetti avec des interfaces de sous-système dépendant d'autres interfaces de sous-système avec un graphe de dépendance qui ressemble à un gâchis pour fonctionner.
Déclarations de transfert vers des types externes
De toutes les techniques que j'ai épuisées pour essayer d'obtenir une ancienne base de code qui prenait deux heures à construire tandis que les développeurs attendaient parfois deux jours pour leur tour chez CI sur nos serveurs de build (vous pouvez presque imaginer ces machines de construction comme des bêtes de somme épuisées essayant frénétiquement pour suivre et échouer pendant que les développeurs poussent leurs modifications), le plus douteux pour moi était les types de déclaration en aval définis dans d'autres en-têtes. Et j’ai réussi à réduire cette base de code à 40 minutes environ après des siècles d’avoir fait cela petit à petit tout en essayant de réduire les "en-têtes spaghettis", la pratique la plus discutable en rétrospective (comme me faire perdre de vue la nature fondamentale de conception alors que tunnel envisageait les interdépendances d’en-têtes) déclarait en aval les types définis dans d’autres en-têtes.
Si vous imaginez un en-
Foo.hpp
tête qui a quelque chose comme:Et il utilise uniquement
Bar
dans l'en-tête une manière qui nécessite sa déclaration, pas sa définition. alors, cela peut sembler une évidence de direclass Bar;
d'éviter de rendreBar
visible la définition de dans l'en-tête. Excepté dans la pratique, vous constaterez souvent que la plupart des unités de compilation utiliséesFoo.hpp
doivent encoreBar
être définies avec le fardeau supplémentaire de devoir s’inclure parBar.hpp
-dessusFoo.hpp
, ou que vous rencontrez un autre scénario dans lequel cela aide réellement. % de vos unités de compilation peuvent travailler sans inclureBar.hpp
, sauf que cela soulève la question plus fondamentale de la conception (ou du moins, je pense que cela devrait être le cas de nos jours): pourquoi elles ont même besoin de voir la déclaration deBar
et pourquoiFoo
il faut même être dérangé de le savoir si cela n’est pas pertinent pour la plupart des cas d’utilisation (pourquoi alourdir une conception avec des dépendances les unes par rapport aux autres)?Parce que conceptuellement, nous ne sommes pas vraiment découplés
Foo
deBar
. Nous venons de faire en sorte que l'en-tête deFoo
ne nécessite pas autant d'informations sur l'en-tête deBar
, ce qui est loin d'être aussi substantiel qu'un design qui rend véritablement ces deux éléments complètement indépendants l'un de l'autre.Script intégré
C’est vraiment pour les bases de code à plus grande échelle, mais une autre technique que je trouve extrêmement utile consiste à utiliser un langage de script intégré pour au moins les parties les plus avancées de votre système. J'ai découvert que j'étais capable d'intégrer Lua en une journée et de l'avoir uniformément capable d'appeler toutes les commandes de notre système (les commandes étaient abstraites, heureusement). Malheureusement, je suis tombé sur un barrage routier où les développeurs se méfiaient de l'introduction d'une autre langue et, ce qui est peut-être le plus étrange, de la performance, leur principal soupçon. Bien que je puisse comprendre d’autres préoccupations, les performances ne devraient pas être un problème si nous n’utilisons le script que pour appeler des commandes lorsque les utilisateurs cliquent sur des boutons, par exemple, n’effectuant aucune boucle lourde (ce que nous essayons de faire, vous inquiétez-vous des différences de nanosecondes dans les temps de réponse pour un clic de bouton?).
Exemple
Entre-temps, le moyen le plus efficace que je connaisse après avoir épuisé les techniques de réduction du temps de compilation dans les bases de code volumineuses consiste en des architectures qui réduisent réellement la quantité d'informations requises pour le fonctionnement d'un élément du système, et non pas simplement le découplage d'un en-tête d'un compilateur. perspective mais en demandant aux utilisateurs de ces interfaces de faire ce qu’ils doivent faire tout en sachant (du point de vue humain et du compilateur, un véritable découplage qui va au-delà des dépendances du compilateur) le strict minimum.
L’ECS n’est qu’un exemple (et je ne vous suggère pas d’en utiliser un), mais sa découverte m’a montré que vous pouvez avoir des bases de code vraiment épiques qui construisent toujours étonnamment rapidement tout en utilisant avec bonheur des modèles et beaucoup d’autres bienfaits, car ECS, par nature, crée une architecture très découplée où les systèmes ont seulement besoin de connaître la base de données ECS et en général seulement quelques types de composants (parfois un seul) pour faire leur travail:
Design, Design, Design
Et ces types de conceptions architecturales découplées au niveau humain et conceptuel sont plus efficaces en termes de réduction des temps de compilation que toutes les techniques que j'ai expliquées ci-dessus à mesure que votre base de code grandit et grandit et grandit, car cette croissance ne correspond pas à votre moyenne. unité de compilation en multipliant la quantité d’information requise lors de la compilation et les temps de liaison pour fonctionner (tout système qui oblige votre développeur moyen à inclure un paquet de données pour faire quoi que ce soit exige de la part du compilateur, et pas seulement du compilateur à connaître beaucoup d’informations pour faire quoi que ce soit ). Il présente également plus d'avantages que des temps de construction réduits et des en-têtes non démêlés, car cela signifie également que vos développeurs n'ont pas besoin d'en savoir plus sur le système, au-delà de ce qui est immédiatement requis pour en faire quelque chose.
Si, par exemple, vous pouvez engager un développeur expert en physique pour développer un moteur physique pour votre jeu AAA qui couvre des millions de LOC, il peut démarrer très rapidement tout en connaissant le minimum d'informations absolues en ce qui concerne les types et les interfaces disponibles. ainsi que vos concepts de système, alors cela va naturellement se traduire par une quantité réduite d'informations pour lui et le compilateur, ce qui se traduira également par une réduction considérable des temps de construction tout en impliquant généralement qu'il n'y a rien qui ressemble à des spaghettis n'importe où dans le système. Et c’est ce que je propose d’accorder la priorité à toutes ces autres techniques: la conception de vos systèmes. Épuiser d’autres techniques sera la cerise sur le gâteau si vous le faites pendant, sinon,
la source
C'est une question d'opinion. Voir cette réponse et celle- là. Et cela dépend aussi beaucoup de la taille du projet (si vous pensez que votre projet contient des millions de lignes de source, ce n’est pas la même chose que quelques dizaines de milliers de lignes).
Contrairement à d'autres réponses, je recommande un en-tête public (plutôt volumineux) par sous-système (pouvant inclure des en-têtes "privés", contenant éventuellement des fichiers distincts pour la mise en oeuvre de nombreuses fonctions intégrées). Vous pourriez même envisager un en-tête n'ayant que plusieurs
#include
directives.Je ne pense pas que beaucoup de fichiers d'en-tête sont recommandés. En particulier, je ne recommande pas un fichier d’en-tête par classe, ni de nombreux petits fichiers d’en-tête de quelques dizaines de lignes chacun.
(Si vous avez beaucoup de petits fichiers, vous devez en inclure beaucoup dans chaque petite unité de traduction , et le temps de construction peut en souffrir)
Ce que vous voulez vraiment, c'est identifier, pour chaque sous-système et chaque fichier, le développeur principal responsable.
Enfin, pour un petit projet (par exemple moins de cent mille lignes de code source), ce n’est pas très important. Pendant le projet, vous pourrez facilement refactoriser le code et le réorganiser dans différents fichiers. Il vous suffit de copier et coller des morceaux de code dans de nouveaux fichiers (en-tête), ce qui n’est pas grave (ce qui est plus difficile, c’est de concevoir judicieusement la façon dont vous réorganiseriez vos fichiers, et qui est spécifique à un projet).
(Ma préférence personnelle est d’éviter les fichiers trop volumineux et trop petits; j’ai souvent des fichiers sources de plusieurs milliers de lignes chacun; et je n’ai pas peur d’un fichier d’en-tête comprenant les définitions de fonctions en ligne de plusieurs centaines ou même de quelques lignes. des milliers d'entre eux)
Notez que si vous souhaitez utiliser des en- têtes pré-compilés avec GCC (ce qui est parfois une approche judicieuse pour réduire le temps de compilation), vous avez besoin d'un fichier d'en-tête unique (comprenant tous les autres, ainsi que les en-têtes système).
Notez qu'en C ++, les fichiers d'en-tête standard extraient beaucoup de code . Par exemple,
#include <vector>
tire plus de dix mille lignes sur mon GCC 6 sous Linux (18100 lignes). Et#include <map>
s'étend à près de 40KLOC. Par conséquent, si vous avez beaucoup de petits fichiers d'en-tête, y compris des en-têtes standard, vous devez ré-analyser plusieurs milliers de lignes lors de la génération et votre temps de compilation en souffre. C'est pourquoi je n'aime pas beaucoup de petites lignes sources C ++ (de quelques centaines de lignes au plus), mais je préfère avoir moins de fichiers C ++, mais de plus grande taille (plusieurs milliers de lignes).(avoir des centaines de petits fichiers C ++ qui incluent toujours indirectement même plusieurs fichiers d’en-tête standard donne un temps de construction énorme, ce qui gêne les développeurs)
Dans le code C, les fichiers d’en-têtes sont souvent plus petits, le compromis est donc différent.
Inspirez-vous également de la pratique antérieure des projets de logiciels libres existants (par exemple, sur github ).
Notez que les dépendances pourraient être traitées avec un bon système d' automatisation de la construction . Étudiez la documentation de GNU make . Tenez compte de divers
-M
indicateurs de préprocesseur sur GCC (utile pour générer automatiquement des dépendances).En d’autres termes, votre projet (avec moins d’une centaine de fichiers et une douzaine de développeurs) n’est probablement pas assez important pour être vraiment concerné par «l’en-tête», votre préoccupation n’est donc pas justifiée . Vous pourriez ne posséder qu'une douzaine de fichiers d'en-tête (voire beaucoup moins), choisir un fichier d'en-tête par unité de traduction, un seul fichier d'en-tête, et quoi que vous choisissiez de faire, ce ne serait pas "header hell" (et le refactoring et la réorganisation de vos fichiers resteraient relativement faciles, le choix initial n’est donc pas vraiment important ).
(Ne concentrez pas vos efforts sur "l'en-tête" - ce qui n'est pas un problème pour vous - mais concentrez-vous sur la conception d'une bonne architecture)
la source