「ものの塊」ユーティリティプロジェクトを「オプション」の依存関係を持つ個々のコンポーネントに分離する


26

何年にもわたる社内プロジェクトでC#/。NETを使用してきた数年間で、1つのライブラリが有機的に成長して1つの巨大なものになりました。それは「Util」と呼ばれ、あなたの多くがあなたのキャリアでこれらの獣の1つを見たと確信しています。

このライブラリの多くの部分は非常にスタンドアロンであり、別々のプロジェクトに分割することができます(オープンソースにしたい)。ただし、これらを個別のライブラリとしてリリースする前に解決する必要がある大きな問題が1つあります。基本的に、これらのライブラリ間に「オプションの依存関係」と呼ぶものがたくさんあります。

これをより適切に説明するには、スタンドアロンライブラリになるのに適したモジュールのいくつかを検討してください。CommandLineParserコマンドラインの解析用です。XmlClassifyクラスをXMLにシリアル化するためのものです。PostBuildCheckコンパイルされたアセンブリに対してチェックを実行し、失敗した場合はコンパイルエラーを報告します。ConsoleColoredString色付きの文字列リテラルのライブラリです。Lingoユーザーインターフェイスの翻訳用です。

これらのライブラリはそれぞれ完全にスタンドアロンで使用できますが一緒に使用する場合は、便利な追加機能が必要です。たとえば、との両方が必要なビルド後のチェック機能CommandLineParserXmlClassify公開しますPostBuildCheck。同様に、はCommandLineParser、を必要とする色付き文字列リテラルを使用してオプションドキュメントを提供することを許可し、をConsoleColoredString介して翻訳可能なドキュメントをサポートしLingoます。

したがって、重要な違いは、これらがオプション機能であることです。ドキュメントを翻訳したり、ビルド後のチェックを実行したりせずに、プレーンで色付けされていない文字列でコマンドラインパーサーを使用できます。または、ドキュメントを翻訳可能にすることもできますが、それでも色付けされません。または、色付きで翻訳可能です。等。

この「Util」ライブラリに目を通すと、ほとんどすべての潜在的に分離可能なライブラリには、それらを他のライブラリに結び付けるようなオプション機能があります。これらのライブラリを依存関係として実際に必要とする場合、このものはまったく解りません。1つだけを使用する場合は、基本的にすべてのライブラリが必要です。

.NETでこのようなオプションの依存関係を管理する確立されたアプローチはありますか?


2
ライブラリが相互に依存している場合でも、それぞれが幅広い機能カテゴリを含む、一貫性のある別個のライブラリに分けることには、いくつかの利点があります。
ロバートハーヴェイ

回答:


20

ゆっくりリファクタリングします。

これは完了するまで時間がかかると予想されUtilsを完全に削除する前に数回の反復にわたって発生する可能性がありますアセンブリを。

全体的なアプローチ:

  1. 最初に時間をかけて、完了したらこれらのユーティリティアセンブリをどのように表示するかを考えます。既存のコードについてあまり心配する必要はありません。最終目標を考えてください。たとえば、次のものがあります。

    • MyCompany.Utilities.Core(アルゴリズム、ロギングなどを含む)
    • MyCompany.Utilities.UI(描画コードなど)
    • MyCompany.Utilities.UI.WinForms(System.Windows.Forms関連のコード、カスタムコントロールなど)
    • MyCompany.Utilities.UI.WPF(WPF関連のコード、MVVM基本クラス)。
    • MyCompany.Utilities.Serializationシリアル化コード)。
  2. これらのプロジェクトごとに空のプロジェクトを作成し、適切なプロジェクト参照(UI参照コア、UI.WinForms参照UI)などを作成します。

  3. Utilsアセンブリから新しいターゲットアセンブリに、影響の少ない果物(依存関係の問題を抱えていないクラスまたはメソッド)を移動します。

  4. NDependとMartin Fowlerのリファクタリングのコピーを入手して、Utilsアセンブリの分析を開始し、より厳しいアセンブリの作業を開始してください。役立つ2つのテクニック:

オプションのインターフェースの処理

