J'écris beaucoup de code qui implique trois étapes de base.
- Obtenez des données quelque part.
- Transformez ces données.
- Mettez ces données quelque part.
Je finis généralement par utiliser trois types de classes - inspirées de leurs modèles de conception respectifs.
- Usines - pour construire un objet à partir d'une ressource.
- Médiateurs - pour utiliser l'usine, effectuer la transformation, puis utiliser le commandant.
- Commandants - pour mettre ces données ailleurs.
Mes classes ont tendance à être assez petites, souvent une seule méthode (publique), par exemple obtenir des données, transformer des données, travailler, enregistrer des données. Cela conduit à une prolifération de classes, mais fonctionne généralement bien.
Là où je me bats, c'est quand je viens aux tests, je me retrouve avec des tests étroitement couplés. Par exemple;
- Usine - lit les fichiers du disque.
- Commander - écrit des fichiers sur le disque.
Je ne peux pas tester l'un sans l'autre. Je pourrais écrire du code «test» supplémentaire pour lire / écrire sur le disque également, mais je me répète.
En regardant .Net, la classe File adopte une approche différente, elle combine les responsabilités (de mon) usine et commandant ensemble. Il a des fonctions pour créer, supprimer, existe et tout lire en un seul endroit.
Dois-je chercher à suivre l'exemple de .Net et combiner - en particulier lorsqu'il s'agit de ressources externes - mes classes ensemble? Le code est toujours couplé, mais il est plus intentionnel - il se produit lors de l'implémentation d'origine, plutôt que dans les tests.
Est-ce que mon problème ici est que j'ai appliqué le principe de responsabilité unique de manière un peu trop zélée? J'ai des classes distinctes responsables de la lecture et de l'écriture. Quand je pourrais avoir une classe combinée chargée de gérer une ressource particulière, par exemple le disque système.
la source
Looking at .Net, the File class takes a different approach, it combines the responsibilities (of my) factory and commander together. It has functions for Create, Delete, Exists, and Read all in one place.
- Notez que vous confondez «responsabilité» avec «chose à faire». Une responsabilité ressemble plus à un «domaine de préoccupation». La responsabilité de la classe File consiste à effectuer des opérations sur les fichiers.File
bibliothèque de C # est, pour tout ce que nous savons, laFile
classe pourrait simplement être une façade, mettant toutes les opérations sur les fichiers en un seul endroit - dans la classe, mais pourrait utiliser en interne une classe de lecture / écriture similaire à la vôtre qui contiennent en fait la logique la plus compliquée pour la gestion des fichiers. Une telle classe (laFile
) adhérerait toujours au SRP, parce que le processus de travail avec le système de fichiers serait abstrait derrière une autre couche - très probablement avec une interface unificatrice. Je ne dis pas que c'est le cas, mais ça pourrait l'être. :)Réponses:
Suivre le principe de la responsabilité unique a peut-être été ce qui vous a guidé ici mais où vous êtes a un nom différent.
Séparation des responsabilités de requête de commande
Allez étudier cela et je pense que vous le trouverez suivant un schéma familier et que vous n'êtes pas seul à vous demander jusqu'où aller. Le test d'acide est si le fait de suivre cela vous apporte de réels avantages ou si c'est juste un mantra aveugle que vous suivez afin que vous n'ayez pas à réfléchir.
Vous avez exprimé votre inquiétude concernant les tests. Je ne pense pas que suivre CQRS empêche d'écrire du code testable. Vous pouvez simplement suivre CQRS d'une manière qui rend votre code non testable.
Il permet de savoir comment utiliser le polymorphisme pour inverser les dépendances du code source sans avoir besoin de modifier le flux de contrôle. Je ne sais pas vraiment où se situe votre compétence en matière de rédaction de tests.
Un mot d'avertissement, suivre les habitudes que vous trouvez dans les bibliothèques n'est pas optimal. Les bibliothèques ont leurs propres besoins et sont franchement vieilles. Ainsi, même le meilleur exemple n'est que le meilleur exemple de l'époque.
Cela ne veut pas dire qu'il n'y a pas d'exemples parfaitement valides qui ne suivent pas le CQRS. Le suivre sera toujours un peu pénible. Ce n'est pas toujours une valeur à payer. Mais si vous en avez besoin, vous serez heureux de l'avoir utilisé.
Si vous l'utilisez, tenez compte de ce mot d'avertissement:
la source
Vous avez besoin d'une perspective plus large pour déterminer si le code est conforme au principe de responsabilité unique. Il ne peut être répondu simplement en analysant le code lui-même, vous devez considérer quelles forces ou quels acteurs pourraient faire évoluer les exigences à l'avenir.
Disons que vous stockez les données d'application dans un fichier XML. Quels facteurs pourraient vous amener à modifier le code lié à la lecture ou à l'écriture? Quelques possibilités:
Dans tous ces cas, vous devrez le changer à la fois la lecture et la logique d'écriture. En d'autres termes, ce ne sont pas des responsabilités séparées.
Mais imaginons un scénario différent: votre application fait partie d'un pipeline de traitement de données. Il lit certains fichiers CSV générés par un système distinct, effectue une analyse et un traitement, puis génère un fichier différent à traiter par un troisième système. Dans ce cas, la lecture et l'écriture sont des responsabilités indépendantes et doivent être découplées.
Conclusion: vous ne pouvez généralement pas dire si la lecture et l'écriture de fichiers sont des responsabilités distinctes, cela dépend des rôles dans l'application. Mais sur la base de votre conseil sur les tests, je suppose que c'est une seule responsabilité dans votre cas.
la source
En général, vous avez la bonne idée.
On dirait que vous avez trois responsabilités. L'OMI, le «médiateur», fait peut-être trop. Je pense que vous devriez commencer par modéliser vos trois responsabilités:
Ensuite, un programme peut être exprimé comme:
Je ne pense pas que ce soit un problème. Beaucoup de petites classes cohésives et testables de l'OMI sont meilleures que les grandes classes moins cohésives.
Chaque pièce doit pouvoir être testée indépendamment. Modélisé ci-dessus, vous pouvez représenter la lecture / écriture dans un fichier comme:
Vous pouvez écrire des tests d'intégration pour tester ces classes afin de vérifier qu'elles lisent et écrivent dans le système de fichiers. Le reste de la logique peut être écrit sous forme de transformations. Par exemple, si les fichiers sont au format JSON, vous pouvez transformer le
String
s.Ensuite, vous pouvez vous transformer en objets appropriés:
Chacun de ces éléments peut être testé indépendamment. Vous pouvez également tester l' unité
program
ci - dessus en se moquantreader
,transformer
etwriter
.la source
FileWriter
en lisant directement à partir du système de fichiers au lieu d'utiliserFileReader
. C'est vraiment à vous de déterminer quels sont vos objectifs. Si vous utilisezFileReader
, le test se cassera si l'unFileReader
ou l' autreFileWriter
est rompu - ce qui peut prendre plus de temps pour déboguer.Donc, l'accent est mis ici sur ce qui les relie . Passez-vous un objet entre les deux (comme un
File
?) Ensuite, c'est le fichier avec lequel ils sont couplés, pas les uns aux autres.De ce que vous avez dit, vous avez séparé vos classes. Le piège est que vous les testez ensemble parce que c'est plus facile ou «logique» .
Pourquoi avez-vous besoin de l'entrée
Commander
pour provenir d'un disque? Tout ce qui compte c'est d'écrire en utilisant une certaine entrée, alors vous pouvez vérifier qu'il a écrit le fichier correctement en utilisant ce qui est dans le test .La partie réelle que vous testez
Factory
est «est-ce qu'il lira correctement ce fichier et affichera la bonne chose»? Donc, moquez le fichier avant de le lire dans le test .Alternativement, tester que Factory et Commander fonctionnent lorsqu'ils sont couplés ensemble est très bien - cela correspond parfaitement aux tests d'intégration. La question ici est plutôt de savoir si vous pouvez ou non les tester séparément.
la source
C'est une approche procédurale typique, dont David Parnas a écrit en 1972. Vous vous concentrez sur la façon dont les choses se passent. Vous prenez la solution concrète de votre problème comme un modèle de niveau supérieur, ce qui est toujours faux.
Si vous poursuivez une approche orientée objet, je préfère me concentrer sur votre domaine . C'est à propos de quoi? Quelles sont les principales responsabilités de votre système? Quels sont les principaux concepts qui se présentent dans la langue de vos experts de domaine? Donc, comprenez votre domaine, décomposez-le, traitez les domaines de responsabilité de niveau supérieur comme vos modules , traitez les concepts de niveau inférieur représentés comme des noms comme vos objets. Voici un exemple que j'ai donné à une question récente, il est très pertinent.
Et il y a un problème évident de cohésion, vous en avez parlé vous-même. Si vous apportez des modifications à une logique d'entrée et écrivez des tests dessus, cela ne prouve en aucun cas que votre fonctionnalité fonctionne, car vous pourriez oublier de passer ces données à la couche suivante. Vous voyez, ces couches sont intrinsèquement couplées. Et un découplage artificiel aggrave encore les choses. Je sais que moi-même: projet de 7 ans avec 100 années-homme derrière mes épaules écrit entièrement dans ce style. Fuyez-le si vous le pouvez.
Et dans l'ensemble SRP. Il s'agit de cohésion appliquée à votre espace problématique, c'est-à-dire au domaine. C'est le principe fondamental derrière SRP. Il en résulte que les objets sont intelligents et assument leurs responsabilités pour eux-mêmes. Personne ne les contrôle, personne ne leur fournit de données. Ils combinent les données et le comportement, exposant uniquement ces derniers. Vos objets combinent donc à la fois la validation des données brutes, la transformation des données (c'est-à-dire le comportement) et la persistance. Cela pourrait ressembler à ceci:
En conséquence, il existe un certain nombre de classes cohérentes représentant certaines fonctionnalités. Notez que la validation va généralement aux objets de valeur - au moins dans l' approche DDD .
la source
Faites attention aux abstractions qui fuient lorsque vous travaillez avec un système de fichiers - je l'ai vu trop souvent négligé, et il présente les symptômes que vous avez décrits.
Si la classe opère sur des données provenant de / allant dans ces fichiers, le système de fichiers devient un détail d'implémentation (E / S) et doit en être séparé. Ces classes (usine / commandant / médiateur) ne devraient pas être au courant du système de fichiers à moins que leur seul travail soit de stocker / lire les données fournies. Les classes qui traitent du système de fichiers doivent encapsuler des paramètres spécifiques au contexte comme les chemins (peuvent être passés par le constructeur), donc l'interface n'a pas révélé sa nature (le mot "Fichier" dans le nom de l'interface est une odeur la plupart du temps).
la source
À mon avis, il semble que vous ayez commencé à emprunter la bonne voie, mais vous ne l'avez pas assez loin. Je pense que diviser la fonctionnalité en différentes classes qui font une chose et le font bien est correct.
Pour aller plus loin, vous devez créer des interfaces pour vos classes Factory, Mediator et Commander. Ensuite, vous pouvez utiliser des versions simulées de ces classes lors de l'écriture de vos tests unitaires pour les implémentations concrètes des autres. Avec les simulations, vous pouvez valider que les méthodes sont appelées dans le bon ordre et avec les bons paramètres et que le code testé se comporte correctement avec différentes valeurs de retour.
Vous pouvez également envisager d'abstraire la lecture / écriture des données. Vous allez maintenant dans un système de fichiers, mais vous voudrez peut-être aller dans une base de données ou même un socket dans le futur. Votre classe de médiateur ne devrait pas avoir à changer si la source / destination des données change.
la source