Comment concevoir une classe «Employé»?

11

J'essaie de créer un programme de gestion des employés. Je ne peux cependant pas comprendre comment concevoir la Employeeclasse. 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 Employeeobjet.

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.

  1. La IDdonné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 encore IDà stocker, alors qu'un objet représentant un employé existant aura un ID. 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 ...).
  2. La Createméthode est censée créer un employé sur la base de données, tandis que le Updateet Deletesont censés agir sur un employé existant (Encore une fois, SRP ...).
  3. 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 Employeeobjet?
  4. La classe doit-elle être immuable?
  5. Comment ça va Updatemarcher? 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).
  6. 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 idparamè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.

Sipo
la source
3
Votre violation actuelle du SRP est que vous avez une classe représentant à la fois une entité et étant responsable de la logique CRUD. Si vous le séparez, que les opérations CRUD et la structure d'entité seront des classes différentes, alors 1. et 2. ne cassent pas SRP. 3. devrait prendre un Employeeobjet 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 du Employeene peut pas récupérer les données de db plus, donc ça répond 6.
Andy
@DavidPacker - Merci. Pourriez-vous mettre cela dans une réponse?
Sipo
5
Ne répétez pas que votre ctor n’a pas accès à la base de données. Cela couple étroitement le code à la base de données et rend les choses terriblement difficiles à tester (même les tests manuels deviennent plus difficiles). Examinez le modèle de référentiel. Pensez-y une seconde, êtes-vous Updateun employé ou mettez-vous à jour un dossier d'employé? Avez-vous Employee.Delete(), ou fait un Boss.Fire(employee)?
RubberDuck
1
Mis à part ce qui a déjà été mentionné, est-il sensé pour vous que vous ayez besoin d'un employé pour créer un employé? Dans l'enregistrement actif, il peut être plus judicieux de créer un nouvel employé, puis d'appeler Save sur cet objet. Même alors, cependant, vous avez maintenant une classe qui est responsable de la logique métier ainsi que de sa propre persistance des données.
M. Cochese

Réponses:

10

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:

  • représente un objet dans votre domaine (comprend des règles métier, sait comment gérer certaines opérations sur l'objet, par exemple si vous pouvez ou ne pouvez pas changer un nom d'utilisateur, etc.),
  • sait comment récupérer, mettre à jour, enregistrer et supprimer l'entité.

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' Employeeentité, peut renvoyer une liste d' Employeeobjets, accepte un Employeeobjet lorsque vous le souhaitez mettre à jour un employé, peut renvoyer un seul Employeepar 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' Employeeentité, peut renvoyer une liste d' Employeeobjets, accepte un Employeeobjet lorsque vous souhaitez mettre à jour un employé, peut renvoyer un seul Employeevia 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:

Un module doit avoir une et une seule raison de changer.

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 interfacemot - 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 Employeeclasse 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 Employeeclasse 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 Employeeclasse, comme changeUsername, markAsDeceasedqui manipulera les données de la Employeeclasse dans la RAM et vous pouvez alors introduire une méthode telle que registerDirtyde 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é la commitmé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 createméthode sera désormais registerNew. Si vous ne le faites pas, je l'appellerais probablement à la saveplace. 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 soit registerNewou save) accepte l' Employeeobjet 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 Employeeobjets, 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 un EmployeeUpdateRepositoryet 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 Employeeclasse simple qui aura désormais (entre autres attributs) l'identifiant. Que l'id soit rempli ou vide (ou null) 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' Employeeentité 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.

Andy
la source
Hmm même que ma réponse, mais pas comme « Edgey » met Shades
Ewan
2
@Ewan Je n'ai pas dévalué votre réponse, mais je peux voir pourquoi certains peuvent l'avoir. Il ne répond pas directement à certaines des questions du PO et certaines de vos suggestions semblent infondées.
Andy
1
Réponse agréable et complète. Frappe l'ongle sur la tête avec la séparation de préoccupation. Et j'aime l'avertissement qui indique le choix important à faire entre un design complexe parfait et un bon compromis.
Christophe
Certes, votre réponse est supérieure
Ewan
lors de la première création d'un nouvel objet employé, l'ID n'aura aucune valeur. Le champ id peut laisser une valeur nulle mais cela entraînera que l'objet employé est dans un état non valide ????
Susantha7
2

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

