Comment éviter les interfaces bavardes

10

Contexte: Je conçois une application serveur et crée des DLL distinctes pour différents sous-systèmes. Pour simplifier les choses, disons que j'ai deux sous-systèmes: 1) Users2)Projects

L'interface publique des utilisateurs a une méthode comme:

IEnumerable<User> GetUser(int id);

Et l'interface publique de Projects a une méthode comme:

IEnumerable<User> GetProjectUsers(int projectId);

Ainsi, par exemple, lorsque nous devons afficher les utilisateurs pour un certain projet, nous pouvons appeler GetProjectUserset cela donnera des objets avec suffisamment d'informations pour les afficher dans une grille de données ou similaire.

Problème: Idéalement, le Projectssous - système ne devrait pas également stocker les informations utilisateur et il devrait simplement stocker les identifiants des utilisateurs participant à un projet. Afin de servir le GetProjectUsers, il doit appeler GetUserle Userssystème pour chaque identifiant utilisateur stocké dans sa propre base de données. Cependant, cela nécessite un grand nombre d' GetUserappels séparés , ce qui entraîne de nombreuses requêtes SQL séparées à l'intérieur du Usersous - système. Je n'ai pas vraiment testé cela, mais avoir cette conception bavarde affectera l'évolutivité du système.

Si je mets de côté la séparation des sous-systèmes, je pourrais stocker toutes les informations dans un seul schéma accessible par les deux systèmes et Projectspourrait simplement faire un JOINpour obtenir tous les utilisateurs du projet dans une seule requête. Projectsaurait également besoin de savoir comment générer des Userobjets à partir des résultats de la requête. Mais cela rompt la séparation qui présente de nombreux avantages.

Question: Quelqu'un peut-il suggérer un moyen de maintenir la séparation tout en évitant tous ces GetUserappels individuels pendant GetProjectUsers?


Par exemple, je pensais que les utilisateurs pouvaient donner aux systèmes externes la possibilité de «baliser» les utilisateurs avec une paire étiquette-valeur et de demander aux utilisateurs avec une certaine valeur, par exemple:

void AddUserTag(int userId, string tag, string value);
IEnumerable<User> GetUsersByTag(string tag, string value);

Ensuite, le système Projets pourrait marquer chaque utilisateur au fur et à mesure qu'il est ajouté au projet:

AddUserTag(userId,"project id", myProjectId.ToString());

et pendant GetProjectUsers, il pourrait demander à tous les utilisateurs du projet en un seul appel:

var projectUsers = usersService.GetUsersByTag("project id", myProjectId.ToString());

La partie dont je ne suis pas sûr est la suivante: oui, les utilisateurs sont indépendants des projets, mais en réalité, les informations sur l'appartenance au projet sont stockées dans le système des utilisateurs, pas dans les projets. Je ne me sens tout simplement pas naturel, alors j'essaie de déterminer s'il y a un gros inconvénient ici qui me manque.

Eren Ersönmez
la source

Réponses:

10

Ce qui manque dans votre système, c'est le cache.

Vous dites:

Cependant, cela nécessite un grand nombre d' GetUserappels séparés , ce qui entraîne de nombreuses requêtes SQL séparées à l'intérieur du Usersous - système.

Le nombre d'appels à une méthode ne doit pas nécessairement être le même que le nombre de requêtes SQL. Vous obtenez les informations sur l'utilisateur une fois, pourquoi vous demande les mêmes informations à nouveau si elle n'a pas changé? Très probablement, vous pouvez même mettre en cache tous les utilisateurs en mémoire, ce qui entraînerait zéro requête SQL (sauf si un utilisateur change).

D'un autre côté, en faisant en sorte que le Projectssous-système interroge à la fois les projets et les utilisateurs avec un INNER JOIN, vous introduisez un problème supplémentaire: vous interrogez la même information à deux endroits différents de votre code, ce qui rend l'invalidation du cache extrêmement difficile. En conséquence:

  • Soit vous n'introduirez pas le cache du tout plus tard,

  • Ou vous passerez des semaines ou des mois à étudier ce qui devrait être invalidé lorsqu'un élément d'information change,

  • Ou vous ajouterez l'invalidation du cache dans des emplacements simples, oubliant les autres et entraînant des bogues difficiles à trouver.


