Gérer et organiser le nombre considérablement accru de classes après le passage à SOLID?


51

Au cours des dernières années, nous avons lentement commencé à adopter un code de mieux en mieux écrit, petit à petit. Nous commençons enfin à passer à quelque chose qui ressemble au moins à SOLID, mais nous n'en sommes pas encore là. Depuis le passage à l'acte, l'un des plus gros griefs des développeurs est qu'ils ne supportent pas l'examen par des pairs et la traversée de dizaines et de dizaines de fichiers, alors que chaque tâche ne nécessitait auparavant que le développeur qui manipule 5 à 10 fichiers.

Avant de commencer à effectuer le changement, notre architecture était organisée de la manière suivante (accordée, avec un ou deux ordres de grandeur en plus):

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

En ce qui concerne les fichiers, tout était incroyablement linéaire et compact. Il y avait évidemment beaucoup de duplication de code, de couplage étroit et de maux de tête, cependant, tout le monde pouvait le parcourir et le résoudre. Les novices complets, ceux qui n’avaient jamais autant ouvert Visual Studio, pourraient le comprendre en quelques semaines seulement. L'absence de complexité générale des fichiers rend relativement simple la tâche des développeurs novices et des nouvelles recrues de commencer à contribuer sans trop de temps de mise en œuvre. Mais c’est à peu près à ce niveau que les avantages du style de code s’échappent.

J'approuve sans réserve toutes les tentatives que nous faisons pour améliorer notre base de code, mais il est très courant que le reste de l'équipe répugne à réagir à des changements de paradigme aussi importants que celui-ci. Quelques-uns des plus gros points d'achoppement sont actuellement:

  • Tests unitaires
  • Nombre de classe
  • Complexité de l'examen par les pairs

Les tests unitaires ont été incroyablement difficiles à vendre à l’équipe car ils pensent tous qu’ils perdent du temps et qu’ils sont capables de tester leur code beaucoup plus rapidement dans son ensemble que pour chaque élément individuellement. L'utilisation de tests unitaires comme une approbation de SOLID a généralement été vaine et est devenue une blague à ce stade-ci.

Le nombre de classes est probablement le plus gros obstacle à surmonter. Les tâches qui prenaient auparavant 5 à 10 fichiers peuvent maintenant en prendre 70 à 100! Bien que chacun de ces fichiers remplisse un objectif spécifique, leur volume peut être accablant. La réponse de l'équipe a été principalement des gémissements et des maux de tête. Auparavant, une tâche pouvait nécessiter un ou deux référentiels, un modèle ou deux, une couche logique et une méthode de contrôleur.

Maintenant, pour construire une simple application de sauvegarde de fichier, vous avez une classe pour vérifier si le fichier existe déjà, une classe pour écrire les métadonnées, une classe pour l’abstraction DateTime.Nowafin que vous puissiez injecter du temps pour les tests unitaires, des interfaces pour chaque fichier contenant de la logique, des fichiers. contenir des tests unitaires pour chaque classe et un ou plusieurs fichiers pour tout ajouter à votre conteneur DI.

SOLID est très facile à vendre pour les applications de petite à moyenne taille. Tout le monde voit les avantages et la facilité de maintenance. Cependant, ils ne voient tout simplement pas une bonne proposition de valeur pour SOLID dans les applications à très grande échelle. J'essaie donc de trouver des moyens d'améliorer l'organisation et la gestion pour nous permettre de surmonter les difficultés de croissance.


Je me suis dit que je donnerais un peu plus fort un exemple du volume de fichier basé sur une tâche récemment terminée. On m'a confié la tâche d'implémenter certaines fonctionnalités de l'un de nos nouveaux microservices afin de recevoir une demande de synchronisation de fichiers. Lorsque la demande est reçue, le service effectue une série de recherches et de contrôles, puis enregistre le document sur un lecteur réseau, ainsi que dans 2 tables de base de données distinctes.

Pour enregistrer le document sur le lecteur réseau, j'avais besoin de quelques classes spécifiques:

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

Cela fait donc 15 classes au total (à l’exclusion des POCO et des échafaudages) pour effectuer une sauvegarde relativement simple. Ce nombre a considérablement augmenté lorsque j'ai eu besoin de créer des POCO pour représenter des entités dans quelques systèmes, de construire quelques référents pour communiquer avec des systèmes tiers incompatibles avec nos autres ORM et de mettre au point des méthodes logiques pour gérer les subtilités de certaines opérations.


52
"Les tâches qui prenaient entre 5 et 10 fichiers peuvent maintenant en prendre entre 70 et 100!" Comment diable? Ce n'est en aucun cas normal. Quel genre de changements faites-vous nécessitant de modifier autant de fichiers?
Euphoric le

43
Le fait que vous deviez modifier plus de fichiers par tâche (beaucoup plus!) Signifie que vous faites mal SOLID. L’essentiel est d’organiser votre code (dans le temps) de manière à refléter les modèles de changement observés, en simplifiant les changements. Chaque principe de SOLID comporte un certain raisonnement derrière lui (quand et pourquoi il devrait être appliqué); on dirait que vous vous êtes mis dans cette situation en appliquant ces techniques à l'aveuglette. Même chose avec les tests unitaires (TDD); si vous le faites sans avoir une bonne compréhension de la conception / de l'architecture, vous allez vous enfoncer dans un trou.
Filip Milovanović

