Il semble assez clair que le "principe de responsabilité unique" ne signifie pas "ne fait qu'une seule chose". C'est à cela que servent les méthodes.
public Interface CustomerCRUD
{
public void Create(Customer customer);
public Customer Read(int CustomerID);
public void Update(Customer customer);
public void Delete(int CustomerID);
}
Bob Martin dit que "les cours ne devraient avoir qu'une seule raison de changer". Mais il est difficile de se faire une idée si vous êtes un programmeur nouveau dans SOLID.
J'ai écrit ne réponse à une autre question , où j'ai suggéré que les responsabilités sont comme des titres de poste, et j'ai dansé autour du sujet en utilisant une métaphore de restaurant pour illustrer mon propos. Mais cela n'articule toujours pas un ensemble de principes que quelqu'un pourrait utiliser pour définir les responsabilités de ses classes.
Alors comment tu fais? Comment déterminez-vous les responsabilités que chaque classe devrait avoir et comment définissez-vous une responsabilité dans le contexte du PÉR?
Une façon de vous en tenir compte est d'imaginer les changements potentiels des exigences dans les projets futurs et de vous demander ce que vous devrez faire pour les réaliser.
Par exemple:
Nouvelle exigence commerciale: les utilisateurs situés en Californie bénéficient d'une remise spéciale.
Exemple de "bon" changement: j'ai besoin de modifier le code dans une classe qui calcule les remises.
Exemple de mauvais changements: j'ai besoin de modifier le code dans la classe User, et ce changement aura un effet en cascade sur les autres classes qui utilisent la classe User, y compris les classes qui n'ont rien à voir avec les remises, par exemple inscription, dénombrement et gestion.
Ou:
Nouvelle exigence non fonctionnelle: nous allons commencer à utiliser Oracle au lieu de SQL Server
Exemple de bon changement: il suffit de modifier une seule classe dans la couche d'accès aux données qui détermine comment conserver les données dans les DTO.
Mauvais changement: je dois modifier toutes mes classes de couche métier car elles contiennent une logique spécifique à SQL Server.
L'idée est de minimiser l'empreinte des futurs changements potentiels, en limitant les modifications de code à une zone de code par zone de changement.
Au minimum, vos cours devraient séparer les préoccupations logiques des préoccupations physiques. Vous trouverez un grand nombre d'exemples dans le System.IO
espace de noms: on y trouve différents types de flux physiques (par exemple FileStream
, MemoryStream
ou NetworkStream
) et divers lecteurs et écrivains (BinaryWriter
, TextWriter
) qui fonctionnent à un niveau logique. En les séparant de cette façon, nous évitons l'explosion combinatoire: au lieu d'avoir besoin de FileStreamTextWriter
, FileStreamBinaryWriter
, NetworkStreamTextWriter
, NetworkStreamBinaryWriter
, MemoryStreamTextWriter
et MemoryStreamBinaryWriter
, il vous suffit de brancher l'écrivain et le flux et vous pouvez avoir ce que vous voulez. Plus tard, nous pouvons ajouter, disons, un XmlWriter
, sans avoir besoin de le réimplémenter séparément pour la mémoire, le fichier et le réseau.
En pratique, les responsabilités sont limitées par les choses qui sont probablement à changer. Ainsi, il n'y a pas de moyen scientifique ou formel pour arriver à ce qui constitue une responsabilité, malheureusement. C'est un appel au jugement.
Il s'agit de ce qui, selon votre expérience, est susceptible de changer.
Nous avons tendance à appliquer le langage du principe dans une rage hyperbolique, littérale et zélée. Nous avons tendance à diviser les classes car elles pourraient changer, ou selon des lignes qui nous aident simplement à résoudre les problèmes. (La dernière raison n'est pas intrinsèquement mauvaise.) Mais, le SRP n'existe pas pour lui-même; il sert à créer logiciel maintenable.
Encore une fois, si les divisions ne sont pas entraînées par les changements de probablement, elles ne sont pas vraiment en service pour le SRP1 si YAGNI est plus applicable. Les deux servir le même objectif ultime. Et les deux sont des questions de jugement - si tout va bien chevronné jugement.
Lorsque l'oncle Bob écrit à ce sujet, il suggère que nous pensons à la "responsabilité" en termes de "qui demande le changement". En d'autres termes, nous ne voulons pas que le parti A perde son emploi parce que le parti B a demandé un changement.
Lorsque vous écrivez un module logiciel, vous voulez vous assurer que lorsque des modifications sont demandées, ces modifications ne peuvent provenir que d'une seule personne, ou plutôt d'un groupe de personnes étroitement couplé représentant une seule fonction métier étroitement définie. Vous souhaitez isoler vos modules de la complexité de l'organisation dans son ensemble et concevoir vos systèmes de telle sorte que chaque module soit responsable (répond) aux besoins de cette seule fonction métier. ( Oncle Bob - Le principe de responsabilité unique )
Les bons développeurs expérimentés auront une idée des changements probables. Et cette liste mentale variera quelque peu selon l'industrie et l'organisation.
Ce qui constitue une responsabilité dans votre application particulière, dans votre organisation particulière, est finalement une question de jugement chevronné. Il s'agit de ce qui est susceptible de changer. Et, dans un sens, il s'agit de à qui appartient la logique interne du module.
1. Pour être clair, cela ne signifie pas que ce sont mauvais divisions. Il peut s'agir de divisions super qui améliorent considérablement la lisibilité du code. Cela signifie simplement qu'ils ne sont pas pilotés par le SRP.
Je suis "les cours ne devraient avoir qu'une seule raison de changer".
Pour moi, cela signifie penser à des schémas farfelus que mon propriétaire de produit pourrait proposer ("Nous devons prendre en charge le mobile!", "Nous devons aller dans le cloud!", "Nous devons prendre en charge le chinois!"). De bonnes conceptions limiteront l'impact de ces programmes sur des zones plus petites et les rendront relativement faciles à réaliser. Les mauvaises conceptions signifient aller à beaucoup de code et faire un tas de changements risqués.
L'expérience est la seule chose que j'ai trouvée pour évaluer correctement la probabilité de ces projets fous - car en rendre un simple pourrait en rendre deux plus difficiles - et évaluer la qualité d'un design. Les programmeurs expérimentés peuvent imaginer ce qu'ils devraient faire pour changer le code, ce qui traîne pour les mordre dans le cul et quelles astuces facilitent les choses. Les programmeurs expérimentés ont une bonne idée de comment ils sont vissés lorsque le propriétaire du produit demande des trucs fous.
Pratiquement, je trouve que les tests unitaires aident ici. Si votre code est inflexible, il sera difficile à tester. Si vous ne pouvez pas injecter de maquette ou d'autres données de test, vous ne pourrez probablement pas injecter ce code SupportChinese
.
Une autre mesure approximative est le pas de l'ascenseur. Les emplacements d'ascenseurs traditionnels sont "si vous étiez dans un ascenseur avec un investisseur, pouvez-vous lui vendre une idée?". Les startups doivent avoir des descriptions simples et brèves de ce qu'elles font - de quoi elles se concentrent. De même, les classes (et fonctions) devraient avoir une description simple de ce qu'elles font. Pas "cette classe implémente une fubar telle que vous pouvez l'utiliser dans ces scénarios spécifiques". Quelque chose que vous pouvez dire à un autre développeur: "Cette classe crée des utilisateurs". Si vous ne pouvez pas communiquer cela à d'autres développeurs, vous êtes allez pour obtenir des bugs.
Personne ne sait. Ou du moins, nous ne pouvons pas nous mettre d'accord sur une définition. C'est ce qui rend le SPR (et d'autres SOLID) assez controversé.
Je dirais que le fait de savoir ce qui est ou n'est pas une responsabilité est l'une des compétences que le développeur de logiciels doit apprendre au cours de sa carrière. Plus vous écrivez et révisez de code, plus vous aurez d'expérience pour déterminer si quelque chose est à responsabilités uniques ou multiples. Ou si la responsabilité unique est divisée entre différentes parties du code.
Je dirais que le principal objectif du PÉR n'est pas d'être une règle stricte. C'est pour nous rappeler de garder à l'esprit la cohésion du code et de toujours faire un effort conscient pour déterminer quel code est cohérent et ce qui ne l'est pas.
Je pense que le terme "responsabilité" est utile comme métaphore, car il nous permet d'utiliser le logiciel pour déterminer dans quelle mesure le logiciel est organisé. En particulier, je me concentrerais sur deux principes:
Ces deux principes nous permettent de distribuer la responsabilité de manière significative car ils se jouent l'un l'autre. Si vous autorisez un morceau de code à faire quelque chose pour vous, il doit avoir la responsabilité de ce qu'il fait. Cela entraîne la responsabilité qu'une classe pourrait avoir à se développer, élargissant c'est "une raison de changer" à des étendues de plus en plus larges. Cependant, à mesure que vous élargissez les choses, vous commencez naturellement à rencontrer des situations où plusieurs entités sont responsables de la même chose. C'est un problème de responsabilité dans la vie réelle, donc c'est aussi un problème de codage. Par conséquent, ce principe entraîne une réduction des étendues, lorsque vous subdivisez la responsabilité en parcelles non dupliquées.
En plus de ces deux, un troisième principe semble raisonnable:
Considérez un programme fraîchement lancé ... une ardoise vierge. Au début, vous n'avez qu'une seule entité, qui est le programme dans son ensemble. Il est responsable de ... tout. Naturellement, à un moment donné, vous commencerez à déléguer la responsabilité à des fonctions ou à des classes. À ce stade, les deux premières règles entrent en jeu, vous obligeant à équilibrer cette responsabilité. Le programme de haut niveau est toujours responsable de la production globale, tout comme un gestionnaire est responsable de la productivité de son équipe, mais chaque sous-entité s'est vu déléguer la responsabilité et, avec elle, le pouvoir de s'acquitter de cette responsabilité.
En prime, cela rend SOLID particulièrement compatible avec tout développement de logiciels d'entreprise que vous pourriez avoir besoin de faire. Chaque entreprise sur la planète a une certaine idée de la façon de déléguer la responsabilité, et ils ne le font pas tous Si vous déléguez la responsabilité au sein de votre logiciel d'une manière qui rappelle la délégation de votre entreprise, il sera beaucoup plus facile pour les futurs développeurs de se familiariser avec la façon dont vous faites les choses dans cette entreprise.
Dans cette conférence à Yale, Oncle Bob donne cet exemple drôle :
Il dit que Employee
a trois raisons de changer, trois sources d'exigences de changement, et donne à cela humoristique et ironique , mais illustrative néanmoins, explication:
Si la méthode
CalcPay()
a une erreur et coûte des millions de dollars à l'entreprise, le CFO vous licenciera .Si la méthode
ReportHours()
a une erreur et coûte des millions de dollars à l'entreprise, le COO vous licenciera .Si la
WriteEmmployee(
) a une erreur qui provoque l'effacement de beaucoup de données et coûte à l'entreprise des millions de US $, le CTO vous licenciera .Donc avoir trois exécutifs de niveau C différents pouvant vous renvoyer pour des erreurs coûteuses dans la même classe signifie que la classe a trop de responsabilités.
Il donne cette solution qui résout la violation de SRP, mais doit encore résoudre la violation de DIP qui n'est pas montrée dans la vidéo.
Le "principe de responsabilité unique" est peut-être un nom déroutant. "Une seule raison de changer" est une meilleure description du principe, mais est toujours facile à comprendre. Nous ne parlons pas de dire ce qui fait que les objets changent d'état au moment de l'exécution. Nous étudions ce qui pourrait amener les développeurs à changer le code à l'avenir.
Sauf si nous corrigeons un bogue, le changement sera dû à une exigence commerciale nouvelle ou modifiée. Vous devrez penser en dehors du code lui-même, et imaginer quels facteurs extérieurs pourraient faire changer les exigences indépendamment . Dire:
Idéalement, vous souhaitez que des facteurs indépendants affectent différentes classes. Par exemple. étant donné que les taux d'imposition changent indépendamment des noms de produits, les modifications ne devraient pas affecter les mêmes classes. Sinon, vous courez le risque d'une modification de la taxe introduisant une erreur de dénomination du produit, qui est le type de couplage étroit que vous souhaitez éviter avec un système modulaire.
Donc, ne vous concentrez pas uniquement sur ce qui pourrait changer - tout ce qui pourrait éventuellement changer à l'avenir. Concentrez-vous sur ce qui pourrait changer indépendamment. Les changements sont généralement indépendants s'ils sont causés par différents acteurs.
Votre exemple avec les titres d'emploi est sur la bonne voie, mais vous devriez le prendre plus littéralement! Si le marketing peut entraîner des modifications du code et que la finance peut entraîner d'autres modifications, ces modifications ne doivent pas affecter le même code, car ce sont des titres d'emploi différents et, par conséquent, les modifications se produisent indépendamment.
Pour citer l'oncle Bob qui a inventé le terme:
Lorsque vous écrivez un module logiciel, vous voulez vous assurer que lorsque des modifications sont demandées, ces modifications ne peuvent provenir que d'une seule personne, ou plutôt d'un groupe de personnes étroitement couplé représentant une seule fonction métier étroitement définie. Vous souhaitez isoler vos modules de la complexité de l'organisation dans son ensemble et concevoir vos systèmes de telle sorte que chaque module soit responsable (répond) aux besoins de cette seule fonction métier.
Pour résumer: une "responsabilité" correspond à une seule fonction commerciale. Si plus d'un acteur peut vous obliger à changer de classe, alors la classe rompt probablement ce principe.
Je pense qu'une meilleure façon de subdiviser les choses que les "raisons de changer" est de commencer par penser s'il serait logique d'exiger que le code qui doit effectuer deux actions (ou plus) doive contenir une référence d'objet distincte. pour chaque action, et s'il serait utile d'avoir un objet public qui pourrait effectuer une action mais pas l'autre.
Si les réponses aux deux questions sont oui, cela suggère que les actions devraient être effectuées par des classes séparées. Si les réponses aux deux questions sont non, cela suggérerait que d'un point de vue public, il devrait y avoir une classe; si le code pour cela serait difficile à manier, il peut être subdivisé en interne en classes privées. Si la réponse à la première question est non, mais la seconde est oui, il devrait y avoir une classe distincte pour chaque action plus une classe composite qui comprend des références aux instances des autres.
Si l'on a des classes distinctes pour le clavier, le bip, la lecture numérique, l'imprimante de reçus et le tiroir-caisse d'une caisse enregistreuse, et qu'il n'y a pas de classe composite pour une caisse enregistreuse complète, le code qui est censé traiter une transaction peut finir par être invoqué accidentellement dans un manière qui prend l'entrée du clavier d'une machine, produit du bruit à partir du bip d'une deuxième machine, affiche les chiffres sur l'écran d'une troisième machine, imprime un reçu sur l'imprimante d'une quatrième machine et ouvre le tiroir-caisse d'une cinquième machine. Chacune de ces sous-fonctions peut être utilement gérée par une classe distincte, mais il devrait également y avoir une classe composite qui les joint. La classe composite devrait déléguer autant de logique que possible aux classes constituantes, mais devrait, lorsque cela est pratique, encapsuler les fonctions de ses composants constituants plutôt que d'exiger que le code client accède directement aux constituants.
On pourrait dire que la "responsabilité" de chaque classe consiste soit à incorporer une logique réelle, soit à fournir un point d'attache commun à plusieurs autres classes qui le font, mais ce qui est important est de se concentrer d'abord et avant tout sur la façon dont le code client doit afficher une classe. S'il est logique que le code client voit quelque chose comme un seul objet, alors le code client doit le voir comme un seul objet.
SRP est difficile à obtenir correctement. Il s'agit principalement d'affecter des "tâches" à votre code et de vous assurer que chaque partie a des responsabilités claires. Comme dans la vraie vie, dans certains cas, la répartition du travail entre les gens peut être assez naturelle, mais dans d'autres cas, cela peut être vraiment délicat, surtout si vous ne les connaissez pas (ou le travail).
Je vous recommande toujours juste écrivez du code simple qui fonctionne en premier, puis refactorisez un peu: vous aurez tendance à voir comment le code se structure naturellement après un certain temps. Je pense que c'est une erreur de forcer les responsabilités avant de connaître le code (ou les gens) et le travail à faire.
Une chose que vous remarquerez est lorsque le module commence à en faire trop et est difficile à déboguer/maintenir. C'est le moment de refactoriser; quel devrait être le travail principal et quelles tâches pourraient être confiées à un autre module? Par exemple, doit-il gérer les contrôles de sécurité et les autres travaux, ou devez-vous d'abord effectuer des contrôles de sécurité ailleurs, ou cela rendra-t-il le code plus complexe?
Utilisez trop d'indirections et cela redevient un gâchis ... comme pour d'autres principes, celui-ci sera en conflit avec d'autres, comme KISS, YAGNI, etc. Tout est une question d'équilibre.
Un bon article qui explique les principes de programmation SOLID et donne des exemples de code suivant et non ces principes est https://scotch.io/bar-talk/solid-the -premier-cinq-principes-de-conception-orientée objet .
Dans l'exemple relatif à la SRP, il donne un exemple de quelques classes de formes (cercle et carré) et d'une classe conçue pour calculer l'aire totale de plusieurs formes.
Dans son premier exemple, il crée la classe de calcul de zone et lui fait retourner sa sortie en HTML. Plus tard, il décide à la place de l'afficher en JSON et doit changer sa classe de calcul de zone.
Le problème avec cet exemple est que sa classe de calcul d'aire est responsable de calculer l'aire des formes ET d'afficher cette aire. Il passe ensuite à travers une meilleure façon de le faire en utilisant une autre classe spécialement conçue pour afficher les zones.
Il s'agit d'un exemple simple (et plus facile à comprendre en lisant l'article car il contient des extraits de code), mais illustre l'idée de base de SRP.
Dans mon esprit, la chose la plus proche d'un SRP qui me vient à l'esprit est un flux d'utilisation. Si vous n'avez pas de flux d'utilisation clair pour une classe donnée, c'est probablement que votre classe a une odeur de conception.
Un flux d'utilisation serait une succession d'appels de méthode donnée qui vous donnerait un résultat attendu (donc testable). Vous définissez essentiellement une classe avec les cas d'utilisation obtenus à mon humble avis, c'est pourquoi toute la méthodologie du programme se concentre sur les interfaces plutôt que sur la mise en œuvre.
C'est pour réaliser que plusieurs changements d'exigences, ne nécessitent pas que votre composant change .
Mais bonne chance pour comprendre cela à première vue, lorsque vous entendez parler de SOLID pour la première fois.
Je vois beaucoup de commentaires disant SRP et YAGNI peuvent se contredire, mais - YAGN J'ai appliqué par TDD (GOOS, London School) m'a appris à penser à et concevoir mes composants du point de vue du client. J'ai commencé à concevoir mes interfaces par ce qui est le moins que tout client voudrait qu'il fasse, c'est le peu qu'il devrait faire. Et cet exercice peut être fait sans aucune connaissance de TDD.
J'aime la technique décrite par l'oncle Bob (je ne me souviens pas d'où, malheureusement), qui va quelque chose comme:
Demandez-vous, que fait ce cours?
Votre réponse contenait-elle soit Et ou Ou
Si oui, extrayez cette partie de la réponse, c'est une responsabilité
Cette technique est un absolu, et comme @svidgen l'a dit, SRP est un appel au jugement, mais lorsque vous apprenez quelque chose de nouveau, les absolus sont les meilleurs, il est plus facile de juste toujours faire quelque chose. Assurez-vous que la raison pour laquelle vous ne vous séparez pas est; une estimation éclairée, et pas parce que vous ne savez pas comment. C'est l'art et cela demande de l'expérience.
Je pense que beaucoup de réponses semblent faire un argument pour le découplage quand on parle de SRP .
SRP est pas pour vous assurer qu'un changement ne se propage pas dans le graphe des dépendances.
Théoriquement, sans SRP , vous n'auriez pas de dépendances ...
Un changement ne devrait pas entraîner de changement à plusieurs endroits dans l'application, mais nous avons obtenu d'autres principes pour cela. SRP améliore cependant le Open Closed Principle. Ce principe concerne davantage l'abstraction, cependant les abstractions plus petites sont plus faciles à réimplémenter .
Ainsi, lorsque vous enseignez SOLID dans son ensemble, veillez à enseigner que SRP vous permet de changer moins de code lorsque les exigences changent, alors qu'en fait, cela vous permet d'écrire moins de nouveau code .
Ce que j'essaie de faire pour écrire du code qui suit le SRP:
Exemple:
Problème: obtenez deux nombres de l'utilisateur, calculez leur somme et envoyez le résultat à l'utilisateur:
//first step: solve the problem right away
static void Main(string[] args)
{
Console.WriteLine("Number 1: ");
int firstNumber = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("Number 2: ");
int secondNumber = Convert.ToInt32(Console.ReadLine());
int result = firstNumber + secondNumber;
Console.WriteLine("Hi there! The result is: {0}", result);
Console.ReadLine();
}
Ensuite, essayez de définir les responsabilités en fonction des tâches à effectuer. À partir de cela, extrayez les classes appropriées:
//Responsible for getting two integers from the user
class Input {
public int FirstNumber { get; set; }
public int SecondNumber { get; set; }
public void Read() {
Console.WriteLine("Number 1: ");
FirstNumber = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("Number 2: ");
SecondNumber = Convert.ToInt32(Console.ReadLine());
}
}
//Responsible for calculating the sum of two integers
class SumOperation {
public int Result { get; set; }
public void Calculate(int a, int b) {
Result = a + b;
}
}
//Responsible for the output of some value to the user
class Output {
public void Write(int result) {
Console.WriteLine("Hello! The result is: {0}", result);
}
}
Ensuite, le programme refactorisé devient:
//Program: responsible for main execution.
//Gets two numbers from user and output their sum.
static void Main(string[] args)
{
var input = new Input();
input.Read();
var operation = new SumOperation();
operation.Calculate(input.FirstNumber, input.SecondNumber);
var output = new Output();
output.Write(operation.Result);
Console.ReadLine();
}
Note: cet exemple très simple ne prend en considération que le principe SRP. L'utilisation des autres principes (par exemple: le code "L" devrait dépendre d'abstractions plutôt que de concrétions) apporterait plus d'avantages au code et le rendrait plus maintenable pour les changements commerciaux.
Il n'y a pas de réponse claire à cela. Bien que la question soit étroite, les explications ne le sont pas.
Pour moi, c'est quelque chose comme le rasoir d'Occam si vous le souhaitez. C'est un idéal où j'essaie de mesurer mon code actuel. Il est difficile de le définir avec des mots clairs et simples. Une autre métaphore serait "un sujet" qui est aussi abstrait, c'est-à-dire difficile à saisir, que "responsabilité unique". Une troisième description serait "traitant d'un niveau d'abstraction".
Qu'est-ce que cela signifie pratiquement?
Dernièrement, j'utilise un style de codage qui se compose principalement de deux phases:
La phase I est mieux décrite comme un chaos créatif. Dans cette phase, j'écris du code au fur et à mesure que les pensées circulent - c'est-à-dire brutes et laides.
La phase II est tout le contraire. C'est comme nettoyer après un ouragan. Cela demande le plus de travail et de discipline. Et puis je regarde le code du point de vue d'un concepteur.
Je travaille principalement en Python maintenant, ce qui me permet de penser aux objets et aux classes plus tard. D'abord Phase I - J'écris uniquement des fonctions et je les répartis presque au hasard dans différents modules. Dans Phase II, après avoir commencé, je regarde de plus près quel module traite quelle partie de la solution. Et tout en parcourant les modules, topics = sont émergentes pour moi. Certaines fonctions sont liées par thème. Ce sont de bons candidats pour classes. Et après avoir transformé les fonctions en classes - ce qui est presque fait avec indentation et en ajoutant self
à la liste des paramètres dans python;) - J'utilise SRP
comme Razor d'Occam pour éliminer les fonctionnalités d'autres modules et classes.
Un exemple courant peut être écriture d'une petite fonctionnalité d'exportation l'autre jour.
Il y avait le besoin de csv, Excel et combiné feuilles Excel dans un Zip.
La fonctionnalité simple a été réalisée en trois vues (= fonctions). Chaque fonction a utilisé une méthode commune pour déterminer les filtres et une deuxième méthode pour récupérer les données. Ensuite, dans chaque fonction, la préparation de l'exportation a eu lieu et a été livrée en tant que réponse du serveur.
Il y avait trop de niveaux d'abstraction mélangés:
I) Traitement des demandes/réponses entrantes/sortantes
II) Déterminer les filtres
III) Récupération des données
IV) transformation des données
L'étape la plus simple a été d'utiliser une abstraction (exporter
) pour traiter les calques II-IV dans un premier temps.
Le seul reste était le sujet traitant des demandes/réponses. Au même niveau d'abstraction est extraction des paramètres de requête ce qui est correct. J'avais donc pour cela voir une "responsabilité".
Deuxièmement, j'ai dû séparer l'exportateur qui, comme nous l'avons vu, consistait en au moins trois autres couches d'abstraction.
Détermination des critères de filtre et réels retrival sont presque au même niveau d'abstraction (les filtres sont nécessaires pour obtenir le bon sous-ensemble des données). Ces niveaux ont été mis dans quelque chose comme un couche d'accès aux données.
Dans l'étape suivante, j'ai séparé les mécanismes d'exportation réels: lorsque l'écriture dans un fichier temporel était nécessaire, je l'ai divisée en deux "responsabilités": une pour l'écriture réelle des données sur le disque et une autre partie qui traitait du format réel.
Le long de la formation des classes et des modules, les choses sont devenues plus claires, ce qui appartenait où. Et toujours la question latente, si la classe en fait trop.
Comment déterminez-vous les responsabilités que chaque classe devrait avoir et comment définissez-vous une responsabilité dans le contexte de la SRP?
Il est difficile de donner une recette à suivre. Bien sûr, je pourrais répéter la règle cryptique "un niveau d'abstraction" - si cela aide.
Surtout pour moi, c'est une sorte d '"intuition artistique" qui mène au design actuel; Je modélise le code comme un artiste peut sculpter de l'argile ou peindre.
Imaginez-moi comme Coding Bob Ross;)
Tiré du livre de Robert C.Martins Clean Architecture: A Craftsman's Guide to Software Structure and Design , publié le 10 septembre 2017, Robert écrit à la page 62 ce qui suit:
Historiquement, le SRP a été décrit de cette façon:
Un module devrait en avoir une, et une seule, raison de changer
Les systèmes logiciels sont modifiés pour satisfaire les utilisateurs et les parties prenantes; ces utilisateurs et parties prenantes sont la "raison de changer". dont le principe parle. En effet, on peut reformuler le principe pour dire ceci:
Un module doit être responsable devant un seul et unique utilisateur ou acteur
Malheureusement, le mot "utilisateur" et "partie prenante" n'est pas vraiment le bon mot à utiliser ici. Il y aura probablement plus d'un utilisateur ou d'une partie prenante qui souhaite que le système soit changé de manière saine. Au lieu de cela, nous parlons vraiment d'un groupe - une ou plusieurs personnes qui ont besoin de ce changement. Nous désignerons ce groupe comme un acteur.
Ainsi, la version finale du SRP est:
Un module doit être responsable devant un et un seul acteur.
Il ne s'agit donc pas de code. Le SRP consiste à contrôler le flux d'exigences et de besoins commerciaux, qui ne peuvent provenir que d'une seule source.
Tout d'abord, ce que vous avez est en fait deux problèmes séparés: le problème des méthodes à mettre dans vos classes et le problème de la surcharge de l'interface.
Vous avez cette interface:
public Interface CustomerCRUD
{
public void Create(Customer customer);
public Customer Read(int CustomerID);
public void Update(Customer customer);
public void Delete(int CustomerID);
}
Vraisemblablement, vous avez plusieurs classes qui se conforment à l'interface CustomerCRUD
(sinon une interface n'est pas nécessaire), et une fonction do_crud(customer: CustomerCRUD)
qui accepte un objet conforme. Mais vous avez déjà rompu le SRP: vous avez lié ces quatre opérations distinctes.
Disons que plus tard, vous utiliserez des vues de base de données. Une vue de base de données a seulement la méthode Read
disponible pour elle. Mais vous voulez écrire une fonction do_query_stuff(customer: ???)
qui opère de manière transparente sur des tables ou des vues complètes; après tout, il utilise uniquement la méthode Read
.
Alors créez une interface
interface publique CustomerReader {Public Customer Read (customerID: int)}
et factorisez votre interface CustomerCrud
comme:
public interface CustomerCRUD extends CustomerReader
{
public void Create(Customer customer);
public void Update(Customer customer);
public void Delete(int CustomerID);
}
Mais il n'y a pas de fin en vue. Il pourrait y avoir des objets que nous pouvons créer mais pas mettre à jour, etc. Ce trou de lapin est trop profond. La seule façon sensée d'adhérer au principe de responsabilité unique est de faire en sorte que toutes vos interfaces contiennent exactement une méthode. Go suit en fait cette méthodologie de ce que j'ai vu, avec la grande majorité des interfaces contenant une seule fonction; si vous souhaitez spécifier une interface qui contient deux fonctions, vous devez créer maladroitement une nouvelle interface qui combine les deux. Vous obtenez bientôt une explosion combinatoire d'interfaces.
Le moyen de sortir de ce gâchis est d'utiliser le sous-typage structurel (implémenté dans OCaml par exemple) au lieu des interfaces (qui sont une forme de sous-typage nominal). Nous ne définissons pas d'interfaces; à la place, nous pouvons simplement écrire une fonction
let do_customer_stuff customer = customer.read ... customer.update ...
qui appelle toutes les méthodes que nous aimons. OCaml utilisera l'inférence de type pour déterminer que nous pouvons passer dans n'importe quel objet qui implémente ces méthodes. Dans cet exemple, il serait déterminé que customer
a le type <read: int -> unit, update: int -> unit, ...>
.
Cela résout le désordre interface; mais nous devons encore implémenter des classes qui contiennent plusieurs méthodes. Par exemple, devons-nous créer deux classes différentes, CustomerReader
et CustomerWriter
? Que se passe-t-il si nous voulons changer la façon dont les tables sont lues (par exemple, nous mettons maintenant en cache nos réponses dans redis avant de récupérer les données), mais maintenant comment elles sont écrites? Si vous suivez cette chaîne de raisonnement jusqu'à sa conclusion logique, vous conduisez inextricablement à la programmation fonctionnelle :)