Rozpoczynamy nowy projekt od zera. Około ośmiu programistów, kilkanaście podsystemów, każdy z czterema lub pięcioma plikami źródłowymi.
Co możemy zrobić, aby zapobiec „nagłówkowi piekła”, AKA „nagłówkom spaghetti”?
- Jeden nagłówek na plik źródłowy?
- Plus jeden na podsystem?
- Oddzielić typdefy, kable i wyliczenia od prototypów funkcji?
- Oddzielić wewnętrzny podsystem od zewnętrznych elementów podsystemu?
- Czy nalegasz, aby każdy pojedynczy plik, niezależnie od tego, czy nagłówek, czy źródło musiał być samodzielny, może zostać skompilowany?
Nie proszę o „najlepszy” sposób, tylko wskazówkę, na co uważać i co może powodować smutek, abyśmy mogli spróbować tego uniknąć.
To będzie projekt w C ++, ale informacje w C pomogłyby przyszłym czytelnikom.
Odpowiedzi:
Prosta metoda: jeden nagłówek na plik źródłowy. Jeśli masz kompletny podsystem, w którym użytkownicy nie powinni wiedzieć o plikach źródłowych, przygotuj jeden nagłówek podsystemu wraz ze wszystkimi wymaganymi plikami nagłówkowymi.
Każdy plik nagłówka powinien być kompilowany samodzielnie (lub powiedzmy, że plik źródłowy, w tym pojedynczy nagłówek, powinien się skompilować). Boli mnie to, że znalazłem, który plik nagłówkowy zawiera to, co chcę, a następnie muszę wyśledzić inne pliki nagłówkowe. Prostym sposobem na wymuszenie tego jest umieszczenie każdego pliku źródłowego w pierwszej kolejności pliku nagłówka (dzięki doug65536, myślę, że robię to przez większość czasu, nawet nie zdając sobie z tego sprawy).
Upewnij się, że korzystasz z dostępnych narzędzi, aby skrócić czas kompilacji - każdy nagłówek musi być dołączony tylko raz, użyj prekompilowanych nagłówków, aby skrócić czas kompilacji, w miarę możliwości użyj wstępnie skompilowanych modułów, aby skrócić czas kompilacji.
źródło
Zdecydowanie najważniejszym wymogiem jest zmniejszenie zależności między plikami źródłowymi. W C ++ często stosuje się jeden plik źródłowy i jeden nagłówek na klasę. Dlatego jeśli masz dobry projekt klasy, nawet nie zbliżysz się do piekła.
Możesz także spojrzeć na to odwrotnie: jeśli masz już piekło nagłówka w swoim projekcie, możesz być całkiem pewien, że należy ulepszyć projekt oprogramowania.
Aby odpowiedzieć na konkretne pytania:
źródło
Oprócz innych zaleceń, podobnie jak ograniczenie zależności (dotyczy głównie C ++):
źródło
Jeden nagłówek na plik źródłowy, który określa, co plik źródłowy implementuje / eksportuje.
Tyle plików nagłówka, ile potrzeba, zawartych w każdym pliku źródłowym (zaczynając od własnego nagłówka).
Unikaj dołączania (minimalizuj dołączanie) plików nagłówkowych do innych plików nagłówkowych (aby uniknąć zależności cyklicznych). Aby uzyskać szczegółowe informacje, zapoznaj się z odpowiedzią na pytanie „czy dwie klasy widzą się za pomocą C ++?”
Jest cała książka na ten temat, Large-Scale C ++ Software Design autorstwa Lakos. Opisuje posiadanie „warstw” oprogramowania: warstwy wysokiego poziomu używają warstw niższego poziomu, a nie odwrotnie, co ponownie pozwala uniknąć zależności cyklicznych.
źródło
Twierdzę, że twoje pytanie jest zasadniczo niemożliwe do odpowiedzi, ponieważ istnieją dwa rodzaje piekła nagłówka:
chodzi o to, że jeśli spróbujesz uniknąć tego pierwszego, do pewnego stopnia skończysz z tym drugim i na odwrót.
Istnieje również trzeci rodzaj piekła, którym są zależności kołowe. Mogą pojawić się, jeśli nie będziesz ostrożny ... unikanie ich nie jest bardzo skomplikowane, ale musisz poświęcić trochę czasu, aby pomyśleć o tym, jak to zrobić. Zobacz wykład Johna Lakosa na temat poziomowania w CppCon 2016 (lub tylko slajdy ).
źródło
Oddzielenie
Ostatecznie chodzi o oddzielenie ode mnie pod koniec dnia na najbardziej podstawowym poziomie projektowania, pozbawionym niuansów charakterystycznych dla naszych kompilatorów i łączników. Mam na myśli to, że możesz zrobić takie rzeczy, aby każdy nagłówek definiował tylko jedną klasę, używaj pimplów, deklaracji przekazywania do typów, które wymagają tylko zadeklarowania, nie są zdefiniowane, może nawet użyj nagłówków, które zawierają tylko deklaracje przekazywania (np .:)
<iosfwd>
, jeden nagłówek na plik źródłowy , konsekwentnie organizuj system w oparciu o rodzaj deklarowanej / definiowanej rzeczy itp.Techniki zmniejszania „zależności czasu kompilacji”
I niektóre techniki mogą nieco pomóc, ale możesz się wyczerpać tymi praktykami, a przecież przeciętny plik źródłowy w twoim systemie potrzebuje dwustronicowej preambuły
#include
dyrektywy, aby zrobić coś nieznacznie znaczącego z niebotycznymi czasami kompilacji, jeśli zbytnio skupiasz się na zmniejszaniu zależności czasu kompilacji na poziomie nagłówka bez zmniejszania logicznych zależności w projektach interfejsów, i chociaż nie można tego uważać za „nagłówki spaghetti” ściśle mówiąc, ja Nadal powiedziałbym, że przekłada się to na podobne szkodliwe problemy, jak wydajność w praktyce. Na koniec dnia, jeśli Twoje jednostki kompilacyjne nadal wymagają dużej ilości informacji, aby były widoczne, aby cokolwiek zrobić, to przełoży się to na wydłużenie czasu kompilacji i zwielokrotnienie powodów, dla których musisz potencjalnie wrócić i zmienić rzeczy podczas tworzenia programistów czują się, jakby próbowali skończyć system, próbując po prostu zakończyć codzienne kodowanie. To'Możesz na przykład sprawić, aby każdy podsystem zapewniał jeden bardzo abstrakcyjny plik nagłówka i interfejs. Ale jeśli podsystemy nie są od siebie oddzielone, to dostajesz znowu coś przypominającego spaghetti z interfejsami podsystemów w zależności od innych interfejsów podsystemów z wykresem zależności, który wygląda jak bałagan, aby działać.
Przekazywanie deklaracji do typów zewnętrznych
Ze wszystkich technik, które wyczerpałem, starając się uzyskać dawną bazę kodu, której skompilowanie zajęło dwie godziny, podczas gdy programiści czasami czekali 2 dni na swoją kolej w CI na naszych serwerach kompilacji (możesz sobie wyobrazić te maszyny do budowania jako wyczerpane bestie obciążone gorączkowo próbujące aby nadążyć i ponieść porażkę, gdy programiści wprowadzają zmiany), najbardziej wątpliwe było dla mnie zadeklarowanie typów zdefiniowanych w innych nagłówkach. Udało mi się sprowadzić ten kod źródłowy do około 40 minut po wiekach robienia tego w niewielkich krokach, próbując zmniejszyć „spaghetti z nagłówkiem”, najbardziej wątpliwą praktykę z perspektywy czasu (ponieważ powodując, że tracę z oczu podstawową naturę podczas projektowania tunelowego na współzależności nagłówków) było zadeklarowane do przodu typy zdefiniowane w innych nagłówkach.
Jeśli wyobrażasz sobie
Foo.hpp
nagłówek, który ma coś takiego:I używa tylko
Bar
w nagłówku sposobu, który wymaga deklaracji, a nie definicji. to może wydawać się oczywiste, aby zadeklarować,class Bar;
aby uniknąć uczynienia definicjiBar
widocznej w nagłówku. Z wyjątkiem sytuacji, w której często praktykujesz, że większość jednostek kompilacji, które używają,Foo.hpp
nadal musiBar
zostać zdefiniowana z dodatkowym obciążeniem związanym z koniecznością umieszczeniaBar.hpp
się na nichFoo.hpp
, lub napotykasz inny scenariusz, w którym to naprawdę pomaga i 99 % twoich jednostek kompilacyjnych może działać bez uwzględnieniaBar.hpp
, z tym wyjątkiem, że rodzi to bardziej fundamentalne pytanie projektowe (a przynajmniej myślę, że powinno to być w dzisiejszych czasach), dlaczego muszą zobaczyć deklaracjęBar
i dlaczegoFoo
nawet trzeba się niepokoić, aby wiedzieć o tym, jeśli jest to nieistotne dla większości przypadków użycia (po co obciążać projekt zależnością od innego, rzadko używanego?).Ponieważ koncepcyjnie tak naprawdę nie oddzieliliśmy się
Foo
odBar
. Właśnie to zrobiliśmy, aby nagłówekFoo
nie potrzebował tyle informacji na temat nagłówkaBar
, a to nie jest tak znaczące jak projekt, który naprawdę sprawia, że te dwa są całkowicie niezależne od siebie.Skrypty osadzone
To jest naprawdę w przypadku baz kodowych na większą skalę, ale inną techniką, którą uważam za niezwykle przydatną, jest użycie wbudowanego języka skryptowego dla przynajmniej najbardziej wysokiego poziomu części twojego systemu. Odkryłem, że byłem w stanie osadzić Luę w ciągu jednego dnia i umożliwić jej jednolite wywoływanie wszystkich poleceń w naszym systemie (na szczęście polecenia były abstrakcyjne). Niestety natknąłem się na przeszkodę, w której deweloperzy nie ufali wprowadzeniu innego języka i, być może najdziwniej, z występem jako największym podejrzeniem. Mimo że mogłem zrozumieć inne obawy, wydajność nie powinna stanowić problemu, jeśli używamy skryptu tylko do wywoływania poleceń, gdy użytkownicy klikają przyciski, na przykład, które nie wykonują własnych dużych pętli (co próbujemy zrobić, martwisz się o nanosekundowe różnice w czasach odpowiedzi dla kliknięcia przycisku?).
Przykład
Tymczasem najbardziej efektywnym sposobem, jaki kiedykolwiek widziałem po wyczerpujących technikach skracających czas kompilacji w dużych bazach kodowych, są architektury, które rzeczywiście zmniejszają ilość informacji potrzebnych do działania jakiejkolwiek rzeczy w systemie, a nie tylko oddzielanie jednego nagłówka od drugiego od kompilatora perspektywa, ale wymagająca od użytkowników tych interfejsów robienia tego, co muszą, wiedząc (zarówno z punktu widzenia człowieka, jak i kompilatora, prawdziwe oddzielenie, które wykracza poza zależności kompilatora), absolutne minimum.
ECS to tylko jeden przykład (i nie sugeruję, abyś go używał), ale napotkanie go pokazało mi, że możesz mieć naprawdę epickie podstawy kodowe, które wciąż budują się zaskakująco szybko, z radością wykorzystując szablony i wiele innych dodatków, ponieważ ECS, dzięki natura tworzy bardzo odsprzężoną architekturę, w której systemy muszą tylko wiedzieć o bazie danych ECS i zwykle tylko garść typów komponentów (czasami tylko jeden), aby wykonać swoje zadanie:
Projektowanie, projektowanie, projektowanie
I tego rodzaju odsprzężone projekty architektoniczne na ludzkim poziomie koncepcyjnym są bardziej skuteczne pod względem minimalizacji czasów kompilacji niż którakolwiek z technik, które zbadałem powyżej, gdy twoja baza kodu rośnie i rośnie, ponieważ wzrost ten nie przekłada się na twoją średnią jednostka kompilacji zwielokrotniająca ilość informacji potrzebnych podczas kompilacji i czasy linków do pracy (każdy system, który wymaga od przeciętnego programisty uwzględnienia mnóstwa rzeczy do zrobienia czegokolwiek, również ich wymaga, a nie tylko kompilator wiedzieć o dużej ilości informacji, aby cokolwiek zrobić ). Ma również więcej zalet niż skrócenie czasu kompilacji i rozplątywanie nagłówków, ponieważ oznacza to również, że programiści nie muszą wiedzieć dużo o systemie poza tym, co jest od razu potrzebne, aby coś z nim zrobić.
Jeśli, na przykład, możesz zatrudnić eksperta w dziedzinie fizyki, aby opracował silnik fizyki dla Twojej gry AAA, który obejmuje miliony LOC, a on może zacząć bardzo szybko, znając absolutnie absolutną minimalną informację w zakresie dostępnych rodzajów i interfejsów jak również koncepcje systemu, to oczywiście przełoży się na zmniejszenie ilości informacji zarówno dla niego, jak i kompilatora, aby wymagało zbudowania silnika fizyki, a także przekłada się na znaczne skrócenie czasu kompilacji, generalnie sugerując, że nie ma nic przypominającego spaghetti w dowolnym miejscu w systemie. I właśnie to sugeruję, aby nadać priorytet wszystkim tym technikom: w jaki sposób projektujesz swoje systemy. Wyczerpanie innych technik będzie wisienką na górze, jeśli zrobisz to, w przeciwnym razie,
źródło
To kwestia opinii. Zobacz tę odpowiedź i że jeden. I zależy to również od wielkości projektu (jeśli uważasz, że w swoim projekcie będziesz mieć miliony linii źródłowych, to nie jest tak samo, jak posiadanie kilkudziesięciu tysięcy).
W przeciwieństwie do innych odpowiedzi, zalecam jeden (raczej duży) nagłówek publiczny na podsystem (który może zawierać nagłówki „prywatne”, być może posiadające osobne pliki do implementacji wielu wbudowanych funkcji). Można nawet rozważyć nagłówek mający tylko kilka
#include
dyrektyw.Nie sądzę, że zaleca się wiele plików nagłówkowych. W szczególności nie polecam jednego pliku nagłówkowego na klasę ani wielu małych plików nagłówkowych zawierających po kilkadziesiąt linii.
(Jeśli masz dużą liczbę małych plików, musisz umieścić wiele z nich w każdej małej jednostce tłumaczeniowej , a ogólny czas kompilacji może ucierpieć)
Naprawdę chcesz zidentyfikować, dla każdego podsystemu i pliku, głównego programistę odpowiedzialnego za to.
Wreszcie w przypadku małego projektu (np. Zawierającego mniej niż sto tysięcy wierszy kodu źródłowego) nie jest to bardzo ważne. Podczas projektu będziesz w stanie z łatwością refaktoryzować kod i zreorganizować go w różne pliki. Będziesz po prostu kopiować i wklejać fragmenty kodu do nowych plików (nagłówka), co nie jest wielkim problemem (trudniejsze jest mądrze zaprojektować sposób reorganizacji plików, i to jest specyficzne dla projektu).
(moje osobiste preferencje to unikanie zbyt dużych i zbyt małych plików; często mam pliki źródłowe zawierające kilka tysięcy wierszy każdy; i nie boję się pliku nagłówkowego - w tym wbudowanych definicji funkcji - zawierającego setki wierszy lub nawet kilku tysiące z nich)
Zauważ, że jeśli chcesz używać prekompilowanych nagłówków z GCC (co czasem jest rozsądnym podejściem do krótszego czasu kompilacji), potrzebujesz jednego pliku nagłówka (w tym wszystkich pozostałych, a także nagłówków systemowych).
Zauważ, że w C ++ standardowe pliki nagłówkowe pobierają dużo kodu . Na przykład
#include <vector>
ciągnie ponad dziesięć tysięcy linii na moim GCC 6 w systemie Linux (18100 linii). I#include <map>
rozwija się do prawie 40KLOC. Dlatego, jeśli masz wiele małych plików nagłówkowych, w tym standardowych nagłówków, kończysz parsowanie wielu tysięcy wierszy podczas kompilacji, a twój czas kompilacji cierpi. Dlatego nie lubię mieć wielu małych linii źródłowych C ++ (najwyżej kilkuset wierszy), ale wolę mieć mniej, ale większe pliki C ++ (kilka tysięcy wierszy).(tak więc posiadanie setek małych plików C ++, które zawsze zawierają - nawet pośrednio - kilka standardowych plików nagłówkowych, daje ogromny czas kompilacji, co denerwuje programistów)
W kodzie C dość często pliki nagłówkowe rozwijają się do czegoś mniejszego, więc kompromis jest inny.
Zainspiruj się także wcześniejszą praktyką w istniejących projektach wolnego oprogramowania (np. Na github ).
Zauważ, że zależności można rozwiązać przy pomocy dobrego systemu automatyzacji kompilacji . Przestudiuj dokumentację marki GNU . Pamiętaj o różnych
-M
flagach preprocesora dla GCC (przydatne do automatycznego generowania zależności).Innymi słowy, twój projekt (z mniej niż setką plików i tuzinem programistów) prawdopodobnie nie jest wystarczająco duży, aby naprawdę zainteresować się „nagłówkiem piekła”, więc twoje obawy nie są uzasadnione . Możesz mieć tylko kilkanaście plików nagłówkowych (lub nawet znacznie mniej), możesz wybrać jeden plik nagłówkowy na jednostkę tłumaczeniową, możesz nawet wybrać jeden plik nagłówkowy, a cokolwiek wybierzesz, nie będzie „nagłówek piekła” (a refaktoryzacja i reorganizacja plików pozostałyby dość łatwe, więc początkowy wybór nie jest tak naprawdę ważny ).
(Nie skupiaj swoich wysiłków na „nagłówku piekła” - co nie jest dla ciebie problemem - ale skup je na zaprojektowaniu dobrej architektury)
źródło