“İsteğe bağlı” bir yardımcı projeyi “isteğe bağlı” bağımlılıkları olan ayrı bileşenlere ayırma


26

Bir sürü kurum içi proje için C # /. NET'i kullandığınız yıllar boyunca, bir kütüphane organik olarak büyük bir yığın halinde büyüdü. Buna "Util" deniyor ve eminim çoğunuz bu canavarlardan birini kariyerinizde görmüşsünüzdür.

Bu kütüphanenin pek çok bölümü bağımsızdır ve ayrı bir projeye ayrılabilir (açık kaynak kodlu olmak istiyoruz). Ancak, bunlar ayrı kütüphaneler olarak serbest bırakılmadan önce çözülmesi gereken önemli bir sorun var. Temel olarak, bu kütüphaneler arasında "isteğe bağlı bağımlılıklar" diyebileceğim birçok durum var .

Bunu daha iyi açıklamak için, bağımsız kütüphaneler olma yolunda iyi aday olan bazı modülleri düşünün. CommandLineParserkomut satırlarını ayrıştırmak içindir. XmlClassifysınıfları XML'e serileştirmek içindir. PostBuildCheckderlenmiş derleme üzerinde denetimler yapar ve başarısız olursa derleme hatası bildirir. ConsoleColoredStringrenkli dize değişmezleri için bir kütüphanedir. Lingokullanıcı arayüzlerini çevirmek içindir.

Bu kitaplıkların her biri tamamen tek başına kullanılabilir, ancak eğer birlikte kullanılırlarsa, sahip olması gereken ek özellikler vardır. Örneğin, her ikisi de gerektirir CommandLineParserve XmlClassifyoluşturma sonrası kontrol işlevini gerektirir PostBuildCheck. Benzer şekilde, CommandLineParseristeğe bağlı belgelerin, renkli dize değişmezleri kullanılarak sağlanmasını sağlar ConsoleColoredString, bunu gerektirir ve çevrilebilir belgelerin üzerinden gönderilmesini destekler Lingo.

Dolayısıyla, temel ayrım, bunların isteğe bağlı özellikler olduğudur . Biri, belgeleri çevirmeden veya herhangi bir yapım sonrası kontrol yapmadan düz, renksiz dizeleri olan bir komut satırı çözümleyici kullanabilir. Veya biri belgelerin çevrilebilir ancak hala renklendirilmemiş olmasına neden olabilir. Veya hem renkli hem de çevrilebilir. Vb.

Bu "Util" kütüphanesini inceleyerek, neredeyse tüm potansiyel olarak ayrılabilir kütüphanelerin onları diğer kütüphanelere bağlayan isteğe bağlı özelliklere sahip olduğunu görüyorum. Eğer gerçekten bu kütüphanelere bağımlılık olarak ihtiyaç duyacak olsaydım, o zaman bu tomar hiç karışmazdı: Eğer sadece bir tane kullanmak istiyorsan temelde tüm kütüphanelere ihtiyaç duyardın.

Bu isteğe bağlı bağımlılıkları .NET'te yönetmek için belirlenmiş herhangi bir yaklaşım var mı?


2
Kütüphaneler birbirlerine bağlı olsalar bile, her birini geniş bir işlevsellik kategorisi içeren tutarlı fakat ayrı kütüphanelere ayırmada bazı faydalar olabilir.
Robert Harvey,

Yanıtlar:


20

Yavaşlatıcı

Bunun tamamlanması biraz zaman alır ve Utils montajınızı tamamen çıkarmadan önce birkaç yinelemede ortaya çıkabilir .

