Memisahkan proyek utilitas "gumpalan barang" menjadi komponen individual dengan dependensi "opsional"

26

Selama bertahun-tahun menggunakan C # /. NET untuk banyak proyek internal, kami memiliki satu perpustakaan yang tumbuh secara organik menjadi satu kumpulan besar barang. Itu disebut "Util", dan saya yakin banyak dari Anda telah melihat salah satu dari binatang buas ini dalam karier Anda.

Banyak bagian dari perpustakaan ini sangat mandiri, dan dapat dipecah menjadi proyek-proyek terpisah (yang kami ingin open-source). Tetapi ada satu masalah besar yang perlu dipecahkan sebelum ini dapat dirilis sebagai perpustakaan terpisah. Pada dasarnya, ada banyak dan banyak kasus dari apa yang saya sebut "dependensi opsional" antara perpustakaan ini.

Untuk menjelaskan hal ini dengan lebih baik, pertimbangkan beberapa modul yang merupakan kandidat yang baik untuk menjadi perpustakaan yang berdiri sendiri. CommandLineParseradalah untuk parsing baris perintah. XmlClassifyadalah untuk serialisasi kelas ke XML. PostBuildCheckmelakukan pemeriksaan pada rakitan yang dikompilasi dan melaporkan kesalahan kompilasi jika gagal. ConsoleColoredStringadalah perpustakaan untuk literal string berwarna. Lingoadalah untuk menerjemahkan antarmuka pengguna.

Masing-masing perpustakaan dapat digunakan sepenuhnya berdiri sendiri, tetapi jika mereka digunakan bersama-sama maka ada fitur tambahan yang berguna yang bisa didapat. Misalnya, baik CommandLineParserdan XmlClassifymengekspos fungsionalitas pemeriksaan pasca-pembangunan, yang mengharuskan PostBuildCheck. Demikian pula, CommandLineParsermemungkinkan opsi dokumentasi disediakan menggunakan string string berwarna, yang membutuhkan ConsoleColoredString, dan mendukung dokumentasi yang dapat diterjemahkan melalui Lingo.

Jadi perbedaan utamanya adalah ini adalah fitur opsional . Seseorang dapat menggunakan parser baris perintah dengan string polos, tidak berwarna, tanpa menerjemahkan dokumentasi atau melakukan pemeriksaan post-build. Atau seseorang dapat membuat dokumentasi itu dapat diterjemahkan tetapi masih belum berwarna. Atau keduanya berwarna dan dapat diterjemahkan. Dll

Melihat melalui pustaka "Util" ini, saya melihat bahwa hampir semua pustaka yang berpotensi dipisah memiliki fitur opsional yang mengikatnya dengan pustaka lain. Jika saya benar-benar membutuhkan pustaka tersebut sebagai dependensi, maka kumpulan hal ini tidak benar-benar tidak kusut sama sekali: Anda pada dasarnya masih memerlukan semua pustaka jika Anda ingin menggunakan satu saja.

Apakah ada pendekatan yang ditetapkan untuk mengelola dependensi opsional seperti itu di .NET?

Roman Starkov
sumber
2
Bahkan jika perpustakaan saling bergantung satu sama lain, masih ada beberapa manfaat dalam memisahkannya menjadi perpustakaan yang koheren tetapi terpisah, masing-masing berisi kategori fungsionalitas yang luas.
Robert Harvey

Jawaban:

20

Reaktor Perlahan.

Harapkan ini membutuhkan waktu untuk diselesaikan , dan mungkin terjadi beberapa kali pengulangan sebelum Anda dapat menghapus unit Utils sepenuhnya .

