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


26

Nel corso degli anni di utilizzo di C # /. NET per una serie di progetti interni, abbiamo avuto una libreria che si è trasformata organicamente in una grande quantità di cose. Si chiama "Util", e sono sicuro che molti di voi hanno visto una di queste bestie nelle loro carriere.

Molte parti di questa libreria sono molto autonome 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 molti casi di quelle che potrei definire "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. PostBuildCheckesegue i controlli sull'assieme compilato e segnala un errore di compilazione in caso di errore. ConsoleColoredStringè una libreria per letterali di stringhe colorate. Lingoserve per tradurre le interfacce utente.

Ognuna di queste librerie può essere utilizzata in modo completamente autonomo, ma se vengono utilizzate insieme ci sono utili funzioni extra. Ad esempio, entrambi CommandLineParsered XmlClassifyesporre la funzionalità di verifica post-build, che richiede PostBuildCheck. Allo stesso modo, CommandLineParserconsente di fornire la documentazione delle opzioni usando i letterali stringa colorati, che richiedono ConsoleColoredStringe supporta la documentazione traducibile via Lingo.

Quindi la distinzione chiave è che queste sono funzionalità opzionali . È possibile utilizzare un parser da riga di comando con stringhe semplici e non colorate, senza tradurre la documentazione o eseguire controlli post-build. Oppure si potrebbe rendere la documentazione traducibile ma ancora incolore. O sia colorato che traducibile. Eccetera.

Guardando attraverso questa libreria "Util", vedo che quasi tutte le librerie potenzialmente separabili hanno tali funzionalità opzionali che le legano ad altre librerie. Se dovessi effettivamente richiedere quelle librerie come dipendenze, questa mazzetta di cose non è affatto districata: avresti comunque bisogno di tutte le librerie se vuoi usarne solo una.

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


2
Anche se le librerie dipendono l'una dall'altra, potrebbero esserci comunque dei vantaggi nel separarle in librerie coerenti ma separate, ognuna contenente un'ampia categoria di funzionalità.
Robert Harvey,

Risposte:


20

Refactor lentamente.

Aspettatevi che questo richieda un po 'di tempo per essere completato e potrebbe verificarsi in diverse iterazioni prima di poter rimuovere completamente il vostro gruppo Utils .

Approccio globale:

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

    • MyCompany.Utilities.Core (contenente algoritmi, registrazione, ecc.)
    • MyCompany.Utilities.UI (Codice disegno, ecc.)
    • MyCompany.Utilities.UI.WinForms (codice relativo a System.Windows.Forms, controlli personalizzati, ecc.)
    • MyCompany.Utilities.UI.WPF (codice relativo a WPF, classi di 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 referenze UI), ecc.

  3. Sposta i frutti a bassa pendenza (classi o metodi che non soffrono dei problemi di dipendenza) dal tuo gruppo Utils nei nuovi gruppi target.

  4. Ottieni una copia di NDepend e del Refactoring di Martin Fowler per iniziare ad analizzare il tuo gruppo Utils per iniziare a lavorare su quelli più difficili. Due tecniche che saranno utili:

Gestione delle interfacce opzionali

Un assembly fa riferimento a un altro assembly oppure non lo fa. L'unico altro modo per utilizzare la funzionalità in un assembly non collegato è tramite un'interfaccia caricata tramite la riflessione da una classe comune. Il rovescio della medaglia è che l'assemblaggio principale dovrà contenere interfacce per tutte le funzionalità condivise, ma il lato positivo è che è possibile distribuire i programmi di utilità in base alle esigenze senza il "batuffolo" 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 assieme principale:

    inserisci qui la descrizione dell'immagine

    Ad esempio, l' IStringColorerinterfaccia sarebbe simile a:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. Quindi, implementare l'interfaccia nell'assieme con la funzione. Ad esempio, la StringColorerclasse sarebbe simile a:

    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. Crea una PluginFinderclasse (o forse InterfaceFinder è un nome migliore in questo caso) che può trovare interfacce dai file DLL nella cartella corrente. Ecco un esempio semplicistico. Il consiglio di @ EdWoodcock (e sono d'accordo), quando i tuoi progetti crescono, suggerirei di utilizzare uno dei framework di Iniezione delle dipendenze disponibili ( mi viene in mente Common Serivce Locator con Unity e Spring.NET ) per un'implementazione più solida con " trovami più avanzato che dispongono delle funzionalità ", altrimenti noto come modello di localizzazione dei servizi . 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, utilizzare queste interfacce negli altri assiemi 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;
        }
    

    }

PIÙ IMPORTANTE: test, test, test tra ogni modifica.


Ho aggiunto l'esempio! :-)
Kevin McCormick il

1
Quella classe PluginFinder sembra sospettosamente come un gestore DI automagical roll-your-own (usando un modello ServiceLocator), ma questo è altrimenti un buon consiglio. Forse sarebbe meglio indirizzare l'OP verso qualcosa come Unity, dal momento che ciò non avrebbe problemi con implementazioni multiple di una particolare interfaccia all'interno delle librerie (StringColourer vs StringColourerWithHtmlWrapper, o qualsiasi altra cosa).
Ed James