60
Vous avez clairement adopté SOLID en tant que religion plutôt qu’un outil pragmatique pour vous aider à accomplir votre travail. Si quelque chose dans SOLID fait plus de travail ou rend les choses plus difficiles, ne le faites pas.
Whatsisname

25
@Euphoric: Le problème peut se produire de deux manières. Je suppose que vous répondez à la possibilité que 70 à 100 classes soient excessives. Mais il n’est pas impossible qu’il s’agisse d’un projet gigantesque, regroupant 5 à 10 fichiers (j’ai travaillé dans des fichiers 20KLOC auparavant ...) et que 70 à 100 est en fait le nombre de fichiers approprié.
Flater

18
Il y a un trouble de la pensée que j'appelle "maladie du bonheur de l'objet", qui est la conviction que les techniques OO sont une fin en soi, plutôt que l'une des nombreuses techniques possibles pour réduire les coûts de travail dans une grande base de code. Vous avez une forme particulièrement avancée, "maladie du bonheur SOLID". SOLID n'est pas le but. L'objectif est de réduire les coûts de maintenance de la base de code. Evaluez vos propositions dans ce contexte, et non si c'est doctrinaire SOLID. (Le fait que vos propositions ne soient probablement pas non plus réellement doctrinaire SOLID est également un bon point à considérer.)
Eric Lippert

Réponses:


104

Maintenant, pour construire une application de sauvegarde de fichier simple, vous devez disposer d'une classe pour vérifier si le fichier existe déjà, d'une classe pour écrire les métadonnées, d'une classe pour extraire DateTime.Now afin que vous puissiez injecter du temps pour les tests unitaires, des interfaces pour chaque fichier contenant logique, les fichiers doivent contenir des tests unitaires pour chaque classe et un ou plusieurs fichiers pour tout ajouter à votre conteneur DI.

Je pense que vous avez mal compris l’idée d’une responsabilité unique. La responsabilité d'une classe peut être "enregistrer un fichier". Pour ce faire, il peut alors diviser cette responsabilité en une méthode qui vérifie si un fichier existe, une méthode pour écrire des métadonnées, etc. Chacune de ces méthodes a ensuite une responsabilité unique, qui fait partie de la responsabilité globale de la classe.

Une classe abstraite DateTime.Nowsonne bien. Mais vous n’avez besoin que d’une seule de celles-ci et elle pourrait être regroupée avec d’autres caractéristiques d’environnement dans une seule classe chargée de l’abstraction des caractéristiques de l’environnement. Encore une fois, une seule responsabilité avec plusieurs sous-responsabilités.

Vous n'avez pas besoin d '"interfaces pour chaque fichier contenant une logique", vous avez besoin d'interfaces pour les classes ayant des effets secondaires, par exemple les classes qui lisent / écrivent dans des fichiers ou des bases de données; et même dans ce cas, elles ne sont nécessaires que pour les parties publiques de cette fonctionnalité. Ainsi, par exemple, dans le cas où AccountRepovous n’auriez pas besoin d’interfaces, vous n’auriez peut-être besoin que d’une interface pour l’accès réel à la base de données injectée dans ce référentiel.

Les tests unitaires ont été incroyablement difficiles à vendre à l’équipe car ils pensent tous qu’ils perdent du temps et qu’ils sont capables de tester leur code beaucoup plus rapidement dans son ensemble que pour chaque élément individuellement. L'utilisation de tests unitaires comme une approbation de SOLID a généralement été vaine et est devenue une blague à ce stade-ci.

Cela suggère que vous avez également mal compris les tests unitaires. L'unité d'un test unitaire n'est pas une unité de code. Qu'est-ce même qu'une unité de code? Une classe? Une méthode? Une variable? Une seule instruction de machine? Non, le terme "unité" désigne une unité d'isolement, c'est-à-dire un code pouvant s'exécuter indépendamment des autres parties du code. Un simple test permettant de déterminer si un test automatisé est un test unitaire ou non est de savoir si vous pouvez l'exécuter en parallèle avec tous vos autres tests unitaires sans affecter le résultat. Il existe quelques règles de base concernant les tests unitaires, mais c'est votre mesure clé.

Donc, si des parties de votre code peuvent effectivement être testées dans leur ensemble sans en affecter d’autres, faites-le.

Soyez toujours pragmatique et rappelez-vous que tout est un compromis. Plus vous adhérez à DRY, plus votre code doit devenir étroitement couplé. Plus vous introduisez des abstractions, plus le code est facile à tester, mais plus il est difficile à comprendre. Évitez l'idéologie et trouvez un bon équilibre entre l'idéal et la simplicité. C'est là que réside le compromis d'efficacité maximale tant pour le développement que pour la maintenance.


