Salvataggio di un oggetto tramite un metodo proprio o tramite un'altra classe?


38

Se voglio salvare e recuperare un oggetto, dovrei creare un'altra classe per gestirlo, o sarebbe meglio farlo nella classe stessa? O forse mescolando entrambi?

Quale è raccomandato secondo il paradigma OOD?

Per esempio

Class Student
{
    public string Name {set; get;}
    ....
    public bool Save()
    {
        SqlConnection con = ...
        // Save the class in the db
    }
    public bool Retrieve()
    {
         // search the db for the student and fill the attributes
    }
    public List<Student> RetrieveAllStudents()
    {
         // this is such a method I have most problem with it 
         // that an object returns an array of objects of its own class!
    }
}

Contro. (So ​​che si consiglia quanto segue, tuttavia mi sembra un po 'contrario alla coesione di Studentclasse)

Class Student { /* */ }
Class DB {
  public bool AddStudent(Student s)
  {

  }
  public Student RetrieveStudent(Criteria)
  {
  } 
  public List<Student> RetrieveAllStudents()
  {
  }
}

Che ne dici di mescolarli?

   Class Student
    {
        public string Name {set; get;}
        ....
        public bool Save()
        {
            /// do some business logic!
            db.AddStudent(this);
        }
        public bool Retrieve()
        {
             // build the criteria 
             db.RetrieveStudent(criteria);
             // fill the attributes
        }
    }

3
Dovresti fare qualche ricerca sul modello di Active Record, come questa domanda o questa
Eric King il

@gnat, ho pensato che chiedere quale fosse meglio è basato sull'opinione, quindi ho aggiunto "pro e contro" e non ho problemi a rimuoverlo, ma in realtà intendo per quanto riguarda il paradigma OOD che è raccomandato
Ahmad,

3
Non c'è una risposta corretta. Dipende dalle tue esigenze, dalle tue preferenze, da come la tua classe deve essere usata e da dove vedi andare il tuo progetto. L'unica cosa OO corretta è che puoi salvare e recuperare come oggetti.
Dunk

Risposte:


34

Principio di responsabilità unica , separazione delle preoccupazioni e coesione funzionale . Se leggi questi concetti, la risposta che ottieni è: separali .

Un semplice motivo per separare la Studentclasse "DB" (o StudentRepository, per seguire convenzioni più popolari) è quello di consentire di modificare le "regole aziendali", presenti nella Studentclasse, senza influire sul codice responsabile della persistenza e viceversa. versa.

Questo tipo di separazione è molto importante, non solo tra le regole aziendali e la persistenza, ma tra le molte preoccupazioni del tuo sistema, per permetterti di apportare modifiche con un impatto minimo in moduli non correlati (minimo perché a volte è inevitabile). Aiuta a costruire sistemi più robusti, più facili da mantenere e più affidabili in caso di cambiamenti costanti.

Avendo le regole di business e la persistenza mescolate insieme, o una singola classe come nel tuo primo esempio, o con DBuna dipendenza di Student, stai accoppiando due preoccupazioni molto diverse. Può sembrare che appartengano insieme; sembrano essere coerenti perché usano gli stessi dati. Ma ecco la cosa: la coesione non può essere misurata solo dai dati condivisi tra le procedure, è necessario considerare anche il livello di astrazione in cui esistono. In effetti, il tipo ideale di coesione è descritto come:

La coesione funzionale si verifica quando le parti di un modulo sono raggruppate perché tutte contribuiscono a un singolo compito ben definito del modulo.

E chiaramente, l'esecuzione di convalide per un Studentpo 'anche persistendo non costituisce "un singolo compito ben definito". Di nuovo, le regole aziendali e i meccanismi di persistenza sono due aspetti molto diversi del sistema, che secondo molti principi di una buona progettazione orientata agli oggetti, dovrebbero essere separati.

Consiglio di leggere di Clean Architecture , di guardare questo discorso sul principio di responsabilità singola (dove viene usato un esempio molto simile) e di guardare anche questo discorso su Clean Architecture . Questi concetti delineano le ragioni alla base di tali separazioni.


11
Ah, ma la Studentclasse dovrebbe essere incapsulata correttamente, sì? Come può quindi una classe esterna gestire la persistenza dello stato privato? L'incapsulamento deve essere bilanciato con SoC e SRP, ma probabilmente la scelta dell'uno o dell'altro senza ponderare attentamente i compromessi è probabilmente errata. Una possibile soluzione a questo enigma sta usando gli accessi pacchetto-privati ​​che il codice di persistenza deve usare.
amon,

1
@amon Sono d'accordo sul fatto che non sia così semplice, ma credo che sia una questione di accoppiamento, non di incapsulamento. Ad esempio, è possibile rendere astratta la classe Student, con metodi astratti per tutti i dati necessari all'azienda, che vengono quindi implementati dal repository. In questo modo, la struttura dei dati del DB è completamente nascosta dietro l'implementazione del repository, quindi è persino incapsulata dalla classe Student. Ma, allo stesso tempo, il repository è profondamente accoppiato alla classe Student - se si cambiano metodi astratti, anche l'implementazione deve cambiare. Si tratta di compromessi. :)
MichelHenrich,

