Come dovrebbe essere progettata una classe "Employee"?


11

Sto cercando di creare un programma per la gestione dei dipendenti. Non riesco, tuttavia, a capire come progettare la Employeeclasse. Il mio obiettivo è essere in grado di creare e manipolare i dati dei dipendenti sul database utilizzando un Employeeoggetto.

L'implementazione di base a cui ho pensato era questa semplice:

class Employee
{
    // Employee data (let's say, dozens of properties).

    Employee() {}
    Create() {}
    Update() {}
    Delete() {}
}

Usando questa implementazione, ho riscontrato diversi problemi.

  1. Il IDdatabase di un dipendente viene fornito dal database, quindi se uso l'oggetto per descrivere un nuovo dipendente, non ci sarà ancora alcun oggetto IDda archiviare, mentre un oggetto che rappresenta un dipendente esistente avrà un ID. Quindi ho una proprietà che a volte descrive l'oggetto ea volte no (il che potrebbe indicare che violiamo SRP ? Dato che usiamo la stessa classe per rappresentare dipendenti nuovi ed esistenti ...).
  2. Il Createmetodo si suppone di creare un dipendente sul database, mentre l' Updatee Deletesi suppone che ad agire su un dipendente esistente (in questo caso, SRP ...).
  3. Quali parametri dovrebbe avere il metodo "Crea"? Dozzine di parametri per tutti i dati dei dipendenti o forse un Employeeoggetto?
  4. La classe dovrebbe essere immutabile?
  5. Come Updatefunzionerà? Prenderà le proprietà e aggiornerà il database? O forse ci vorranno due oggetti: uno "vecchio" e uno "nuovo", e aggiornerà il database con le differenze tra loro? (Penso che la risposta abbia a che fare con la risposta sulla mutabilità della classe).
  6. Quale sarebbe la responsabilità del costruttore? Quali sarebbero i parametri che richiede? Recupererebbe i dati dei dipendenti dal database usando un idparametro e popolerebbe le proprietà?

Quindi, come puoi vedere, ho un po 'di confusione in testa e sono molto confuso. La prego di aiutarmi a capire come dovrebbe essere una classe del genere?

Si prega di notare che non voglio opinioni, solo per capire come viene generalmente progettata una classe così frequentemente utilizzata.


3
La tua violazione violenta dell'SRP è che hai una classe che rappresenta sia un'entità sia responsabile della logica CRUD. Se lo separi, le operazioni CRUD e la struttura dell'entità saranno classi diverse, quindi 1. e 2. non interrompono SRP. 3. dovrebbe prendere un Employeeoggetto per fornire astrazione, le domande 4. e 5. sono generalmente senza risposta, dipendono dalle tue esigenze e se separi la struttura e le operazioni CRUD in due classi, allora è abbastanza chiaro, il costruttore del Employeenon può recuperare i dati da db più, in modo che risponda 6.
Andy,

@DavidPacker - Grazie. Potresti metterlo in una risposta?
Sipo,

5
Non lo ripeto, non ho il tuo ctor che raggiunge il database. Fare così strettamente accoppia il codice al database e rende le cose terribilmente difficili da testare (anche i test manuali diventano più difficili). Guarda nel modello di repository. Pensaci per un secondo, sei Updateun dipendente o aggiorni il record di un dipendente? Lo fai Employee.Delete()o fai un Boss.Fire(employee)?
RubberDuck,

1
A parte quanto è già stato menzionato, ha senso per te che hai bisogno di un dipendente per crearlo? Nel registro attivo, potrebbe avere più senso rinnovare un Dipendente e quindi chiamare Salva su quell'oggetto. Anche allora, tuttavia, ora hai una classe che è responsabile della logica aziendale e della sua persistenza dei dati.
Sig. Cochese,

Risposte:


10

Questa è una trascrizione più ben definita del mio commento iniziale sotto la tua domanda. Le risposte alle domande poste dal PO possono essere trovate in fondo a questa risposta. Controlla anche la nota importante che si trova nello stesso posto.


Quello che stai attualmente descrivendo, Sipo, è un modello di progettazione chiamato Record attivo . Come per tutto, anche questo ha trovato il suo posto tra i programmatori, ma è stato scartato a favore del repository e dei modelli di mappatura dei dati per un semplice motivo, la scalabilità.

