23

現在大規模なモノリシック ライブラリに依存性注入を導入する作業を計画しています。これは、ライブラリの単体テストを容易にし、理解しやすくし、おまけとしてより柔軟にするためです。

私はNInjectを使用することに決めました。ネイトのモットーである「1 つのことをうまく行う」(言い換え) が本当に好きで、DI のコンテキスト内で特にうまくいくようです。

私が今疑問に思っているのは、現在単一の大きなアセンブリであるものを、ばらばらな機能セットを持つ複数の小さなアセンブリに分割する必要があるかどうかということです。これらの小さなアセンブリの一部には相互依存関係がありますが、コードのアーキテクチャはすでにかなり疎結合されているため、すべてではありません。

これらの機能セットは、それ自体が些細で小さいものではないことに注意してください...クライアント/サーバー通信、シリアル化、カスタム コレクション タイプ、ファイル IO 抽象化、共通ルーチン ライブラリ、スレッド ライブラリ、標準ロギングなどを含みます。

前の質問がわかりました:多くの小さなアセンブリと 1 つの大きなアセンブリのどちらが優れていますか? 種類はこの問題に対処しますが、これよりもさらに細かい粒度であると思われるため、この場合でもそこにある答えが適用されるかどうか疑問に思いますか?

また、このトピックの近くにあるさまざまな質問の中で、「多すぎる」アセンブリが不特定の「痛み」と「問題」を引き起こしているというのが一般的な答えです。このアプローチの潜在的なマイナス面を具体的に知りたいです。

以前は 1 つしか必要なかったのに 8 つのアセンブリを追加するのは「少し面倒」であることに同意しますが、すべてのアプリケーションに大きなモノリシック ライブラリを含めなければならないことも理想的ではありません...さらに、8 つのアセンブリを追加することは、あなただけが行うことです。ですから、私はその議論にほとんど同情しません (たとえ最初は他のみんなと一緒に文句を言うかもしれませんが)。

補遺:
これまでのところ、小規模なアセンブリに反対する説得力のある理由は見られませんでした。そのため、これが問題ではないかのように、今のところ続行すると思います。検証可能な事実を裏付ける確かな理由を誰かが思いつくことができれば、私はまだそれらについて聞くことに非常に興味があります. (可視性を高めるために、できるだけ早く報奨金を追加します)

編集:パフォーマンス分析と結果を別の回答に移動しました(以下を参照)。

4

7 に答える 7

33

パフォーマンス分析が予想より少し長くなったので、別の回答に入れました。ピーターの回答は公式として受け入れますが、それは私が自分で測定を実行する動機付けに最も役立ち、測定する価値があるかもしれないものについて最もインスピレーションを与えたので、測定が欠けていました.

分析:
これまでに言及された具体的な欠点はすべて、ある種類の別のパフォーマンスに焦点を当てているように見えますが、実際の定量的データは欠落していました。私は次のいくつかの測定を行いました:

  • IDE でソリューションをロードする時間
  • IDE でコンパイルする時間
  • アセンブリの読み込み時間 (アプリケーションの読み込みにかかる時間)
  • 失われたコードの最適化 (アルゴリズムの実行にかかる時間)

この分析では、一部の人々が回答で言及している「設計の品質」を完全に無視しています。これは、品質がこのトレードオフの変数であるとは考えていないためです。私は、開発者が何よりもまず、可能な限り最高の設計を実現したいという欲求に基づいて実装を行うことを想定しています。ここでのトレードオフは、(ある程度の) パフォーマンスのために、設計が厳密に要求するよりも大きなアセンブリに機能を集約する価値があるかどうかです。

アプリケーションの構造:
私が作成したアプリケーションは、テストするために多数のソリューションとプロジェクトが必要だったため、やや抽象的です。そのため、それらすべてを生成するためのコードをいくつか書きました。

アプリケーションには 1000 個のクラスが含まれており、相互に継承する 5 つのクラスの 200 セットにグループ化されています。クラスの名前は、Axxx、Bxxx、Cxxx、Dxxx、および Exxx です。クラス A は完全に抽象的で、BD は部分的に抽象的で、それぞれ A のメソッドの 1 つをオーバーライドします。E は具体的です。メソッドは、E のインスタンスで 1 つのメソッドを呼び出すと、階層チェーンの複数の呼び出しが実行されるように実装されています。すべてのメソッド本体は十分に単純であるため、理論的にはすべてインライン化する必要があります。

これらのクラスは、2 つの次元に沿って 8 つの異なる構成でアセンブリ全体に分散されました。

  • 組立数:10、20、50、100
  • 切断方向: 継承階層全体 (AE が同じアセンブリに一緒に存在することはありません)、および継承階層に沿って