public class Employee
{
    public string Id { get; set; }

    public string Name { get; set; }

    private IEmployeeRepo repo;

    //with the OOP approach you want the save method to be on the Employee Object
    //so you inject the IEmployeeRepo in the Employee constructor
    public Employee(IEmployeeRepo repo)
    {
        this.repo = repo;
        this.Id = Guid.NewGuid().ToString();
    }

    public bool Save()
    {
        return repo.Save(this);
    }
}

public interface IEmployeeRepo
{
    bool Save(Employee employee);

    Employee Get(string employeeId);
}

public class EmployeeRepoSql : IEmployeeRepo
{
    public Employee Get(string employeeId)
    {
        var sql = "Select * from Employee where Id=@Id";
        //more db code goes here
        Employee employee = new Employee(this);
        //populate object from datareader
        employee.Id = datareader["Id"].ToString();

    }

    public bool Save(Employee employee)
    {
        var sql = "Insert into Employee (....";
        //db logic
    }
}

public class MyADMProgram
{
    public void Main(string id)
    {
        //with ADM don't inject the repo into employee, just use it in your program
        IEmployeeRepo repo = new EmployeeRepoSql();
        var emp = repo.Get(id);

        //do business logic
        emp.Name = TextBoxNewName.Text;

        //save to DB
        repo.Save(emp);

    }
}
Ewan
la source
1
Le modèle de domaine anémique a très peu à voir avec la logique CRUD. Il s'agit d'un modèle qui, bien qu'appartenant à la couche domaine, n'a aucune fonctionnalité et toutes les fonctionnalités sont servies par le biais de services, auxquels ce modèle de domaine est transmis en tant que paramètre.
Andy
Exactement, dans ce cas, le dépôt est le service et les fonctions sont les opérations CRUD.
Ewan
@DavidPacker dites-vous que le modèle de domaine anémique est une bonne chose?
candied_orange
1
@CandiedOrange Je n'ai pas exprimé mon opinion dans le commentaire, mais non, si vous décidez d'aller jusqu'à plonger votre application dans des couches où une couche n'est responsable que de la logique métier, je suis avec M. Fowler qu'un modèle de domaine anémique est en fait un anti-modèle. Pourquoi devrais-je avoir besoin d'un UserUpdateservice avec une changeUsername(User user, string newUsername)méthode, alors que je peux tout aussi bien ajouter la changeUsernameméthode Userdirectement à la classe . Créer un service pour cela est un non-sens.
Andy
1
Je pense que dans ce cas, injecter le dépôt juste pour mettre la logique CRUD sur le modèle n'est pas optimal.
Ewan
1

Examen de votre conception

Votre Employeeest 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:

  • Si l'ID est défini, vous disposez d'un objet de base de données correspondant.
  • Si l'ID n'est pas défini, il n'y a pas d'objet de base de données correspondant: Employeepeut encore être créé, ou il pourrait simplement avoir été supprimé.
  • Vous avez besoin d'un mécanisme pour initier la relation pour les employés existants et les enregistrements de base de données existants qui ne sont pas encore chargés en mémoire.

Vous devez également gérer un état pour l'objet. Par exemple:

  • lorsqu'un employé n'est pas encore lié à un objet de base de données via la création ou la récupération de données, vous ne devriez pas être en mesure d'effectuer des mises à jour ou des suppressions
  • les données de l'employé dans l'objet sont-elles synchronisées avec la base de données ou des modifications ont-elles été apportées?

Dans cet esprit, nous pourrions opter pour:

class Employee
{
    ...
    Employee () {}       // Initialize an empty Employee
    Load(IDType ID) {}   // Load employee with known ID from the database
    bool Create() {}     // Create an new employee an set its ID 
    bool Update() {}     // Update the employee (can ID be changed?)
    bool Delete() {}     // Delete the employee (and reset ID because there's no corresponding ID. 
    bool isClean () {}   // true if ID empty or if all properties match database
}

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

  1. 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.

  2. 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.

  3. 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().

  4. 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.

  5. 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.

  6. 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.

Christophe
la source