Teilen eines Hilfsprojekts "wad of stuff" in einzelne Komponenten mit "optionalen" Abhängigkeiten

26

Im Laufe der Jahre, in denen wir C # /. NET für eine Reihe von internen Projekten verwendet haben, ist eine Bibliothek organisch zu einem großen Haufen Material gewachsen. Es heißt "Util" und ich bin sicher, dass viele von Ihnen eines dieser Tiere in ihrer Karriere gesehen haben.

Viele Teile dieser Bibliothek sind sehr eigenständig und können in separate Projekte aufgeteilt werden (die wir gerne als Open Source-Version anbieten würden). Es gibt jedoch ein großes Problem, das gelöst werden muss, bevor diese als separate Bibliotheken veröffentlicht werden können. Grundsätzlich gibt es sehr viele Fälle von "optionalen Abhängigkeiten" zwischen diesen Bibliotheken.

Um dies besser zu erklären, ziehen Sie einige der Module in Betracht, die sich als eigenständige Bibliotheken eignen. CommandLineParserdient zum Parsen von Befehlszeilen. XmlClassifydient zum Serialisieren von Klassen nach XML. PostBuildCheckÜberprüft die kompilierte Assembly und meldet einen Kompilierungsfehler, wenn sie fehlschlägt. ConsoleColoredStringist eine Bibliothek für farbige Stringliterale. Lingodient zum Übersetzen von Benutzeroberflächen.

Jede dieser Bibliotheken kann vollständig eigenständig verwendet werden. Wenn sie jedoch zusammen verwendet werden, stehen nützliche Zusatzfunktionen zur Verfügung. Beispielsweise können Sie beide CommandLineParserund XmlClassifydie erforderlichen Funktionen für die Überprüfung nach der Erstellung bereitstellen PostBuildCheck. Ebenso können mit der CommandLineParserOption Dokumentationen unter Verwendung der farbigen String-Literale bereitgestellt werden ConsoleColoredString, und es werden übersetzbare Dokumentationen über unterstützt Lingo.

Der Hauptunterschied besteht also darin, dass dies optionale Funktionen sind . Sie können einen Befehlszeilenparser mit einfachen, nicht gefärbten Zeichenfolgen verwenden, ohne die Dokumentation zu übersetzen oder Prüfungen nach der Erstellung durchzuführen. Oder man könnte die Dokumentation übersetzbar, aber immer noch ungefärbt machen. Oder sowohl farbig als auch übersetzbar. Etc.

Wenn ich mir diese "Util" -Bibliothek ansehe, sehe ich, dass fast alle potenziell trennbaren Bibliotheken solche optionalen Funktionen haben, die sie an andere Bibliotheken binden. Wenn ich diese Bibliotheken tatsächlich als Abhängigkeiten benötige, ist dieses Zeug nicht wirklich entwirrt: Sie würden im Grunde immer noch alle Bibliotheken benötigen, wenn Sie nur eine verwenden möchten.

Gibt es etablierte Ansätze zum Verwalten solcher optionalen Abhängigkeiten in .NET?

Roman Starkov
quelle
2
Selbst wenn die Bibliotheken voneinander abhängig sind, könnte es dennoch von Vorteil sein, sie in zusammenhängende, aber separate Bibliotheken zu unterteilen, die jeweils eine breite Kategorie von Funktionen enthalten.
Robert Harvey

Antworten:

20

Refactor Langsam.

Dies kann einige Zeit in Anspruch nehmen und über mehrere Iterationen hinweg auftreten, bevor Sie die Utils- Assembly vollständig entfernen können .

