F#についてよく聞かれる不満の一つに、コードを 依存順 に書く必要があるという点があります。つまり、コンパイラがまだ認識していないコードへの前方参照を使うことができません。
典型的な例をいくつか紹介します:
「.fsファイルの順序がコンパイルを難しくしています...私のF#アプリケーションはたった50行程度のコードですが、ごく小さな非自明なアプリケーションをコンパイルするのにすでに労力がかかりすぎています。F#コンパイラをC#コンパイラのようにして、ファイルがコンパイラに渡される順序に密接に結びついていないようにする方法はありませんか?」 [fpish.net]
別の例:
「おもちゃの域をわずかに超えるプロジェクトをF#で構築しようとした後、現在のツールでは中程度の複雑さのプロジェクトでさえ維持するのがかなり難しいだろうという結論に達しました。」 [www.ikriv.com]
さらに別の例:
「F#コンパイラは線形すぎます。F#コンパイラは、宣言の順序に関係なく、すべての型解決を自動的に処理すべきです。」 [www.sturmnet.org]
もう一つ:
「F#プロジェクトシステムの煩わしい(そして個人的には不必要な)制限についてはすでにこのフォーラムで議論されています。コンパイル順序の制御方法について話しています。」 [fpish.net]
しかし、これらの不満には根拠がありません。F#を使って大規模なプロジェクトを構築・維持することは、十分に可能です。F#のコンパイラとコアライブラリがその好例です。
これらの問題のほとんどは、結局、「F#はなぜC#のようにならないのか」という疑問に帰結します。C#から来た人は、コンパイラがすべてを自動的に接続してくれることに慣れています。依存関係を明示的に扱う必要があることは非常に面倒です。古臭く、退歩的にさえ感じられます。
この投稿の目的は、(a) 依存関係の管理がなぜ重要なのか、そして(b) 依存関係を管理するためのいくつかのテクニックについて説明することです。
依存関係は厄介なもので...
依存関係は、私たちの日常における悩みの種であることは皆知っています。アセンブリの依存関係、設定の依存関係、データベースの依存関係、ネットワークの依存関係など、常に何かが存在します。
そのため、私たち開発者は職業として、依存関係をより管理しやすくするために多くの労力を費やす傾向があります。この目標は様々な形で現れます:インターフェース分離の原則、依存性逆転の原則と依存性の注入、NuGetによるパッケージ管理、puppet/chefによる設定管理などです。ある意味、これらのアプローチはすべて、意識しなければならないものの数と、壊れる可能性のあるものの数を減らそうとしています。
もちろん、これは新しい問題ではありません。古典的な書籍『大規模C++ソフトウェアデザイン』の大部分は依存関係の管理に充てられています。著者のJohn Lakosが言うように:
「コンポーネント間の不必要な依存関係を避けることで、サブシステムのメンテナンスコストを大幅に削減できます」
ここでのキーワードは「不必要な」です。何が「不必要な」依存関係なのでしょうか?もちろん、状況によります。しかし、ある特定の種類の依存関係はほぼ常に不必要です - それが循環依存です。
...そして循環依存は悪
循環依存がなぜ悪なのかを理解するために、「コンポーネント」の意味を再確認してみましょう。
コンポーネントは良いものです。パッケージ、アセンブリ、モジュール、クラスなど、何と呼ぶかは別として、その主な目的は大量のコードをより小さく管理しやすい部分に分割することです。つまり、ソフトウェア開発の問題に分割統治アプローチを適用しているのです。
しかし、メンテナンス、デプロイメント、その他の目的で有用であるためには、コンポーネントは単なるランダムな要素の集まりであってはいけません。(もちろん)関連するコードのみをグループ化すべきです。
理想的な世界では、各コンポーネントは他のコンポーネントから完全に独立しているはずです。しかし一般的に(当然ながら)、いくつかの依存は常に必要です。
しかし、依存関係を持つコンポーネントができたので、これらの依存関係を管理する方法が必要になります。標準的な方法の一つが「レイヤリング」の原則です。「高レベル」レイヤーと「低レベル」レイヤーを持つことができ、重要なルールは次のとおりです:各レイヤーはその下のレイヤーにのみ依存し、上のレイヤーには決して依存しない。
これには馴染みがあるはずです。以下は単純なレイヤーの図です:
しかし、最下層から最上層への依存を導入するとどうなるでしょうか?
最下層から最上層への依存を導入することで、悪の「循環依存」を生み出してしまいました。
なぜ悪なのでしょうか?それは、どんな代替のレイヤリング方法も有効になってしまうからです!
たとえば、最下層を最上層に置くこともできます:
論理的な観点から見れば、この代替レイヤリングは元のレイヤリングと同じです。
では、中間層を最上層に置くのはどうでしょうか?
何かがひどく間違っています!明らかに、物事を台無しにしてしまいました。
実際、コンポーネント間に何らかの循環依存がある場合、できることはすべてのコンポーネントを同じレイヤーに置くことだけです。
言い換えれば、循環依存は「分割統治」アプローチを完全に破壊し、そもそもコンポーネントを持つ理由を台無しにしてしまいます。3つのコンポーネントを持つ代わりに、今や1つの「スーパーコンポーネント」を持つことになり、それは必要以上に3倍大きく複雑になっています。
これが循環依存が悪である理由です。
このトピックの詳細については、StackOverflowの回答とPatrick Smacchia(NDepend)によるレイヤリングに関する記事を参照してください。
実世界における循環依存
まず、.NETアセンブリ間の循環依存を見てみましょう。Brian McNamaraの経験談をいくつか紹介します(強調は筆者による):
.Net Framework 2.0 ではこの問題が特に顕著です。System.dll、System.Configuration.dll、System.Xml.dllは互いに絡み合っており、非常に厄介です。これは様々な問題を引き起こします。たとえば、デバッグ時にブレークポイントに到達するとデバッグ対象がクラッシュしてしまうという Visual Studio デバッガーの[バグ]を発見したのですが、原因はこのアセンブリ間の循環依存でした。もう一つの話として、知り合いの開発者が Silverlight の初期バージョンでこの 3 つのアセンブリを軽量化するというタスクを担当したのですが、最初にしなければならなかったのは、この循環依存を解きほぐすという骨の折れる作業でした。「相互再帰できる」ことは、小さな規模では便利でも、大きな規模になると破滅をもたらします。
VS2008 は当初の予定より 1 週間遅れて出荷されました。というのも、VS2008 は SQL Server に依存しており、SQL Server は逆に VS に依存していたからです。そしてなんと、最終的にすべてのビルド番号を揃えた完全な製品版をリリースすることができず、なんとか機能させるために慌てて対応しなければなりませんでした。 [fpish.net]
このように、アセンブリ間の循環依存が悪いという証拠は十分にあります。実際、Visual Studioがアセンブリ間の循環依存を許可していないのは、それがとても悪いことだからです!
「もちろん、アセンブリの循環依存が悪いのはわかります。でも、アセンブリ内のコードについてなぜ気にする必要があるのでしょうか?」と思うかもしれません。
それは、まさに同じ理由からです!レイヤリングは、より良い分割、容易なテスト、クリーンなリファクタリングを可能にします。実世界の循環依存を説明する関連記事でC#プロジェクトとF#プロジェクトを比較しているのを見れば、私が言いたいことがわかると思います。F# プロジェクトの依存関係は、はるかにスパゲッティコード的ではありません。
Brianの(優れた)コメントからもう一つ引用します:
ここで私はあまり人気のない立場を主張しますが、私の経験上、システムのあらゆるレベルで「ソフトウェアコンポーネントの依存順」を検討し、管理することを強制されると、世の中のすべてがより良くなると思います。F#の特定のUI/ツールはまだ理想的ではないかもしれませんが、原則は正しいと思います。これは歓迎すべき負担なのです。確かに多くの作業が必要です。「ユニットテスト」も多くの作業を必要としますが、長期的には時間を節約できるという点で、その作業には「価値がある」というのが一般的な認識になっています。私は「順序付け」についても同じように感じています。システム内のクラスやメソッドの間には依存関係があります。そうした依存関係を無視すると、自分で自分の首を絞めることになります。この依存関係グラフ(大まかに言えば、コンポーネントのトポロジカルソート)を考慮するようにシステムが強制すると、よりクリーンなアーキテクチャ、より良いシステムレイヤリング、そして不必要な依存関係が少ないソフトウェアを開発する方向に導かれるでしょう。
循環依存の検出と除去
さて、循環依存が悪いことに同意したとして、それらをどのように検出し、取り除けばよいのでしょうか?
まず検出から始めましょう。コード内の循環依存を検出するのに役立つツールがいくつかあります。
- C#を使用している場合は、極めて便利なNDependのようなツールが必要になります。
- Javaを使用している場合は、JDependなどの同等のツールがあります。
- しかし、F#を使用している場合は幸運です!循環依存の検出が無料で付いてきます!
「冗談でしょう」と思うかもしれません。「F#の循環依存の禁止についてはすでに知っています - それが私を悩ませているのです!問題を解決してコンパイラを満足させるには、どうすればよいのでしょうか?」
その答えは、次の投稿で説明します...