この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。
41. プロジェクトに新しいライブラリーを追加しない
プロジェクトに X 機能を実装する必要があるとします。ソフトウェア開発の理論家は、既存のライブラリー Y を使用して必要な機能を実装すべきだと言うでしょう。実際、既存のライブラリーまたはサードパーティー・ライブラリーを再利用することは、ソフトウェア開発における伝統的なアプローチです。そして、ほとんどのプログラマーがこの方法を採用しています。
しかし、そうすることで、約 10 年後に数十のサードパーティー・ライブラリーをサポートしなければならなくなることには言及していません。
私は、プロジェクトに新しいライブラリーを追加しないように強く推奨します。勘違いしないでください。ライブラリーを全く使用せずに、すべて自分で記述すべきだと言っているのではありません。そうではなく、ときとして、ちょっとクールな些細な「機能」をプロジェクトに追加したいというある開発者の気まぐれによって、新しいライブラリーがプロジェクトに追加されることがあります。プロジェクトに新しいライブラリーを追加することは難しくありませんが、チーム全体が何年もの間サポートを負担しなければならなくなります。
いくつかの大規模なプロジェクトの進化を追跡することで、私は多数のサードパーティー・ライブラリーによって多くの問題が引き起こされるのを目にしてきました。ここでは、その一部のみを紹介しますが、このリストを見ただけでも考えさせられるものがあると思います。
- 新しいライブラリーの追加は、プロジェクト・サイズを増加させます。今日の高速インターネットと大容量 SSD ドライブの時代にあっては、これは大きな問題になりませんが、バージョン・コントロール・システムからのダウンロード時間が 1 分から 10 分になったら快く思わないでしょう。
- ライブラリー機能の 1% のみを使用する場合であっても、通常、プロジェクトにはライブラリー全体が含まれます。ライブラリーをコンパイルされたモジュール形式 (DLL など) で使用する場合、配布サイズは大幅に増加します。ソースコード形式で使用する場合は、コンパイル時間が大幅に増加します。
- プロジェクトのコンパイルに関連する環境が複雑になります。一部のライブラリーは、追加のコンポーネントを必要とします。単純な例として、ビルドに Python* が必要な場合、プロジェクトをビルドするには、多数の追加のプログラムが必要になります。そのため、問題が発生する可能性が高くなります。これは説明するのは難しく、実際に経験する必要があります。大規模なプロジェクトでは、常に何らかの問題が発生し、すべてが動作しコンパイルできるようにするため、多大な労力を費やす必要があります。
- 脆弱性への対策が重要な場合は、サードパーティー・ライブラリーを定期的に更新する必要があります。攻撃者は、コード・ライブラリーの脆弱性を見つけることに興味を持っています。多くのライブラリーはオープンソースであり、ライブラリーの脆弱性が見つかると、そのライブラリーが使用されている多数のアプリケーションへの突破口となります。
- ライブラリーのライセンス形態は、急に変更される可能性があります。そのことを念頭に置き、変更を追跡する必要があります。また、ライセンス形態が変更された場合、どうすべきかが不明です。例えば、かつて非常に広範に使用されていた SoftFloat は、個人的な合意から BSD に移行しました。
- 新しいバージョンのコンパイラーへアップグレードする際に問題が生じます。新しいコンパイラーに未対応のライブラリーが必ずいくつかあり、対応されるのを待つか、自身でライブラリーを修正する必要があります。
- 異なるコンパイラーへ移行する際に問題が生じます。例えば、現在 Visual C++* を使用しており、インテル® C++ コンパイラーへ移行する場合、いくつかのライブラリーで何らかの問題が生じるでしょう。
- 異なるプラットフォームへ移行する際に問題が生じます。これは、完全に異なるプラットフォームとは限りません。Win32 アプリケーションを Win64 へ移行する場合であっても、同様の問題が生じます。おそらく、いくつかのライブラリーは Win64 に対応しておらず、対応を検討しなければならないでしょう。特に、ライブラリーの開発が休止状態にあり、新しいバージョンがリリースされていない場合、対応に困ります。
- 型が名前空間に格納されない C ライブラリーを多用すると、遅かれ早かれ名前の衝突が起こります。これは、コンパイルエラーや隠れたエラーを引き起こします。例えば、意図したものではなく、正しくない enum 定数が使用される可能性があります。
- プロジェクトがすでに多数のライブラリーを使用している場合、もう 1 つ追加しても害がなさそうに見えるかもしれません。これは、割れ窓理論に似ています。しかし、結果として、プロジェクトの成長は無秩序な混乱を招きます。
- さらに、私が気付いていないだけで、新しいライブラリーの追加はほかの多くのデメリットをもたらす可能性があります。どのような場合でも、追加のライブラリーによりプロジェクト・サポートの複雑さが増します。思いがけない場所で問題が起こる可能性があります。
繰り返しになりますが、私はサードパーティー・ライブラリーを全く使用すべきでないとは言っていません。私自身、プログラムで PNG 形式の画像を処理する場合、自分でライブラリーを開発する代わりに、LibPNG ライブラリーを使用します。
しかし、PNG の処理であっても、立ち止まって良く考える必要があります。本当にライブラリーが必要なのか? 画像を使用してどのような処理をしたいのか? 画像を *.png ファイル形式で保存するだけなら、システム関数で十分です。例えば、Windows* アプリケーションの場合、WIC (英語) を使用できます。すでに MFC ライブラリーを使用している場合は、CImage クラス (詳細は、StackOverflow の議論 (英語) を参照) を利用できます。これで、ライブラリーが 1 つ減ります。
私の経験から、1 つの例を紹介します。PVS-Studio アナライザーの開発中に、いくつかの診断で単純な正規表現を使用する必要がありました。一般に、私は静的解析で正規表現を使用することは適切ではないと考えています。非常に非効率的なアプローチだからです。私は、このトピックに関して記事 (英語) も執筆しています。しかし、場合によっては、正規表現を使用して文字列から何かを見つけなければならないことがあります。
既存のライブラリーを追加することは可能でしたが、すべてが冗長になることは明らかでした。同時に、どうしても正規表現が必要であり、対応方法を考えなければなりませんでした。
全く偶然にも、そのとき私は『Beautiful Code』 (ISBN 9780596510046) を読んでいました。単純で洗練されたソリューションについての本でした。そこで、私は正規表現の非常に単純な実装方法に出会いました。わずか数十行の文字列です。たったそれだけです。
その実装を PVS-Studio で使用することにしました。結果的に、複雑な正規表現を必要としない我々にとって、この実装は十分でした。
結論: 新しいライブラリーを追加する代わりに、30 分で必要な機能を記述できました。新たなライブラリーを 1 つ追加せずに済みました。これは非常に良い決断となりました。それから 5 年以上が立ちますが、我々はその実装に満足しています。
この経験から、私は単純なソリューションのほうが優れていると確信を持つようになりました。(できるだけ) 新しいライブラリーを追加しないようにすることで、単純なプロジェクトになります。
皆さんのご参考までに、以下が『Beautiful Code』 (ISBN 9780596510046) に掲載されていた正規表現を検索するコードです。非常に洗練されているでしょう。PVS-Studio へ統合する際には、このコードをわずかに変更しましたが、主なアイデアは同じです。
// regular expression format // c Matches any "c" letter //.(dot) Matches any (singular) symbol //^ Matches the beginning of the input string //$ Matches the end of the input string # Match the appearance of the preceding character zero or // several times int matchhere(char *regexp, char *text); int matchstar(int c, char *regexp, char *text); // match: search for regular expression anywhere in text int match(char *regexp, char *text) { if (regexp[0] == '^') return matchhere(regexp+1, text); do { /* must look even if string is empty */ if (matchhere(regexp, text)) return 1; } while (*text++ != '\0'); return 0; } // matchhere: search for regexp at beginning of text int matchhere(char *regexp, char *text) { if (regexp[0] == '\0') return 1; if (regexp[1] == '*') return matchstar(regexp[0], regexp+2, text); if (regexp[0] == '$' && regexp[1] == '\0') return *text == '\0'; if (*text!='\0' && (regexp[0]=='.' || regexp[0]==*text)) return matchhere(regexp+1, text+1); return 0; } // matchstar: search for c*regexp at beginning of text int matchstar(int c, char *regexp, char *text) { do { /* * a * matches zero or more instances */ more instances */ if (matchhere(regexp, text)) return 1; } while (*text != '\0' && (*text++ == c || c == '.')); return 0; }
このバージョンは非常に単純で、機能が限定されていますが、これまでこれで十分でしたし、これからもそうであると思っています。これは、単純なソリューションが複雑なソリューションよりも優れている良い例です。
推奨事項
新しいライブラリーは、急いでプロジェクトに追加せずに、他の方法では対応できない場合にのみ追加します。
以下は、可能性のある回避策です。
- システム API または既存のライブラリーに必要な機能がないか確認することをお勧めします。
- ライブラリーの小さな機能のみを使用する場合、自身で実装したほうが良いでしょう。「念のため」ライブラリー全体を追加するというのは良くありません。ほぼ確実に、そのライブラリーは将来あまり使用されないでしょう。プログラマーは、実際には必要のない普遍性を求めることがあります。
- タスクを解決するいくつかのライブラリーがある場合、要件を満たす最も単純なものを選択します。前述のとおり、「素晴らしいライブラリーだから、追加しておこう」という考えは捨てるべきです。
- 新しいライブラリーを追加する前に、いったん立ち止まって考えてください。休憩を取り、コーヒーを飲み、同僚と議論するのも良いでしょう。サードパーティー・ライブラリーを使用せずに、全く異なる方法で問題を解決できることに気付くかもしれません。
補足: 誰もがここで述べたことに完全に同意できるわけではないでしょう。例えば、ユニバーサル・ポータブル・ライブラリーの代わりに WinAPI の使用を推奨していることなどです。これは、プロジェクトを 1 つのオペレーティング・システムに「バインド」し、プログラムの移植性を著しく妨げるという考えに基づいて異論が生じるかもしれません。しかし、私はそうは思いません。通常、「異なるオペレーティング・システムへ移植する」というのは、プログラマーの考え方でしかなく、このようなタスクは管理者にとっては不必要かもしれません。あるいは、プロジェクトはその複雑さと普遍性が原因で、人気を得て移植が必要になる前に失敗に終わるかもしれません。前述のサードパーティー・ライブラリーの問題リストの 8 についても忘れないでください。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。