J'ai une méthode qui met à jour les données des employés dans la base de données. La classe Employee
est immuable, donc "mettre à jour" l'objet signifie réellement instancier un nouvel objet.
Je veux que la méthode Update
renvoie une nouvelle instance de Employee
avec les données mises à jour, mais depuis, je peux dire que la responsabilité de la méthode est de mettre à jour les données des employés, et récupérer dans la base de données un nouvel objet Employee
, cela viole-t-il le principe de responsabilité unique =?
L'enregistrement DB lui-même est mis à jour. Ensuite, un nouvel objet est instancié pour représenter cet enregistrement.
Comme pour toute règle, je pense que l'important ici est de considérer le but de la règle, l'esprit, et de ne pas s'embourber dans l'analyse exacte de la façon dont la règle a été formulée dans certains manuels et comment l'appliquer à ce cas. Nous n'avons pas besoin d'aborder cela comme des avocats. Le but des règles est de nous aider à écrire de meilleurs programmes. Ce n'est pas comme si le but d'écrire des programmes était de faire respecter les règles.
Le but de la règle de la responsabilité unique est de rendre les programmes plus faciles à comprendre et à maintenir en faisant en sorte que chaque fonction fasse une chose autonome et cohérente.
Par exemple, j'ai écrit une fois une fonction que j'ai appelée quelque chose comme "checkOrderStatus", qui déterminait si une commande était en attente, expédiée, en attente, peu importe, et renvoyait un code indiquant lequel. Puis un autre programmeur est venu et a modifié cette fonction pour mettre également à jour la quantité en stock lors de l'expédition de la commande. Cela violait gravement le principe de la responsabilité unique. Un autre programmeur lisant ce code plus tard verrait le nom de la fonction, verrait comment la valeur de retour a été utilisée et pourrait bien ne jamais soupçonner qu'il a fait une mise à jour de la base de données. Quelqu'un qui avait besoin d'obtenir le statut de la commande sans mettre à jour la quantité disponible serait dans une position délicate: devrait-il écrire une nouvelle fonction qui duplique la partie du statut de la commande? Ajouter un indicateur pour lui dire s'il faut faire la mise à jour de la base de données? Etc. (Bien sûr, la bonne réponse serait de casser la fonction en deux, mais cela pourrait ne pas être pratique pour de nombreuses raisons.)
D'un autre côté, je ne taperais pas sur ce qui constitue "deux choses". J'ai récemment écrit une fonction qui envoie des informations client de notre système au système de notre client. Cette fonction effectue un certain formatage des données pour répondre à leurs besoins. Par exemple, nous avons certains champs qui peuvent être nuls dans notre base de données, mais ils n'autorisent pas les nuls, nous devons donc remplir du texte factice, "non spécifié" ou j'oublie les mots exacts. On peut dire que cette fonction fait deux choses: reformater les données ET les envoyer. Mais j'ai délibérément mis cela dans une seule fonction plutôt que d'avoir "reformater" et "envoyer" parce que je ne veux jamais, jamais envoyer sans reformater. Je ne veux pas que quelqu'un écrive un nouvel appel et ne réalise pas qu'il doit appeler reformater puis envoyer.
Dans votre cas, mettre à jour la base de données et renvoyer une image de l'enregistrement écrit semble être deux choses qui pourraient bien aller de pair logiquement et inévitablement. Je ne connais pas les détails de votre candidature, je ne peux donc pas dire avec certitude si c'est une bonne idée ou non, mais cela semble plausible.
Si vous créez un objet en mémoire qui contient toutes les données de l'enregistrement, effectuez les appels de base de données pour écrire ceci, puis renvoyez l'objet, cela a beaucoup de sens. Vous avez l'objet entre vos mains. Pourquoi ne pas simplement le rendre? Si vous ne renvoyez pas l'objet, comment l'appelant l'obtiendrait-il? Aurait-il à lire la base de données pour obtenir l'objet que vous venez d'écrire? Cela semble plutôt inefficace. Comment trouverait-il le disque? Connaissez-vous la clé primaire? Si quelqu'un déclare qu'il est "légal" que la fonction d'écriture retourne la clé primaire afin que vous puissiez relire l'enregistrement, pourquoi ne pas simplement renvoyer l'enregistrement entier pour que vous n'ayez pas à le faire? Quelle est la différence?
D'un autre côté, si la création de l'objet est un travail assez différent de l'écriture de l'enregistrement de base de données, et qu'un appelant pourrait bien vouloir écrire mais ne pas créer l'objet, cela pourrait être un gaspillage. Si un appelant peut vouloir l'objet mais ne pas écrire, alors vous devrez fournir une autre façon d'obtenir l'objet, ce qui pourrait signifier écrire du code redondant.
Mais je pense que le scénario 1 est plus probable, donc je dirais, probablement pas de problème.
Le concept du SRP est d'empêcher les modules de faire 2 choses différentes lorsque les faire individuellement permet une meilleure maintenance et moins de spaghetti dans le temps. Comme le dit le PÉR, "une raison de changer".
Dans votre cas, si vous aviez une routine qui "met à jour et retourne l'objet mis à jour", vous changez toujours l'objet une fois - lui donnant 1 raison de changer. Que vous renvoyiez l'objet tout de suite n'est ni ici ni là-bas, vous travaillez toujours sur cet objet unique. Le code a la responsabilité d'une seule et unique chose.
Le SRP ne consiste pas vraiment à réduire toutes les opérations à un seul appel, mais à réduire ce sur quoi vous travaillez et comment vous travaillez dessus. Donc, une seule mise à jour qui renvoie l'objet mis à jour est très bien.
Comme toujours, c'est une question de degré. Le SRP devrait vous empêcher d'écrire une méthode qui récupère un enregistrement d'une base de données externe, effectue une transformation de Fourier rapide dessus et met à jour un registre de statistiques globales avec le résultat. Je pense que presque tout le monde conviendrait que ces choses devraient être faites par différentes méthodes. Postuler une responsabilité nique pour chaque méthode est simplement la façon la plus économique et la plus mémorable de faire valoir ce point.
À l'autre extrémité du spectre se trouvent des méthodes qui fournissent des informations sur l'état d'un objet. Le isActive
typique a la responsabilité de donner ces informations. Tout le monde est probablement d'accord pour dire que tout va bien.
Maintenant, certains étendent le principe jusqu'à ce qu'ils envisagent de renvoyer un indicateur de réussite une responsabilité différente de l'exécution de l'action dont le succès est signalé. Selon une interprétation extrêmement stricte, cela est vrai, mais comme l'alternative serait d'appeler une deuxième méthode pour obtenir le statut de réussite, ce qui complique l'appelant, de nombreux programmeurs sont parfaitement en mesure de renvoyer les codes de réussite d'une méthode avec des effets secondaires.
Rendre le nouvel objet est un pas de plus sur la voie de multiples responsabilités. Obliger l'appelant à effectuer un deuxième appel pour un objet entier est légèrement plus raisonnable que d'exiger un deuxième appel juste pour voir si le premier a réussi ou non. Pourtant, de nombreux programmeurs envisageraient de renvoyer parfaitement le résultat d'une mise à jour. Bien que cela puisse être interprété comme deux responsabilités légèrement différentes, ce n'est certainement pas l'un des abus flagrants qui a inspiré le principe pour commencer.
Est-ce que cela viole le principe de responsabilité unique?
Pas nécessairement. Si quoi que ce soit, cela viole le principe de la séparation commande-requête .
La responsabilité est
mettre à jour les données des employés
étant entendu implicitement que cette responsabilité comprend le statut de l'opération; par exemple, si l'opération échoue, une exception est levée, si elle réussit, l'employé de mise à jour est renvoyé, etc.
Encore une fois, tout cela est une question de degré et de jugement subjectif.
Alors qu'en est-il de la séparation commande-requête?
Eh bien, ce principe existe, mais renvoyer le résultat d'une mise à jour est en fait très courant.
(1) Java Set#add(E)
ajoute l'élément et renvoie son inclusion précédente dans l'ensemble.
if (visited.add(nodeToVisit)) {
// perform operation once
}
C'est plus efficace que l'alternative CQS, qui peut avoir à effectuer deux recherches.
if (!visited.contains(nodeToVisit)) {
visited.add(nodeToVisit);
// perform operation once
}
(2) Compare-and-swap , fetch-and-add , et test-and-set sont des primitives courantes qui permettent la programmation simultanée de exister. Ces modèles apparaissent souvent, des instructions CPU de bas niveau aux collections simultanées de haut niveau.
La responsabilité unique concerne une classe n'ayant pas besoin de changer pour plus d'une raison.
Par exemple, un employé a une liste de numéros de téléphone. Lorsque la façon dont vous gérez les numéros de téléphone change (vous pouvez ajouter des indicatifs de pays), cela ne devrait pas du tout changer la classe des employés.
Je n'aurais pas besoin que la classe des employés sache comment elle se sauvegarde dans la base de données, car elle changerait alors pour les changements d'employés et les changements dans la façon dont les données sont stockées.
De même, il ne devrait pas y avoir de méthode CalculateSalary dans la classe des employés. Une propriété salariale est acceptable, mais le calcul de l'impôt, etc. devrait être fait ailleurs.
Mais que la méthode Update renvoie ce qu'elle vient de mettre à jour, c'est bien.
Wrt. le cas spécifique:
Employee Update(Employee, name, password, etc)
(utilisant en fait un Builder car j'ai beaucoup de paramètres).
Il semble la méthode Update
prend un Employee
existant comme premier paramètre pour identifier (?) L'employé existant et un ensemble de paramètres à modifier sur cet employé.
Je fais pense que cela pourrait être fait plus propre. Je vois deux cas:
(a)Employee
contient en fait une base de données/un identifiant unique par lequel il peut toujours être identifié dans la base de données. (Autrement dit, vous n'avez pas besoin de l'ensemble des valeurs d'enregistrement définies pour le trouver dans la base de données.
Dans ce cas, je préférerais une méthode void Update(Employee obj)
, qui trouve simplement l'enregistrement existant par ID puis met à jour les champs de l'objet transmis. Ou peut-être une void Update(ID, EmployeeBuilder values)
Une variante de cela que j'ai trouvé utile est de n'avoir qu'une méthode void Put(Employee obj)
qui insère ou met à jour, selon que l'enregistrement (par ID) existe.
(b) L'enregistrement existant complet est nécessaire pour la recherche de base de données, dans ce cas, il pourrait être plus judicieux d'avoir: void Update(Employee existing, Employee newData)
.
Pour autant que je puisse voir ici, je dirais en effet que la responsabilité de construire un nouvel objet (ou des valeurs de sous-objet) pour le stocker et le stocker est orthogonale, donc je les séparerais.
Les exigences simultanées mentionnées dans d'autres réponses (atomic set-and-retrieve/compare-swap, etc.) n'ont pas été un problème dans le code DB sur lequel j'ai travaillé jusqu'à présent. Lorsque je parle à une base de données, je pense que cela devrait être géré au niveau de la transaction normalement, pas au niveau de l'instruction individuelle. (Cela ne veut pas dire qu'il pourrait ne pas y avoir de conception où je suis "atomique" Employee[existing data] Update(ID, Employee newData)
ne pouvait pas avoir de sens, mais avec les accès DB, ce n'est pas quelque chose que je vois normalement.)
Jusqu'à présent, tout le monde ici parle de cours. Mais pensez-y du point de vue de l'interface.
Si une méthode d'interface déclare un type de retour et/ou une promesse implicite sur la valeur de retour, chaque implémentation doit renvoyer l'objet mis à jour.
Vous pouvez donc demander:
Pensez également aux objets fantaisie pour les tests unitaires. Évidemment, il sera plus facile de se moquer sans la valeur de retour. Et les composants dépendants sont plus faciles à tester, si leurs dépendances injectées ont des interfaces plus simples.
Sur la base de ces considérations, vous pouvez prendre une décision.
Et si vous le regrettez plus tard, vous pouvez toujours introduire une deuxième interface, avec des adaptateurs entre les deux. Bien sûr, ces interfaces supplémentaires augmentent la complexité de la composition, c'est donc tout un compromis.
Votre approche est bonne. L'immuabilité est une affirmation forte. La seule chose que je demanderais est: Y a-t-il un autre endroit où vous construisez l'objet. Si votre objet n'était pas immuable, vous devrez répondre à des questions supplémentaires car "État" est introduit. Et le changement d'état d'un objet peut se produire pour différentes raisons. Vous devez alors connaître vos cas et ils ne doivent pas être redondants ou divisés.