Genel Yaklaşım:

  1. İlk önce biraz zaman ayırın ve bu yardımcı program derlemelerinin işiniz bitince nasıl görünmesini istediğinizi düşünün. Mevcut kodunuz hakkında çok fazla endişelenmeyin, hedefin sonunu düşünün. Örneğin, şunlara sahip olmak isteyebilirsiniz:

    • MyCompany.Utilities.Core (Algoritmalar içeren, kayıt vb.)
    • MyCompany.Utilities.UI (Çizim kodu, vb.)
    • MyCompany.Utilities.UI.WinForms (System.Windows.Forms ile ilgili kod, özel kontroller vb.)
    • MyCompany.Utilities.UI.WPF (WPF ile ilgili kod, MVVM temel sınıfları).
    • MyCompany.Utilities.Serialization (Seri hale getirme kodu).
  2. Bu projelerden her biri için boş projeler oluşturun ve uygun proje referansları oluşturun (UI referansları Core, UI.WinForms referansları UI), vb.

  3. Asılı meyvelerden herhangi birini (bağımlılık sorunları bulunmayan sınıflar veya yöntemler) Utils montajınızdan yeni hedef montajlara taşıyın.

  4. Sert olanlar üzerinde çalışmaya başlamak için Utils montajınızı analiz etmeye başlamak için NDepend ve Martin Fowler's Refactoring'in bir kopyasını alın . Yardımcı olacak iki teknik:

    • Çoğu durumda, arayüzler, delegeler veya olaylar aracılığıyla Denetimi Tersine çevirmeniz gerekecektir .
    • “İsteğe bağlı” bağımlılıklarınız için aşağıya bakınız.

İsteğe Bağlı Arabirimleri Kullanma

Bir derleme başka bir derleme başvuruyor veya değil. Bağlantısız bir montajda işlevselliği kullanmanın diğer yolu, ortak bir sınıftan gelen yansıma yoluyla yüklenen bir arayüzdür. Bunun dezavantajı, çekirdek derlemenizin tüm paylaşılan özelliklerin arabirimlerini içermesi gerektiğidir, ancak bunun yanı sıra, her dağıtım senaryosuna bağlı olarak, DLL dosyalarının "tokası" olmadan yardımcı programlarınızı gerektiği gibi dağıtabilirsiniz. Örnek olarak renkli dizgiyi kullanarak bu durumu nasıl ele alacağım:

  1. İlk önce, çekirdek montajınızdaki ortak arayüzleri tanımlayın:

    görüntü tanımını buraya girin

    Örneğin, IStringColorerarayüz şöyle görünür:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. Ardından, bu özelliği içeren montajdaki arayüzü uygulayın. Örneğin, StringColorersınıf şöyle görünür:

    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. PluginFinderGeçerli klasördeki DLL dosyalarından arayüzleri bulabilen bir (veya Interface Interface bu durumda daha iyi bir isimdir) sınıfı oluşturun. İşte basit bir örnek. EdWoodcock'un tavsiyesine göre (ve aynı fikirdeyim), projeleriniz büyüdükçe , daha gelişmiş bir uygulama için mevcut Bağımlılık Enjeksiyonu çerçevelerinden birini ( Unity ve Spring. ile Ortak Serivce Bulucu ) akla gelmesini öneririm. " Bu, " Hizmet Bulucu Modeli olarak bilinen" özelliklere sahiptir . Gereksinimlerinize göre değiştirebilirsiniz.

    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. Son olarak, bu arabirimleri FindInterface yöntemini çağırarak diğer derlemelerinizde kullanın. İşte bir örnek 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;
        }
    

    }

EN ÖNEMLİ: Her değişiklik arasında test, test, test.


Örnek ekledim! :-)
Kevin McCormick,

1
Bu PluginFinder sınıfı şüpheli bir şekilde kendi otomajik DI işlemcinizi (bir ServiceLocator modeli kullanarak) göründüğü gibi görünüyor, ancak bu başka türlü bir tavsiyedir. Belki de OP'yi Unity gibi bir şeye yönlendirmeniz daha iyi olur, çünkü kütüphanelerde belirli bir arabirimin birden fazla uygulamasıyla ilgili sorunlarınız olmazdı (StringColourer vs StringColourerWithHtmlWrapper veya her neyse).
Ed James