測定値はすべて正確に測定されているわけではありません。ストップウォッチによって行われたものもあり、誤差の範囲が大きくなっています。測定値は次のとおりです。

  • VS2008 でソリューションを開く (ストップウォッチ)
  • ソリューションのコンパイル (ストップウォッチ)
  • IDE: 開始から最初に実行されたコード行までの時間 (ストップウォッチ)
  • IDE: IDE 内の 200 個のグループごとに Exxx の 1 つをインスタンス化する時間 (コード内)
  • IDE: IDE の各 Exxx で 100,000 回の呼び出しを実行する時間 (コード内)
  • 最後の 3 つの 'In IDE' 測定値ですが、'Release' ビルドを使用したプロンプトから

結果:
VS2008 でソリューションを開く

                               ----- in the IDE ------   ----- from prompt -----
Cut    Asm#   Open   Compile   Start   new()   Execute   Start   new()   Execute
Across   10    ~1s     ~2-3s       -   0.150    17.022       -   0.139    13.909
         20    ~1s       ~6s       -   0.152    17.753       -   0.132    13.997
         50    ~3s       15s   ~0.3s   0.153    17.119    0.2s   0.131    14.481
        100    ~6s       37s   ~0.5s   0.150    18.041    0.3s   0.132    14.478

Along    10    ~1s     ~2-3s       -   0.155    17.967       -   0.067    13.297
         20    ~1s       ~4s       -   0.145    17.318       -   0.065    13.268
         50    ~3s       12s   ~0.2s   0.146    17.888    0.2s   0.067    13.391
        100    ~6s       29s   ~0.5s   0.149    17.990    0.3s   0.067    13.415

所見:

  • アセンブリの数 (切断方向ではない) は、ソリューションを開くのにかかる時間にほぼ線形の影響を与えるようです。これは私には驚くべきことではありません。
  • 約 6 秒で、ソリューションを開くのにかかる時間は、アセンブリの数を制限するための引数ではないように思えます。(ソース管理の関連付けがこの時間に大きな影響を与えたかどうかは測定しませんでした)。
  • この測定では、コンパイル時間は線形より少し長くなります。これのほとんどは、アセンブリ間のシンボル解決ではなく、コンパイルのアセンブリごとのオーバーヘッドによるものだと思います。この軸に沿って、それほど重要でないアセンブリがより適切にスケーリングされることを期待しています。それでも、特にほとんどの場合、一部のアセンブリのみが再コンパイルを必要とすることに注意してください。
  • かろうじて測定可能ですが、起動時間の顕著な増加が見られます。アプリケーションが最初に行うことは、コンソールに行を出力することです。「開始」時間は、実行の開始からこの行が表示されるまでにかかった時間です (最悪の場合でも正確に測定するには速すぎるため、これらは推定値であることに注意してください)。 .
  • 興味深いことに、IDE アセンブリの読み込みの外側は、IDE の内側よりも (わずかに) 効率的であるように見えます。これはおそらく、デバッガーを接続する労力などと関係があります。
  • また、IDE の外部でアプリケーションを再起動すると、最悪の場合でも起動時間がさらに短縮されることに注意してください。起動時の 0.3 秒が受け入れられないシナリオもあるかもしれませんが、これが多くの場所で問題になるとは思えません。
  • アセンブリの分割に関係なく、IDE 内の初期化と実行時間は安定しています。これは、アセンブリ間でシンボルを解決しやすくするためにデバッグする必要があるという事実のケースである可能性があります。
  • IDE の外でも、この安定性は続きますが、注意点が 1 つあります...アセンブリの数は実行には関係ありませんが、継承階層を横断する場合、実行時間は. 違いが小さすぎて体系的ではないことに注意してください。おそらく、同じ最適化を行う方法を理解するためにランタイムが1回かかるのは余分な時間です...率直に言って、これをさらに調査することはできますが、違いは非常に小さいため、あまり心配する必要はありません.

以上のことから、より多くのアセンブリの負担は主に開発者が負担し、そのほとんどがコンパイル時間の形であることがわかります。既に述べたように、これらのプロジェクトは非常に単純で、それぞれのコンパイルに 1 秒もかからず、アセンブリごとのコンパイル オーバーヘッドが支配的でした。多数のアセンブリにわたる 1 秒未満のアセンブリ コンパイルは、これらのアセンブリが妥当な範囲を超えて分割されていることを強く示していると思います。また、プリコンパイルされたアセンブリを使用すると、分割 (コンパイル時間)に対する開発者の主な議論もなくなります。