Pendekatan Keseluruhan:

  1. Pertama-tama luangkan waktu dan pikirkan bagaimana Anda ingin rakitan utilitas ini terlihat ketika Anda selesai. Jangan terlalu khawatir tentang kode yang ada, pikirkan tujuan akhir. Misalnya, Anda mungkin ingin memiliki:

    • MyCompany.Utilities.Core (Mengandung algoritma, masuk, dll.)
    • MyCompany.Utilities.UI (Menggambar kode, dll.)
    • MyCompany.Utilities.UI.WinForms (kode terkait System.Windows.Forms, kontrol khusus, dll.)
    • MyCompany.Utilities.UI.WPF (kode terkait WPF, kelas dasar MVVM).
    • MyCompany.Utilities.Serialization (Kode serialisasi).
  2. Buat proyek kosong untuk masing-masing proyek ini, dan buat referensi proyek yang sesuai (UI referensi Core, UI.WinForms referensi UI), dll.

  3. Pindahkan salah satu dari buah yang menggantung rendah (kelas atau metode yang tidak menderita masalah ketergantungan) dari perakitan Utils Anda ke majelis target baru.

  4. Dapatkan salinan Refactoring NDepend dan Martin Fowler untuk mulai menganalisis unit Utils Anda untuk mulai bekerja pada yang lebih sulit. Dua teknik yang akan membantu:

Menangani Antarmuka Opsional

Entah majelis referensi majelis lain, atau tidak. Satu-satunya cara lain untuk menggunakan fungsionalitas dalam perakitan non-terhubung adalah melalui antarmuka yang dimuat melalui refleksi dari kelas umum. Kelemahan dari hal ini adalah bahwa perakitan inti Anda harus mengandung antarmuka untuk semua fitur yang dibagikan, tetapi sisi baiknya adalah Anda dapat menggunakan utilitas sesuai kebutuhan tanpa "gumpalan" file DLL tergantung pada setiap skenario penempatan. Inilah cara saya menangani kasus ini, menggunakan string berwarna sebagai contoh:

  1. Pertama, tentukan antarmuka umum dalam perakitan inti Anda:

    masukkan deskripsi gambar di sini

    Misalnya, IStringColorerantarmuka akan terlihat seperti:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. Kemudian, implementasikan antarmuka dalam perakitan dengan fitur. Sebagai contoh, StringColorerkelas akan terlihat seperti:

    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. Buat kelas PluginFinder(atau mungkin InterfaceFinder adalah nama yang lebih baik dalam kasus ini) yang dapat menemukan antarmuka dari file DLL di folder saat ini. Ini adalah contoh sederhana. Per @ EdWoodcock saran (dan saya setuju), ketika proyek Anda tumbuh saya akan menyarankan menggunakan salah satu kerangka kerja Ketergantungan Injeksi yang tersedia ( Common Serivce Locator dengan Unity and Spring.NET datang ke pikiran) untuk implementasi yang lebih kuat dengan lebih maju "temukan saya fitur itu "kemampuan, atau dikenal sebagai Pola Pencari Layanan . Anda dapat memodifikasinya sesuai dengan kebutuhan Anda.

    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. Terakhir, gunakan antarmuka ini di majelis Anda yang lain dengan memanggil metode FindInterface. Ini adalah contoh dari 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;
        }
    

    }

PALING PENTING: Tes, uji, uji antara setiap perubahan.

Kevin McCormick
sumber
Saya menambahkan contoh! :-)
Kevin McCormick
1
Kelas PluginFinder itu tampak mencurigakan seperti penangan DI otomatis automagical roll-Anda sendiri (menggunakan pola ServiceLocator), tetapi ini adalah saran yang masuk akal. Mungkin Anda akan lebih baik untuk hanya mengarahkan OP pada sesuatu seperti Unity, karena itu tidak akan memiliki masalah dengan beberapa implementasi dari antarmuka tertentu dalam perpustakaan (StringColourer vs StringColourerWithHtmlWrapper, atau apa pun).
Ed James
@ EdWoodcock Good point Ed, dan saya tidak percaya saya tidak memikirkan pola Service Locator saat menulis ini. PluginFinder jelas merupakan implementasi yang tidak matang dan kerangka kerja DI pasti akan bekerja di sini.
Kevin McCormick
Saya telah memberi Anda hadiah untuk upaya ini, tetapi kami tidak akan menempuh rute ini. Berbagi perakitan inti antarmuka berarti bahwa kami hanya berhasil memindahkan implementasinya, tetapi masih ada perpustakaan yang berisi sejumlah kecil antarmuka terkait (terkait melalui dependensi opsional, seperti sebelumnya). Pengaturannya jauh lebih rumit sekarang dengan sedikit manfaat untuk perpustakaan sekecil ini. Kompleksitas ekstra mungkin sepadan untuk proyek-proyek besar, tetapi tidak untuk ini.
Roman Starkov
@romkyns Jadi rute apa yang Anda ambil? Membiarkan apa adanya? :)
Max
5