@EdWoodcock İyi nokta Ed, ve bunu yazarken Servis Belirleme modelini düşünmediğime inanamıyorum. PluginFinder kesinlikle olgunlaşmamış bir uygulama ve DI çerçeve kesinlikle burada işe yarar.
Kevin McCormick,

Size çaba için ödül verdim, ama bu rotaya gitmeyeceğiz. Çekirdek arabirimler düzeneğini paylaşmak, yalnızca uygulamalardan uzaklaşmayı başardığımız anlamına gelir, ancak yine de (önceden olduğu gibi isteğe bağlı bağımlılıklar ile ilgili) bir miktar küçük ilişkili arabirim içeren bir kütüphane vardır. Kurulum, şimdi bu kadar küçük kütüphaneler için çok az fayda ile çok daha karmaşık. Ekstra karmaşıklık buna değersiz projeler için değebilir, ancak bunlar değil.
Roman Starkov

@romkyns Peki hangi rotayı kullanıyorsunuz? Olduğu gibi bırakmak? :)
Maksimum

5

Ek bir kütüphanede belirtilen arayüzleri kullanabilirsiniz.

Bir bağımlılığı enjeksiyon (MEF, Birlik vb.) Kullanarak bir sözleşmeyi (arayüz yoluyla sınıf) çözmeye çalışın. Bulunamadıysa, boş bir örnek döndürecek şekilde ayarlayın.
Ardından, örneğin null olup olmadığını kontrol edin, bu durumda ekstra işlevler yapmazsınız.

Bunun özellikle MEF ile yapılması kolaydır çünkü ders kitabı bunun için kullanılır.

Kütüphaneleri derlemenizi sağlar, onları n + 1 harçlara bölmek pahasına.

HTH.


Bu neredeyse doğru geliyor - sadece orijinal bir tomarın iskeletlerine benzeyen ekstra bir DLL olmasaydı. Uygulamaların hepsi bölünmüş durumda, ancak hala bir "iskelet tomar" kaldı. Sanırım bunun bazı avantajları var, ancak bu belirli kütüphaneler için tüm masrafları karşıladığından emin değilim ...
Roman Starkov

Ek olarak, bütün bir çerçevenin dahil edilmesi tamamen geri adımdır; olduğu gibi bu kütüphane, bu çerçevelerden birinin büyüklüğüyle ilgili olup, yararı tamamen göz ardı etmektedir. Bir şey varsa, bir uygulamanın mevcut olup olmadığını görmek için biraz yansıma kullanırdım, çünkü yalnızca sıfır ile bir arasında olabilir ve harici yapılandırma gerekli değildir.
Roman Starkov

2

Düşüncelerin ne olduğunu görmek için şimdiye kadar bulduğumuz en uygun seçeneği göndereceğimi düşündüm.

Temel olarak, her bir bileşeni sıfır referansla bir kütüphaneye ayırırdık; referans gerektiren tüm kod #if/#endif, uygun ada sahip bir bloğa yerleştirilecektir . Örneğin, CommandLineParserbu tutamaçlardaki kod ConsoleColoredStringyerleştirilir #if HAS_CONSOLE_COLORED_STRING.

Başka CommandLineParserherhangi bir bağımlılık olmadığından, sadece kutuyu dahil etmek isteyen herhangi bir çözüm kolaylıkla yapabilir. Bununla birlikte, eğer çözüm ConsoleColoredStringprojeyi de içeriyorsa , programcının şu seçeneği vardır:

  • bir referans eklemek CommandLineParseriçinConsoleColoredString
  • HAS_CONSOLE_COLORED_STRINGtanımını CommandLineParserproje dosyasına ekleyin .

Bu, ilgili işlevselliği kullanılabilir hale getirir.

