私たちはゼロから新しいプロジェクトを始めています。約8人の開発者、それぞれが4つまたは5つのソースファイルを持つ12のサブシステム。
「ヘッダー地獄」、別名「スパゲッティヘッダー」を防ぐために何ができるでしょうか。
私は「最善の」方法を求めているのではなく、何に注意すべきか、何が悲しみを引き起こしているのかを指摘するだけで、それを回避しようとしています。
これはC++プロジェクトになりますが、C情報は将来の読者に役立つでしょう。
簡単な方法:ソースファイルごとに1つのヘッダー。ユーザーがソースファイルについて知る必要がない完全なサブシステムがある場合は、必要なすべてのヘッダーファイルを含むサブシステムのヘッダーを1つ用意します。
ヘッダーファイルはそれ自体でコンパイル可能である必要があります(または、単一のヘッダーを含むソースファイルをコンパイルする必要があるとしましょう)。どのヘッダーファイルに必要なものが含まれているのかを見つけて、他のヘッダーファイルを探し出さなければならないのは面倒です。これを強制する簡単な方法は、すべてのソースファイルに最初にヘッダーファイルを含めることです(doug65536に感謝します。ほとんどの場合、気付かないうちにそれを行うと思います)。
使用可能なツールを使用してコンパイル時間を短縮するようにしてください。各ヘッダーは1回だけ含める必要があります。事前コンパイル済みヘッダーを使用してコンパイル時間を短縮し、可能であれば事前コンパイル済みモジュールを使用して、コンパイル時間をさらに短縮します。
最も重要な要件は、ソースファイル間の依存関係を減らすことです。 C++では、クラスごとに1つのソースファイルと1つのヘッダーを使用するのが一般的です。したがって、優れたクラス設計があれば、ヘッダー地獄に近づくことすらありません。
これを逆に見ることもできます。プロジェクトにすでにヘッダー地獄がある場合は、ソフトウェア設計を改善する必要があることを確信できます。
特定の質問に答えるには:
他の推奨事項に加えて、依存関係を削減する方法に沿って(主にC++に適用可能):
ソースファイルごとに1つのヘッダー。ソースファイルが実装/エクスポートするものを定義します。
各ソースファイルに含まれる必要な数のヘッダーファイル(独自のヘッダーから開始)。
他のヘッダーファイル内にヘッダーファイルを含めない(含めるのを最小限に抑えます)(循環依存を避けるため)。詳細については、 を参照してください。「2つのクラスがC++を使用して相互に参照できますか?」に対するこの回答
このテーマに関する本全体が、LakosによるLarge-Scale C++ Software Designです。それはソフトウェアの「レイヤー」を持つことを説明します。高レベルのレイヤーは低レベルのレイヤーを使用するのではなく、その逆ではなく、循環依存関係を回避します。
2種類のヘッダー地獄があるので、あなたの質問は根本的に答えられないと主張します:
問題は、前者を避けようとすると、ある程度、後者で終わり、逆もまた同様です。
3番目の種類の地獄もあります。これは循環依存です。注意しないとこれらがポップアップする可能性があります...それらを回避することはそれほど複雑ではありませんが、それを行う方法について考えるために時間をかける必要があります。 John Lakos talkCppCon 2016 (または slides )のレベル化について参照してください。
デカップリング
結局のところ、コンパイラとリンカの特性のニュアンスを欠いている最も基本的な設計レベルでの結局のところ、私にとってデカップリングについてです。つまり、各ヘッダーにクラスを1つだけ定義させたり、pimplsを使用したり、宣言のみが必要なタイプに宣言を転送したり、定義したりせず、転送宣言だけを含むヘッダーを使用することもできます(例:<iosfwd>
)、ソースファイルごとに1つのヘッダー。宣言/定義されるもののタイプに基づいてシステムを一貫して編成します。
「コンパイル時の依存関係」を減らすためのテクニック
そして、いくつかのテクニックはかなり役立ちますが、これらのプラクティスを使い果たしても、システム内の平均的なソースファイルは、急上昇したビルド時間で少し意味のあることを行うために#include
ディレクティブの2ページのプリアンブルを必要とすることがわかりますインターフェイスデザインの論理的な依存関係を減らすことなく、ヘッダーレベルでコンパイル時の依存関係を減らすことに重点を置きすぎた場合、厳密に言えば「スパゲッティヘッダー」とは見なされないかもしれませんが、それでも同様の有害な問題につながると言います。実際の生産性に。 1日の終わりに、コンパイルユニットが何かを実行するために可視の大量の情報を必要とする場合、ビルド時間の増加につながり、開発者が作成している間に戻って物事を変更しなければならない可能性がある理由を増やします。彼らは日々のコーディングを終えようとしているだけでシステムに頭を突っ込んでいるように感じます。それは、適切なデカップリングが存在しないこれらのタイプのテクニックが、一度に1つのスパゲッティヌードルを手渡すかもしれないようなものですが、とにかくコンパイルユニットでスパゲッティを作ることになります。
たとえば、各サブシステムに1つの非常に抽象的なヘッダーファイルとインターフェイスを提供させることができます。しかし、サブシステムが互いに分離されていない場合、機能するために混乱したように見える依存グラフを持つ他のサブシステムインターフェースに依存するサブシステムインターフェースを使用して、スパゲッティに似た何かが再び得られます。
外部型への前方宣言
ビルドに2時間かかった以前のコードベースを取得するために私が使い果たしたすべての手法の中で、開発者はビルドサーバーでCIがオンになるのを2日間待つこともありました(これらのビルドマシンは、必死に努力する負担の枯渇した獣とほぼ想像できるでしょう)。開発者が変更をプッシュしている間、継続して失敗するため)、私にとって最も疑わしいのは、他のヘッダーで定義された型を前方宣言することでした。そして、私はなんとかして、コードベースを40分程度にまで減らしました。その後、少しずつインクリメンタルなステップでこれを実行し、「ヘッダースパゲッティ」を削減しようとしました。トンネルがヘッダーの相互依存関係を想定している間の設計)は、他のヘッダーで定義された型を前方宣言していました。
次のようなFoo.hpp
ヘッダーを想像してみてください。
#include "Bar.hpp"
そして、ヘッダーでは宣言ではなく定義ではなくBar
のみを使用します。その場合、Bar
の定義がヘッダーに表示されないようにするためにclass Bar;
を宣言するのは簡単です。実際の場合を除いて、ほとんどの場合、Foo.hpp
を使用するほとんどのコンパイルユニットは、結局Bar
を定義する必要があり、Bar.hpp
自体を含める必要があるという追加の負担があります。またはFoo.hpp
の真の効果があるか、コンパイルユニットの99%がBar.hpp
を含めなくても機能するという別のシナリオに遭遇した場合は、より基本的な設計上の問題(または少なくともI最近は)Bar
の宣言を確認する必要がある理由と、ほとんどのユースケースに関係がない場合(なぜデザインに負担がかかるのか)についてFoo
までわざわざ知る必要がある理由かろうじて使用された別のものへの依存関係はありますか?).
概念的には、Foo
をBar
から実際に分離していないためです。 Foo
のヘッダーがBar
のヘッダーに関する情報をあまり必要としないように作成しました。これは、これら2つを完全に独立させる設計ほど重要ではありません。お互いの。
埋め込みスクリプト
これは本当に大規模なコードベース用ですが、私が非常に便利だと思うもう1つの手法は、少なくともシステムの最も高レベルな部分に埋め込みスクリプト言語を使用することです。 Luaを1日で埋め込むことができ、システム内のすべてのコマンドを均一に呼び出すことができた(コマンドは抽象的でありがたいことに)。残念ながら、私は開発者が別の言語の導入を信用せず、おそらく最も奇妙なことに、パフォーマンスが最大の疑いであるという障害に遭遇しました。それでも他の懸念事項は理解できましたが、たとえばユーザーがボタンをクリックしたときにスクリプトを使用してコマンドを呼び出すだけの場合、パフォーマンスは問題にはなりません。たとえば、独自の大きなループを実行しない場合(何をしようとしているのか、ボタンクリックの応答時間のナノ秒の違いについて心配ですか?)。それはあなたにはそれほど当てはまらないかもしれませんが、「ヘッダースパゲッティ」に関連する他の問題よりもビルド時間の問題に重点を置いた大規模なコードベースの場合に検討するオプションです。プリコンパイルおよびリンクされるようになりました(現在、すべての統合テストでさえ、ビルドシステムとCIのロードオフである高レベルスクリプトで記述しています)。
例
一方、大規模なコードベースでコンパイル時間を短縮するためのテクニックを使い尽くした後、私が今まで目にした最も効果的な方法は、システム内の1つのものに必要な情報量を正確に削減するアーキテクチャですコンパイラーの観点からヘッダーを別のヘッダーから切り離すだけでなく、これらのインターフェイスのユーザーに、最小限の知識(コンパイラーの依存関係を超えた真のデカップリング)を理解しながら、実行する必要があることを実行するように要求します。
ECSは1つの例にすぎません(使用することはお勧めしません)が、これに遭遇すると、テンプレートや他の多くの便利な機能をECSを活用することで、驚くほど迅速にビルドできる非常にエピックなコードベースがいくつかあることがわかりました。自然は、非常に分離されたアーキテクチャを作成します。システムは、ECSデータベースについてのみ知る必要があり、通常は、少数のコンポーネントタイプ(場合によっては1つ)だけがその機能を実行します。
デザイン、デザイン、デザイン
そして、人間の概念レベルでのこの種の分離されたアーキテクチャ設計は、コードベースが成長し、成長し、成長するにつれて、その成長が平均に変換されないため、上記で検討したどの手法よりもコンパイル時間を最小限に抑えるという点でより効果的です。コンパイル時に必要な量の情報を乗算してコンパイルするコンパイルユニット(平均的な開発者が何かを実行するために大量のデータを含める必要があるシステムでは、コンパイラーだけでなく、何かを実行するために大量の情報を知っている必要があります。 )。また、開発者はシステムで何かを行うためにすぐに必要なことを超えてシステムについて多くを知る必要がないことを意味するため、ビルド時間の短縮やヘッダーのもつれよりも多くの利点があります。
たとえば、数百万のLOCにまたがるAAAゲーム用の物理エンジンを開発するために専門の物理学開発者を雇うことができ、利用可能なタイプやインターフェースなどの最低限の情報を知りながら、非常に迅速に始めることができる場合システムの概念だけでなく、物理エンジンを構築するために必要な情報量も、コンパイラーとコンパイラーの両方にとって自然に減少し、同様に、一般的にスパゲッティに似ていないことを意味しながら、ビルド時間の大幅な短縮につながります。システムのどこにでも。そして、それが、他のすべての手法よりも優先することを提案するものです。つまり、システムの設計方法です。他のテクニックを使い果たしてしまうと、最悪の場合にはケーキを使わずにアイシングをするようになります。
それは意見の問題です。this 回答と that oneを参照してください。また、プロジェクトのサイズにも大きく依存します(プロジェクトに数百万のソース行があると思われる場合、それは数十万のソース行を持つことと同じではありません)。
他の回答とは対照的に、サブシステムごとに1つの(かなり大きな)パブリックヘッダーをお勧めします(「プライベート」ヘッダーを含めることができ、おそらく多くのインライン関数の実装用に個別のファイルを持っている可能性があります)。いくつかの#include
ディレクティブのみを含むヘッダーを検討することもできます。
多くのヘッダーファイルが推奨されているとは思いません。特に、クラスごとに1つのヘッダーファイルを使用することや、それぞれ数十行の小さなヘッダーファイルを多数使用することはお勧めしません。
(小さなファイルがたくさんある場合は、すべての小さな translation unit に多くのファイルを含める必要があり、全体的なビルド時間が低下する可能性があります)
実際に必要なのは、サブシステムとファイルごとに、その責任を負う主な開発者を特定することです。
最後に、小さなプロジェクト(たとえば、ソースコードが10万行未満)の場合、それはそれほど重要ではありません。プロジェクトの間、コードをリファクタリングし、別のファイルに再編成することは非常に簡単です。コードのチャンクをコピーして新しい(ヘッダー)ファイルに貼り付けるだけで、大したことではありません(ファイルを再編成する方法を賢く設計することはさらに難しく、それはプロジェクト固有です)。
(私の個人的な好みは、ファイルが大きすぎたり小さすぎたりしないようにすることです。多くの場合、それぞれ数千行のソースファイルがあります。数百行または数行のヘッダーファイル(インライン関数定義を含む)を恐れていません。それらの数千)
[pre-compiled headers with [〜#〜] gcc [〜#〜] (whichsometimesは、コンパイル時間を短縮するための賢明なアプローチです)単一のヘッダーファイル(他のすべてのファイルとシステムヘッダーも含む)が必要です。
C++では、標準ヘッダーファイルがcodeのlotをプルしていることに注意してください。たとえば、#include <vector>
は、Linux上のGCC 6(18100行)で1万行を超えています。そして#include <map>
は、ほぼ40KLOCに拡張されます。したがって、標準ヘッダーを含む多くの小さなヘッダーファイルがある場合、ビルド中に何千行もの行を再解析することになり、コンパイル時間が長くなります。これが、多くの小さなC++ソース行(せいぜい数百行)が嫌いですが、(ただし数千行の)少ないが大きなC++ファイルを持つことを好みます。
(つまり、数百のsmallC++ファイルに常に間接的に含まれる-いくつかの標準ヘッダーファイルが含まれていると、ビルド時間が非常に長くなり、開発者を困らせます)
Cコードでは、ヘッダーファイルがかなり小さいものに拡張されることがよくあるため、トレードオフは異なります。
existingfree software プロジェクト(例 github )での以前の慣例も参考にしてください。
依存関係は適切な ビルドオートメーション システムで処理できることに注意してください。 GNU make のドキュメントを調べます。 さまざまな-M
プリプロセッサフラグ をGCCに注意してください(自動的に依存関係を生成するのに役立ちます) 。
言い換えると、プロジェクト(ファイル数が100未満で開発者が数十人)は、「ヘッダー地獄」を心配するほど大きくはないので、懸念事項はnotjustified。ヘッダーファイルは数十個(またはそれよりもはるかに少ない)にすることも、翻訳単位ごとに1つのヘッダーファイルを選択することも、1つのヘッダーファイルを選択することもできます。 "header hell"(およびファイルのリファクタリングと再編成はかなり簡単なままなので、最初の選択はnot本当にimportant)です。
(あなたの努力を「ヘッダー地獄」に集中しないでください-これはあなたにとって問題ではありませんが、良いアーキテクチャを設計するために集中してください)