Question Des inconvénients spécifiques pour beaucoup de petits ensembles?


Je planifie des travaux pour introduire l'injection de dépendance dans ce qui est actuellement une grande bibliothèque monolithique, dans le but de rendre la bibliothèque plus facile à tester, plus facile à comprendre et éventuellement plus flexible.

J'ai décidé d'utiliser NInject, et j'aime beaucoup la devise de Nate: «faites une chose, faites-le bien» (paraphrasée), et cela semble aller particulièrement bien dans le contexte de l'ID.

Ce que je me demandais maintenant, c'est si je devais diviser ce qui est actuellement un seul assemblage unique en plusieurs assemblages plus petits avec des ensembles de fonctionnalités disjoints. Certains de ces assemblages plus petits auront des interdépendances, mais loin de tous, car l'architecture du code est déjà assez peu couplée.

Notez que ces ensembles de fonctionnalités ne sont pas triviaux et petits pour eux-mêmes non plus ... cela englobe des communications client / serveur, la sérialisation, des types de collection personnalisés, des abstractions de fichiers-IO, des bibliothèques de routine communes, la journalisation standard, etc.

Je vois qu'une question précédente: Quoi de mieux, beaucoup de petites assemblées, ou une grande assemblée? ce genre de réponses à ce problème, mais avec ce qui semble être une granularité encore plus fine que cela, ce qui me fait me demander si les réponses y s'appliquent toujours dans ce cas?

En outre, dans les différentes questions qui se rapprochent de ce sujet, une réponse commune est que le fait d'avoir «trop» d'assemblées a provoqué des «douleurs» et des «problèmes» non spécifiés. J'aimerais vraiment savoir concrètement quels pourraient être les inconvénients de cette approche.

Je suis d'accord pour dire que l'ajout de 8 assemblées alors que seulement 1 était nécessaire était un peu pénible, mais le fait de devoir inclure une grande bibliothèque monolithique pour chaque application n'est pas non plus idéal ... une fois, donc j'ai très peu de sympathie pour cet argument (même si je me plaindrais probablement avec tout le monde au début).

Addenda:
Jusqu'à présent, je n'ai vu aucune raison impérieuse contre de plus petites assemblées, alors je pense que je continuerai pour l'instant comme s'il s'agissait d'un problème. Si quelqu'un peut trouver de bonnes raisons solides avec des faits vérifiables pour les appuyer, je serais très intéressé à en entendre parler. (Je vais ajouter une prime dès que possible pour augmenter la visibilité)

MODIFIER: Déplacement de l'analyse de performance et des résultats dans une réponse séparée (voir ci-dessous).


18
2017-07-28 05:00


origine


Réponses:


Je vais vous donner un exemple concret où l'utilisation de nombreux (très) petits assemblages a produit .NET DLL Hell.