Anda dapat menggunakan antarmuka yang dideklarasikan di perpustakaan tambahan.

Cobalah untuk menyelesaikan kontrak (kelas via antarmuka) menggunakan injeksi dependensi (MEF, Unity dll). Jika tidak ditemukan, atur untuk mengembalikan turunan nol.
Kemudian periksa apakah instance adalah null, dalam hal ini Anda tidak melakukan fungsionalitas tambahan.

Ini sangat mudah dilakukan dengan MEF, karena ini adalah buku teks yang digunakan untuk itu.

Ini akan memungkinkan Anda untuk mengkompilasi perpustakaan, dengan biaya membaginya menjadi n + 1 dll.

HTH.

Louis Kottmann
sumber
Ini kedengarannya hampir benar - jika saja bukan untuk DLL ekstra itu, yang pada dasarnya seperti sekelompok kerangka gumpalan barang asli. Semua implementasinya terpisah, tetapi masih ada "segumpal kerangka" yang tersisa. Saya kira itu memiliki beberapa kelebihan, tapi saya tidak yakin bahwa kelebihannya melebihi semua biaya untuk set perpustakaan khusus ini ...
Roman Starkov
Selain itu, termasuk seluruh kerangka kerja adalah langkah mundur; perpustakaan ini apa adanya adalah tentang ukuran salah satu kerangka kerja itu, sama sekali meniadakan manfaatnya. Jika ada, saya hanya akan menggunakan sedikit refleksi untuk melihat apakah implementasi tersedia, karena hanya ada antara nol dan satu, dan konfigurasi eksternal tidak diperlukan.
Roman Starkov
2

Saya pikir saya akan memposting opsi yang paling layak yang telah kami buat sejauh ini, untuk melihat apa yang dipikirkan.

Pada dasarnya, kami akan memisahkan setiap komponen menjadi pustaka dengan nol referensi; semua kode yang membutuhkan referensi akan ditempatkan ke dalam #if/#endifblok dengan nama yang sesuai. Misalnya, kode CommandLineParseryang menangani ConsoleColoredStrings akan ditempatkan #if HAS_CONSOLE_COLORED_STRING.

Setiap solusi yang ingin memasukkan hanya CommandLineParserdapat dengan mudah melakukannya, karena tidak ada ketergantungan lebih lanjut. Namun, jika solusinya juga mencakup ConsoleColoredStringproyek, programmer sekarang memiliki opsi untuk:

  • tambahkan referensi CommandLineParserkeConsoleColoredString
  • tambahkan HAS_CONSOLE_COLORED_STRINGdefine ke CommandLineParserfile proyek.

Ini akan membuat fungsionalitas yang relevan tersedia.

Ada beberapa masalah dengan ini:

  • Ini adalah solusi sumber saja; setiap pengguna perpustakaan harus memasukkannya sebagai kode sumber; mereka tidak bisa hanya menyertakan biner (tapi ini bukan persyaratan mutlak bagi kami).
  • The perpustakaan file proyek perpustakaan mendapat beberapa solusi suntingan -specific, dan itu tidak persis jelas bagaimana perubahan ini berkomitmen untuk SCM.