In breve, un record attivo è un oggetto che:

  • rappresenta un oggetto nel tuo dominio (include le regole aziendali, sa come gestire determinate operazioni sull'oggetto, ad esempio se puoi o non puoi cambiare un nome utente e così via),
  • sa come recuperare, aggiornare, salvare ed eliminare l'entità.

Affronti diversi problemi con il tuo progetto attuale e il problema principale del tuo progetto è affrontato nell'ultimo, sesto punto (ultimo ma non meno importante, suppongo). Quando hai una classe per la quale stai progettando un costruttore e non sai nemmeno cosa dovrebbe fare il costruttore, probabilmente la classe sta facendo qualcosa di sbagliato. È successo nel tuo caso.

Ma riparare il progetto è in realtà piuttosto semplice suddividendo la rappresentazione dell'entità e la logica CRUD in due (o più) classi.

Ecco come appare il tuo design ora:

  • Employee- contiene informazioni sulla struttura del dipendente (i suoi attributi) e i metodi su come modificare l'entità (se si decide di procedere in modo mutevole), contiene la logica CRUD per l' Employeeentità, può restituire un elenco di Employeeoggetti, accettare un Employeeoggetto quando si desidera aggiornare un dipendente, può restituire un singolo Employeetramite un metodo comegetSingleById(id : string) : Employee

Caspita, la classe sembra enorme.

Questa sarà la soluzione proposta:

  • Employee - contiene informazioni sulla struttura dei dipendenti (i suoi attributi) e sui metodi per modificare l'entità (se si decide di procedere in modo mutevole)
  • EmployeeRepository- contiene la logica CRUD per l' Employeeentità, può restituire un elenco di Employeeoggetti, accetta un Employeeoggetto quando si desidera aggiornare un dipendente, può restituire un singolo Employeetramite un metodo comegetSingleById(id : string) : Employee

Hai sentito della separazione delle preoccupazioni ? No, lo farai ora. È la versione meno rigorosa del principio di responsabilità singola, che afferma che una classe dovrebbe effettivamente avere una sola responsabilità, o come dice lo zio Bob:

Un modulo dovrebbe avere una e una sola ragione per cambiare.

È abbastanza chiaro che se fossi stato in grado di dividere chiaramente la tua classe iniziale in due che hanno ancora un'interfaccia ben arrotondata, probabilmente la classe iniziale stava facendo troppo, e lo era.

Ciò che è bello del modello di repository, non solo funge da astrazione per fornire uno strato intermedio tra il database (che può essere qualsiasi cosa, file, noSQL, SQL, orientato agli oggetti), ma non deve nemmeno essere concreto classe. In molti linguaggi OO, è possibile definire l'interfaccia come effettiva interface(o una classe con un metodo virtuale puro se si è in C ++) e quindi avere più implementazioni.

Questo solleva completamente la decisione se un repository è un'implementazione effettiva di te semplicemente basandosi sull'interfaccia facendo affidamento su una struttura con la interfaceparola chiave. E il repository è esattamente questo, è un termine sofisticato per l'astrazione del livello dati, vale a dire mappare i dati sul tuo dominio e viceversa.

Un'altra cosa grandiosa di separarlo in (almeno) due classi è che ora la Employeeclasse può chiaramente gestire i propri dati e farlo molto bene, perché non ha bisogno di occuparsi di altre cose difficili.

Domanda 6: Quindi cosa dovrebbe fare il costruttore nella Employeeclasse appena creata ? È semplice. Dovrebbe prendere gli argomenti, verificare se sono validi (come un'età probabilmente non dovrebbe essere negativa o il nome non dovrebbe essere vuoto), generare un errore quando i dati non erano validi e se la convalida passata assegna gli argomenti alle variabili private dell'entità. Ora non è in grado di comunicare con il database, perché semplicemente non ha idea di come farlo.


Domanda 4: non è possibile rispondere a tutti, non in generale, perché la risposta dipende fortemente da ciò di cui hai esattamente bisogno.


Domanda 5: Ora che avete separato la classe gonfio in due, è possibile avere più metodi di aggiornamento direttamente sulla Employeeclasse, come changeUsername, markAsDeceased, che manipolare i dati della Employeeclasse solo nella RAM e quindi si potrebbe introdurre un metodo come registerDirtydalla Modello di unità di lavoro per la classe repository, tramite la quale si comunica al repository che questo oggetto ha modificato le proprietà e dovrà essere aggiornato dopo aver chiamato il commitmetodo.

Ovviamente, per un aggiornamento un oggetto richiede di avere un ID e quindi essere già salvato, ed è responsabilità del repository rilevare questo e generare un errore quando i criteri non sono soddisfatti.


Domanda 3: se decidi di seguire il modello Unità di lavoro, il createmetodo sarà ora registerNew. Se non lo fai, probabilmente lo chiamerei saveinvece. L'obiettivo di un repository è di fornire un'astrazione tra il dominio e il livello dati, per questo ti consiglierei che questo metodo (sia esso registerNewo save) accetta l' Employeeoggetto e spetta alle classi che implementano l'interfaccia del repository, quali attributi decidono di togliere dall'entità. Passare un intero oggetto è meglio, quindi non è necessario disporre di molti parametri opzionali.


Domanda 2: Entrambi i metodi faranno ora parte dell'interfaccia del repository e non violano il principio della responsabilità singola. La responsabilità del repository è quella di fornire operazioni CRUD per gli Employeeoggetti, questo è ciò che fa (oltre a Leggi ed Elimina, CRUD si traduce sia in Crea sia in Aggiorna). Ovviamente, potresti dividere ulteriormente il repository avendo un EmployeeUpdateRepositorye così via, ma ciò è raramente necessario e una singola implementazione di solito può contenere tutte le operazioni CRUD.


Domanda 1: hai finito con una semplice Employeeclasse che ora (tra gli altri attributi) avrà id. Se l'id è pieno o vuoto (o null) dipende dal fatto che l'oggetto sia già stato salvato. Tuttavia, un ID è ancora un attributo dell'entità e la responsabilità Employeedell'entità è quella di prendersi cura dei suoi attributi, quindi prendersi cura del suo ID.

Il fatto che un'entità abbia o meno un ID di solito non ha importanza fino a quando non si tenta di eseguire una logica di persistenza su di essa. Come indicato nella risposta alla domanda 5, è responsabilità del repository rilevare che non si sta tentando di salvare un'entità che è già stata salvata o che si sta tentando di aggiornare un'entità senza un ID.


Nota importante

Si prega di essere consapevoli del fatto che sebbene la separazione delle preoccupazioni sia grande, in realtà progettare un livello di repository funzionale è un lavoro piuttosto noioso e nella mia esperienza è un po 'più difficile da ottenere rispetto all'approccio di registrazione attiva. Ma finirai con un design che è molto più flessibile e scalabile, il che può essere una buona cosa.


Hmm stesso della mia risposta, ma non come 'bordato' mette in ombra
Ewan

2
@Ewan Non ho votato in negativo la tua risposta, ma vedo perché alcuni potrebbero averlo. Non risponde direttamente ad alcune delle domande del PO e alcuni dei tuoi suggerimenti sembrano infondati.
Andy,

1
Risposta piacevole e comprensiva. Colpisce l'unghia sulla testa con la separazione delle preoccupazioni. E mi piace l'avvertimento che indica la scelta importante da fare tra un design complesso perfetto e un bel compromesso.
Christophe,

È vero, la tua risposta è superiore
Ewan,

quando si crea per la prima volta un nuovo oggetto dipendente, non ci sarà alcun valore per ID. il campo id può essere lasciato con valore nullo ma ciò causerà che l'oggetto dipendente è in stato non valido ????
Susantha7,

2

Innanzitutto creare una struttura dipendente contenente le proprietà dell'impiegato concettuale.

Quindi creare un database con la struttura di tabella corrispondente, ad esempio mssql

Quindi creare un repository dei dipendenti per quel database EmployeeRepoMsSql con le varie operazioni CRUD richieste.

Quindi creare un'interfaccia IEmployeeRepo che espone le operazioni CRUD

Quindi espandere la struttura Employee in una classe con un parametro di costruzione di IEmployeeRepo. Aggiungi i vari metodi Salva / Elimina ecc richiesti e usa il EmployeeRepo iniettato per implementarli.

Quando si coniuga a Id ti suggerisco di utilizzare un GUID che può essere generato tramite codice nel costruttore.

Per lavorare con oggetti esistenti, il codice può recuperarli dal database tramite il repository prima di chiamare il loro metodo di aggiornamento.

In alternativa puoi optare per il modello Anemic Domain Object accigliato (ma a mio avviso superiore) in cui non aggiungi i metodi CRUD al tuo oggetto e passa semplicemente l'oggetto al repository per essere aggiornato / salvato / eliminato

L'immutabilità è una scelta progettuale che dipenderà dai modelli e dallo stile di codifica. Se stai andando tutto funzionale, prova anche a essere immutabile. Ma se non sei sicuro che un oggetto mutabile sia probabilmente più facile da implementare.

Invece di Create () andrei con Save (). Creare lavori con il concetto di immutabilità, ma trovo sempre utile essere in grado di costruire un oggetto che non è ancora "salvato", ad esempio hai un'interfaccia utente che ti consente di popolare un oggetto o gli oggetti dei dipendenti e quindi verificarli di nuovo alcune regole prima salvataggio nel database.

***** codice di esempio

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);

    }
}