Au travail, nous avons un grand cadre interne qui est long dans la dent (.Net 1.1). Outre le code de plomberie habituel de type framework (y compris la journalisation, le workflow, la mise en file d'attente, etc.), il existait également diverses entités d'accès aux bases de données encapsulées, jeux de données typés et autres codes de logique métier. Je n'étais pas là pour le développement initial et la maintenance ultérieure de ce framework, mais j'ai hérité de son utilisation. Comme je l'ai mentionné, toute cette structure a généré de nombreuses petites DLL. Et, quand je dis beaucoup, nous parlons de plus de 100 - pas de la 8 gérable que vous avez mentionnée. En outre, les assemblages étaient tous signés, versionnés et publiés dans le GAC.

Ainsi, quelques années plus tard et un certain nombre de cycles de maintenance se sont déroulés rapidement, et les dépendances entre les DLL et les applications prises en charge ont fait des ravages. Sur chaque machine de production se trouve une énorme section de redirection d'assembly dans le fichier machine.config qui garantit que "l'assemblage correct" est chargé par Fusion, quel que soit l'assemblage demandé. Cette difficulté découle de la difficulté rencontrée lors de la reconstruction de chaque structure dépendante et de chaque assemblage d’applications qui dépendait de celui qui avait été modifié ou mis à niveau. De grandes douleurs (généralement) ont été prises pour s'assurer qu'aucun changement de rupture n'a été apporté aux assemblages lorsqu'ils ont été modifiés. Les assemblys ont été reconstruits et une nouvelle entrée ou mise à jour a été faite dans le fichier machine.config.

Voilà, je vais faire une pause pour écouter le son d'un énorme gémissement collectif et du souffle!

Ce scénario particulier est le poster-affiche pour quoi ne pas faire. En effet, dans cette situation, vous vous retrouvez dans une situation complètement incertain. Je me souviens qu'il m'a fallu 2 jours pour configurer mon ordinateur pour le développement avec ce cadre lorsque j'ai commencé à travailler avec lui - résoudre les différences entre mon GAC et un environnement d'exécution GAC, les redirections d'assembly machine.config, références incorrectes ou, plus probablement, conflit de versions dû au référencement direct des composants A et B, mais le composant B a référencé le composant A, mais une version différente de la référence directe de mon application. Vous avez eu l'idée.

Le vrai problème avec ce scénario spécifique est que le contenu de l’assemblage était beaucoup trop granulaire. Et, en fin de compte, c'est ce qui a causé le réseau enchevêtré d'interdépendances. Je pense que les architectes initiaux pensaient que cela créerait un système de code hautement maintenable - il suffirait de reconstruire de très petits changements dans les composants du système. En fait, le contraire était vrai. En outre, pour certaines des autres réponses affichées ici, lorsque vous accédez à ce nombre d’assemblages, le chargement d’une tonne d’assemblages entraîne une baisse de performances - certainement pendant la résolution, et je suppose que, même si je n’ai aucune preuve empirique, le runtime peut souffrir dans certaines situations extrêmes, en particulier lorsque la réflexion peut entrer en jeu - peut être erroné sur ce point.

Vous pensez que je serais méprisé, mais je pense qu'il existe des séparations logiques pour les assemblages - et quand je dis "assemblages" ici, je suppose un assemblage par DLL. Tout ce qui se résume à cela sont les interdépendances. Si j'ai un assemblage A qui dépend de l'assemblage B, je me demande toujours si j'aurai jamais besoin de faire référence à l'assemblage B sans assemblage A. Ou bien, cette séparation présente-t-elle un avantage. Regarder comment les assemblages sont référencés est généralement un bon indicateur. Si vous deviez diviser votre grande bibliothèque en assemblys A, B, C, D et E. Si vous avez référencé l'assemblage 90% du temps et à cause de cela, vous deviez toujours référencer les assemblages B et C car A en dépendait , alors c'est probablement une meilleure idée que les assemblages A, B et C soient combinés, à moins qu'il y ait un argument vraiment convaincant pour leur permettre de rester séparés. Enterprise Library en est un exemple classique, où vous devez presque toujours référencer 3 assemblages pour utiliser une seule facette de la bibliothèque - dans le cas d’Enterprise Library, la possibilité de créer des fonctionnalités et du code de base. la réutilisation est la raison de son architecture.

Regarder l'architecture est une autre bonne directive. Si vous avez une belle architecture empilée proprement, où vos dépendances d'assemblage se présentent sous la forme d'une pile, dites "vertical", par opposition à un "Web", qui commence à se former lorsque vous avez des dépendances dans tous les sens. sur les limites fonctionnelles est logique. Sinon, cherchez à faire tourner les choses en un seul ou cherchez à ré-architecturer.

De toute façon, bonne chance!


14
2017-07-31 21:46



Étant donné que l’analyse de la performance est devenue un peu plus longue que prévu, je la mets dans sa propre réponse. J'accepterai la réponse de Peter comme officielle, même si elle manquait de mesures, car cela m'a beaucoup aidé à effectuer les mesures moi-même et cela m'a beaucoup inspiré pour ce qui pourrait valoir la peine d'être mesuré.

Une analyse:
Les inconvénients concrets mentionnés jusqu’à présent semblent se concentrer sur la performance d’un type d’autre mais les données quantitatives réelles manquent, j’ai fait quelques mesures sur les points suivants:

  • Temps de chargement de la solution dans l'EDI
  • Temps de compilation dans l'EDI
  • Temps de chargement de l'assemblage (temps nécessaire au chargement de l'application)
  • Optimisations de code perdues (le temps nécessaire à l'exécution d'un algorithme)