アセンブリが別のアセンブリを参照するか、参照しません。リンクされていないアセンブリで機能を使用する他の唯一の方法は、共通クラスからのリフレクションを通じて読み込まれたインターフェイスを使用することです。これの欠点は、コアアセンブリにすべての共有機能のインターフェイスを含める必要があることですが、欠点は、各展開シナリオに応じてDLLファイルの「ワッド」なしで必要に応じてユーティリティを展開できることです。色付きの文字列を例として使用して、この場合の処理​​方法を次に示します。

  1. まず、コアアセンブリで共通のインターフェイスを定義します。

    ここに画像の説明を入力してください

    たとえば、IStringColorerインターフェースは次のようになります。

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. 次に、機能を使用してアセンブリにインターフェイスを実装します。たとえば、StringColorerクラスは次のようになります。

    using MyCompany.Utilities.Core.OptionalInterfaces;
    namespace MyCompany.Utilities.Console
    {
        class StringColorer : IStringColorer
        {
            #region IStringColorer Members
    
            public string Decorate(string s)
            {
                return "*" + s + "*";   //TODO: implement coloring
            }
    
            #endregion
        }
    }
    
  3. PluginFinder現在のフォルダー内のDLLファイルからインターフェイスを検索できるクラスを作成します(この場合はInterfaceFinderの方が良い場合があります)。簡単な例を示します。@EdWoodcockのアドバイス(そして同意します)に従って、プロジェクトが大きくなったら、利用可能なDependency Injectionフレームワーク(UnityおよびSpring.NETを備えたCommon Serivce Locatorが思い浮かぶ)を使用して、より高度な「私を見つける」ことをお勧めしますサービスロケーターパターンとも呼ばれます。ニーズに合わせて変更できます。

    using System;
    using System.Linq;
    using System.IO;
    using System.Reflection;
    
    namespace UtilitiesCore
    {
        public static class PluginFinder
        {
            private static bool _loadedAssemblies;
    
            public static T FindInterface<T>() where T : class
            {
                if (!_loadedAssemblies)
                    LoadAssemblies();
    
                //TODO: improve the performance vastly by caching RuntimeTypeHandles
    
                foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
                {
                    foreach (Type type in assembly.GetTypes())
                    {
                        if (type.IsClass && typeof(T).IsAssignableFrom(type))
                            return Activator.CreateInstance(type) as T;
                    }
                }
    
                return null;
            }
    
            private static void LoadAssemblies()
            {
                foreach (FileInfo file in new DirectoryInfo(Directory.GetCurrentDirectory()).GetFiles())
                {
                    if (file.Extension != ".DLL")
                        continue;
    
                    if (!AppDomain.CurrentDomain.GetAssemblies().Any(a => a.Location == file.FullName))
                    {
                        try
                        {
                            //TODO: perhaps filter by certain known names
                            Assembly.LoadFrom(file.FullName);
                        }
                        catch { }
                    }
                }
            }
        }
    }
    
  4. 最後に、FindInterfaceメソッドを呼び出して、他のアセンブリでこれらのインターフェイスを使用します。以下に例を示しCommandLineParserます。

    static class CommandLineParser
    {
        public static string ParseCommandLine(string commandLine)
        {
            string parsedCommandLine = ParseInternal(commandLine);
    
            IStringColorer colorer = PluginFinder.FindInterface<IStringColorer>();
    
            if(colorer != null)
                parsedCommandLine = colorer.Decorate(parsedCommandLine);
    
            return parsedCommandLine;
        }
    
        private static string ParseInternal(string commandLine)
        {
            //TODO: implement parsing as desired
            return commandLine;
        }
    

    }

最も重要なこと: テスト、テスト、各変更間のテスト。


サンプルを追加しました!:-)
ケビンマコーミック

1
このPluginFinderクラスは、(ServiceLocatorパターンを使用した)独自の自動DIハンドラーのように見えますが、それ以外の点では適切なアドバイスです。ライブラリ内の特定のインターフェイスの複数の実装(StringColourer対StringColourerWithHtmlWrapperなど)で問題が発生しないため、UnityのようなものにOPを向けた方が良いかもしれません。
エドジェームズ

@EdWoodcock良い点エド、そしてこれを書いている間、Service Locatorパターンを考えていなかったとは信じられません。PluginFinderは間違いなく未熟な実装であり、DIフレームワークはここで確実に機能します。
ケビンマコーミック

努力の報奨金を授与しましたが、私たちはこの道を行くつもりはありません。インターフェースのコアアセンブリを共有するということは、実装を取り除くことに成功したことを意味しますが、ほとんど関連のないインターフェース(以前のように、オプションの依存関係を介して関連する)を含むライブラリがまだあります。現在、セットアップははるかに複雑で、これほど小さなライブラリにはほとんどメリットがありません。巨大なプロジェクトの場合、余分な複雑さはそれだけの価値があるかもしれませんが、そうではありません。
ローマスターコフ

@romkynsそれで、あなたはどんなルートを取っていますか?そのままにしておきますか?:)
マックス

5

追加のライブラリで宣言されたインターフェイスを使用できます。

依存性注入(MEF、Unityなど)を使用して、コントラクト(インターフェイス経由のクラス)を解決してください。見つからない場合は、nullインスタンスを返すように設定します。
次に、インスタンスがnullであるかどうかを確認します。その場合、追加の機能は実行しません。

これは、MEFで使用する教科書であるため、MEFを使用すると特に簡単です。

ライブラリをn + 1個のdllに分割する代わりに、ライブラリをコンパイルできます。

HTH。