27
J'aimerais ajouter qu'un mal de tête similaire se produit lorsque les gens essaient d'adhérer au mantra trop répété de "méthodes ne doivent faire qu'une chose" et se retrouvent avec des tonnes de méthodes d'une seule ligne simplement parce qu'elles peuvent techniquement être transformées en une méthode. .
Logarr le

8
Re "Soyez toujours pragmatique et souvenez-vous que tout est un compromis" : les disciples de l'oncle Bob ne sont pas connus pour cela (peu importe l'intention originale).
Peter Mortensen Le

13
Pour résumer la première partie, vous avez généralement un stagiaire en café, et non une suite complète de percolateur plug-in, un commutateur à bascule, un test de remplissage si nécessaire, un réfrigérateur ouvert, un lait de sortie, un des cuillères à soupe, des tasses à café, des tasses à café, des sucres, du lait, des tasses à mélanger et des tasses à servir. ; P
Justin Time 2 Rétablir Monica

12
La cause fondamentale du problème du PO semble être une incompréhension de la différence entre les fonctions devant exécuter une tâche unique et les classes devant avoir une responsabilité
alephzero le

6
"Les règles sont pour la direction des hommes sages et l'obéissance des imbéciles." - Douglas Bader
Calanus

30

Les tâches qui prenaient auparavant 5 à 10 fichiers peuvent maintenant en prendre 70 à 100!

C'est le contraire du principe de responsabilité unique (SRP). Pour arriver à ce point, vous devez avoir divisé vos fonctionnalités de manière très fine, mais ce n’est pas l’objet du PRS: c’est ignorer l’idée clé de la cohésion .

Selon le SRP, les logiciels devraient être divisés en modules selon des lignes définies par leurs raisons éventuelles de modification, de sorte qu’une seule modification de conception puisse être appliquée à un seul module sans nécessiter de modification ailleurs. Un seul "module" dans ce sens peut correspondre à plus d'une classe, mais si une modification nécessite de toucher des dizaines de fichiers, il s'agit de multiples modifications ou bien vous ne réalisez pas correctement la SRP.

Bob Martin, qui a initialement formulé le PÉR, a écrit un blog il y a quelques années pour tenter de clarifier la situation. Il discute assez longuement de ce qu'est une "raison de changer" aux fins du PÉR. Cela vaut la peine d'être lu dans son intégralité, mais parmi les éléments méritant une attention particulière se trouve cette formulation alternative du PÉR:

Rassemblez les choses qui changent pour les mêmes raisons . Séparez ces choses qui changent pour différentes raisons.