Gesamtkonzept:

  1. Nehmen Sie sich zunächst etwas Zeit und überlegen Sie, wie diese Dienstprogramm-Assemblys aussehen sollen, wenn Sie fertig sind. Sorgen Sie sich nicht zu sehr um Ihren vorhandenen Code, sondern denken Sie an das Endziel. Zum Beispiel möchten Sie vielleicht:

    • MyCompany.Utilities.Core (Enthält Algorithmen, Protokollierung usw.)
    • MyCompany.Utilities.UI (Zeichnungscode usw.)
    • MyCompany.Utilities.UI.WinForms (System.Windows.Forms-bezogener Code, benutzerdefinierte Steuerelemente usw.)
    • MyCompany.Utilities.UI.WPF (WPF-bezogener Code, MVVM-Basisklassen).
    • MyCompany.Utilities.Serialization (Serialisierungscode).
  2. Erstellen Sie leere Projekte für jedes dieser Projekte und erstellen Sie entsprechende Projektreferenzen (Benutzeroberflächenreferenzen Core, Benutzeroberfläche.WinForms-Referenz-Benutzeroberfläche) usw.

  3. Verschieben Sie eine der niedrig hängenden Komponenten (Klassen oder Methoden, bei denen die Abhängigkeitsprobleme nicht auftreten) von Ihrer Utils-Assembly in die neuen Ziel-Assemblys.

  4. Holen Sie sich eine Kopie von NDepend und Martin Fowlers Refactoring , um mit der Analyse Ihrer Utils-Baugruppe zu beginnen und die Arbeit an den härteren zu beginnen. Zwei Techniken, die hilfreich sein werden:

Umgang mit optionalen Schnittstellen

Entweder verweist eine Assembly auf eine andere Assembly oder nicht. Die einzige andere Möglichkeit, die Funktionalität in einer nicht verknüpften Assembly zu verwenden, besteht darin, eine Schnittstelle zu verwenden, die durch Reflektion einer gemeinsamen Klasse geladen wird. Der Nachteil dabei ist, dass Ihre Core-Assembly Schnittstellen für alle gemeinsam genutzten Features enthalten muss. Der Vorteil ist jedoch, dass Sie Ihre Dienstprogramme je nach Bereitstellungsszenario nach Bedarf bereitstellen können, ohne dass DLL-Dateien "wackeln". Hier ist, wie ich mit diesem Fall umgehen würde, am Beispiel der farbigen Zeichenfolge:

  1. Definieren Sie zunächst die allgemeinen Schnittstellen in Ihrer Kernbaugruppe:

    Bildbeschreibung hier eingeben

    Zum Beispiel IStringColorerwürde die Schnittstelle so aussehen:

     namespace MyCompany.Utilities.Core.OptionalInterfaces
     {
         public interface IStringColorer
         {
             string Decorate(string s);
         }
     }
    
  2. Implementieren Sie dann die Schnittstelle in der Assembly mit dem Feature. Zum Beispiel StringColorerwürde die Klasse so aussehen:

    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. Erstellen Sie eine PluginFinderKlasse (oder in diesem Fall ist InterfaceFinder eine bessere Bezeichnung), die Schnittstellen aus DLL-Dateien im aktuellen Ordner finden kann. Hier ist ein vereinfachtes Beispiel. Nach dem Rat von @ EdWoodcock (und ich stimme dem zu) würde ich vorschlagen, wenn Ihr Projekt wächst, eines der verfügbaren Dependency Injection-Frameworks ( Common Serivce Locator mit Unity und Spring.NET in den Sinn zu bringen) für eine robustere Implementierung mit erweiterter Funktionalität zu verwenden "Funktionen", die auch als " Service Locator Pattern" bezeichnet werden . Sie können es an Ihre Bedürfnisse anpassen.

    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. Verwenden Sie diese Schnittstellen zuletzt in Ihren anderen Assemblys, indem Sie die FindInterface-Methode aufrufen. Hier ist ein Beispiel für 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;
        }
    

    }

WICHTIGSTES: Testen, testen, testen Sie zwischen den einzelnen Änderungen.

