Au cours des dernières années, nous avons progressivement basculé vers un code écrit de mieux en mieux, quelques étapes à la fois. Nous commençons enfin à passer à quelque chose qui ressemble au moins à SOLID, mais nous n'en sommes pas encore là. Depuis le changement, l'une des plus grandes plaintes des développeurs est qu'ils ne supportent pas la révision par des pairs et la traversée de dizaines et de dizaines de fichiers alors qu'auparavant, chaque tâche ne nécessitait que le développeur touche 5 à 10 fichiers.
Avant de commencer à faire le changement, notre architecture était organisée à peu près comme suit (accordé, avec un ou deux ordres de grandeur de fichiers supplémentaires):
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 serré et de maux de tête, cependant, tout le monde pouvait le parcourir et le comprendre. Des novices complets, des gens qui n'avaient jamais autant ouvert Visual Studio, pouvaient le comprendre en quelques semaines. Le manque de complexité globale des fichiers fait qu'il est relativement simple pour les développeurs novices et les nouveaux employés de commencer à contribuer sans trop de temps de montée en puissance. Mais c'est à peu près là où les avantages du style de code disparaissent.
J'approuve de tout cœur toutes les tentatives que nous faisons pour améliorer notre base de code, mais il est très courant d'obtenir des retours en arrière du reste de l'équipe sur des changements de paradigme massifs comme celui-ci. Quelques-uns des principaux points de blocage actuellement sont:
Les tests unitaires ont été incroyablement difficiles à vendre à l'équipe car ils pensent tous que c'est une perte de temps et qu'ils sont capables de gérer-tester leur code beaucoup plus rapidement dans son ensemble que chaque pièce individuellement. L'utilisation de tests unitaires comme approbation pour SOLID a été principalement futile et est surtout devenue une blague à ce stade.
Le nombre de classes est probablement le plus grand obstacle à surmonter. Les tâches qui prenaient auparavant 5 à 10 fichiers peuvent désormais en prendre 70 à 100! Bien que chacun de ces fichiers ait un objectif distinct, le volume considérable de fichiers peut être écrasant. La réponse de l'équipe a principalement été des gémissements et des grattements de tête. Auparavant, une tâche pouvait nécessiter un ou deux référentiels, un ou deux modèles, une couche logique et une méthode de contrôleur.
Maintenant, pour construire une simple application de sauvegarde de fichiers, vous avez une classe pour vérifier si le fichier existe déjà, une classe pour écrire les métadonnées, une classe pour abstraire DateTime.Now
afin que vous puissiez injecter des temps pour les tests unitaires, des interfaces pour chaque fichier contenant de la logique, des fichiers pour contenir des tests unitaires pour chaque classe et un ou plusieurs fichiers pour tout ajouter à votre conteneur DI.
Pour les applications de petite à moyenne taille, SOLID est une vente super facile. Tout le monde voit l'avantage et la facilité de maintenance. Cependant, ils ne voient tout simplement pas une bonne proposition de valeur pour SOLID sur les applications à très grande échelle. J'essaie donc de trouver des moyens d'améliorer l'organisation et la gestion pour surmonter les difficultés croissantes.
J'ai pensé que je donnerais un peu plus fort d'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 dans l'un de nos nouveaux microservices pour recevoir une demande de synchronisation de fichiers. Lorsque la demande est reçue, le service effectue une série de recherches et de vérifications et enregistre enfin le document sur un lecteur réseau, ainsi que 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
C'est donc un total de 15 classes (hors POCO et échafaudage) pour effectuer une sauvegarde assez 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, créé quelques référentiels pour communiquer avec des systèmes tiers qui sont incompatibles avec nos autres ORM et conçu des méthodes logiques pour gérer les subtilités de certaines opérations.
Maintenant, pour construire une simple application de sauvegarde de fichiers, vous avez une classe pour vérifier si le fichier existe déjà, une classe pour écrire les métadonnées, une classe pour résumer DateTime.Vous pouvez maintenant injecter des heures pour les tests unitaires, des interfaces pour chaque fichier contenant logique, des fichiers pour 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é unique d'une classe peut être "enregistrer un fichier". Pour ce faire, il peut alors décomposer cette responsabilité en une méthode qui vérifie s'il existe un fichier, une méthode qui écrit des métadonnées, etc. Chacune de ces méthodes a alors une seule responsabilité, qui fait partie de la responsabilité globale de la classe.
Une classe pour faire abstraction DateTime.Now
ça m'a l'air bien. Mais vous n'avez besoin que de l'une d'entre elles et elle pourrait être regroupée avec d'autres fonctionnalités d'environnement dans une seule classe avec la responsabilité d'abstraire les fonctionnalités d'environnement. Encore une fois, une seule responsabilité avec plusieurs sous-responsabilités.
Vous n'avez pas besoin d '"interfaces pour chaque fichier contenant de la logique", vous avez besoin d'interfaces pour les classes qui ont des effets secondaires, par ex. les classes qui lisent/écrivent dans des fichiers ou des bases de données; et même alors, ils ne sont nécessaires que pour les parties publiques de cette fonctionnalité. Ainsi, par exemple dans AccountRepo
, vous n'aurez peut-être pas besoin d'interfaces, vous n'aurez peut-être besoin que d'une interface pour l'accès à la base de données réelle qui est injectée dans ce référentiel.
Les tests unitaires ont été incroyablement difficiles à vendre à l'équipe car ils pensent tous que c'est une perte de temps et qu'ils sont capables de gérer-tester leur code beaucoup plus rapidement dans son ensemble que chaque pièce individuellement. L'utilisation de tests unitaires comme approbation pour SOLID a été principalement futile et est surtout devenue une blague à ce stade.
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 qu'une unité de code? Une classe? Une méthode? Une variable? Une seule instruction machine? Non, l '"unité" fait référence à une unité d'isolement, c'est-à-dire un code qui peut s'exécuter indépendamment des autres parties du code. Un simple test pour savoir 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 son résultat. Il y a quelques règles de base supplémentaires autour des tests unitaires, mais c'est votre mesure clé.
Donc, si des parties de votre code peuvent effectivement être testées dans leur ensemble sans affecter d'autres parties, faites-le.
Soyez toujours pragmatique et rappelez-vous que tout est un compromis. Plus vous adhérez à SEC, plus votre code doit être étroitement couplé. Plus vous introduisez d'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 l'efficacité idéale pour le développement et la maintenance.
Les tâches qui prenaient auparavant 5 à 10 fichiers peuvent désormais en prendre 70 à 100!
C'est le ci-contre 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 le but du SRP - faire cela ignore l'idée clé de cohésion.
Selon le SRP, les logiciels devraient être divisés en modules selon des lignes définies par leurs raisons possibles de changement, de sorte qu'un seul changement de conception puisse être appliqué dans le module juste un sans nécessiter de modifications ailleurs. Un seul "module" dans ce sens peut correspondre à plus d'une classe, mais si un changement vous oblige à toucher des dizaines de fichiers, alors c'est vraiment plusieurs changements ou vous faites une erreur SRP.
Bob Martin, qui a initialement formulé le PÉR, a écrit n article de blog il y a quelques années pour essayer de clarifier la situation. Il explique en détail ce qu'est une "raison de changer" aux fins du PÉR. Il vaut la peine d'être lu dans son intégralité, mais parmi les choses méritant une attention particulière, il y a cette formulation alternative du SRP:
Rassemblez les choses qui changent pour les mêmes raisons . Séparez les choses qui changent pour différentes raisons.
(c'est moi qui souligne). Le SRP est pas à propos de diviser les choses en morceaux les plus minuscules 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. Il semble que vous essayiez de vendre votre équipe sur la base de considérations de tests unitaires, mais ce serait mettre le chariot avant le cheval.
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 de diviser votre code aussi finement que le SRP, et il s'aligne généralement assez bien avec le SRP. Qu'une interface contienne certains méthodes que certains les clients n'utilisent pas n'est pas une raison pour la casser. Vous recherchez à nouveau la cohésion.
De plus, je vous exhorte à ne pas prendre le principe ouvert-fermé ou le principe de substitution Liskov comme une raison de favoriser les hiérarchies d'héritage profondes. Il n'y a pas de couplage plus serré qu'une sous-classe avec ses superclasses, et le couplage serré est un problème de conception. Au lieu de cela, privilégiez la composition plutôt que l'héritage chaque fois qu'il est logique de le faire. Cela réduira votre couplage, et donc le nombre de fichiers qu'un changement particulier devra peut-être toucher, et il s'aligne bien avec l'inversion de dépendance.
Les tâches qui prenaient auparavant 5 à 10 fichiers peuvent désormais 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 juste pour créer un monde bonjour.
Oh, bien sûr, vous ne les remarquez pas parce que vous ne les avez pas écrites. Vous ne les regardez donc pas. Tu leur fais confiance.
Le problème n'est pas le nombre de fichiers. C'est que vous avez maintenant tellement de choses que vous ne faites pas confiance.
Donc, découvrez comment faire fonctionner ces tests au point qu'une fois qu'ils ont réussi, vous faites confiance à ces fichiers comme vous le faites pour les fichiers dans .NET. Faire cela est le point du test unitaire. Personne ne se soucie du nombre de fichiers. Ils se soucient du nombre de choses auxquelles ils ne peuvent pas faire confiance.
Pour les applications de petite à moyenne taille, SOLID est une vente super facile. Tout le monde voit l'avantage et la facilité de maintenance. Cependant, ils ne voient tout simplement pas une bonne proposition de valeur pour SOLID sur les applications à très grande échelle.
Le changement est difficile sur des applications à très grande échelle, peu importe ce que vous faites. La meilleure sagesse à appliquer ici ne vient pas de l'oncle Bob. Il vient de Michael Feathers dans son livre Working Effectively with Legacy Code.
Ne démarrez pas un festival de réécriture. L'ancien code représente une connaissance durement acquise. Le jeter parce qu'il a des problèmes et n'est pas exprimé dans un paradigme nouveau et amélioré X demande simplement un nouvel ensemble de problèmes et aucune connaissance durement acquise.
Au lieu de cela, trouvez des moyens de rendre testable votre ancien code non testable (le code hérité dans Feathers parle). Dans cette métaphore, le code est comme une chemise. Les grandes pièces sont jointes au niveau des coutures naturelles qui peuvent être annulées pour séparer le code de la manière dont vous supprimeriez les coutures. Faites cela pour vous permettre d'attacher des "manchons" de test qui vous permettent d'isoler le reste du code. Maintenant, lorsque vous créez les manches d'essai, vous avez confiance dans les manches parce que 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 que, comme dans la plupart des magasins, les seules exigences à jour se trouvent dans le code de travail. 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 la moindre partie de son état de fonctionnement éprouvé. Maintenant, avec cette première vague de tests en place, vous pouvez commencer à apporter des modifications qui rendent le code "hérité" (non testable) testable. Vous pouvez être audacieux parce que les tests de couture vous soutiennent en disant que c'est ce qu'il a toujours fait et les nouveaux tests montrent que votre code fait réellement ce que vous pensez qu'il fait.
Qu'est-ce que cela a à voir avec:
Gérer et organiser le nombre massivement accru de cours après le passage à SOLID?
Abstraction.
Vous pouvez me faire détester n'importe quelle 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 organisez-la pour que je puisse trouver des choses et je m'en fiche si nous avons utilisé 10, 100 ou 1000 fichiers.
Vous m'aidez à trouver des choses avec de bons noms descriptifs. Mettez des choses avec de bons noms dans des choses avec de bons noms.
Si vous faites tout cela correctement, vous résumerez les fichiers là où la fin d'une tâche ne vous dépend que de 3 à 5 autres fichiers. Les fichiers 70-100 sont toujours là. Mais ils se cachent derrière le 3 à 5. Cela ne fonctionne que si vous faites confiance au 3 à 5 pour le faire correctement.
Donc, ce dont vous avez vraiment besoin, c'est du vocabulaire pour trouver de bons noms pour toutes ces choses et des tests auxquels les gens font confiance pour qu'ils arrêtent de patauger dans tout. Sans ça, tu me rendrais 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 un certain temps pour s'y habituer au-dessus de la barre du 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 sur la destination des plats. Dans une grande base de code, le problème est que vous ne pouvez déplacer que certains plats à 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 dépasser cela, la seule chose à faire est de continuer à déplacer la vaisselle.
Le problème, c'est que vous aimeriez vraiment savoir si la vaisselle au bar du petit-déjeuner en vaut la peine avant de passer par toutes ces bêtises. Eh bien, tout ce que je peux recommander, c'est faire du camping.
Lorsque vous essayez un nouveau paradigme pour la première fois, le dernier endroit où vous devez l'appliquer est dans une grande base de code. Cela vaut pour chaque membre de l'équipe. Personne ne devrait croire que SOLID fonctionne, que OOP fonctionne, ou que la programmation fonctionnelle fonctionne. Chaque membre de l'équipe devrait avoir la chance de 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 que cela ne fait pas bien.
Donner aux gens un endroit sûr pour jouer les aidera à adopter de nouvelles idées et leur donnera l'assurance que les plats pourraient vraiment fonctionner dans leur nouvelle maison.
Il semble que votre code ne soit pas très bien découplé et/ou que la taille de vos tâches soit trop grande.
Modifications du code devrait être de 5 à 10 fichiers, sauf si vous effectuez un codemod ou une refactorisation à grande échelle. Si une seule modification touche un grand nombre de fichiers, cela signifie probablement que vos modifications se mettent en cascade. Certaines abstractions améliorées (plus de responsabilité unique, ségrégation d'interface, inversion de dépendance) devraient aider. Il est également possible que vous ayez trop une seule responsabilité et que vous utilisiez un peu plus de pragmatisme - des hiérarchies de type plus courtes et plus fines. Cela devrait également faciliter la compréhension du code, car vous n'avez pas besoin de comprendre des dizaines de fichiers pour savoir ce que fait le code.
Cela pourrait également être un signe que votre travail est trop grand. Au lieu de "hé, ajoutez cette fonctionnalité" (qui nécessite des changements d'interface utilisateur et des changements d'api et des changements d'accès aux données et des changements de sécurité et des changements de test et ...) le décomposer en morceaux plus utiles. Cela devient plus facile à examiner et à comprendre car cela vous oblige à mettre en place des contrats décents entre les bits.
Et bien sûr, les tests unitaires aident à tout cela. Ils vous obligent à créer des interfaces décentes. Ils vous obligent à rendre votre code suffisamment flexible pour injecter les bits nécessaires au test (s'il est difficile à tester, il sera difficile à réutiliser). Et ils éloignent les gens de la suringénierie, car plus vous concevez, plus vous devez tester.
Je voudrais exposer certaines des choses déjà mentionnées ici, mais plus du point de vue de l'endroit où les limites des objets sont tracées. Si vous suivez quelque chose de similaire à la conception pilotée par domaine, vos objets représenteront probablement des aspects de votre entreprise. Customer
et Order
, par exemple, seraient des objets. Maintenant, si je devais faire une supposition en fonction des noms de classe que vous aviez comme point de départ, votre classe AccountLogic
avait du code qui s'exécuterait pour tout compte. Dans OO, cependant, chaque classe est censée avoir un contexte et une identité. Vous ne devez pas obtenir un objet Account
, puis le passer dans une classe AccountLogic
et demander à cette classe d'apporter des modifications à l'objet Account
. C'est ce qu'on appelle un modèle anémique et ne représente pas très bien OO. Au lieu de cela, votre classe Account
devrait avoir un comportement, tel que Account.Close()
ou Account.UpdateEmail()
, et ces comportements n'affecteraient que cette instance du compte.
Maintenant, COMMENT ces comportements sont gérés peuvent (et dans de nombreux cas devraient) être déchargés dans des dépendances représentées par des abstractions (c'est-à-dire des interfaces). Account.UpdateEmail
, par exemple, peut vouloir mettre à jour une base de données ou un fichier, ou envoyer un message à un bus de service, etc. Et cela pourrait changer à l'avenir. Ainsi, votre classe Account
peut avoir une dépendance, par exemple, sur un IEmailUpdate
, qui pourrait être l'une des nombreuses interfaces implémentées par un objet AccountRepository
. Vous ne voudriez pas passer une interface IAccountRepository
entière à l'objet Account
car cela en ferait probablement trop, comme rechercher et trouver d'autres (tous) comptes, dont vous ne voudrez peut-être pas Account
objet auquel accéder, mais même si AccountRepository
peut implémenter les interfaces IAccountRepository
et IEmailUpdate
, l'objet Account
n'aurait que l'accès aux petites portions dont il a besoin. Cela vous aide à maintenir le principe de ségrégation d'interface.
En réalité, comme d'autres personnes l'ont mentionné, si vous avez affaire à une explosion de classes, il est probable que vous utilisez SOLID principe (et, par extension, OO) dans le mauvais sens. = SOLID devrait vous aider à simplifier votre code, pas à le compliquer. Mais il faut du temps pour vraiment comprendre ce que des choses comme le SRP signifient. La chose la plus importante, cependant, est que comment SOLID works va être très dépendant de votre domaine et des contextes délimités (un autre terme DDD). Il n'y a pas de solution miracle ou de taille unique.
Une autre chose que j'aime souligner aux personnes avec qui je travaille: encore une fois, un objet OOP doit avoir un comportement, et est en fait défini par son comportement, pas par ses données. Si votre objet n'a rien d'autre que des propriétés et des champs, il a toujours un comportement, mais probablement pas le comportement que vous vouliez. Une propriété publiquement accessible/définissable sans autre logique d'ensemble implique que le comportement de sa classe contenante est que n'importe qui n'importe où pour quelque raison et à il est possible de modifier la valeur de cette propriété sans aucune logique métier ni validation entre les deux. Ce n'est généralement pas le comportement que les gens envisagent, mais si vous avez un modèle anémique, c'est généralement le comportement que vos classes annoncent à quiconque les utilise.
C'est donc un total de 15 classes (hors POCO et échafaudage) pour effectuer une sauvegarde assez simple.
C'est fou ... mais ces cours sonnent comme quelque chose que j'écrirais moi-même. Jetons-y donc un œil. Ignorons pour l'instant les interfaces et les tests.
BasePathProvider
- À mon humble avis, tout projet non trivial travaillant avec des fichiers en a besoin. Je suppose donc qu'il existe déjà une telle chose et que vous pouvez l'utiliser telle quelle.UniqueFilenameProvider
- Bien sûr, vous l'avez déjà, n'est-ce pas?NewGuidProvider
- Le même cas, à moins que vous commenciez juste à utiliser GUID.FileExtensionCombiner
- Le même cas.PatientFileWriter
- Je suppose que c'est la classe principale pour la tâche en cours.Pour moi, cela semble bon: vous devez écrire une nouvelle classe qui a besoin de quatre classes auxiliaires. Les quatre classes d'assistance semblent assez réutilisables, donc je parie qu'elles sont déjà quelque part dans votre base de code. Sinon, c'est soit la malchance (êtes-vous vraiment la personne de votre équipe pour écrire des fichiers et utiliser des GUID ???) ou un autre problème.
Concernant 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:
Concernant les interfaces, elles ne sont nécessaires que lorsque votre framework DI ou votre framework de test ne peut pas gérer les classes. Vous pouvez les voir comme un péage pour les 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 de la source d'une interface prend beaucoup moins de temps que la lecture de la source de son implémentation.
Selon les abstractions, la création de classes à responsabilité unique et la rédaction de tests unitaires ne sont pas des sciences exactes. Il est parfaitement normal de se balancer trop loin dans une direction lors de l'apprentissage, d'aller à l'extrême, puis de trouver une norme qui a du sens. Il semble que votre pendule ait trop oscillé et pourrait même être bloqué.
Voici où je soupçonne que cela sort des rails:
Les tests unitaires ont été incroyablement difficiles à vendre à l'équipe car ils pensent tous que c'est une perte de temps et qu'ils sont capables de gérer-tester leur code beaucoup plus rapidement dans son ensemble que chaque pièce individuellement. L'utilisation de tests unitaires comme approbation pour SOLID a été principalement futile et est surtout devenue une blague à ce stade.
L'un des avantages qui vient de la plupart des principes SOLID (certainement pas le seul avantage) est qu'il facilite l'écriture de tests unitaires pour notre code. Si une classe dépend d'une abstraction, nous pouvons nous moquer des abstractions Les abstractions qui sont séparées sont plus faciles à simuler. Si une classe fait une chose, elle sera probablement moins complexe, 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 choses liées se produisent:
Premièrement, ils font beaucoup de travail supplémentaire pour créer toutes ces interfaces et classes sans réaliser tous les avantages. Il faut un peu de temps et de pratique pour voir comment la rédaction de tests unitaires nous facilite la vie. Il y a des raisons pour lesquelles les gens qui apprennent à écrire des tests unitaires s'y tiennent, mais vous devez persister assez longtemps pour les découvrir par vous-même. Si votre équipe ne tente pas cela, elle aura l'impression que le reste du travail supplémentaire qu'elle fait est inutile.
Par exemple, que se passe-t-il lorsqu'ils ont besoin de refactoriser? S'ils ont une centaine de petites classes mais aucun test pour leur dire si leurs changements 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 la quantité d'abstraction dont votre code a réellement besoin. Comme je l'ai dit, ce n'est pas une science. Nous commençons mal, virons partout, et nous nous améliorons. Les tests unitaires ont une façon 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 de minuscules abstractions et classes. Mais si vous n'écrivez pas les tests, comment savoir? Jusqu'où allons-nous? Nous pouvons devenir obsédés par la rupture de plus en plus petite. C'est un trou de lapin. La possibilité d'écrire des tests pour notre code nous aide à voir quand nous avons atteint notre objectif afin que nous puissions cesser d'être obsédés, passer à autre chose et nous amuser à écrire plus de code.
Les tests unitaires ne sont pas une solution miracle qui résout tout, mais ils sont une solution vraiment géniale qui améliore la vie des développeurs. Nous ne sommes pas parfaits, et nos tests non plus. Mais les tests nous donnent confiance. Nous nous attendons à ce que notre code soit correct et nous sommes surpris quand il est incorrect, 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 mordre les ongles lorsque notre code est déployé et de nous demander ce qui va casser cette fois et si ce sera de notre faute.
En plus de cela, une fois que nous avons compris, l'écriture de tests unitaires rend le développement de code plus rapide et non plus lent. Nous passons moins de temps à revoir l'ancien code ou à déboguer pour trouver des problèmes qui sont comme 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 lubie ou une huile de serpent. C'est vrai. De nombreux développeurs en témoigneront. Si votre équipe n'a pas vécu cela, elle doit passer à travers cette courbe d'apprentissage et surmonter la bosse. Donnez-lui une chance, en réalisant qu'ils n'obtiendront pas de résultats instantanément. Mais quand cela arrivera, ils seront heureux de l'avoir fait et ils 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 en programmation accumulées sont une perte de temps.)
Depuis le changement, l'une des plus grandes plaintes des développeurs est qu'ils ne supportent pas la révision par des pairs et la traversée de dizaines et de dizaines de fichiers alors qu'auparavant, chaque tâche ne nécessitait que le développeur touche 5 à 10 fichiers.
L'examen par les pairs est beaucoup plus facile lorsque tous les tests unitaires réussissent et qu'une grande partie de cet examen consiste simplement à s'assurer que les tests sont significatifs.