Agak tidak cantik, tapi tetap saja, ini yang paling dekat dengan kami.

Satu gagasan lebih lanjut yang kami pertimbangkan adalah menggunakan konfigurasi proyek daripada mengharuskan pengguna untuk mengedit file proyek perpustakaan. Tapi ini benar-benar tidak bisa dijalankan di VS2010 karena ia menambahkan semua konfigurasi proyek ke solusi yang tidak diinginkan .

Roman Starkov
sumber
1

Saya akan merekomendasikan buku Pengembangan Aplikasi Brownfield di .Net . Dua bab yang relevan secara langsung adalah 8 & 9. Bab 8 berbicara tentang menyampaikan aplikasi Anda, sementara bab 9 berbicara tentang menjinakkan dependensi, inversi kontrol, dan dampaknya pada pengujian.

Tangurena
sumber
1

Pengungkapan penuh, saya seorang pria Jawa. Jadi saya mengerti Anda mungkin tidak mencari teknologi yang akan saya sebutkan di sini. Tapi masalahnya sama, jadi mungkin itu akan mengarahkan Anda ke arah yang benar.

Di Jawa, ada sejumlah sistem build yang mendukung gagasan repositori artefak terpusat yang menampung "artefak" yang dibangun - sepengetahuan saya ini agak analog dengan GAC di .NET (mohon eksekusi ketidaktahuan saya jika ini adalah anaologi yang tegang) tetapi lebih dari itu karena digunakan untuk menghasilkan bangunan berulang yang independen pada setiap titik waktu.

Bagaimanapun, fitur lain yang didukung (dalam Maven, misalnya) adalah ide ketergantungan OPTIONAL, kemudian tergantung pada versi atau rentang tertentu dan berpotensi mengecualikan dependensi transitif. Ini kedengarannya seperti apa yang Anda cari, tapi saya bisa saja salah. Lihatlah halaman intro ini tentang manajemen ketergantungan dari Maven dengan teman yang mengenal Java dan lihat apakah masalahnya terdengar familier. Ini akan memungkinkan Anda untuk membangun aplikasi Anda dan membangunnya dengan atau tanpa ketersediaan dependensi ini.

Ada juga konstruksi jika Anda benar-benar dinamis, arsitektur yang dapat dicolokkan; salah satu teknologi yang mencoba menangani bentuk resolusi ketergantungan runtime ini adalah OSGI. Ini adalah mesin di balik sistem plugin Eclipse . Anda akan melihatnya dapat mendukung dependensi opsional dan rentang versi minimum / maksimum. Tingkat modularitas runtime ini membebankan sejumlah kendala pada Anda dan bagaimana Anda berkembang. Kebanyakan orang bisa bertahan dengan tingkat modularitas yang disediakan oleh Maven.

Satu kemungkinan gagasan lain yang bisa Anda perhatikan yang mungkin merupakan urutan besarnya yang lebih sederhana untuk diterapkan bagi Anda adalah dengan menggunakan gaya arsitektur Pipes and Filters. Ini adalah apa yang membuat UNIX ekosistem yang sukses dan bertahan lama yang telah bertahan dan berkembang selama setengah abad. Lihatlah artikel ini tentang Pipa dan Filter di .NET untuk beberapa ide tentang bagaimana menerapkan pola semacam ini dalam kerangka kerja Anda.

cwash
sumber
0

Mungkin buku "Desain perangkat lunak C ++ skala besar" oleh John Lakos berguna (tentu saja C # dan C ++ atau tidak sama, tetapi Anda dapat menyaring teknik yang berguna dari buku).

Pada dasarnya, faktor ulang dan pindahkan fungsionalitas yang menggunakan dua atau lebih pustaka ke dalam komponen terpisah yang bergantung pada pustaka ini. Jika perlu, gunakan teknik seperti jenis buram dll.

Kasper van den Berg
sumber