Separazione di un progetto di utilità "wad of stuff" in singoli componenti con dipendenze "opzionali"

26

Nel corso degli anni di utilizzo di C # /. NET per un gruppo di progetti interni, abbiamo avuto una biblioteca crescere organicamente in un enorme mucchio di roba. Si chiama "Util", e sono sicuro che molti di voi hanno visto una di queste bestie nella propria carriera.

Molte parti di questa libreria sono molto indipendenti e potrebbero essere suddivise in progetti separati (che vorremmo open-source). Ma c'è un grosso problema che deve essere risolto prima che questi possano essere rilasciati come librerie separate. Fondamentalmente, ci sono un sacco e molti casi di ciò che potrei chiamare "dipendenze opzionali" tra queste librerie.

Per spiegarlo meglio, considera alcuni dei moduli che sono buoni candidati per diventare librerie autonome. CommandLineParser è per l'analisi delle righe di comando. XmlClassify è per serializzare le classi in XML. PostBuildCheck esegue controlli sull'assieme compilato e segnala un errore di compilazione in caso di errore. ConsoleColoredString è una libreria per valori letterali stringa colorati. Lingo è per la traduzione di interfacce utente.

Ciascuna di queste librerie può essere utilizzata completamente stand-alone, ma se vengono utilizzati insieme, allora ci sono utili funzionalità extra da avere. Ad esempio, sia CommandLineParser che XmlClassify espongono la funzionalità di controllo post-build, che richiede PostBuildCheck . Allo stesso modo, CommandLineParser consente di fornire la documentazione delle opzioni utilizzando i valori letterali delle stringhe colorate, che richiedono ConsoleColoredString , e supporta la documentazione traducibile tramite Lingo .

Quindi la distinzione principale è che queste sono caratteristiche opzionali . Si può usare un parser da riga di comando con stringhe non colorate, senza tradurre la documentazione o eseguire controlli post-compilazione. O si potrebbe rendere la documentazione traducibile ma ancora incolore. O sia colorati che traducibili. Ecc.

Guardando attraverso questa libreria "Util", vedo che quasi tutte le librerie potenzialmente separabili hanno caratteristiche opzionali tali che li legano ad altre librerie. Se dovessi effettivamente richiedere quelle librerie come dipendenze, questo mazzetto di roba non è affatto districato: in pratica avresti ancora bisogno di tutte le librerie se vuoi usarne solo uno.

Esistono approcci consolidati per la gestione di tali dipendenze opzionali in .NET?

    
posta Roman Starkov 15.12.2011 - 14:59
fonte

6 risposte

20

Refactor Slow.

Aspettati che ci vorrà un po 'di tempo per completare e potrebbe verificarsi in più iterazioni prima di poter rimuovere completamente l'assembly Utils .

Approccio globale:

  1. Prima prendi un po 'di tempo e pensa a come vuoi che questi gruppi di utilità appaiano quando hai finito. Non preoccuparti troppo del tuo codice esistente, pensa all'obiettivo finale. Ad esempio, potresti desiderare di avere:

    • MyCompany.Utilities.Core (Contenendo algoritmi, registrazione, ecc.)
    • MyCompany.Utilities.UI (codice di disegno, ecc.)
    • MyCompany.Utilities.UI.WinForms (codice relativo a System.Windows.Forms, controlli personalizzati, ecc.)
    • MyCompany.Utilities.UI.WPF (codice relativo a WPF, classi base MVVM).
    • MyCompany.Utilities.Serialization (codice di serializzazione).
  2. Crea progetti vuoti per ciascuno di questi progetti e crea riferimenti di progetto appropriati (riferimenti UI Core, UI.WinForms reference UI), ecc.

  3. Sposta qualsiasi frutto a basso impatto (classi o metodi che non soffrono dei problemi di dipendenza) dal tuo assembly Utils ai nuovi gruppi di destinazione.

  4. Ottieni una copia di NDepend e di Refactoring per iniziare ad analizzare l'assembly di Utils per iniziare a lavorare su quelli più difficili. Due tecniche che saranno utili:

    • In molti casi è necessario invertire il controllo attraverso i mezzi di interfacce, delegati o eventi .
    • Nel caso delle tue dipendenze "opzionali" , vedi sotto.

