Wir starten ein neues Projekt von Grund auf. Ungefähr acht Entwickler, etwa ein Dutzend Subsysteme mit jeweils vier oder fünf Quelldateien.
Was können wir tun, um "Header Hell", AKA "Spaghetti Headers", zu verhindern?
- Ein Header pro Quelldatei?
- Plus eins pro Subsystem?
- Typdefs, Stucts & Enums von Funktionsprototypen trennen?
- Subsystem intern von Subsystem extern trennen?
- Bestehen Sie darauf, dass jede einzelne Datei, ob Header oder Quelle, eigenständig kompilierbar sein muss?
Ich frage nicht nach einem „besten“ Weg, sondern zeige nur, worauf zu achten ist und was Trauer verursachen kann, damit wir versuchen können, es zu vermeiden.
Dies wird ein C ++ - Projekt sein, aber C-Info würde zukünftigen Lesern helfen.
Antworten:
Einfache Methode: Ein Header pro Quelldatei. Wenn Sie über ein vollständiges Subsystem verfügen, von dem nicht erwartet wird, dass Benutzer die Quelldateien kennen, verfügen Sie über einen Header für das Subsystem, der alle erforderlichen Headerdateien enthält.
Jede Header-Datei sollte für sich kompilierbar sein (oder sagen wir, eine Quelldatei mit einem einzelnen Header sollte kompiliert werden). Es ist ein Schmerz, wenn ich finde, welche Header-Datei das enthält, was ich will, und dann muss ich die anderen Header-Dateien suchen. Eine einfache Möglichkeit, dies zu erzwingen, besteht darin, dass jede Quelldatei zuerst die Headerdatei enthält (danke doug65536, ich glaube, ich mache das die meiste Zeit, ohne es zu merken).
Stellen Sie sicher, dass Sie verfügbare Tools verwenden, um die Kompilierungszeiten niedrig zu halten. Jeder Header darf nur einmal enthalten sein. Verwenden Sie vorkompilierte Header, um die Kompilierungszeiten niedrig zu halten. Verwenden Sie vorkompilierte Module, wenn möglich, um die Kompilierungszeiten weiter niedrig zu halten.
quelle
Die bei weitem wichtigste Anforderung besteht darin, die Abhängigkeiten zwischen Ihren Quelldateien zu verringern. In C ++ ist es üblich, eine Quelldatei und einen Header pro Klasse zu verwenden. Wenn Sie also ein gutes Design haben, werden Sie der Hölle nicht einmal nahe kommen.
Sie können dies auch in umgekehrter Richtung anzeigen: Wenn Ihr Projekt bereits eine Kopfzeile enthält, können Sie sicher sein, dass das Softwaredesign verbessert werden muss.
Um Ihre spezifischen Fragen zu beantworten:
quelle
Zusätzlich zu den anderen Empfehlungen zur Verringerung der Abhängigkeiten (hauptsächlich für C ++):
quelle
Ein Header pro Quelldatei, der definiert, was die Quelldatei implementiert / exportiert.
In jeder Quelldatei sind so viele Header-Dateien wie erforderlich enthalten (beginnend mit einem eigenen Header).
Vermeiden Sie es, Header-Dateien in andere Header-Dateien aufzunehmen (die Aufnahme zu minimieren) (um zirkuläre Abhängigkeiten zu vermeiden). Einzelheiten finden Sie in dieser Antwort auf "Können sich zwei Klassen mithilfe von C ++ gegenseitig sehen?"
Zu diesem Thema gibt es ein ganzes Buch, Large-Scale C ++ Software Design von Lakos. Es beschreibt das Vorhandensein von "Ebenen" von Software: Ebenen auf hoher Ebene verwenden Ebenen auf niedrigerer Ebene und nicht umgekehrt, wodurch wiederum kreisförmige Abhängigkeiten vermieden werden.
quelle
Ich würde behaupten, Ihre Frage ist grundsätzlich unbeantwortet, da es zwei Arten von Header-Hölle gibt:
Die Sache ist, wenn Sie versuchen, das erstere zu vermeiden, landen Sie in gewissem Maße beim letzteren und umgekehrt.
Es gibt auch eine dritte Art von Hölle, nämlich kreisförmige Abhängigkeiten. Diese können auftauchen, wenn Sie nicht aufpassen ... Es ist nicht sehr kompliziert, sie zu vermeiden, aber Sie müssen sich die Zeit nehmen, um darüber nachzudenken, wie Sie es tun können. Sehen Sie John Lakos Vortrag über Levelization auf der CppCon 2016 (oder nur die Folien ).
quelle
Entkopplung
Es geht mir letztendlich darum, mich am Ende des Tages auf der grundlegendsten Designebene zu entkoppeln, ohne die Nuance der Eigenschaften unserer Compiler und Linker. Ich meine, Sie können beispielsweise festlegen, dass jeder Header nur eine Klasse definiert, Pimpls verwendet, Deklarationen an Typen weiterleitet, die nur deklariert werden müssen, nicht definiert, oder sogar Header verwenden, die nur Forward-Deklarationen enthalten (z. B.
<iosfwd>
), einen Header pro Quelldatei , organisieren Sie das System konsistent basierend auf der Art der deklarierten / definierten Dinge usw.Techniken zum Reduzieren von "Abhängigkeiten bei der Kompilierung"
Und einige der Techniken können einiges helfen, aber Sie können feststellen, dass diese Praktiken anstrengend sind und Ihre durchschnittliche Quelldatei in Ihrem System immer noch eine zweiseitige Präambel benötigt
#include
Anweisungen, die mit schnellen Erstellungszeiten nichts Sinnvolles anstellen, wenn Sie sich zu sehr auf die Reduzierung der Kompilierungszeitabhängigkeiten auf Kopfzeilenebene konzentrieren, ohne die logischen Abhängigkeiten in Ihren Schnittstellendesigns zu reduzieren, und obwohl dies streng genommen nicht als "Spaghetti-Kopfzeilen" betrachtet werden kann Ich würde immer noch sagen, dass dies zu ähnlichen nachteiligen Auswirkungen auf die Produktivität in der Praxis führt. Am Ende des Tages, wenn Ihre Kompilierungseinheiten immer noch eine Schiffsladung von Informationen benötigen, um sichtbar zu sein, wird dies zu einer Erhöhung der Build-Zeiten führen und die Gründe multiplizieren, die Sie haben, um möglicherweise zurück zu gehen und Dinge zu ändern, während Sie Entwickler machen Ich habe das Gefühl, als würden sie das System einfach überraschen, um ihre tägliche Programmierung zu beenden. Es'Sie können beispielsweise festlegen, dass jedes Subsystem eine sehr abstrakte Header-Datei und Schnittstelle bereitstellt. Wenn die Subsysteme jedoch nicht voneinander entkoppelt sind, erhalten Sie wieder etwas Ähnliches wie Spaghetti mit Subsystemschnittstellen, abhängig von anderen Subsystemschnittstellen mit einem Abhängigkeitsdiagramm, das wie ein Durcheinander aussieht, um zu funktionieren.
Deklarationen an externe Typen weiterleiten
Von all den Techniken, die ich erschöpft habe, um zu versuchen, eine frühere Codebasis zu erhalten, die zwei Stunden in Anspruch nahm, während die Entwickler manchmal zwei Tage auf ihren Einsatz bei CI auf unseren Build-Servern warteten Um Schritt zu halten und Fehler zu machen, während Entwickler ihre Änderungen vorantreiben, war es für mich am fraglichsten, Typen zu deklarieren, die in anderen Headern definiert sind. Und ich habe es geschafft, diese Codebasis in kleinen Schritten auf ungefähr 40 Minuten zu reduzieren, während ich versucht habe, "Header - Spaghetti" zu reduzieren, was im Nachhinein die fragwürdigste Praxis ist (da ich die grundlegende Natur von aus den Augen verlor) Das Design, während der Tunnelblick auf Header-Abhängigkeiten gerichtet war, deklarierte Typen, die in anderen Headern definiert waren.
Wenn Sie sich einen
Foo.hpp
Header vorstellen, der ungefähr so aussieht:Und es verwendet
Bar
im Header nur eine Art und Weise, die eine Deklaration erfordert, keine Definition. Dann scheint es ein Kinderspiel zu sein, zu deklarierenclass Bar;
, um zu vermeiden, dass die Definition vonBar
in der Kopfzeile sichtbar wird. Außer in der Praxis oft werden Sie entweder die meisten der Kompilierungseinheiten finden, verwendenFoo.hpp
immer noch am Ende brauchenBar
sowieso definiert werden mit der zusätzlichen Last, enthaltenBar.hpp
sich auf der OberseiteFoo.hpp
, oder Sie laufen in ein anderes Szenario , in dem die wirklich Hilfe tut und 99 % Ihrer Kompilierungseinheiten können ohne Einschluss arbeitenBar.hpp
, außer dann wirft sie die grundlegendere Entwurfsfrage auf (oder zumindest denke ich, dass dies heutzutage der Fall sein sollte), warum sie überhaupt die Deklaration vonBar
und warum sehen müssenFoo
Man muss sich sogar die Mühe machen, davon zu erfahren, wenn es für die meisten Anwendungsfälle irrelevant ist (warum sollte man ein Design mit Abhängigkeiten zu einem anderen belasten, das kaum jemals verwendet wurde?).Weil konzeptionell haben wir nicht wirklich entkoppelt
Foo
vonBar
. Wir haben es gerade so gemacht, dass der Header vonFoo
nicht so viele Informationen über den Header von benötigtBar
, und das ist bei weitem nicht so aussagekräftig wie ein Design, das diese beiden wirklich völlig unabhängig voneinander macht.Embedded Scripting
Dies ist wirklich für größere Codebasen gedacht, aber eine andere Technik, die ich als äußerst nützlich empfinde, ist die Verwendung einer eingebetteten Skriptsprache für mindestens die übergeordneten Teile Ihres Systems. Ich fand heraus, dass ich Lua in einen Tag einbetten und alle Befehle in unserem System einheitlich aufrufen konnte (die Befehle waren zum Glück abstrakt). Leider bin ich auf eine Straßensperre gestoßen, bei der die Entwickler der Einführung einer anderen Sprache misstrauten und, vielleicht am bizarrsten, Leistung als größten Verdacht empfanden. Obwohl ich andere Bedenken verstehen konnte, sollte die Leistung kein Problem sein, wenn wir das Skript nur zum Aufrufen von Befehlen verwenden, wenn Benutzer beispielsweise auf Schaltflächen klicken, die selbst keine großen Schleifen ausführen (was versuchen wir zu tun, Sorgen Sie sich über Nanosekunden-Unterschiede in den Reaktionszeiten bei einem Tastenklick?).
Beispiel
Der effektivste Weg, den ich je gesehen habe, nachdem ich Techniken zur Verkürzung der Kompilierungszeiten in großen Codebasen angewendet habe, sind Architekturen, die die Menge an Informationen, die für das Funktionieren einer Sache im System erforderlich sind, wirklich reduzieren, und nicht nur das Entkoppeln eines Headers von einem anderen von einem Compiler Die Benutzer dieser Schnittstellen müssen jedoch das Nötigste tun, um zu wissen, was sie tun müssen (sowohl vom menschlichen Standpunkt als auch vom Standpunkt des Compilers aus, echte Entkopplung, die über Compiler-Abhängigkeiten hinausgeht).
Das ECS ist nur ein Beispiel (und ich schlage nicht vor, dass Sie eines verwenden), aber als ich es sah, konnte ich feststellen, dass Sie einige wirklich epische Codebasen haben können, die sich immer noch überraschend schnell aufbauen lassen, während Sie Vorlagen und viele andere Extras verwenden, weil das ECS Natur schafft eine sehr entkoppelte Architektur, in der Systeme nur über die ECS-Datenbank Bescheid wissen müssen und in der Regel nur eine Handvoll Komponententypen (manchmal nur eine), um ihre Aufgabe zu erfüllen:
Design, Design, Design
Und diese Art von entkoppelten Architekturentwürfen auf menschlicher, konzeptioneller Ebene ist effektiver im Hinblick auf die Minimierung der Kompilierzeiten als alle Techniken, die ich oben untersucht habe, da Ihre Codebasis wächst und wächst und wächst, da sich dieses Wachstum nicht in Ihrem Durchschnitt niederschlägt Kompilierungseinheit, die die zum Zeitpunkt der Kompilierung und Verknüpfung für die Arbeit erforderlichen Mengeninformationen multipliziert (bei jedem System, bei dem ein durchschnittlicher Entwickler eine Schiffsladung von Dingen einbeziehen muss, um etwas zu tun, müssen auch diese Informationen vorhanden sein, und nicht nur der Compiler muss über eine Vielzahl von Informationen Bescheid wissen, um etwas zu tun ). Es hat auch mehr Vorteile als reduzierte Erstellungszeiten und das Entwirren von Headern, da Ihre Entwickler nicht viel über das System wissen müssen, was unmittelbar erforderlich ist, um etwas damit zu tun.
Wenn Sie zum Beispiel einen erfahrenen Physikentwickler beauftragen, eine Physik-Engine für Ihr AAA-Spiel zu entwickeln, die Millionen von LOC umfasst, und der sehr schnell mit der Kenntnis des absoluten Minimums an verfügbaren Informationen wie Typen und Schnittstellen beginnen kann Neben Ihren Systemkonzepten bedeutet dies natürlich auch, dass sowohl er als auch der Compiler weniger Informationen benötigen, um seine Physik-Engine zu erstellen. Gleichzeitig bedeutet dies eine erhebliche Verkürzung der Build-Zeiten und im Allgemeinen, dass es nichts gibt, was Spaghetti ähnelt überall im System. Und das ist es, was ich vorschlage, um all diesen anderen Techniken Vorrang einzuräumen: wie Sie Ihre Systeme entwerfen. Erschöpfende andere Techniken werden das i-Tüpfelchen sein, wenn Sie es tun, während, sonst
quelle
Es ist Ansichtssache. Siehe diese und jene Antwort . Und es hängt auch stark von der Projektgröße ab (wenn Sie glauben, dass Sie Millionen von Quelltextzeilen in Ihrem Projekt haben, ist dies nicht dasselbe wie ein paar Dutzendtausende davon).
Im Gegensatz zu anderen Antworten empfehle ich einen (ziemlich großen) öffentlichen Header pro Subsystem (der "private" Header enthalten könnte, möglicherweise mit separaten Dateien für die Implementierung vieler inline Funktionen). Sie könnten sogar einen Header mit nur mehreren
#include
Direktiven in Betracht ziehen .Ich denke nicht, dass viele Header-Dateien empfohlen werden. Insbesondere empfehle ich nicht eine Header-Datei pro Klasse oder viele kleine Header-Dateien mit jeweils ein paar Dutzend Zeilen.
(Wenn Sie viele kleine Dateien haben, müssen Sie viele davon in jede kleine Übersetzungseinheit einbinden , und die gesamte Erstellungszeit kann darunter leiden.)
Was Sie wirklich wollen, ist, für jedes Subsystem und jede Datei den Hauptentwickler zu identifizieren, der dafür verantwortlich ist.
Schließlich ist es für ein kleines Projekt (z. B. mit weniger als hunderttausend Zeilen Quellcode) nicht sehr wichtig. Während des Projekts können Sie den Code ganz einfach umgestalten und in verschiedenen Dateien neu organisieren. Sie kopieren einfach Codestücke in neue (Header-) Dateien und fügen sie ein, was keine große Sache ist (was schwieriger ist, klug zu gestalten, wie Sie Ihre Dateien neu organisieren würden, und das ist projektspezifisch).
(Ich persönlich bevorzuge es, zu große und zu kleine Dateien zu vermeiden. Ich habe oft Quelldateien mit jeweils mehreren tausend Zeilen. Ich habe keine Angst vor einer Header-Datei mit eingebetteten Funktionsdefinitionen mit mehreren hundert Zeilen oder sogar ein paar Zeilen Tausende von ihnen)
Beachten Sie, dass Sie, wenn Sie vorkompilierte Header mit GCC verwenden möchten (was manchmal sinnvoll ist, um die Kompilierungszeit zu verkürzen), eine einzelne Header-Datei benötigen (einschließlich aller anderen und auch der System-Header).
Beachten Sie, dass in C ++ in Standardheaderdateien viel Code abgerufen wird . Zum Beispiel
#include <vector>
zieht mehr als zehntausend Zeilen auf meinem GCC 6 unter Linux (18100 Zeilen). Und#include <map>
erweitert sich auf fast 40KLOC. Wenn Sie also viele kleine Header-Dateien einschließlich Standardheader haben, werden beim Erstellen viele tausend Zeilen erneut analysiert, und die Kompilierungszeit leidet darunter. Aus diesem Grund mag ich nicht viele kleine C ++ - Quelltextzeilen (höchstens einige hundert Zeilen), aber ich bevorzuge es, weniger, aber größere C ++ - Dateien (mit mehreren tausend Zeilen) zu haben.(Hunderte kleiner C ++ - Dateien, die - auch indirekt - immer mehrere Standardheaderdateien enthalten, bedeuten eine enorme Erstellungszeit, die die Entwickler ärgert.)
In C-Code werden Header-Dateien häufig auf eine kleinere Größe erweitert, sodass der Kompromiss anders ist.
Informieren Sie sich auch über frühere Übungen in vorhandenen freien Softwareprojekten (z . B. auf Github ).
Beachten Sie, dass Abhängigkeiten mit einem guten Build-Automatisierungssystem behandelt werden können. Studieren Sie die Dokumentation von GNU make . Beachten Sie die verschiedenen
-M
Präprozessor-Flags für GCC (nützlich, um automatisch Abhängigkeiten zu generieren).Mit anderen Worten, Ihr Projekt (mit weniger als einhundert Dateien und einem Dutzend Entwicklern) ist wahrscheinlich nicht groß genug, um wirklich von der "Header-Hölle" betroffen zu sein, sodass Ihre Sorge nicht gerechtfertigt ist . Sie könnten nur ein Dutzend Header-Dateien (oder noch viel weniger) haben, Sie könnten sich für eine Header-Datei pro Übersetzungseinheit entscheiden, Sie könnten sich sogar für eine einzelne Header-Datei entscheiden, und was auch immer Sie tun, es wird keine sein "Header Hölle" (und Refactoring und Reorganisation Ihrer Dateien würde einigermaßen einfach bleiben, so dass die anfängliche Auswahl nicht wirklich wichtig ist ).
(Konzentrieren Sie Ihre Bemühungen nicht auf "Header Hell", was für Sie kein Problem ist, sondern konzentrieren Sie sich darauf, eine gute Architektur zu entwerfen.)
quelle