Cette analyse ignore complètement la «qualité du design», que certaines personnes ont mentionnée dans leurs réponses, car je ne considère pas la qualité comme une variable dans ce compromis. Je suppose que le développeur laissera avant tout que son implémentation soit guidée par le désir d'obtenir le meilleur design possible. Le compromis est ici de savoir si cela vaut la peine d’agréger la fonctionnalité dans des assemblages plus grands que ce que le design appelle strictement, pour des raisons de (certaines mesures).

Structure d'application:
L'application que j'ai créée est quelque peu abstraite car j'avais besoin d'un grand nombre de solutions et de projets à tester, alors j'ai écrit du code pour les générer tous pour moi.

L'application contient 1000 classes, regroupées en 200 ensembles de 5 classes qui héritent les unes des autres. Les classes sont nommées Axxx, Bxxx, Cxxx, Dxxx et Exxx. Les classes A sont complètement abstraites, B-D sont partiellement abstraites, remplaçant l'une des méthodes de A chacune et E est concrète. Les méthodes sont implémentées de sorte qu'un appel d'une méthode sur des instances de E effectuera plusieurs appels dans la chaîne hiérarchique. Tous les corps de méthodes sont suffisamment simples pour qu'ils soient théoriquement tous alignés.

Ces classes ont été réparties sur 8 assemblages différents selon 2 dimensions:

  • Nombre d'assemblages: 10, 20, 50, 100
  • Sens de coupe: dans la hiérarchie d'héritage (aucun des éléments A-E ne se trouve jamais dans le même assemblage) et dans la hiérarchie d'héritage

Les mesures ne sont pas toutes exactement mesurées. certains ont été effectués par chronomètre et ont une plus grande marge d'erreur. Les mesures prises sont:

  • Ouvrir la solution dans VS2008 (chronomètre)
  • Compiler la solution (chronomètre)
  • Dans IDE: Temps entre le début et la première ligne de code exécutée (chronomètre)
  • Dans IDE: temps d'instancier l'un des Exxx pour chacun des 200 groupes de l'EDI (dans le code)
  • Dans IDE: Temps d'exécution de 100 000 appels sur chaque Exxx dans l'EDI (dans le code)
  • Les trois dernières mesures "In IDE", mais à partir de l'invite utilisant la version "Release"

Résultats:
Ouvrir la solution dans VS2008

                               ----- in the IDE ------   ----- from prompt -----
Cut    Asm#   Open   Compile   Start   new()   Execute   Start   new()   Execute
Across   10    ~1s     ~2-3s       -   0.150    17.022       -   0.139    13.909
         20    ~1s       ~6s       -   0.152    17.753       -   0.132    13.997
         50    ~3s       15s   ~0.3s   0.153    17.119    0.2s   0.131    14.481
        100    ~6s       37s   ~0.5s   0.150    18.041    0.3s   0.132    14.478

Along    10    ~1s     ~2-3s       -   0.155    17.967       -   0.067    13.297
         20    ~1s       ~4s       -   0.145    17.318       -   0.065    13.268
         50    ~3s       12s   ~0.2s   0.146    17.888    0.2s   0.067    13.391
        100    ~6s       29s   ~0.5s   0.149    17.990    0.3s   0.067    13.415