1
Anemic Domain Model ha ben poco a che fare con la logica CRUD. È un modello che, sebbene appartenga al livello di dominio, non ha funzionalità e tutte le funzionalità sono offerte attraverso servizi, ai quali questo modello di dominio viene passato come parametro.
Andy,

Esatto, in questo caso il repository è il servizio e le funzioni sono le operazioni CRUD.
Ewan,

@DavidPacker stai dicendo che il modello di dominio Anemic è una buona cosa?
candied_orange,

1
@CandiedOrange Non ho espresso la mia opinione nel commento, ma no, se decidi di andare oltre l'immersione della tua applicazione in livelli in cui un livello è responsabile solo della logica aziendale, sono con Mr. Fowler che un modello di dominio anemico è in effetti un anti-pattern. Perché dovrei aver bisogno di un UserUpdateservizio con un changeUsername(User user, string newUsername)metodo, quando posso anche aggiungere direttamente il changeUsernamemetodo alla classe User. Creare un servizio per questo è un non senso.
Andy,

1
Penso che in questo caso l'iniezione del repository solo per mettere la logica CRUD sul modello non sia ottimale.
Ewan,

1

Revisione del tuo design

EmployeeIn realtà il tuo è un tipo di proxy per un oggetto gestito in modo persistente nel database.