Kevin McCormick
quelle
Ich habe das Beispiel hinzugefügt! :-)
Kevin McCormick
1
Diese PluginFinder-Klasse sieht verdächtig aus wie ein automatischer DI-Handler, den Sie selbst erstellen (mithilfe eines ServiceLocator-Musters), aber dies ist ansonsten ein guter Rat. Vielleicht ist es besser, das OP auf etwas wie Unity zu richten, da dies keine Probleme mit mehreren Implementierungen einer bestimmten Schnittstelle innerhalb der Bibliotheken hätte (StringColourer vs StringColourerWithHtmlWrapper oder was auch immer).
Ed James
@EdWoodcock Guter Punkt Ed, und ich kann nicht glauben, dass ich beim Schreiben nicht an das Service Locator-Muster gedacht habe. Der PluginFinder ist definitiv eine unausgereifte Implementierung und ein DI-Framework würde hier sicherlich funktionieren.
Kevin McCormick
Ich habe dir das Kopfgeld für die Mühe gegeben, aber wir werden diesen Weg nicht gehen. Die gemeinsame Nutzung einer Kerngruppe von Schnittstellen bedeutet, dass es uns nur gelungen ist, die Implementierungen zu entfernen, aber es gibt immer noch eine Bibliothek, die ein Bündel von wenig verwandten Schnittstellen enthält (die wie zuvor durch optionale Abhängigkeiten verbunden sind). Die Einrichtung ist jetzt viel komplizierter, was für so kleine Bibliotheken kaum von Vorteil ist. Die zusätzliche Komplexität könnte sich für gewaltige Projekte lohnen, aber nicht für diese.
Roman Starkov
@romkyns Also, welchen Weg nimmst du? Belassen Sie es wie es ist? :)
Max
5

Sie können Schnittstellen verwenden, die in einer zusätzlichen Bibliothek deklariert sind.

Versuchen Sie, einen Vertrag (Klasse über Schnittstelle) mithilfe einer Abhängigkeitsinjektion (MEF, Unity usw.) aufzulösen. Wenn nicht gefunden, geben Sie eine Nullinstanz zurück.
Überprüfen Sie dann, ob die Instanz null ist. In diesem Fall werden die zusätzlichen Funktionen nicht ausgeführt.

Dies ist besonders einfach mit MEF zu tun, da es das Lehrbuch dafür ist.

Es würde Ihnen erlauben, die Bibliotheken zu kompilieren, auf Kosten der Aufteilung in n + 1-DLLs.

HTH.

Louis Kottmann
quelle
Das hört sich fast richtig an - wenn es nicht nur diese eine zusätzliche DLL gäbe, die im Grunde genommen wie ein Haufen Skelette des ursprünglichen Packs von Dingen ist. Die Implementierungen sind alle aufgeteilt, aber es ist noch ein "Bündel von Skeletten" übrig. Ich nehme an, das hat einige Vorteile, aber ich bin nicht überzeugt, dass die Vorteile alle Kosten für diese bestimmte Reihe von Bibliotheken überwiegen ...
Roman Starkov
Darüber hinaus ist das Einbeziehen eines gesamten Frameworks ein Rückschritt. Diese Bibliothek hat in etwa die Größe eines dieser Frameworks und macht den Nutzen vollständig zunichte. Wenn überhaupt, würde ich nur ein wenig darüber nachdenken, ob eine Implementierung verfügbar ist, da es nur zwischen null und eins geben kann und keine externe Konfiguration erforderlich ist.
Roman Starkov
2

Ich dachte, ich würde die bestmögliche Option posten, die wir uns bisher ausgedacht haben, um zu sehen, was die Gedanken sind.

Grundsätzlich würden wir jede Komponente in eine Bibliothek mit null Referenzen aufteilen. Der gesamte Code, für den ein Verweis erforderlich ist, wird in einen #if/#endifBlock mit dem entsprechenden Namen eingefügt. Zum Beispiel würde der Code in CommandLineParserden Handles ConsoleColoredStringin platziert werden #if HAS_CONSOLE_COLORED_STRING.

Jede Lösung, die nur das CommandLineParsereinbeziehen möchte, kann dies problemlos tun, da es keine weiteren Abhängigkeiten gibt. Wenn die Lösung jedoch auch das ConsoleColoredStringProjekt enthält, hat der Programmierer jetzt die Möglichkeit:

  • fügen Sie einen Verweis auf CommandLineParserzuConsoleColoredString
  • Fügen Sie das HAS_CONSOLE_COLORED_STRINGDefine zur CommandLineParserProjektdatei hinzu.

Dies würde die relevante Funktionalität verfügbar machen.

Hiermit sind mehrere Probleme verbunden:

  • Dies ist eine reine Quelllösung. Jeder Verbraucher der Bibliothek muss ihn als Quellcode einbinden. Sie können nicht nur eine Binärdatei enthalten (dies ist jedoch für uns keine zwingende Voraussetzung).
  • Die Bibliothek Projektdatei der Bibliothek bekommt ein paar Lösung -spezifische Änderungen, und es ist nicht genau klar , wie sich diese Änderung auf SCM verpflichtet.