Observations:

  • Le nombre d'assemblages (mais pas le sens de coupe) semble avoir un impact à peu près linéaire sur le temps nécessaire pour ouvrir la solution. Cela ne me semble pas surprenant.
  • À environ 6 secondes, le temps nécessaire pour ouvrir la solution ne me semble pas être un argument pour limiter le nombre d'assemblages. (Je n'ai pas mesuré si l'association du contrôle de la source avait un impact majeur sur cette période).
  • Le temps de compilation augmente un peu plus que linéairement dans cette mesure. J'imagine que la plupart de ces problèmes sont dus à la surcharge par assemblage de la compilation, et non aux résolutions de symboles entre assemblages. Je m'attendrais à ce que les assemblages moins triviaux évoluent mieux sur cet axe. Malgré cela, je ne trouve pas personnellement que le délai de compilation des années 30 constitue un argument contre la division, en particulier lorsque certains les assemblages devront être recompilés.
  • Il semble y avoir une augmentation à peine mesurable, mais notable du temps de démarrage. La première chose que fait l’application est d’afficher une ligne sur la console, l’heure de début correspond au temps nécessaire pour que cette ligne apparaisse dès le début de l’exécution .
  • Il est intéressant de noter qu’en dehors de l’assemblage IDE, le chargement est (très légèrement) plus efficace que dans l’EDI. Cela a probablement quelque chose à voir avec l'effort de joindre le débogueur, ou certains autres.
  • Notez également que le redémarrage de l'application en dehors de l'EDI a encore réduit le temps de démarrage dans le pire des cas. Il peut y avoir des scénarios où 0,3 pour le démarrage est inacceptable, mais je ne peux pas imaginer que cela importera dans beaucoup des endroits.
  • L'initialisation et le temps d'exécution dans l'EDI sont solides, quelle que soit la répartition de l'assemblage. Cela peut être dû au fait qu'il doit déboguer, ce qui facilite la résolution des symboles dans les assemblages.
  • En dehors de l’IDE, cette stabilité continue, avec une réserve nombre d'assemblages n'a pas d'importance pour l'exécution, mais lors de la coupe à travers la hiérarchie d'héritage, le temps d'exécution est une fraction pire que lors de la coupe le long de. Notez que la différence me semble trop petite pour être systémique; C'est probablement un temps supplémentaire qu'il faut à l'exécution une fois pour déterminer comment faire les mêmes optimisations ... Franchement, même si je pouvais approfondir cette question, les différences sont si faibles que je ne suis pas trop enclin à m'inquiéter.

Ainsi, de tout cela, il apparaît que le fardeau de plusieurs assemblages est principalement supporté par le développeur, puis principalement sous la forme de temps de compilation. Comme je l’ai déjà dit, ces projets étaient si simples qu’il fallait moins d’une seconde pour les compiler, ce qui a entraîné la domination de la surcharge de compilation par assemblage. J'imagine que la compilation de sous-secondes d'assemblage à travers un grand nombre d'assemblages est une indication forte que ces assemblages ont été divisés plus loin que ce qui est raisonnable. En outre, lors de l'utilisation d'assemblys précompilés, l'argument majeur du développeur contre le fractionnement (temps de compilation) disparaîtrait également.

Dans ces mesures, je peux voir très peu de preuves, voire aucune, concernant la division en assemblages plus petits pour des raisons de performances d'exécution. La seule chose à surveiller (dans une certaine mesure) consiste à éviter de traverser l'héritage autant que possible. J'imagine que la plupart des conceptions sensées limiteraient cela de toute façon, car l'héritage ne se produirait généralement que dans une zone fonctionnelle, qui se retrouverait normalement dans un seul assemblage.


28
2017-08-02 05:21



Il y a une légère baisse de performances lors du chargement de chaque assemblage (encore plus si elles sont signées), ce qui explique pourquoi les grappes les plus utilisées sont regroupées dans le même assemblage. Je ne pense pas qu'il y ait une grosse surcharge une fois que les choses sont chargées (bien qu'il puisse y avoir des choses d'optimisation statique que le JIT pourrait avoir plus de difficulté à effectuer lors du franchissement des limites d'un assemblage).