これらの測定では、実行時のパフォーマンスのために小さなアセンブリに分割することに対する証拠があったとしても、ほとんど確認できません。(ある程度) 注意すべき唯一のことは、可能な限り継承を切断しないようにすることです。継承は通常、機能領域内でのみ発生し、通常は単一のアセンブリ内で終了するため、ほとんどの健全な設計ではこれが制限されると思います。

于 2009-08-02T05:21:10.303 に答える
15

多数の (非常に) 小さいアセンブリを使用すると .Net DLL Hell が生成される実際の例を示します。

職場では、歯が長い大規模な自家製フレームワーク (.Net 1.1) があります。通常のフレームワーク タイプのプラミング コード (ロギング、ワークフロー、キューイングなどを含む) の他に、さまざまなカプセル化されたデータベース アクセス エンティティ、型指定されたデータセット、およびその他のビジネス ロジック コードもありました。私はこのフレームワークの初期開発とその後のメンテナンスには参加していませんでしたが、その使用法を継承しました。前述したように、このフレームワーク全体から多数の小さな DLL が作成されました。そして、私が多数と言うとき、私たちは 100 以上のことを話している - あなたが言及した管理可能な 8 かそこらではありません. さらに複雑な問題は、すべてのアセンブリが厳密に署名され、バージョン管理され、GAC に表示されることでした。

数年後、数回のメンテナンス サイクルを経て、DLL とそれらがサポートするアプリケーションの相互依存関係が大混乱を引き起こしました。すべてのプロダクション マシンには、machine.config ファイル内の巨大なアセンブリ リダイレクト セクションがあり、どのアセンブリが要求されても、「正しい」アセンブリが Fusion によってロードされるようにします。これは、変更またはアップグレードされたものに依存するすべての依存フレームワークおよびアプリケーション アセンブリを再構築するために遭遇した困難から生じました。アセンブリが変更されたときに重大な変更がアセンブリに加えられないようにするために、(通常は) 多大な労力が費やされました。アセンブリが再構築され、新しいエントリまたは更新されたエントリが machine.config に作成されました。

ここで私は一時停止して、巨大な集合的なうめき声とあえぎの音を聞きます!

この特定のシナリオは、何をすべきでないかの代表的なものです。実際、この状況では、完全に保守不可能な状況に陥ります。最初にこのフレームワークを使用して作業を開始したとき、このフレームワークに対する開発用にマシンをセットアップするのに 2 日かかったのを思い出します。自分の GAC とランタイム環境の GAC の違い、machine.config アセンブリのリダイレクト、コンパイル時のバージョンの競合を解決するために、コンポーネント A とコンポーネント B を直接参照しているが、コンポーネント B はコンポーネント A を参照していたが、アプリケーションの直接参照とは異なるバージョンを参照していたため、不適切な参照、またはバージョンの競合が発生する可能性が高いです。あなたはアイデアを得る。

この特定のシナリオの実際の問題は、アセンブリの内容が細かすぎることです。そして、これが相互依存関係の絡み合った Web を最終的に引き起こした原因です。私の考えでは、初期のアーキテクトは、これにより保守性の高いコードのシステムが作成されると考えていました。システムのコンポーネントへの非常に小さな変更を再構築するだけで済みます。実際、その逆でした。さらに、すでにここに投稿されている他の回答のいくつかに対して、この数のアセンブリに到達すると、大量のアセンブリをロードするとパフォーマンスが低下します-間違いなく解決中に、経験的な証拠はありませんが、特にリフレクションが発生する可能性があるいくつかのエッジケースの状況では、ランタイムが問題になる可能性があります。その点で間違っている可能性があります。

軽蔑されると思われるかもしれませんが、アセンブリには論理的な物理的な分離があると思います。ここで「アセンブリ」と言うときは、DLL ごとに 1 つのアセンブリを想定しています。要するに、相互依存関係です。アセンブリ B に依存するアセンブリ A がある場合、アセンブリ A なしでアセンブリ B を参照する必要があるかどうかを常に自問します。または、その分離には利点があります。アセンブリがどのように参照されているかを見ることも、通常は良い指標です。大規模なライブラリをアセンブリ A、B、C、D、および E に分割するとします。アセンブリ A を 90% の確率で参照すると、A はアセンブリ B と C に依存しているため、常にアセンブリ B と C を参照する必要がありました。の場合は、アセンブリ A、B、および C を組み合わせる方がよいでしょう。彼らが分離したままでいることを可能にするための本当に説得力のある議論. Enterprise Library はこの典型的な例であり、ほとんどの場合、ライブラリの 1 つのファセットを使用するために 3 つのアセンブリを参照する必要があります。ただし、Enterprise Library の場合、コア機能とコードの上に構築する機能再利用がそのアーキテクチャの理由です。