Eher nicht hübsch, aber das ist das Nächste, was wir uns einfallen lassen.

Eine weitere Überlegung bestand darin, Projektkonfigurationen zu verwenden, anstatt dass der Benutzer die Bibliotheksprojektdatei bearbeiten muss. In VS2010 ist dies jedoch absolut nicht möglich, da alle Projektkonfigurationen unerwünscht zur Lösung hinzugefügt werden .

Roman Starkov
quelle
1

Ich werde das Buch Brownfield Application Development in .Net empfehlen . Zwei direkt relevante Kapitel sind 8 und 9. Kapitel 8 befasst sich mit dem Relayering Ihrer Anwendung, während Kapitel 9 die Zähmung von Abhängigkeiten, die Umkehrung der Kontrolle und die Auswirkungen auf das Testen behandelt.

Tangurena
quelle
1

Vollständige Offenlegung, ich bin ein Java-Typ. Ich verstehe, dass Sie wahrscheinlich nicht nach den Technologien suchen, die ich hier erwähnen werde. Aber die Probleme sind die gleichen, vielleicht weist es Sie in die richtige Richtung.

In Java gibt es eine Reihe von Build-Systemen, die die Idee eines zentralisierten Artefakt-Repositorys unterstützen, das gebaute "Artefakte" beherbergt. Meines Wissens entspricht dies in etwa dem GAC in .NET (bitte führen Sie meine Unkenntnis aus, wenn es sich um eine gespannte Anaologie handelt). aber mehr als das, weil es verwendet wird, um zu jedem Zeitpunkt unabhängige, wiederholbare Builds zu erstellen.

Ein weiteres Feature, das unterstützt wird (zum Beispiel in Maven), ist die Idee einer OPTIONALEN Abhängigkeit, die dann von bestimmten Versionen oder Bereichen abhängt und möglicherweise transitive Abhängigkeiten ausschließt. Das klingt für mich nach dem, wonach Sie suchen, aber ich könnte mich irren. Schauen Sie sich diese Einführungsseite über das Abhängigkeitsmanagement von Maven mit einem Freund an, der Java kennt, und prüfen Sie, ob die Probleme bekannt sind. Auf diese Weise können Sie Ihre Anwendung erstellen und mit oder ohne diese Abhängigkeiten erstellen.

Es gibt auch Konstrukte, wenn Sie eine wirklich dynamische, steckbare Architektur benötigen. Eine Technologie, die versucht, diese Form der Laufzeitabhängigkeitsauflösung zu beheben, ist OSGI. Dies ist die Engine hinter dem Eclipse-Plugin-System . Sie werden sehen, dass es optionale Abhängigkeiten und einen minimalen / maximalen Versionsbereich unterstützen kann. Dieses Maß an Laufzeitmodularität stellt Sie und Ihre Entwicklung vor zahlreiche Einschränkungen. Die meisten Menschen kommen mit dem Grad an Modularität aus, den Maven bietet.

Eine andere mögliche Idee, die Sie prüfen könnten, ist die Verwendung eines Pipes and Filters-Architekturstils. Dies hat UNIX zu einem so langjährigen und erfolgreichen Ökosystem gemacht, das ein halbes Jahrhundert überlebt und sich weiterentwickelt hat. In diesem Artikel zu Pipes und Filtern in .NET finden Sie einige Vorschläge zur Implementierung dieser Art von Muster in Ihrem Framework.

cwash
quelle
0

Vielleicht ist das Buch "Large-scale C ++ Software Design" von John Lakos nützlich (natürlich C # und C ++ oder nicht dasselbe, aber Sie können nützliche Techniken aus dem Buch herausarbeiten).

Grundsätzlich müssen Sie die Funktionalität, die zwei oder mehr Bibliotheken verwendet, neu faktorisieren und in eine separate Komponente verschieben, die von diesen Bibliotheken abhängt. Verwenden Sie bei Bedarf Techniken wie opake Typen usw.

Kasper van den Berg
quelle