(c'est moi qui souligne). Le SRP ne consiste pas à diviser les choses en minuscules morceaux possibles. Ce n'est pas un bon design et votre équipe a raison de résister. Cela rend votre base de code plus difficile à mettre à jour et à maintenir. On dirait que vous essayez peut-être de vendre votre équipe dessus en vous basant sur des considérations relatives aux tests unitaires, mais ce serait mettre la charrue avant les boeufs.

De même, le principe de ségrégation des interfaces ne doit pas être considéré comme un absolu. Ce n'est pas plus une raison pour diviser votre code aussi finement que le SRP, et il s'aligne généralement assez bien avec le SRP. Le fait qu'une interface contienne des méthodes que certains clients n'utilisent pas n'est pas une raison pour la décomposer. Vous recherchez encore la cohésion.

De plus, je vous exhorte à ne pas prendre le principe ouvert-fermé ou le principe de substitution de Liskov comme une raison de favoriser les hiérarchies d'héritage profondes. Il n'y a pas de couplage plus étroit qu'une sous-classe avec ses super-classes, et un couplage étroit est un problème de conception. Privilégiez plutôt la composition que l’héritage, là où il est logique de le faire. Cela réduira votre couplage et, par conséquent, le nombre de fichiers qu'une modification particulière peut avoir besoin de toucher, et cela correspond parfaitement à l'inversion de dépendance.


1
Je suppose que je suis juste en train d'essayer de comprendre où se trouve la limite. Dans une tâche récente, j'ai dû effectuer une opération assez simple, mais c'était dans une base de code sans beaucoup d'échafaudages ou de fonctionnalités existants. En tant que tel, tout ce que je devais faire était très simple, mais tout à fait unique et ne semblait pas correspondre aux classes partagées. Dans mon cas, je devais enregistrer un document sur un lecteur réseau et le connecter à deux tables de base de données distinctes. Les règles entourant chaque étape étaient assez particulières. Même la génération de nom de fichier (un simple guide) comportait quelques classes pour faciliter les tests.
JD Davis

3
Encore une fois, @JDDavis, choisir plusieurs classes sur une seule à des fins de testabilité, c'est mettre la charrue avant les boeufs, et cela va directement à l'encontre du PRS, qui appelle le regroupement de fonctionnalités cohérentes. Je ne peux pas vous conseiller sur des détails particuliers, mais le problème que des modifications fonctionnelles individuelles nécessitent de modifier de nombreux fichiers est un problème que vous devriez aborder (et tenter d'éviter), et non un problème que vous devriez essayer de justifier.
John Bollinger Le

D'accord, j'ajoute ceci. Pour citer Wikipedia, "Martin définit une responsabilité comme une raison de changer et conclut qu'une classe ou un module devrait avoir une et une seule raison à modifier (c.-à-d. À réécrire)". et "il a déclaré plus récemment que" ce principe concerne les personnes "." En fait, je pense que cela signifie que la "responsabilité" dans le PRP fait référence aux parties prenantes et non à la fonctionnalité. Une classe devrait être responsable des changements requis par un seul intervenant (personne exigeant que vous changiez de programme), afin que vous changiez le moins de choses possible en réponse aux différents intervenants exigeant des changements.
Corrodias

13

Les tâches qui prenaient auparavant 5 à 10 fichiers peuvent maintenant en prendre 70 à 100!

Ceci est un mensonge. Les tâches n'ont jamais pris que 5 à 10 fichiers.

Vous ne résolvez aucune tâche avec moins de 10 fichiers. Pourquoi? Parce que vous utilisez C #. C # est un langage de haut niveau. Vous utilisez plus de 10 fichiers uniquement pour créer hello world.

Oh, bien sûr, vous ne les remarquez pas parce que vous ne les avez pas écrites. Donc, vous ne regardez pas dedans. Vous leur faites confiance.

Le problème n'est pas le nombre de fichiers. C'est que vous avez maintenant tellement de choses en lesquelles vous ne faites pas confiance.

Essayez donc de faire en sorte que ces tests fonctionnent au point qu’une fois qu’ils ont réussi, vous faites confiance à ces fichiers de la même manière que vous le faites en .NET. Faire cela est le point des tests unitaires. Personne ne se soucie du nombre de fichiers. Ils se soucient du nombre de choses auxquelles ils ne peuvent faire confiance.

SOLID est très facile à vendre pour les applications de petite à moyenne taille. Tout le monde voit les avantages et la facilité de maintenance. Cependant, ils ne voient tout simplement pas une bonne proposition de valeur pour SOLID dans les applications à très grande échelle.

Le changement est difficile sur les applications à très grande échelle, peu importe ce que vous faites. La meilleure sagesse à appliquer ici ne vient pas de Oncle Bob. Cela vient de Michael Feathers dans son livre Working Effectiveively with Legacy Code.

Ne commencez pas un festival de réécriture. L'ancien code représente des connaissances durement acquises. S'en débarrasser parce qu'il y a des problèmes et qu'il n'est pas exprimé dans le paradigme nouveau et amélioré X consiste simplement à demander un nouvel ensemble de problèmes et à éviter toute connaissance acquise de manière difficile.

Au lieu de cela, trouvez des moyens de rendre testable votre ancien code non vérifiable (le code hérité dans Feathers parle). Dans cette métaphore, le code est comme une chemise. Les grandes pièces sont jointes à des joints naturels qui peuvent être annulés pour séparer le code de la même manière que vous supprimeriez les joints. Faites ceci pour vous permettre d’attacher des "manches" de test qui vous permettent d’isoler le reste du code. Maintenant, lorsque vous créez les manches de test, vous avez confiance dans les manches car vous l'avez fait avec une chemise de travail. (ow, cette métaphore commence à faire mal).

Cette idée découle de l'hypothèse selon laquelle, comme dans la plupart des magasins, les seules exigences à jour sont dans le code en vigueur. Cela vous permet de verrouiller cela dans des tests qui vous permettent d’apporter des modifications au code de travail éprouvé sans qu'il perde tout son statut de travail prouvé. Maintenant, avec cette première vague de tests en place, vous pouvez commencer à apporter des modifications qui permettent de tester le code "hérité" (indéterminé). Vous pouvez être audacieux parce que les tests de coutures vous soutiennent en disant que c'est ce qu'il a toujours fait et que les nouveaux tests montrent que votre code fait réellement ce que vous pensez qu'il fait.

Qu'est-ce que tout cela a à voir avec:

Gérer et organiser le nombre considérablement accru de classes après le passage à SOLID?

Abstraction.

Vous pouvez me faire détester toute base de code avec de mauvaises abstractions. Une mauvaise abstraction est quelque chose qui me fait regarder à l'intérieur. Ne me surprends pas quand je regarde à l'intérieur. Soyez à peu près ce que j'attendais.

Donnez-moi un bon nom, des tests lisibles (exemples) qui montrent comment utiliser l'interface et l'organiser afin que je puisse trouver des éléments et que cela ne me dérange pas que nous utilisions 10, 100 ou 1000 fichiers.

Vous m'aidez à trouver des choses avec de bons noms descriptifs. Mettez les choses avec les bons noms dans les choses avec les bons noms.

Si vous faites tout cela correctement, vous résumerez les fichiers à l’endroit où terminer une tâche ne dépend que de 3 à 5 autres fichiers. Les fichiers 70-100 sont toujours là. Mais ils se cachent derrière les 3 contre 5. Cela ne fonctionne que si vous faites confiance aux 3 à 5 pour faire ce qu’il faut.

Donc, ce dont vous avez vraiment besoin, c’est le vocabulaire nécessaire pour trouver les bons noms pour toutes ces choses et des tests en lesquels les gens ont confiance, afin qu’ils cessent de fouiller dans tout. Sans cela, vous me rendriez fou aussi.

@Delioth fait un bon point sur les douleurs de croissance. Lorsque vous êtes habitué à ce que la vaisselle soit dans le placard au-dessus du lave-vaisselle, il faut s’y habituer au-dessus du comptoir à petit-déjeuner. Rend certaines choses plus difficiles. Rend certaines choses plus faciles. Mais cela provoque toutes sortes de cauchemars si les gens ne sont pas d’accord pour savoir où vont les plats. Dans une base de code volumineuse, le problème est que vous ne pouvez déplacer qu'une partie de la vaisselle à la fois. Alors maintenant, vous avez des plats à deux endroits. C'est confu. Il est difficile de croire que les plats sont là où ils sont censés être. Si vous voulez éviter cela, la seule chose à faire est de continuer à faire bouger les assiettes.

Le problème, c’est que vous voudriez vraiment savoir si la vaisselle au bar du petit-déjeuner en vaut la peine avant de passer à travers toutes ces bêtises. Eh bien, je ne peux que recommander de faire du camping.

Lorsque vous essayez un nouveau paradigme pour la première fois, vous devez l’appliquer en dernier lieu dans une grande base de code. Cela vaut pour tous les membres de l'équipe. Personne ne devrait croire que SOLID fonctionne, que OOP fonctionne ou que la programmation fonctionnelle fonctionne. Chaque membre de l'équipe doit pouvoir jouer avec la nouvelle idée, quelle qu'elle soit, dans un projet de jouet. Cela leur permet de voir au moins comment cela fonctionne. Cela leur permet de voir ce qui ne va pas bien. Cela leur permet d'apprendre à bien faire les choses avant de causer un grand désordre.

Donner aux gens un endroit où jouer en toute sécurité les aidera à adopter de nouvelles idées et leur donnera l’assurance que les plats pourraient bien fonctionner dans leur nouveau foyer.


3
Il est peut-être intéressant de mentionner qu'une partie de la douleur de la question ne fait probablement que s'aggraver - alors que, oui, ils pourraient avoir besoin de créer 15 fichiers pour cette seule chose ... maintenant, ils ne doivent plus jamais écrire un GUIDProvider, ou un BasePathProvider , ou un fournisseur d'extension, etc. C'est le même genre d'obstacle que vous rencontrez lorsque vous démarrez un nouveau projet "greenfield": plusieurs fonctions de support, pour la plupart triviales, stupides à écrire, mais qui nécessitent encore d'être écrites. Ça craint de les construire, mais une fois qu'ils sont là, vous ne devriez pas avoir à y penser ... jamais.
Delioth

@Delioth Je suis incroyablement enclin à croire que c'est le cas. Auparavant, si nous avions besoin d'un sous-ensemble de fonctionnalités (disons que nous voulions simplement une URL hébergée dans AppSettings), nous avions simplement une classe massive qui était transmise et utilisée. Avec la nouvelle approche, il n'y a aucune raison de faire le tour de AppSettingstout pour obtenir un URL ou un chemin de fichier.
JD Davis

1
Ne commencez pas un festival de réécriture. L'ancien code représente des connaissances durement acquises. S'en débarrasser parce qu'il y a des problèmes et qu'il n'est pas exprimé dans le paradigme nouveau et amélioré X consiste simplement à demander un nouvel ensemble de problèmes et à ne pas avoir de connaissances durement acquises. Cette. Absolument.
Flot2011

10

Il semble que votre code ne soit pas très bien découplé et / ou que la taille de votre tâche soit trop importante.

Les modifications de code devraient porter sur 5 à 10 fichiers, sauf si vous effectuez un codemod ou un refactoring à grande échelle. Si un seul changement touche beaucoup de fichiers, cela signifie probablement que vos changements sont en cascade. Certaines abstractions améliorées (plus de responsabilité unique, de ségrégation d’interface, d’inversion de dépendance) devraient aider. Il est également possible que vous ayez trop de responsabilité et que vous utilisiez un peu plus de pragmatisme - des hiérarchies de types plus courtes et plus minces. Cela devrait également rendre le code plus facile à comprendre puisqu'il n'est pas nécessaire de comprendre des dizaines de fichiers pour savoir ce que le code fait.

Cela pourrait également indiquer que votre travail est trop volumineux. Au lieu de "hé, ajoutez cette fonctionnalité" (qui nécessite des modifications de l'interface utilisateur, des modifications de l'interface API, des modifications de l'accès aux données, des modifications de la sécurité et des modifications de test et ...), décomposez-la en plusieurs parties réparables. Cela devient plus facile à analyser et à comprendre, car vous devez définir des contrats décents entre les bits.

Et bien sûr, les tests unitaires aident tout cela. Ils vous obligent à faire des interfaces décentes. Ils vous obligent à rendre votre code suffisamment souple pour injecter les bits nécessaires au test (si c'est difficile à tester, ce sera difficile à réutiliser). Et ils éloignent les gens de la sur-ingénierie, car plus vous ingéniez, plus vous avez à tester.


2
Les fichiers 5-10 à 70-100 sont un peu plus hypothétiques. Ma dernière tâche consistait à créer des fonctionnalités dans l'un de nos nouveaux microservices. Le nouveau service était censé recevoir une demande et enregistrer un document. Pour ce faire, il me fallait des classes représentant les entités utilisateur dans deux bases de données distinctes et des dépôts pour chacune. Repos pour représenter les autres tables que je devais écrire. Des classes dédiées pour gérer la vérification des données de fichiers et la génération de noms. Et la liste continue. Sans oublier que chaque classe contenant de la logique était représentée par une interface afin de pouvoir être simulée pour les tests unitaires.
JD Davis

1
En ce qui concerne nos anciennes bases de code, elles sont toutes étroitement couplées et incroyablement monolithiques. Avec l'approche SOLID, le seul couplage entre classes a été dans le cas des POCO, tout le reste est passé via DI et des interfaces.
JD Davis

3
@JDDavis - attendez, pourquoi un microservice fonctionne-t-il directement avec plusieurs bases de données?
Telastyn le

1
C'était un compromis avec notre responsable de développement. Il préfère massivement les logiciels monolithiques et procéduraux. En tant que tels, nos microservices sont bien plus macro qu’ils ne devraient l’être. Au fur et à mesure que notre infrastructure s'améliorera, les choses évolueront lentement vers leurs propres microservices. Pour l'instant, nous suivons quelque peu l'approche de l'étrangleur pour transférer certaines fonctionnalités dans des microservices. Étant donné que plusieurs services doivent avoir accès à une ressource spécifique, nous les transférons également dans leurs propres microservices.
JD Davis le

4

Je voudrais expliquer quelques-uns des éléments déjà mentionnés ici, mais plus dans la perspective des limites des objets. Si vous suivez quelque chose qui s'apparente à la conception par domaine, vos objets vont probablement représenter des aspects de votre entreprise. Customeret Order, par exemple, seraient des objets. Maintenant, si je devais deviner en fonction des noms de classe que vous aviez comme point de départ, votre AccountLogicclasse aurait un code pouvant être exécuté pour n’importe quel compte. Dans OO, cependant, chaque classe est censée avoir un contexte et une identité. Vous ne devez pas obtenir d' Accountobjet, puis le transmettre à une AccountLogicclasse et lui demander de modifier l' Accountobjet. C'est ce qu'on appelle un modèle anémique et qui ne représente pas très bien OO. Au lieu de cela, votreAccountclasse devrait avoir un comportement, tel que Account.Close()ou Account.UpdateEmail(), et ces comportements n’affecteraient que cette instance du compte.

Maintenant, la façon dont ces comportements sont gérés peut (et dans de nombreux cas devrait être) être déchargée sur des dépendances représentées par des abstractions (c'est-à-dire des interfaces). Account.UpdateEmailVous pouvez par exemple souhaiter mettre à jour une base de données, un fichier ou envoyer un message à un bus de service, etc. Cela pourrait changer dans le futur. Ainsi, votre Accountclasse peut avoir une dépendance sur, par exemple, une IEmailUpdateinterface pouvant être l'une des nombreuses interfaces implémentées par un AccountRepositoryobjet. Vous ne voudriez pas passer toute une IAccountRepositoryinterface à l' Accountobjet car il en ferait probablement trop, comme rechercher et trouver d'autres comptes (n'importe lesquels), auxquels vous ne souhaitez peut-être pas que l' Accountobjet ait accès, mais même si vous AccountRepositorypouvez implémenter les deux. IAccountRepositoryet IEmailUpdateinterfaces, leAccountobjet n'aurait accès qu'aux petites portions dont il a besoin. Cela vous aide à maintenir le principe de séparation des interfaces .

De manière réaliste, comme d'autres personnes l'ont mentionné, si vous faites face à une explosion de classes, il est probable que vous utilisiez le principe SOLID (et, par extension, OO) dans le mauvais sens. SOLID devrait vous aider à simplifier votre code et non à le compliquer. Mais il faut du temps pour vraiment comprendre ce que signifie le PÉR. Le plus important, cependant, est que le fonctionnement de SOLID dépendra beaucoup de votre domaine et de vos contextes liés (un autre terme DDD). Il n'y a pas de solution miracle ni de solution unique.

Une dernière chose que je tiens à souligner auprès des personnes avec qui je travaille: encore une fois, un objet POO doit avoir un comportement et est en fait défini par son comportement et non par ses données. Si votre objet n'a que des propriétés et des champs, il a toujours un comportement, mais probablement pas le comportement que vous aviez prévu. Une propriété publiquement inscriptible / paramétrable sans autre logique d'ensemble implique que le comportement de sa classe contenante est que n'importe qui, n'importe où, pour quelque raison que ce soit et à tout moment, est autorisé à modifier la valeur de cette propriété sans aucune logique métier ou validation nécessaire entre les deux. Ce n'est généralement pas le comportement souhaité par les gens, mais si vous avez un modèle anémique, c'est généralement le comportement que vos classes annoncent à ceux qui l'utilisent.


2

Cela fait donc 15 classes au total (à l’exclusion des POCO et des échafaudages) pour effectuer une sauvegarde relativement simple.

C'est fou ... mais ces cours ressemblent à quelque chose que j'écrirais moi-même. Alors regardons-les. Ignorons les interfaces et les tests pour le moment.

  • BasePathProvider- IMHO, tout projet non-trivial utilisant des fichiers en a besoin. Donc, je suppose, il existe déjà une telle chose et vous pouvez l'utiliser comme tel.
  • UniqueFilenameProvider - Bien sûr, vous l'avez déjà, n'est-ce pas?
  • NewGuidProvider - Même cas, à moins que vous ne commenciez à utiliser le GUID.
  • FileExtensionCombiner - Le même cas.
  • PatientFileWriter - Je suppose que c'est la classe principale pour la tâche en cours.

Pour moi, ça a l'air bien: vous devez écrire une nouvelle classe qui nécessite quatre classes d'assistance. Les quatre classes d’aide semblent très réutilisables, alors je parierais qu’elles sont déjà quelque part dans votre code. Sinon, c'est soit de la malchance (êtes-vous vraiment la personne de votre équipe pour écrire des fichiers et utiliser des GUID ???) ou un autre problème.


En ce qui concerne les classes de test, bien sûr, lorsque vous créez une nouvelle classe ou la mettez à jour, elle doit être testée. Donc, écrire cinq classes signifie aussi écrire cinq classes de test. Mais cela ne complique pas la conception:

  • Vous n'utiliserez jamais les classes de test ailleurs car elles seront exécutées automatiquement et c'est tout.
  • Vous souhaitez les consulter à nouveau, sauf si vous mettez à jour les classes testées ou si vous les utilisez comme documentation (idéalement, les tests montrent clairement comment une classe est censée être utilisée).

En ce qui concerne les interfaces, elles ne sont nécessaires que lorsque votre infrastructure DI ou votre infrastructure de test ne peut pas gérer les classes. Vous pouvez les voir comme un péage pour des outils imparfaits. Ou vous pouvez les voir comme une abstraction utile vous permettant d'oublier qu'il y a des choses plus compliquées - la lecture du source d'une interface prend beaucoup moins de temps que celle du source de son implémentation.


Je suis reconnaissant pour ce point de vue. Dans ce cas précis, j’écrivais des fonctionnalités dans un microservice relativement nouveau. Malheureusement, même dans notre base de code principale, bien que certains des éléments ci-dessus soient utilisés, rien n’est réellement réutilisable à distance. Tout ce qui doit être réutilisable a fini dans une classe statique ou est simplement copié et collé autour du code. Je pense que je vais encore un peu loin, mais je conviens que tout ne doit pas être complètement disséqué et découplé.
JD Davis

@JDDavis J'essayais d'écrire quelque chose de différent des autres réponses (avec lesquelles je suis plutôt d'accord). Chaque fois que vous copiez et collez quelque chose, vous empêchez la réutilisation car au lieu de généraliser, vous créez un autre morceau de code non réutilisable, ce qui vous oblige à copier et coller davantage un jour. IMHO c'est le deuxième plus grand péché, juste après avoir suivi aveuglément les règles. Vous devez trouver votre position privilégiée, car suivre les règles vous rend plus productif (en particulier en ce qui concerne les modifications futures) et les enfreindre un peu aide parfois dans les cas où l'effort ne serait pas inapproprié. C'est tout relatif.
Maaartinus

@JDDavis Et tout dépend de la qualité de vos outils. Exemple: Il y a des gens qui prétendent que le DI est une entreprise complexe et complexe, alors que je prétends que c'est principalement gratuit . +++En ce qui concerne le non-respect des règles: il existe quatre classes, il me faut des endroits, où je ne pouvais les injecter qu’après une refactorisation majeure rendant le code plus moche (du moins pour mes yeux), alors j’ai décidé de les créer à Je trouverais peut-être un meilleur moyen, mais j'en suis heureux (le nombre de ces singletons n'a pas changé depuis des siècles).
Maaartinus

Cette réponse exprime à peu près ce à quoi je pensais lorsque le PO a ajouté l’exemple à la question. @JDDavis Permettez-moi d'ajouter que vous pouvez enregistrer du code / des classes standard en utilisant des outils fonctionnels pour les cas simples. Un fournisseur d'interface graphique, par exemple - au lieu d'introduire une nouvelle interface, une nouvelle classe pour cela, pourquoi ne pas simplement l'utiliser Func<Guid>pour cela et injecter une méthode anonyme comme ()=>Guid.NewGuid()dans le constructeur? Et il n’est pas nécessaire de tester cette fonction du framework .Net, c’est quelque chose que Microsoft a fait pour vous. Au total, cela vous fera économiser 4 cours.
Doc Brown

... et vous devriez vérifier si les autres cas que vous avez présentés peuvent être simplifiés de la même manière (probablement pas tous).
Doc Brown

2

Selon les abstractions, créer des classes à responsabilité unique et écrire des tests unitaires ne sont pas des sciences exactes. Il est parfaitement normal d'aller trop loin dans une direction lorsqu'on apprend, d'aller à l'extrême, puis de trouver une norme qui ait du sens. On dirait simplement que votre pendule a trop basculé et pourrait même être bloqué.

Voici où je soupçonne que cela a déraillé:

Les tests unitaires ont été incroyablement difficiles à vendre à l’équipe car ils pensent tous qu’ils perdent du temps et qu’ils sont capables de tester leur code beaucoup plus rapidement dans son ensemble que pour chaque élément individuellement. L'utilisation de tests unitaires comme une approbation de SOLID a généralement été vaine et est devenue une blague à ce stade-ci.

Un des avantages de la plupart des principes SOLID (et certainement pas le seul) est qu’il facilite l’écriture de tests unitaires pour notre code. Si une classe dépend d'abstractions, nous pouvons nous moquer de ces abstractions. Les abstractions qui sont séparées sont plus faciles à simuler. Si une classe fait une chose, elle aura probablement une complexité moindre, ce qui signifie qu'il est plus facile de connaître et de tester tous ses chemins possibles.

Si votre équipe n'écrit pas de tests unitaires, deux événements liés se produisent:

Premièrement, ils font beaucoup de travail supplémentaire pour créer toutes ces interfaces et classes sans en tirer tous les avantages. Il faut un peu de temps et de pratique pour voir comment la rédaction de tests unitaires facilite notre vie. Il y a des raisons pour lesquelles les personnes qui apprennent à écrire des tests unitaires s'y tiennent, mais vous devez persister suffisamment longtemps pour les découvrir par vous-même. Si votre équipe n'essaie pas cela, elle aura l'impression que le reste du travail supplémentaire qu'il fait est inutile.

Par exemple, que se passe-t-il quand ils ont besoin de refactoriser? S'ils ont cent petites classes mais qu'aucun test ne leur indique si leurs modifications fonctionneront ou non, ces classes et interfaces supplémentaires vont sembler être un fardeau, pas une amélioration.

Deuxièmement, l'écriture de tests unitaires peut vous aider à comprendre combien d'abstraction votre code a réellement besoin. Comme je l'ai dit, ce n'est pas une science. Nous commençons mal, nous tournons dans tous les sens et nous nous améliorons. Les tests unitaires ont une manière particulière de compléter SOLID. Comment savoir quand vous devez ajouter une abstraction ou casser quelque chose? En d'autres termes, comment savez-vous quand vous êtes "assez solide"? Souvent, la réponse est lorsque vous ne pouvez pas tester quelque chose.

Peut-être que votre code serait testable sans créer autant d'abstractions et de classes minuscules. Mais si vous n'écrivez pas les tests, comment pouvez-vous le savoir? Jusqu'où allons-nous? Nous pouvons devenir obsédés par la division des choses de plus en plus petites. C'est un trou de lapin. La possibilité d'écrire des tests pour notre code nous permet de voir quand nous avons atteint notre objectif afin que nous puissions cesser d'être obsédés, passer à autre chose et avoir du plaisir à écrire davantage de code.

Les tests unitaires ne sont pas une solution miracle, mais ils constituent une solution vraiment impressionnante qui améliore la vie des développeurs. Nous ne sommes pas parfaits, pas plus que nos tests. Mais les tests nous donnent confiance. Nous nous attendons à ce que notre code soit correct et nous sommes surpris quand il ne va pas, et non l'inverse. Nous ne sommes pas parfaits et nos tests non plus. Mais lorsque notre code est testé, nous avons confiance. Nous sommes moins susceptibles de nous ronger les ongles lorsque notre code est déployé et de nous demander ce qui va se casser cette fois-ci et si ce sera notre faute.

En plus de cela, une fois que nous avons compris, l'écriture de tests unitaires accélère le développement du code, pas le ralentit. Nous passons moins de temps à revoir l'ancien code ou à déboguer pour trouver des problèmes qui ressemblent à des aiguilles dans une botte de foin.

Les bugs diminuent, nous en faisons plus et nous remplaçons l’anxiété par la confiance. Ce n'est pas une huile de mode ou de serpent. C'est vrai. De nombreux développeurs vont en témoigner. Si votre équipe ne l'a pas expérimenté, il lui faut franchir cette courbe d'apprentissage et franchir le cap. Donnez-lui une chance en réalisant qu'ils n'obtiendront pas de résultats instantanément. Mais quand cela se produira, ils seront heureux de le faire et ne regarderont jamais en arrière. (Ou ils deviendront des parias isolés et écriront des articles de blog en colère sur la façon dont les tests unitaires et la plupart des autres connaissances acquises en matière de programmation sont une perte de temps.)

Depuis le passage à l'acte, l'un des plus gros griefs des développeurs est qu'ils ne supportent pas l'examen par des pairs et la traversée de dizaines et de dizaines de fichiers, alors que chaque tâche ne nécessitait auparavant que le développeur qui manipule 5 à 10 fichiers.

L'examen par les pairs est beaucoup plus facile lorsque tous les tests unitaires sont réussis et qu'une grande partie de cet examen consiste simplement à s'assurer que les tests ont un sens.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.