Gestione delle interfacce opzionali

O un assembly fa riferimento a un altro assembly o non lo fa. L'unico altro modo per utilizzare la funzionalità in un assembly non collegato è attraverso un'interfaccia caricata tramite il reflection da una classe comune. Lo svantaggio di questo è che l'assembly principale dovrà contenere interfacce per tutte le funzionalità condivise, ma il lato positivo è che è possibile distribuire le utility in base alle esigenze senza "l'uv" di file DLL a seconda di ogni scenario di distribuzione. Ecco come gestirò questo caso, usando la stringa colorata come esempio:

  1. Innanzitutto, definisci le interfacce comuni nel tuo core assembly:

    Adesempio,l'interfacciaIStringColorersaràsimilea:

    namespaceMyCompany.Utilities.Core.OptionalInterfaces{publicinterfaceIStringColorer{stringDecorate(strings);}}
  2. Quindi,implemental'interfaccianell'assiemeconlafunzione.Adesempio,laclasseStringColorersaràsimilea:

    usingMyCompany.Utilities.Core.OptionalInterfaces;namespaceMyCompany.Utilities.Console{classStringColorer:IStringColorer{#regionIStringColorerMemberspublicstringDecorate(strings){return"*" + s + "*";   //TODO: implement coloring
            }
    
            #endregion
        }
    }
    
  3. Crea una classe PluginFinder (o forse InterfaceFinder è un nome migliore in questo caso) che può trovare interfacce dai file DLL nella cartella corrente. Ecco un esempio semplicistico. Per @ il consiglio di EdWoodcock (e sono d'accordo), quando i tuoi progetti cresceranno suggerirei di utilizzare uno dei framework di Dependency Injection disponibili ( Local Serivce Locator with Unity e Spring.NET vengono in mente) per un'implementazione più robusta con funzionalità avanzate "find me that feature", altrimenti conosciute come il Pattern di localizzazione del servizio . Puoi modificarlo in base alle tue esigenze.

    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. Infine, utilizza queste interfacce negli altri assembly chiamando il metodo FindInterface. Ecco un esempio di 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;
        }
    

    }

MOST IMPORTANTLY: testa, prova, prova tra ogni modifica.

    
risposta data 09.01.2012 - 19:13
fonte
5

Potresti utilizzare le interfacce dichiarate in una libreria aggiuntiva.

Prova a risolvere un contratto (classe tramite interfaccia) usando un'iniezione di dipendenza (MEF, Unity ecc.). Se non trovato, impostalo per restituire un'istanza nulla.
Quindi controlla se l'istanza è nullo, nel qual caso non esegui le funzionalità extra.

Questo è particolarmente facile da fare con MEF, dato che è il libro di testo ad usarlo.

Ti permetterebbe di compilare le librerie, al costo di dividerle in n + 1 dll.

HTH.

    
risposta data 26.12.2011 - 15:56
fonte
2

Ho pensato di pubblicare l'opzione più valida che abbiamo escogitato finora, per vedere quali sono i pensieri.

Fondamentalmente, dovremmo separare ciascun componente in una libreria con riferimenti zero; tutto il codice che richiede un riferimento verrà inserito in un blocco #if/#endif con il nome appropriato. Ad esempio, il codice in CommandLineParser che gestisce ConsoleColoredString s verrebbe inserito in #if HAS_CONSOLE_COLORED_STRING .

Qualsiasi soluzione che desideri includere solo CommandLineParser può farlo facilmente, poiché non ci sono ulteriori dipendenze. Tuttavia, se la soluzione include anche il progetto ConsoleColoredString , il programmatore ora ha la possibilità di:

  • aggiungi un riferimento in CommandLineParser a ConsoleColoredString
  • aggiungi la HAS_CONSOLE_COLORED_STRING definita al file di progetto CommandLineParser .