@EdWoodcock Ottimo punto Ed, e non posso credere di non aver pensato al modello di Service Locator mentre scrivevo questo. PluginFinder è sicuramente un'implementazione immatura e un framework DI funzionerebbe sicuramente qui.
Kevin McCormick,

Ti ho assegnato la generosità per lo sforzo, ma non seguiremo questa strada. Condividere un nucleo di interfacce significa che siamo riusciti solo ad allontanare le implementazioni, ma c'è ancora una libreria che contiene una mazzetta di interfacce poco correlate (correlate tramite dipendenze opzionali, come prima). Il set-up è molto più complicato ora con pochi vantaggi per le librerie piccole come questa. La complessità in più potrebbe valerne la pena per progetti giganteschi, ma non per questi.
Roman Starkov,

@romkyns Quindi quale strada stai prendendo? Lasciandolo così com'è? :)
Max

5

È possibile 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, impostarlo per restituire un'istanza nulla.
Quindi controlla se l'istanza è nulla, nel qual caso non esegui le funzionalità extra.

Questo è particolarmente facile da fare con MEF, poiché è l'uso del libro di testo per questo.

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

HTH.


Sembra quasi giusto - se non fosse per quella DLL aggiuntiva, che è fondamentalmente come un mucchio di scheletri della mazzetta originale di roba. Le implementazioni sono tutte divise, ma rimane ancora una "mazzetta di scheletri". Suppongo che abbia alcuni vantaggi, ma non sono convinto che i vantaggi superino tutti i costi per questo particolare set di biblioteche ...
Roman Starkov

Inoltre, includere un intero framework è totalmente un passo indietro; questa libreria così com'è ha le dimensioni di uno di quei framework, negandone totalmente il beneficio. Semmai, vorrei solo usare un po 'di riflessione per vedere se un'implementazione è disponibile, dal momento che può esserci solo tra zero e uno e la configurazione esterna non è richiesta.
Roman Starkov,

2

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

Fondamentalmente, separeremmo ogni componente in una libreria con zero riferimenti; tutto il codice che richiede un riferimento verrà inserito in un #if/#endifblocco con il nome appropriato. Ad esempio, il codice in CommandLineParserquella maniglia ConsoleColoredStringverrà inserito #if HAS_CONSOLE_COLORED_STRING.

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

  • aggiungere un riferimento CommandLineParseraConsoleColoredString
  • aggiungere la HAS_CONSOLE_COLORED_STRINGdefinizione al CommandLineParserfile di progetto.

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).
  • La libreria di file di progetto della biblioteca ottiene un paio di soluzioni modifiche specifico d', e non è esattamente chiaro come questo cambiamento si è impegnata a SCM.

Piuttosto poco carino, ma comunque, questo è il più vicino che abbiamo inventato.

Un'ulteriore idea che abbiamo preso in considerazione è stata l'utilizzo delle configurazioni del progetto piuttosto che richiedere all'utente di modificare il file di progetto della libreria. Ma questo è assolutamente impraticabile in VS2010 perché aggiunge involontariamente tutte le configurazioni di progetto alla soluzione .


1

Raccomanderò il libro Brownfield Application Development in .Net . Due capitoli direttamente rilevanti sono 8 e 9. Il capitolo 8 parla dell'inoltro dell'applicazione, mentre il capitolo 9 parla delle dipendenze da domare, dell'inversione del controllo e dell'impatto che ciò ha sui test.


1

Divulgazione completa, sono un ragazzo Java. Quindi capisco che probabilmente non stai cercando le tecnologie che menzionerò qui. Ma i problemi sono gli stessi, quindi forse ti indicherà la giusta direzione.

In Java, ci sono un certo numero di sistemi di build che supportano l'idea di un repository di artefatti centralizzato che ospita "artefatti" costruiti - per quanto ne sappia questo è in qualche modo analogo al GAC in .NET (per favore, esegui la mia ignoranza se si tratta di un anaologia teso) ma soprattutto perché viene utilizzato per produrre build ripetibili indipendenti in qualsiasi momento.

Ad ogni modo, un'altra funzionalità supportata (ad esempio in Maven) è l'idea di una dipendenza OPZIONALE, quindi a seconda di versioni o intervalli specifici e potenzialmente escludendo dipendenze transitive. Mi sembra quello che stai cercando, ma potrei sbagliarmi. Dai un'occhiata a questa pagina introduttiva sulla gestione delle dipendenze di Maven con un amico che conosce Java e vedi se i problemi sembrano familiari. Ciò ti consentirà di creare la tua applicazione e costruirla con o senza queste dipendenze disponibili.

Ci sono anche costrutti se hai bisogno di un'architettura veramente dinamica e collegabile; una tecnologia che tenta di affrontare questa forma di risoluzione della dipendenza dal runtime è l'OSGI. Questo è il motore dietro il sistema di plugin di Eclipse . Vedrai che può supportare dipendenze opzionali e un intervallo di versioni minimo / massimo. Questo livello di modularità di runtime impone un buon numero di vincoli su di te e su come sviluppi. Molte persone riescono a cavarsela con il grado di modularità offerto da Maven.

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


0

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

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.