Suggerisco quindi di pensare all'ID come se fosse un riferimento all'oggetto del database. Con questa logica in mente puoi continuare il tuo progetto come faresti per gli oggetti non di database, l'ID che ti consente di implementare la logica di composizione tradizionale:

  • Se l'ID è impostato, si dispone di un oggetto database corrispondente.
  • Se l'ID non è impostato, non esiste alcun oggetto database corrispondente: Employeepotrebbe ancora essere creato o potrebbe essere stato semplicemente eliminato.
  • È necessario un meccanismo per avviare la relazione per i dipendenti in uscita e per i record di database in uscita che non sono ancora stati caricati in memoria.

Dovresti anche gestire uno stato per l'oggetto. Per esempio:

  • quando un Dipendente non è ancora collegato a un oggetto DB tramite creazione o recupero dati, non si dovrebbe essere in grado di eseguire aggiornamenti o eliminazioni
  • i dati del dipendente nell'oggetto sono sincronizzati con il database o sono state apportate modifiche?

Con questo in mente, potremmo optare per:

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
}

Per poter gestire lo stato dell'oggetto in modo affidabile, è necessario garantire un migliore incapsulamento rendendo le proprietà private e consentire l'accesso solo tramite getter e setter che impostano lo stato di aggiornamento.

Le tue domande

  1. Penso che la proprietà ID non violi l'SRP. La sua unica responsabilità è fare riferimento a un oggetto database.

  2. Il tuo Dipendente nel suo insieme non è conforme all'SRP, perché è responsabile del collegamento con il database, ma anche del mantenimento di modifiche temporanee e di tutte le transazioni che avvengono su quell'oggetto.

    Un altro progetto potrebbe essere quello di mantenere i campi modificabili in un altro oggetto che verrebbero caricati solo quando è necessario accedere ai campi.

    È possibile implementare le transazioni del database sul Dipendente utilizzando il modello di comando . Questo tipo di progettazione faciliterebbe anche il disaccoppiamento tra gli oggetti aziendali (Dipendente) e il sistema di database sottostante, isolando idiomi e API specifici del database.

  3. Non aggiungerei dozzine di parametri Create(), perché gli oggetti business potrebbero evolversi e rendere tutto ciò molto difficile da mantenere. E il codice diventerebbe illeggibile. Hai 2 scelte qui: passare un set di parametri minimalista (non più di 4) che sono assolutamente necessari per creare un impiegato nel database ed eseguire le modifiche rimanenti tramite aggiornamento, oppure si passa un oggetto. Tra l'altro, nella progettazione Capisco che hai già scelto: my_employee.Create().

  4. La classe dovrebbe essere immutabile? Vedi la discussione sopra: nel tuo disegno originale n. Opterei per un ID immutabile ma non per un Dipendente immutabile. Un dipendente si evolve nella vita reale (nuova posizione lavorativa, nuovo indirizzo, nuova situazione matrimoniale, persino nuovi nomi ...). Penso che sarà più facile e più naturale lavorare con questa realtà in mente, almeno a livello di logica aziendale.

  5. Se si considera l'utilizzo di un comando per l'aggiornamento e di oggetti distinti per (GUI?) Per conservare le modifiche desiderate, è possibile optare per un approccio vecchio / nuovo. In tutti gli altri casi, opterei per l'aggiornamento di un oggetto mutabile. Attenzione: l'aggiornamento potrebbe innescare il codice del database, quindi è necessario assicurarsi che dopo un aggiornamento l'oggetto sia ancora realmente sincronizzato con il DB.

  6. Penso che recuperare un dipendente da DB nel costruttore non sia una buona idea, perché il recupero potrebbe andare storto e, in molte lingue, è difficile far fronte a costruzioni fallite. Il costruttore dovrebbe inizializzare l'oggetto (in particolare l'ID) e il suo stato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.