J'essaie de créer un programme de gestion des employés. Je ne peux cependant pas comprendre comment concevoir la Employee
classe. Mon objectif est de pouvoir créer et manipuler les données des employés sur la base de données à l'aide d'un Employee
objet.
L'implémentation de base à laquelle j'ai pensé était la plus simple:
class Employee
{
// Employee data (let's say, dozens of properties).
Employee() {}
Create() {}
Update() {}
Delete() {}
}
En utilisant cette implémentation, j'ai rencontré plusieurs problèmes.
- La
ID
donnée d'un employé est donnée par la base de données, donc si j'utilise l'objet pour décrire un nouvel employé, il n'y en aura pas encoreID
à stocker, alors qu'un objet représentant un employé existant aura unID
. J'ai donc une propriété qui décrit parfois l'objet et parfois non (ce qui pourrait indiquer que nous violons SRP ? Puisque nous utilisons la même classe pour représenter les employés nouveaux et existants ...). - La
Create
méthode est censée créer un employé sur la base de données, tandis que leUpdate
etDelete
sont censés agir sur un employé existant (Encore une fois, SRP ...). - Quels paramètres la méthode «Créer» devrait-elle avoir? Des dizaines de paramètres pour toutes les données des employés ou peut-être un
Employee
objet? - La classe doit-elle être immuable?
- Comment ça va
Update
marcher? Va-t-il prendre les propriétés et mettre à jour la base de données? Ou peut-être qu'il faudra deux objets - un "ancien" et un "nouveau", et mettre à jour la base de données avec les différences entre eux? (Je pense que la réponse a à voir avec la réponse sur la mutabilité de la classe). - Quelle serait la responsabilité du constructeur? Quels seraient les paramètres nécessaires? Va-t-il récupérer les données des employés de la base de données en utilisant un
id
paramètre et les remplir les propriétés?
Donc, comme vous pouvez le voir, j'ai un peu de gâchis dans ma tête et je suis très confus. Pourriez-vous m'aider à comprendre à quoi devrait ressembler une telle classe?
Veuillez noter que je ne veux pas d'opinions, juste pour comprendre comment une classe aussi fréquemment utilisée est généralement conçue.
Employee
objet pour fournir l'abstraction, les questions 4. et 5. sont généralement sans réponse, dépendent de vos besoins, et si vous séparez la structure et les opérations CRUD en deux classes, alors c'est assez clair, le constructeur duEmployee
ne peut pas récupérer les données de db plus, donc ça répond 6.Update
un employé ou mettez-vous à jour un dossier d'employé? Avez-vousEmployee.Delete()
, ou fait unBoss.Fire(employee)
?Réponses:
Ceci est une transcription plus bien formée de mon commentaire initial sous votre question. Les réponses aux questions posées par le PO se trouvent au bas de cette réponse. Veuillez également vérifier la note importante située au même endroit.
Ce que vous décrivez actuellement, Sipo, est un modèle de conception appelé enregistrement actif . Comme pour tout, même celui-ci a trouvé sa place parmi les programmeurs, mais a été rejeté au profit du référentiel et des modèles de mappeur de données pour une raison simple, l'évolutivité.
En bref, un enregistrement actif est un objet qui:
Vous abordez plusieurs problèmes avec votre conception actuelle et le problème principal de votre conception est résolu au dernier, 6ème point (dernier mais non le moindre, je suppose). Lorsque vous avez une classe pour laquelle vous concevez un constructeur et que vous ne savez même pas ce que le constructeur doit faire, la classe fait probablement quelque chose de mal. C'est arrivé dans votre cas.
Mais la fixation de la conception est en fait assez simple en divisant la représentation d'entité et la logique CRUD en deux (ou plus) classes.
Voici à quoi ressemble votre design maintenant:
Employee
- contient des informations sur la structure des employés (ses attributs) et les méthodes de modification de l'entité (si vous décidez de suivre la voie mutable), contient la logique CRUD pour l'Employee
entité, peut renvoyer une liste d'Employee
objets, accepte unEmployee
objet lorsque vous le souhaitez mettre à jour un employé, peut renvoyer un seulEmployee
par une méthode commegetSingleById(id : string) : Employee
Wow, la classe semble énorme.
Ce sera la solution proposée:
Employee
- contient des informations sur la structure des employés (ses attributs) et les méthodes de modification de l'entité (si vous décidez de suivre la voie mutable)EmployeeRepository
- contient la logique CRUD pour l'Employee
entité, peut renvoyer une liste d'Employee
objets, accepte unEmployee
objet lorsque vous souhaitez mettre à jour un employé, peut renvoyer un seulEmployee
via une méthode commegetSingleById(id : string) : Employee
Avez-vous entendu parler de la séparation des préoccupations ? Non, vous le ferez maintenant. C'est la version moins stricte du principe de responsabilité unique, qui dit qu'une classe ne devrait en fait avoir qu'une seule responsabilité, ou comme le dit l'oncle Bob:
Il est tout à fait clair que si je pouvais diviser clairement votre classe initiale en deux qui ont toujours une interface bien arrondie, la classe initiale en faisait probablement trop, et c'était le cas.
Ce qui est génial avec le modèle de référentiel, il agit non seulement comme une abstraction pour fournir une couche intermédiaire entre les bases de données (qui peut être n'importe quoi, fichier, noSQL, SQL, orienté objet), mais il n'a même pas besoin d'être concret classe. Dans de nombreux langages OO, vous pouvez définir l'interface comme une réelle
interface
(ou une classe avec une méthode virtuelle pure si vous êtes en C ++), puis avoir plusieurs implémentations.Cela lève complètement la décision de savoir si un référentiel est une implémentation réelle de vous reposez simplement sur l'interface en vous appuyant réellement sur une structure avec le
interface
mot - clé. Et le référentiel est exactement cela, c'est un terme de fantaisie pour l'abstraction de la couche de données, à savoir le mappage des données à votre domaine et vice versa.Une autre grande chose à propos de la séparer en (au moins) deux classes est que maintenant la
Employee
classe peut clairement gérer ses propres données et le faire très bien, car elle n'a pas besoin de s'occuper d'autres choses difficiles.Question 6: Que doit donc faire le constructeur dans la
Employee
classe nouvellement créée ? C'est simple. Il doit prendre les arguments, vérifier s'ils sont valides (comme un âge ne doit probablement pas être négatif ou un nom ne doit pas être vide), déclencher une erreur lorsque les données ne sont pas valides et si la validation réussie attribue les arguments à des variables privées de l'entité. Il ne peut plus communiquer avec la base de données, car il ne sait tout simplement pas comment procéder.Question 4: Pas de réponse du tout, pas de manière générale, car la réponse dépend fortement de ce dont vous avez besoin.
Question 5: Maintenant que vous avez séparé la classe pléthorique en deux, vous pouvez avoir plusieurs méthodes de mise à jour directement sur la
Employee
classe, commechangeUsername
,markAsDeceased
qui manipulera les données de laEmployee
classe dans la RAM et vous pouvez alors introduire une méthode telle queregisterDirty
de la Modèle d' unité de travail à la classe de référentiel, à travers lequel vous indiquerez au référentiel que cet objet a changé de propriétés et devra être mis à jour après avoir appelé lacommit
méthode.De toute évidence, pour une mise à jour, un objet nécessite d'avoir un identifiant et donc d'être déjà enregistré, et c'est la responsabilité du référentiel de le détecter et de générer une erreur lorsque les critères ne sont pas remplis.
Question 3: Si vous décidez de suivre le modèle d'unité de travail, la
create
méthode sera désormaisregisterNew
. Si vous ne le faites pas, je l'appellerais probablement à lasave
place. Le but d'un référentiel est de fournir une abstraction entre le domaine et la couche de données, pour cette raison, je vous recommande que cette méthode (que ce soitregisterNew
ousave
) accepte l'Employee
objet et qu'elle appartient aux classes implémentant l'interface du référentiel, qui attribue ils décident de sortir de l'entité. Il est préférable de passer un objet entier, vous n'avez donc pas besoin d'avoir de nombreux paramètres facultatifs.Question 2: Les deux méthodes feront désormais partie de l'interface du référentiel et ne violent pas le principe de responsabilité unique. La responsabilité du référentiel est de fournir des opérations CRUD pour les
Employee
objets, c'est ce qu'il fait (en plus de Lire et Supprimer, CRUD se traduit à la fois par Créer et Mettre à jour). Évidemment, vous pouvez diviser encore plus le référentiel en ayant unEmployeeUpdateRepository
et ainsi de suite, mais cela est rarement nécessaire et une seule implémentation peut généralement contenir toutes les opérations CRUD.Question 1: Vous vous êtes retrouvé avec une
Employee
classe simple qui aura désormais (entre autres attributs) l'identifiant. Que l'id soit rempli ou vide (ounull
) dépend de si l'objet a déjà été enregistré. Néanmoins, un id est toujours un attribut que possède l'entité et la responsabilité de l'Employee
entité est de prendre soin de ses attributs, donc de prendre soin de son id.Qu'une entité ait ou non un identifiant n'a généralement pas d'importance jusqu'à ce que vous essayiez de faire une logique de persistance sur elle. Comme mentionné dans la réponse à la question 5, il est de la responsabilité du référentiel de détecter que vous n'essayez pas de sauvegarder une entité qui a déjà été enregistrée ou que vous essayez de mettre à jour une entité sans identifiant.
Note importante
Veuillez noter que bien que la séparation des préoccupations soit grande, la conception d'une couche de référentiel fonctionnel est un travail assez fastidieux et, selon mon expérience, est un peu plus difficile à bien comprendre que l'approche d'enregistrement actif. Mais vous vous retrouverez avec un design beaucoup plus flexible et évolutif, ce qui peut être une bonne chose.
la source
Créez d'abord une structure d'employé contenant les propriétés de l'employé conceptuel.
Ensuite, créez une base de données avec une structure de table correspondante, par exemple mssql
Créez ensuite un référentiel d'employés pour cette base de données EmployeeRepoMsSql avec les diverses opérations CRUD dont vous avez besoin.
Créez ensuite une interface IEmployeeRepo exposant les opérations CRUD
Développez ensuite votre structure Employee à une classe avec un paramètre de construction IEmployeeRepo. Ajoutez les différentes méthodes Save / Delete etc. dont vous avez besoin et utilisez le EmployeeRepo injecté pour les implémenter.
Quand il s'agit d'Id, je vous suggère d'utiliser un GUID qui peut être généré via du code dans le constructeur.
Pour travailler avec des objets existants, votre code peut les récupérer de la base de données via le référentiel avant d'appeler leur méthode de mise à jour.
Alternativement, vous pouvez opter pour le modèle d'objet de domaine anémique (mais à mon avis supérieur) où vous n'ajoutez pas les méthodes CRUD à votre objet, et passez simplement l'objet au référentiel pour être mis à jour / enregistré / supprimé
L'immuabilité est un choix de conception qui dépendra de vos modèles et de votre style de codage. Si vous allez tout fonctionnel, essayez également d'être immuable. Mais si vous n'êtes pas sûr, un objet modifiable est probablement plus facile à implémenter.
Au lieu de Create (), j'irais avec Save (). Créer des œuvres avec le concept d'immuabilité, mais je trouve toujours utile de pouvoir construire un objet qui n'est pas encore `` enregistré '', par exemple, vous avez une interface utilisateur qui vous permet de remplir un ou plusieurs objets d'employé puis de les vérifier à nouveau enregistrement dans la base de données.
***** exemple de code
la source
UserUpdate
service avec unechangeUsername(User user, string newUsername)
méthode, alors que je peux tout aussi bien ajouter lachangeUsername
méthodeUser
directement à la classe . Créer un service pour cela est un non-sens.Examen de votre conception
Votre
Employee
est en réalité une sorte de proxy pour un objet géré en permanence dans la base de données.Je suggère donc de penser à l'ID comme s'il s'agissait d'une référence à votre objet de base de données. Avec cette logique à l'esprit, vous pouvez continuer votre conception comme vous le feriez pour des objets non-base de données, l'ID vous permettant d'implémenter la logique de composition traditionnelle:
Employee
peut encore être créé, ou il pourrait simplement avoir été supprimé.Vous devez également gérer un état pour l'objet. Par exemple:
Dans cet esprit, nous pourrions opter pour:
Afin de pouvoir gérer le statut de votre objet de manière fiable, vous devez assurer une meilleure encapsulation en rendant les propriétés privées et donner l'accès uniquement via des getters et des setters qui mettent à jour le statut des setters.
Vos questions
Je pense que la propriété ID ne viole pas le SRP. Sa seule responsabilité est de faire référence à un objet de base de données.
Votre Employé dans son ensemble n'est pas conforme au SRP, car il est responsable du lien avec la base de données, mais aussi de la tenue des modifications temporaires, et de toutes les transactions qui arrivent à cet objet.
Une autre conception pourrait consister à conserver les champs modifiables dans un autre objet qui ne serait chargé que lorsque les champs doivent être accessibles.
Vous pouvez implémenter les transactions de base de données sur l'employé à l'aide du modèle de commande . Ce type de conception faciliterait également le découplage entre vos objets métier (Employé) et votre système de base de données sous-jacent, en isolant les idiomes et les API spécifiques à la base de données.
Je n'ajouterais pas une douzaine de paramètres
Create()
, car les objets métier pourraient évoluer et rendre tout cela très difficile à maintenir. Et le code deviendrait illisible. Vous avez 2 choix ici: soit passer un ensemble minimaliste de paramètres (pas plus de 4) qui sont absolument nécessaires pour créer un employé dans la base de données et effectuer les modifications restantes via la mise à jour, OU vous passez un objet. Soit dit en passant, dans votre conception Je comprends que vous avez déjà choisi:my_employee.Create()
.La classe doit-elle être immuable? Voir la discussion ci-dessus: dans votre conception originale no. Je choisirais une pièce d'identité immuable mais pas un employé immuable. Un employé évolue dans la vie réelle (nouveau poste, nouvelle adresse, nouvelle situation matrimoniale, même nouveaux noms ...). Je pense qu'il sera plus facile et plus naturel de travailler avec cette réalité à l'esprit, au moins dans la couche logique métier.
Si vous envisagez d'utiliser des commandes de mise à jour et des objets distincts pour (GUI?) Pour conserver les modifications souhaitées, vous pouvez opter pour une approche ancienne / nouvelle. Dans tous les autres cas, j'opterais pour la mise à jour d'un objet mutable. Attention: la mise à jour peut déclencher le code de la base de données, vous devez donc vous assurer qu'après une mise à jour, l'objet est toujours vraiment synchronisé avec la base de données.
Je pense que la récupération d'un employé de la base de données dans le constructeur n'est pas une bonne idée, car la récupération peut mal tourner, et dans de nombreuses langues, il est difficile de faire face à une construction qui a échoué. Le constructeur doit initialiser l'objet (en particulier l'ID) et son état.
la source