En relisant votre question, je remarque un mot-clé que j'ai manqué la première fois: l' évolutivité . En règle générale, vous pouvez suivre le modèle suivant:

  1. Demandez-vous si le système est lent (c.-à-d. Qu'il viole une exigence non fonctionnelle de performance, ou est simplement un cauchemar à utiliser).

    Si le système n'est pas lent, ne vous souciez pas des performances. Vous vous souciez du code propre, de la lisibilité, de la maintenabilité, des tests, de la couverture des succursales, de la conception propre, de la documentation détaillée et facile à comprendre, des bons commentaires sur le code.

  2. Si oui, recherchez le goulot d'étranglement. Vous faites cela non pas en devinant, mais en profilant . En profilant, vous déterminez l'emplacement exact du goulot d'étranglement (étant donné que lorsque vous devinez , vous pouvez presque à chaque fois vous tromper), et pouvez maintenant vous concentrer sur cette partie du code.

  3. Une fois le goulot d'étranglement trouvé, recherchez des solutions. Vous faites cela en devinant, en comparant, en profilant, en écrivant des alternatives, en comprenant les optimisations du compilateur, en comprenant les optimisations qui vous appartiennent, en posant des questions sur Stack Overflow et en passant à des langages de bas niveau (y compris Assembleur, si nécessaire).

Quel est le problème réel avec le Projectssous-système demandant des informations au Userssous-système?

Le futur problème d'évolutivité futur? Ce n'est pas un problème. L'évolutivité peut devenir un cauchemar si vous commencez à tout fusionner en une seule solution monolithique ou à rechercher les mêmes données à partir de plusieurs emplacements (comme expliqué ci-dessous, en raison de la difficulté à introduire le cache).

S'il existe déjà un problème de performances notable, à l'étape 2, recherchez le goulot d'étranglement.

S'il apparaît qu'en effet, le goulot d'étranglement existe et est dû au fait que les Projectsdemandes d'utilisateurs via le Userssous-système (et se situent au niveau de l'interrogation de la base de données), alors seulement vous devez rechercher une alternative.

L'alternative la plus courante serait d'implémenter la mise en cache, réduisant considérablement le nombre de requêtes. Si vous êtes dans une situation où la mise en cache n'aide pas, un profilage supplémentaire peut vous montrer que vous devez réduire le nombre de requêtes, ou ajouter (ou supprimer) des index de base de données, ou jeter plus de matériel, ou repenser complètement l'ensemble du système. .

Arseni Mourzenko
la source
À moins que je ne vous comprenne mal, vous dites "conservez les appels GetUser individuels, mais utilisez la mise en cache pour éviter les allers-retours db".
Eren Ersönmez
@ ErenErsönmez: GetUserau lieu d'interroger la base de données, il cherchera dans le cache. Cela signifie que peu importe le nombre de fois que vous appellerez GetUser, car il chargera les données de la mémoire au lieu de la base de données (sauf si le cache a été invalidé).
Arseni Mourzenko
c'est une bonne suggestion, étant donné que je n'ai pas fait du bon travail en soulignant le problème principal, qui est de "se débarrasser du bavardage sans fusionner les systèmes en un seul système". Mon exemple d'utilisateurs et de projets vous ferait naturellement croire qu'il y a un nombre relativement restreint d'utilisateurs et qui changent rarement. Un meilleur exemple aurait peut-être été Documents et projets. Imaginez que vous ayez quelques millions de documents, des milliers étant ajoutés chaque jour et que le système Project utilise le système Document pour stocker ses documents. Recommanderiez-vous toujours la mise en cache alors? Probablement non, non?
Eren Ersönmez
@ ErenErsönmez: plus vous avez de données, plus la mise en cache critique apparaît. En règle générale, comparez le nombre de lectures au nombre d'écritures. Si "des milliers" de documents sont ajoutés par jour et qu'il y a des millions de selectrequêtes par jour, vous feriez mieux d'utiliser la mise en cache. D'un autre côté, si vous ajoutez des milliards d'entités à une base de données mais que vous n'obtenez que quelques milliers de selects avec des s très sélectifs where, la mise en cache peut ne pas être très utile.
Arseni Mourzenko
vous avez probablement raison - j'essaie probablement de résoudre un problème que je n'ai pas encore. Je vais probablement implémenter tel quel et essayer d'améliorer plus tard si nécessaire. Si la mise en cache n'est pas appropriée car, par exemple, les entités ne sont susceptibles d'être lues qu'une ou deux fois après avoir été ajoutées, pensez-vous que la solution I possible que j'ai ajoutée à la question pourrait fonctionner? Voyez-vous un énorme problème avec cela?
Eren Ersönmez