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. CommandLineParser
dient zum Parsen von Befehlszeilen. XmlClassify
dient zum Serialisieren von Klassen nach XML. PostBuildCheck
Überprüft die kompilierte Assembly und meldet einen Kompilierungsfehler, wenn sie fehlschlägt. ConsoleColoredString
ist eine Bibliothek für farbige Stringliterale. Lingo
dient 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 CommandLineParser
und XmlClassify
die erforderlichen Funktionen für die Überprüfung nach der Erstellung bereitstellen PostBuildCheck
. Ebenso können mit der CommandLineParser
Option 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?
quelle
Antworten:
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:
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:
Erstellen Sie leere Projekte für jedes dieser Projekte und erstellen Sie entsprechende Projektreferenzen (Benutzeroberflächenreferenzen Core, Benutzeroberfläche.WinForms-Referenz-Benutzeroberfläche) usw.
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.
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:
Definieren Sie zunächst die allgemeinen Schnittstellen in Ihrer Kernbaugruppe:
Zum Beispiel
IStringColorer
würde die Schnittstelle so aussehen:Implementieren Sie dann die Schnittstelle in der Assembly mit dem Feature. Zum Beispiel
StringColorer
würde die Klasse so aussehen:Erstellen Sie eine
PluginFinder
Klasse (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.Verwenden Sie diese Schnittstellen zuletzt in Ihren anderen Assemblys, indem Sie die FindInterface-Methode aufrufen. Hier ist ein Beispiel für
CommandLineParser
:}
WICHTIGSTES: Testen, testen, testen Sie zwischen den einzelnen Änderungen.
quelle
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.
quelle
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/#endif
Block mit dem entsprechenden Namen eingefügt. Zum Beispiel würde der Code inCommandLineParser
den HandlesConsoleColoredString
in platziert werden#if HAS_CONSOLE_COLORED_STRING
.Jede Lösung, die nur das
CommandLineParser
einbeziehen möchte, kann dies problemlos tun, da es keine weiteren Abhängigkeiten gibt. Wenn die Lösung jedoch auch dasConsoleColoredString
Projekt enthält, hat der Programmierer jetzt die Möglichkeit:CommandLineParser
zuConsoleColoredString
HAS_CONSOLE_COLORED_STRING
Define zurCommandLineParser
Projektdatei hinzu.Dies würde die relevante Funktionalität verfügbar machen.
Hiermit sind mehrere Probleme verbunden:
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 .
quelle
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.
quelle
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.
quelle
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.
quelle