L'approche que j'essaie de prendre est la suivante: les espaces de noms sont pour l'organisation logique. Les assemblys sont destinés à regrouper des classes / espaces de noms qui doivent être physiquement utilisés ensemble. C'est à dire. Si vous ne vous attendez pas à vouloir ClassA et non ClassB (ou vice versa), ils appartiennent au même assemblage.


3
2017-07-28 05:08



les monstres monolithiques rendent plus coûteuse la réutilisation d’une partie du code pour un travail ultérieur. et conduit à un couplage (souvent explicite) entre des classes qui n'ont pas besoin d'être couplées, ce qui entraîne un coût de maintenance plus élevé puisque les tests et la correction des erreurs seront de ce fait plus difficiles.

L'inconvénient de nombreux projets réside dans le fait que la compilation (au moins dans VS) prend un certain temps avant de se comparer à peu de projets.


2
2017-07-28 05:19



Le facteur le plus important dans votre organisation d'assemblage doit être votre graphique de dépendance, tant au niveau de la classe que de l'assemblage.

Les assemblages ne doivent pas avoir de références circulaires. Cela devrait être assez évident pour commencer.

Les classes les plus dépendantes les unes des autres doivent se trouver dans un seul assemblage.

Si une classe A dépend de la classe B et que B ne dépend pas directement de A, il est peu probable qu’elle soit utilisée en dehors de A, alors elles doivent partager un assemblage.

Vous pouvez également utiliser des assemblys pour imposer la séparation des préoccupations - le fait d'avoir votre code GUI dans un assemblage alors que votre logique métier réside dans un autre assurera un certain niveau d'application de votre logique métier agnostique à votre interface graphique.

La séparation des assemblages en fonction de l'endroit où le code sera exécuté est un autre point à prendre en compte: le code commun entre les exécutables doit (généralement) se trouver dans un assemblage commun, au lieu de se référer directement à un autre.

L'une des choses les plus importantes pour lesquelles vous pouvez utiliser des assemblys consiste peut-être à différencier les API publiques des objets utilisés en interne pour permettre aux API publiques de fonctionner. En plaçant une API dans un assembly séparé, vous pouvez appliquer l'opacité de son API.


2
2017-08-01 08:11



Je suppose que si vous ne parlez que d'une douzaine, vous devriez aller bien. Je travaille sur une application avec plus de 100 assemblages, et c'est très douloureux.

Si vous n'avez pas de gestion des dépendances - sachant ce qui va se briser si vous modifiez l'assemblage X, vous êtes en difficulté.

Un problème «intéressant» que j'ai rencontré concerne l'assemblage A faisant référence aux assemblages B et C, et B référençant V1 de l'assemblage D, tandis que C fait référence à V2 de l'assemblage D. ('Twisted diamond' serait un bon nom)

Si vous voulez avoir une construction automatisée, vous allez vous amuser à maintenir le script de construction (qui devra être construit dans l'ordre inverse des dépendances), ou bien avoir une solution unique pour les gouverner tous, ce qui sera presque impossible. à utiliser dans Visual Studio si vous avez beaucoup d'assemblages.

MODIFIER Je pense que la réponse à votre question dépend beaucoup de la sémantique de vos assemblées. Les différentes applications sont-elles susceptibles de partager un assemblage? Voulez-vous pouvoir mettre à jour les assemblages pour les deux applications séparément? Avez-vous l'intention d'utiliser le GAC? Ou copier les assemblées à côté des exécutables?


1
2017-07-28 05:42



Personnellement, j'aime l'approche monolithique.

Mais parfois, vous ne pouvez pas aider à créer plus d'assemblées. .NET Remoting est normalement responsable de cela, lorsque vous avez besoin d'un assemblage d'interface commun.

Je ne suis pas sûr de la lourdeur du chargement d’un assemblage. (peut-être que quelqu'un peut nous éclairer)


0
2017-07-28 05:08