4
L'affermazione secondo cui SRP rende i programmi più facili da mantenere è, nella migliore delle ipotesi, dubbia. Il problema che SRP crea è che invece di avere una raccolta di classi ben denominate, in cui il nome dice tutto ciò che la classe può e non può fare (in altre parole facile da capire che porta a facile manutenzione) si finisce con centinaia / migliaia di classi di cui le persone avevano bisogno per usare un thesaurus per scegliere un nome univoco, molti nomi sono simili ma non del tutto e l'ordinamento attraverso tutta quella pula è un lavoro ingrato. IOW, non mantenibile affatto, IMO.
Dunk

3
@Dunk parli come se le classi fossero l'unica unità di modularizzazione disponibile. Ho visto e scritto molti progetti dopo SRP, e sono d'accordo che il tuo esempio si verifichi, ma solo se non usi correttamente pacchetti e librerie. Se si separano le responsabilità in pacchetti, dal momento che il contesto è implicito da esso, è possibile nominare nuovamente le classi liberamente. Quindi, per mantenere un sistema come questo, è sufficiente eseguire il drill-drown nel pacchetto giusto prima di cercare la classe che è necessario modificare. :)
MichelHenrich,

5
I principi OOD di @Dunk possono essere dichiarati come: "In linea di principio, farlo è meglio grazie a X, Y e Z". Non significa che dovrebbe sempre essere applicato; le persone devono essere pragmatiche e ponderare i compromessi. Nei grandi progetti, i benefici sono tali che di solito superano la curva di apprendimento di cui parli, ma ovviamente questa non è la realtà per la maggior parte delle persone e quindi non dovrebbe essere applicata così pesantemente. Tuttavia, anche nelle applicazioni SRP più leggere, penso che possiamo entrambi concordare sul fatto che separare la persistenza dalle regole aziendali produce sempre vantaggi oltre i suoi costi (tranne forse per il codice usa e getta).
MichelHenrich,

10

Entrambi gli approcci violano il principio di responsabilità singola. La tua prima versione assegna alla Studentclasse molte responsabilità e la lega a una specifica tecnologia di accesso al DB. Il secondo conduce a una grande DBclasse che sarà responsabile non solo degli studenti, ma di qualsiasi altro tipo di oggetto dati nel programma. EDIT: il tuo terzo approccio è il peggiore, poiché crea una dipendenza ciclica tra la classe DB e la Studentclasse.

Quindi se non hai intenzione di scrivere un programma giocattolo, non usarne nessuno. Invece, usa una classe diversa come una StudentRepositoryper fornire un'API per il caricamento e il salvataggio, supponendo che tu stia implementando il codice CRUD da solo. Potresti anche considerare di utilizzare un framework ORM , che può fare il duro lavoro per te (e il framework in genere imporrà alcune decisioni in cui devono essere posizionate le operazioni Load and Save).


Grazie, ho modificato la mia risposta, che dire del terzo approccio? comunque penso che qui il punto sia quello di creare livelli distinti
Ahmad

Qualcosa mi suggerisce anche di usare StudentRepository invece di DB (non so perché! Forse ancora una volta responsabilità singola) ma non riesco ancora a capire perché repository diverso per ogni classe? se è solo un livello di accesso ai dati, allora ci sono più gruppi di funzioni di utilità statiche e possono stare insieme, no? solo allora sarebbe diventata una classe così sporca e affollata (scusate il mio inglese)
Ahmad,

1
@Ahmad: ci sono un paio di ragioni e un paio di pro e contro da considerare. Questo potrebbe riempire un intero libro. Il ragionamento alla base di questo si riduce alla testabilità, manutenibilità e evolvibilità a lungo termine dei tuoi programmi, soprattutto quando hanno un ciclo di vita duraturo.
Doc Brown,