建築を見ることも良いガイドラインです。アセンブリの依存関係がスタックの形式である、きれいにスタックされたアーキテクチャがある場合、すべての方向に依存関係があるときに形成され始める「ウェブ」ではなく、「垂直」と言って、アセンブリの分離機能的な境界については理にかなっています。それ以外の場合は、物事を 1 つにまとめるか、再構築することを検討してください。

いずれにせよ、頑張ってください!

于 2009-07-31T21:46:29.590 に答える
4

各アセンブリをロードするとパフォーマンスがわずかに低下します (署名されている場合はさらに大きくなります)。これが、よく使用されるものを同じアセンブリにまとめる傾向がある理由の 1 つです。読み込まれた後に大きなオーバーヘッドがあるとは思いません (ただし、アセンブリの境界を越えるときに JIT が実行するのがより困難な静的な最適化がいくつかあるかもしれません)。

私が取ろうとしているアプローチは次のとおりです。名前空間は論理的な編成のためのものです。アセンブリは、物理的に一緒に使用する必要があるクラス/名前空間をグループ化することです。すなわち。ClassB ではなく ClassA が必要であると予想しない場合 (またはその逆)、それらは同じアセンブリに属します。

于 2009-07-28T05:08:30.587 に答える
2

アセンブリ構成の最大の要素は、クラスおよびアセンブリ レベルでの依存関係グラフです。

アセンブリには循環参照があってはなりません。それは開始するのにかなり明白なはずです。

相互に最も多くの依存関係を持つクラスは、1 つのアセンブリ内にある必要があります。

クラス A がクラス B に依存し、B が A に直接依存していなくても、A とは別に使用される可能性が低い場合、これらはアセンブリを共有する必要があります。

アセンブリを使用して懸念事項の分離を強制することもできます。GUI コードをあるアセンブリに配置し、ビジネス ロジックを別のアセンブリに配置することで、ビジネス ロジックが GUI に依存しないことをある程度強制できます。

コードが実行される場所に基づくアセンブリの分離は、考慮すべきもう 1 つのポイントです。実行可能ファイル間の共通コードは、(一般に) 1 つの .exe が別の .exe を直接参照するのではなく、共通のアセンブリにある必要があります。

おそらく、アセンブリを使用できる最も重要なことの 1 つは、パブリック API と、パブリック API を機能させるために内部で使用されるオブジェクトを区別することです。API を別のアセンブリに配置することで、その API の不透明性を強制できます。

于 2009-08-01T08:11:15.963 に答える
2

モノリシック モンスターは、後で作業するためにコードの一部を再利用すると、必要以上にコストがかかります。また、結合する必要のないクラス間の結合 (多くの場合明示的) につながり、結果としてテストとエラー修正が難しくなるため、保守コストが高くなります。

多くのプロジェクトを持つことのマイナス面は、(少なくとも VS では) 少数のプロジェクトに比べてコンパイルにかなりの時間がかかることです。

于 2009-07-28T05:19:45.510 に答える
1

1ダース程度しか話していないなら、大丈夫だと思います。私は 100 以上のアセンブリを含むアプリケーションに取り組んでいますが、非常に苦痛です。

何らかの方法で依存関係を管理していない場合 (アセンブリ X を変更すると何が壊れるかを知っている場合)、問題が発生します。

私が遭遇した「良い」問題の 1 つは、アセンブリ A がアセンブリ B と C を参照し、B がアセンブリ D の V1 を参照し、C がアセンブリ D の V2 を参照する場合です (「ツイスト ダイヤモンド」は、そのための非常に適切な名前です)。

自動化されたビルドが必要な場合は、ビルド スクリプト (依存関係の逆の順序でビルドする必要があります) を維持することを楽しむか、「すべてを支配する 1 つのソリューション」を用意することになりますが、これはほとんど不可能です。多数のアセンブリがある場合に Visual Studio で使用します。

編集 あなたの質問への答えは、アセンブリのセマンティクスに大きく依存すると思います。異なるアプリケーションがアセンブリを共有する可能性はありますか? 両方のアプリケーションのアセンブリを個別に更新できるようにしますか? GAC を使用する予定ですか? または、実行可能ファイルの横にあるアセンブリをコピーしますか?

于 2009-07-28T05:42:28.630 に答える
0

個人的には、モノリシックなアプローチが好きです。

ただし、アセンブリをさらに作成せざるを得ない場合もあります。共通のインターフェイス アセンブリが必要な場合は、通常、.NET リモート処理がこれを担当します。

アセンブリをロードするオーバーヘッドがどれほど「重い」かはわかりません。(おそらく誰かが私たちを啓発することができます)

于 2009-07-28T05:08:19.027 に答える