Bununla ilgili birkaç sorun var:

  • Bu sadece kaynaklardan oluşan bir çözümdür; kütüphanenin her tüketicisi onu kaynak kod olarak içermelidir; sadece bir ikili dosya içeremezler (ancak bu bizim için mutlak bir gereklilik değildir).
  • Kütüphane kütüphane proje dosyası bir çift alır çözüm ya spesifik düzenlemeler ve bu değişikliğin SCM kararlıdır tam olarak nasıl belli değil.

Oldukça hoş değil, ama yine de, bu yaklaştığımız en yakın şey.

Düşündüğümüz bir başka fikir, kullanıcının kitaplık proje dosyasını düzenlemesini istemek yerine proje yapılandırmalarını kullanmaktı. Ancak bu kesinlikle VS2010'da kullanılamaz çünkü çözüme tüm proje yapılandırmalarını istenmeyen bir şekilde ekler .



1

Tam açıklama, ben bir Java'lıyım. Anladığım kadarıyla muhtemelen burada bahsedeceğim teknolojileri aramıyorsunuz. Ancak sorunlar aynıdır, bu yüzden belki de sizi doğru yöne işaret eder.

Java'da, "yapay yapı" barındıran merkezi bir yapay yapı deposu fikrini destekleyen bir dizi yapı sistemi var - benim bildiğim kadarıyla bu, .NET'teki GAC'ye benzer bir şey (benim gergin bir anaoloji ise lütfen cehaletimi yürütün) fakat bundan daha fazlası, zamanın herhangi bir noktasında bağımsız tekrarlanabilir yapılar üretmek için kullanıldığı için.

Her neyse, desteklenen bir başka özellik (örneğin Maven'de) OPSİYONEL bağımlılık fikridir, daha sonra belirli versiyonlara veya aralıklara bağlı olarak ve geçişli bağımlılıkları potansiyel olarak hariç tutar. Bu bana aradığın gibi geliyor ama yanılıyor olabilirim. Java'yı bilen bir arkadaşıyla Maven'in bağımlılık yönetimi hakkındaki bu giriş sayfasına bakın ve sorunların tanıdık gelip gelmediğini görün. Bu, uygulamanızı inşa etmenize ve bu bağımlılıkları mevcut ya da olmasa da inşa etmenize olanak sağlar.

Gerçekten dinamik, takılabilir mimariye ihtiyacınız varsa yapılar da var; Bu çalışma zamanı bağımlılık çözüm biçimini ele almaya çalışan bir teknoloji OSGI'dir. Bu Eclipse'in eklenti sisteminin arkasındaki motor . İsteğe bağlı bağımlılıkları ve minimum / maksimum sürüm aralığını destekleyebileceğini göreceksiniz. Bu çalışma zamanı modülerliği düzeyi, size ve nasıl gelişim göstereceğinize çok sayıda kısıtlama getirir. Maven'in sağladığı modülerlik derecesi ile çoğu insan geçebilir.

Bunu göz önüne alabileceğiniz bir başka olası fikir, sizin için uygulaması daha kolay olan büyüklük emirleri olabilir. Mimari bir Borular ve Filtreler tarzı kullanmaktır. Bu, UNIX'i bu kadar uzun süredir ayakta kalan ve yarım yüzyıl boyunca hayatta kalan ve gelişen bir ekosistem haline getiren şeydir. Bu tür bir kalıbı kendi çerçevenizde nasıl uygulayacağınızla ilgili fikir edinmek için .NET'teki Borular ve Filtreler hakkındaki bu makaleye göz atın .


0

Belki de John Lakos'un "Büyük Ölçekli C ++ yazılım tasarımı" kitabı kullanışlıdır (elbette C # ve C ++ ile aynı değildir, ancak faydalı teknikleri kitaptan damlatabilirsiniz).

Temel olarak, iki veya daha fazla kitaplığı kullanan işlevselliği, bu kitaplıklara bağlı olan ayrı bir bileşene yeniden yerleştirin ve taşıyın. Gerekirse, opak tipler vb. Teknikleri kullanın.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.