Qualcuno ha lavorato a un progetto in cui si è verificato un cambiamento significativo nella tecnologia DB? (senza una massiccia riscrittura?) Non l'ho fatto. In tal caso, seguire SRP, come suggerito qui per evitare di essere legati a quel DB, è stato di grande aiuto?
user949300,

@ user949300: Penso di non essermi espresso chiaramente. La classe studentesca non si lega a una specifica tecnologia DB, ma si lega a una specifica tecnologia di accesso al DB, quindi si aspetta qualcosa di simile a un DB SQL per la persistenza. Mantenere la classe Studente completamente inconsapevole del meccanismo di persistenza consente un riutilizzo della classe molto più semplice in contesti diversi. Ad esempio, in un contesto di test o in un contesto in cui gli oggetti sono memorizzati in un file. E questo è qualcosa che ho fatto davvero in passato molto spesso. Non è necessario modificare l'intero stack DB del sistema per trarne vantaggio.
Doc Brown,

3

Esistono alcuni modelli che possono essere utilizzati per la persistenza dei dati. C'è il modello Unità di lavoro , c'è il modello Repository , ci sono alcuni modelli aggiuntivi che possono essere usati come la facciata remota, e così via.

Molti di questi hanno i loro fan e i loro critici. Spesso si tratta di scegliere ciò che sembra più adatto all'applicazione e di attenersi ad esso (con tutti i suoi lati positivi e negativi, cioè non usare entrambi i modelli contemporaneamente ... a meno che tu non ne sia veramente sicuro).

Come nota a margine: nel tuo esempio RetrieveStudent, AddStudent dovrebbe essere metodi statici (perché non dipendono dall'istanza).

Un altro modo di avere metodi di salvataggio / caricamento in classe è:

class Animal{
    public string Name {get; set;}
    public void Save(){...}
    public static Animal Load(string name){....}
    public static string[] GetAllNames(){...} // if you just want to list them
    public static Animal[] GetAllAnimals(){...} // if you actually need to retrieve them all
}

Personalmente utilizzerei questo approccio solo in applicazioni abbastanza piccole, possibilmente strumenti per uso personale o in cui posso prevedere in modo affidabile che non avrò casi d'uso più complicati del semplice salvataggio o caricamento di oggetti.

Anche personalmente, vedi il modello Unità di lavoro. Quando lo conosci, è davvero buono sia in casi piccoli che grandi. Ed è supportato da molti framework / apis, ad esempio EntityFramework o RavenDB.


2
Nota a margine: sebbene, concettualmente, vi sia un solo DB mentre l'applicazione è in esecuzione, ciò non significa che si dovrebbero rendere statici i metodi RetriveStudent e AddStudent. I repository sono generalmente realizzati con interfacce per consentire agli sviluppatori di cambiare implementazione senza influire sul resto dell'applicazione. Ciò può anche consentire di distribuire l'applicazione con il supporto per diversi database, ad esempio. Questa stessa logica, ovviamente, si applica a molte altre aree oltre alla persistenza, come, ad esempio, l'interfaccia utente.
MichelHenrich,

Hai assolutamente ragione, ho fatto molte ipotesi basate sulla domanda originale.
Gerino,

grazie, penso che l'approccio migliore sia usare i livelli come menzionato nel modello di deposito
Ahmad

1

Se si tratta di un'app molto semplice in cui l'oggetto è più o meno legato al datastore e viceversa (cioè può essere pensato come una proprietà del datastore), allora potrebbe avere senso avere un metodo .save () per quella classe.

Ma penso che sarebbe piuttosto eccezionale.

Piuttosto, di solito è meglio lasciare che la classe gestisca i suoi dati e le sue funzionalità (come un buon cittadino OO) ed esternalizzare il meccanismo di persistenza ad un'altra classe o insieme di classi.

L'altra opzione è quella di utilizzare un framework di persistenza che definisce la persistenza in modo dichiarativo (come con le annotazioni), ma che sta ancora esternando la persistenza.


-1
  • Definire un metodo su quella classe che serializza l'oggetto e restituisce una sequenza di byte che è possibile inserire in un database o file o qualsiasi altra cosa
  • Avere un costruttore per quell'oggetto che accetta una tale sequenza di byte come input

Per quanto riguarda il RetrieveAllStudents()metodo, il tuo feeling è giusto, probabilmente è fuori luogo, perché potresti avere più liste distinte di studenti. Perché non tenere semplicemente gli elenchi fuori dalla Studentclasse?


Sai che ho visto una tale tendenza in alcuni framework PHP, ad esempio Laravel, se lo conosci. Il modello è collegato al database e può persino restituire una serie di oggetti.
Ahmad,
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.