これはほとんど正しいように思えます-それがその1つの余分なDLLのためではなかった場合、それは基本的に元のものの束のスケルトンの束のようなものです。実装はすべて分割されていますが、まだ「スケルトンの塊」が残っています。私はいくつかの利点があると、私は利点が...ライブラリのこの特定のセットのすべてのコストを上回ることを確信していない
ローマStarkov

さらに、フレームワーク全体を含めることは完全に一歩後退しています。このライブラリは現状のままで、これらのフレームワークの1つのサイズとほぼ同じであり、メリットはまったくありません。どちらかといえば、実装が使用可能かどうかを確認するために、少しのリフレクションを使用します。これは、ゼロと1の間のみであり、外部構成は必要ないためです。
ローマスターコフ

2

私は、考えが何であるかを見るために、これまでに考え出した最も実行可能なオプションを投稿すると思いました。

基本的に、各コンポーネントを参照なしのライブラリに分離します。参照を必要とするすべてのコード#if/#endifは、適切な名前でブロックに配置されます。たとえば、s CommandLineParserを処理するコードConsoleColoredStringは次の場所に配置されます。#if HAS_CONSOLE_COLORED_STRINGます。

CommandLineParser追加の依存関係がないため、ソリューションのみを含めることを簡単に行うことができます。ただし、ソリューションにConsoleColoredStringプロジェクトも含まれている場合、プログラマには次のオプションがあります。

  • に参照を追加する CommandLineParserするにConsoleColoredString
  • プロジェクトファイルにHAS_CONSOLE_COLORED_STRING定義を追加しCommandLineParserます。

これにより、関連する機能が利用可能になります。

これにはいくつかの問題があります。

  • これはソースのみのソリューションです。ライブラリのすべてのコンシューマは、ソースコードとしてライブラリを含める必要があります。バイナリを含めることはできません(ただし、これは絶対的な要件ではありません)。
  • ライブラリライブラリのプロジェクトファイルには、いくつ取得ソリューション固有の編集を、そしてそれは、この変更は、SCMにコミットしているかを正確に明らかではありません。

どちらかと言えばきれいではありませんが、それでも、これは私たちが思いついた最も近いものです。

さらに考えたアイデアの1つは、ユーザーにライブラリプロジェクトファイルの編集を要求するのではなく、プロジェクト構成を使用することでした。しかし、これはすべてのプロジェクト構成を不要にソリューション追加するため、VS2010 ではまったく機能しません。


1

.NetのBrownfield Application Developmentという本をお勧めします。直接関連する2つの章は8と9です。第8章ではアプリケーションのリレーについて説明し、第9章では依存関係の調整、制御の反転、およびテストへの影響について説明します。


1

完全な開示、私はJavaの男です。ですから、おそらくここで言及するテクノロジーをお探しではないことを理解しています。しかし、問題は同じなので、おそらく正しい方向にあなたを向けるでしょう。

Javaには、ビルドされた「アーティファクト」を収容する中央のアーティファクトリポジトリのアイデアをサポートするビルドシステムがいくつかあります。私の知る限り、これは.NETのGACに似ています(緊張したアナロジーの場合は、無知を恐れてください)しかし、それはいつでも独立した反復可能なビルドを生成するために使用されるためです。

とにかく、(たとえばMavenで)サポートされている別の機能は、特定のバージョンまたは範囲に依存し、潜在的な推移的な依存関係を除外するOPTIONAL依存関係のアイデアです。これはあなたが探しているもののように聞こえますが、私は間違っているかもしれません。Javaを知っている友人とMavenの依存関係管理に関するこの紹介ページを見て、問題がよく知られているかどうかを確認してください。これにより、アプリケーションを構築し、これらの依存関係を使用可能または使用せずに構築できます。

本当に動的でプラグ可能なアーキテクチャが必要な場合は、構成要素もあります。この形式のランタイム依存関係解決に対処しようとする技術の1つがOSGIです。これは、Eclipseのプラグインシステムの背後にあるエンジンです。オプションの依存関係と最小/最大バージョン範囲をサポートできることがわかります。このレベルのランタイムモジュール性は、開発者と開発方法にかなりの数の制約を課します。ほとんどの人は、Mavenが提供するモジュール性の程度で対処できます。

あなたがあなたのために実装するのが何桁も簡単かもしれないあなたが調べることができる1つの他の考えは、アーキテクチャのパイプとフィルタースタイルを使用することです。これが、UNIXを半世紀にわたって生き延び進化させてきた長年にわたる成功したエコシステムの主な理由です。フレームワークにこの種のパターンを実装する方法についてのいくつかのアイデアについては、.NETのパイプとフィルターに関するこの記事をご覧ください。


0

おそらく、John Lakosの著書「大規模C ++ソフトウェア設計」は便利です(もちろん、C#とC ++か同じではありませんが、この本から有用なテクニックを見つけることができます)。

基本的に、2つ以上のライブラリを使用する機能を、これらのライブラリに依存する個別のコンポーネントにリファクタリングおよび移動します。必要に応じて、不透明(OPAQUE)型などの手法を使用します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.