Ciò renderebbe disponibili le funzionalità pertinenti.

Ci sono diversi problemi con questo:

  • Questa è una soluzione di sola fonte; ogni consumatore della biblioteca deve includerlo come codice sorgente; non possono semplicemente includere un binario (ma questo non è un requisito assoluto per noi).
  • Il file di progetto della libreria della libreria ottiene un paio di modifiche specifiche della soluzione e non è esattamente ovvio in che modo viene applicata questa modifica a SCM.

Piuttosto poco carina, ma ancora, questa è la cosa più vicina che abbiamo trovato.

Un'ulteriore idea che abbiamo considerato era l'utilizzo di configurazioni di progetto piuttosto che richiedere all'utente di modificare il file di progetto della libreria. Ma questo è assolutamente impraticabile in VS2010 perché aggiunge tutte le configurazioni di progetto alla soluzione indesiderata .

    
risposta data 26.12.2011 - 13:03
fonte
1

Raccomanderò il libro Sviluppo applicazioni brownfield in .Net . Due capitoli direttamente rilevanti sono 8 e amp; 9. Il capitolo 8 parla di inoltro della domanda, mentre il capitolo 9 parla delle dipendenze di domare, dell'inversione del controllo e dell'impatto che questo ha sui test.

    
risposta data 10.01.2012 - 00:35
fonte
1

Full disclosure, sono un ragazzo di Java. Quindi capisco che probabilmente non stai cercando le tecnologie che menzionerò qui. Ma i problemi sono gli stessi, quindi forse ti indirizzerà nella giusta direzione.

In Java, ci sono diversi sistemi di build che supportano l'idea di un repository di artefatti centralizzato che contiene "artefatti" costruiti - per quanto ne so, questo è in qualche modo analogo al GAC in .NET (per favore usa la mia ignoranza se è un anaology strained) ma più di quello perché è usato per produrre build ripetibili indipendenti in qualsiasi momento.

Ad ogni modo, un'altra caratteristica supportata (in Maven, per esempio) è l'idea di una dipendenza OPZIONALE, quindi dipende da specifiche versioni o intervalli e potenzialmente esclude le dipendenze transitive. Questo mi suona come quello che stai cercando, ma potrei sbagliarmi. Dai un'occhiata a questa pagina di introduzione su gestione delle dipendenze da Maven con un amico che conosce Java e verifica se i problemi sembrano familiari. Questo ti permetterà di costruire la tua applicazione e di costruirla con o senza avere queste dipendenze disponibili.

Esistono anche costrutti se hai bisogno di un'architettura veramente dinamica e collegabile; una tecnologia che tenta di affrontare questa forma di risoluzione delle dipendenze runtime è OSGI. Questo è il motore dietro Il sistema di plugin di Eclipse . Vedrai che può supportare dipendenze opzionali e un intervallo di versione minimo / massimo. Questo livello di modularità runtime impone un buon numero di limitazioni su di te e su come ti sviluppi. La maggior parte delle persone può cavarsela con il grado di modularità fornito da Maven.

Un'altra possibile idea che potresti esaminare potrebbe essere un ordine di grandezza più semplice da implementare per te è usare uno stile di architettura Pipes and Filters. Questo è in gran parte ciò che ha reso UNIX un ecosistema di successo di lunga data che è sopravvissuto e si è evoluto per mezzo secolo. Dai un'occhiata a questo articolo su Tubi e filtri in .NET per alcune idee su come implementare questo tipo di pattern nel tuo framework.

    
risposta data 13.01.2012 - 22:58
fonte
0

Forse il libro "Progetto software C ++ su larga scala" di John Lakos è utile (ovviamente C # e C ++ o non lo stesso, ma puoi distillare tecniche utili dal libro).

Fondamentalmente, ridimensionare e spostare la funzionalità che utilizza due o più librerie in un componente separato che dipende da queste librerie. Se necessario, utilizza tecniche come i tipi opachi ecc.

    
risposta data 09.01.2012 - 19:12
fonte

Leggi altre domande sui tag