Au cours des années d'utilisation de C # /. NET pour un tas de projets internes, nous avons vu une bibliothèque se développer organiquement en une énorme liasse de choses. Cela s'appelle "Util", et je suis sûr que beaucoup d'entre vous ont vu une de ces bêtes dans votre carrière.
De nombreuses parties de cette bibliothèque sont très autonomes et pourraient être divisées en projets distincts (que nous aimerions ouvrir). Mais il y a un problème majeur qui doit être résolu avant de pouvoir les publier sous forme de bibliothèques distinctes. Fondamentalement, il y a beaucoup, beaucoup de cas de ce que je pourrais appeler des "dépendances facultatives" entre ces bibliothèques.
Pour mieux expliquer cela, considérez certains des modules qui sont de bons candidats pour devenir des bibliothèques autonomes. CommandLineParser
sert à analyser les lignes de commande. XmlClassify
sert à sérialiser des classes en XML. PostBuildCheck
effectue des vérifications sur l'assembly compilé et signale une erreur de compilation en cas d'échec. ConsoleColoredString
est une bibliothèque de littéraux de chaîne colorés. Lingo
sert à traduire les interfaces utilisateur.
Chacune de ces bibliothèques peut être utilisée de manière totalement autonome, mais si elles sont utilisées ensemble, il existe des fonctionnalités supplémentaires utiles. Par exemple, les deux CommandLineParser
et XmlClassify
exposent la fonctionnalité de vérification post-génération, ce qui nécessite PostBuildCheck
. De même, la CommandLineParser
documentation de l'option permet d'être fournie à l'aide des littéraux de chaîne de couleur, exigeant ConsoleColoredString
, et il prend en charge la documentation traduisible via Lingo
.
La principale distinction est donc qu'il s'agit de fonctionnalités facultatives . On peut utiliser un analyseur de ligne de commande avec des chaînes simples et non colorées, sans traduire la documentation ni effectuer de vérifications après la construction. Ou on pourrait rendre la documentation traduisible mais toujours incolore. Ou à la fois coloré et traduisible. Etc.
En parcourant cette bibliothèque "Util", je constate que presque toutes les bibliothèques potentiellement séparables ont des fonctionnalités optionnelles qui les lient à d'autres bibliothèques. Si je devais réellement avoir besoin de ces bibliothèques en tant que dépendances, cette liasse de choses n'est pas vraiment démêlée du tout: vous auriez toujours besoin de toutes les bibliothèques si vous voulez en utiliser une seule.
Existe-t-il des approches établies pour gérer ces dépendances facultatives dans .NET?
la source
Réponses:
Refactorise lentement.
Attendez-vous à ce que cela prenne un certain temps et peut se produire sur plusieurs itérations avant de pouvoir supprimer complètement votre ensemble Utils .
Approche générale:
Prenez d'abord un peu de temps et réfléchissez à la façon dont vous souhaitez que ces assemblages utilitaires se présentent lorsque vous avez terminé. Ne vous inquiétez pas trop de votre code existant, pensez à l'objectif final. Par exemple, vous souhaiterez peut-être:
Créez des projets vides pour chacun de ces projets, et créez des références de projet appropriées (UI référence Core, UI.WinForms référence UI), etc.
Déplacez l'un des fruits bas (classes ou méthodes qui ne souffrent pas des problèmes de dépendance) de votre assembly Utils vers les nouveaux assemblys cibles.
Obtenez une copie de NDepend et du refactoring de Martin Fowler pour commencer à analyser votre assemblage Utils pour commencer à travailler sur les plus difficiles. Deux techniques qui vous seront utiles:
Gestion des interfaces facultatives
Soit un assembly fait référence à un autre assembly, soit il ne le fait pas. La seule autre façon d'utiliser des fonctionnalités dans un assembly non lié consiste à utiliser une interface chargée via la réflexion d'une classe commune. L'inconvénient est que votre assemblage principal devra contenir des interfaces pour toutes les fonctionnalités partagées, mais l'inconvénient est que vous pouvez déployer vos utilitaires selon vos besoins sans la "liasse" de fichiers DLL en fonction de chaque scénario de déploiement. Voici comment je gérerais ce cas, en utilisant la chaîne colorée comme exemple:
Tout d'abord, définissez les interfaces communes dans votre assemblage principal:
Par exemple, l'
IStringColorer
interface ressemblerait à:Ensuite, implémentez l'interface dans l'assemblage avec la fonction. Par exemple, la
StringColorer
classe ressemblerait à:Créez une
PluginFinder
classe (ou peut-être InterfaceFinder est un meilleur nom dans ce cas) qui peut trouver des interfaces à partir de fichiers DLL dans le dossier en cours. Voici un exemple simpliste. Selon les conseils de @ EdWoodcock (et je suis d'accord), lorsque vos projets se développeront, je suggérerais d'utiliser l'un des cadres d'injection de dépendance disponibles ( Common Serivce Locator avec Unity et Spring.NET me viennent à l'esprit) pour une implémentation plus robuste avec des fonctionnalités avancées. qui disposent de capacités ", autrement connu comme le modèle de localisateur de service . Vous pouvez le modifier selon vos besoins.Enfin, utilisez ces interfaces dans vos autres assemblys en appelant la méthode FindInterface. Voici un exemple de
CommandLineParser
:}
LE PLUS IMPORTANT: testez, testez, testez entre chaque changement.
la source
Vous pouvez utiliser des interfaces déclarées dans une bibliothèque supplémentaire.
Essayez de résoudre un contrat (classe via interface) en utilisant une injection de dépendance (MEF, Unity, etc.). S'il n'est pas trouvé, définissez-le pour renvoyer une instance nulle.
Vérifiez ensuite si l'instance est nulle, auquel cas vous ne faites pas les fonctionnalités supplémentaires.
C'est particulièrement facile à faire avec MEF, car c'est l'utilisation des manuels pour cela.
Cela vous permettrait de compiler les bibliothèques, au prix de les diviser en n + 1 dll.
HTH.
la source
Je pensais que je publierais l'option la plus viable que nous ayons trouvée jusqu'à présent, pour voir quelles sont les pensées.
Fondamentalement, nous séparerions chaque composant dans une bibliothèque avec zéro référence; tout le code qui nécessite une référence sera placé dans un
#if/#endif
bloc avec le nom approprié. Par exemple, le codeCommandLineParser
qui gèreConsoleColoredString
s serait placé dans#if HAS_CONSOLE_COLORED_STRING
.Toute solution qui souhaite inclure uniquement le
CommandLineParser
peut facilement le faire, car il n'y a plus de dépendances. Cependant, si la solution inclut également leConsoleColoredString
projet, le programmeur a désormais la possibilité de:CommandLineParser
àConsoleColoredString
HAS_CONSOLE_COLORED_STRING
définition auCommandLineParser
fichier de projet.Cela rendrait les fonctionnalités pertinentes disponibles.
Il y a plusieurs problèmes avec ceci:
Plutôt pas joli, mais c'est le plus proche que nous ayons trouvé.
Une autre idée que nous avons envisagée était d'utiliser des configurations de projet plutôt que d'exiger que l'utilisateur modifie le fichier de projet de bibliothèque. Mais cela est absolument irréalisable dans VS2010 car il ajoute toutes les configurations de projet à la solution de manière indésirable .
la source
Je vais recommander le livre Brownfield Application Development in .Net . Deux chapitres directement pertinents sont 8 et 9. Le chapitre 8 parle de relayer votre application, tandis que le chapitre 9 parle d'apprivoiser les dépendances, l'inversion du contrôle et l'impact que cela a sur les tests.
la source
Divulgation complète, je suis un gars Java. Je comprends donc que vous ne cherchez probablement pas les technologies que je mentionnerai ici. Mais les problèmes sont les mêmes, alors peut-être que cela vous indiquera la bonne direction.
En Java, il existe un certain nombre de systèmes de construction qui prennent en charge l'idée d'un référentiel d'artefacts centralisé qui héberge des "artefacts" construits - à ma connaissance, cela est quelque peu analogue au GAC dans .NET (veuillez excuser mon ignorance s'il s'agit d'une anaologie tendue) mais plus que cela, car il est utilisé pour produire des versions répétables indépendantes à tout moment.
Quoi qu'il en soit, une autre fonctionnalité prise en charge (dans Maven, par exemple) est l'idée d'une dépendance OPTIONNELLE, puis en fonction de versions ou de plages spécifiques et potentiellement en excluant les dépendances transitives. Cela me semble être ce que vous recherchez, mais je peux me tromper. Jetez un œil à cette page d'introduction sur la gestion des dépendances de Maven avec un ami qui connaît Java et voyez si les problèmes vous semblent familiers. Cela vous permettra de construire votre application et de la construire avec ou sans avoir ces dépendances disponibles.
Il existe également des constructions si vous avez besoin d'une architecture véritablement dynamique et enfichable; OSGI est une technologie qui essaie de résoudre ce type de résolution de dépendance à l'exécution. C'est le moteur derrière le système de plugins d'Eclipse . Vous verrez qu'il peut prendre en charge des dépendances facultatives et une plage de versions minimale / maximale. Ce niveau de modularité d'exécution vous impose un bon nombre de contraintes et de développement. La plupart des gens peuvent s'en sortir avec le degré de modularité fourni par Maven.
Une autre idée possible que vous pourriez étudier et qui pourrait être plus simple à implémenter serait d'utiliser un style d'architecture Pipes and Filters. C'est en grande partie ce qui a fait d'UNIX un écosystème aussi ancien et prospère qui a survécu et évolué pendant un demi-siècle. Jetez un œil à cet article sur les tuyaux et filtres dans .NET pour quelques idées sur la façon d'implémenter ce type de modèle dans votre framework.
la source
Peut-être que le livre "Large-scale C ++ software design" de John Lakos est utile (bien sûr C # et C ++ ou pas le même, mais vous pouvez distiller des techniques utiles du livre).
Fondamentalement, reformatez et déplacez la fonctionnalité qui utilise deux bibliothèques ou plus dans un composant distinct qui dépend de ces bibliothèques. Si nécessaire, utilisez des techniques telles que les types opaques, etc.
la source