2014-12-31 16 views
8

Ho un'applicazione che utilizza un database (MongoDB) per memorizzare le informazioni. In passato ho usato una classe piena di metodi statici per salvare e recuperare i dati, ma da allora ho capito che non è molto orientato agli oggetti, né a prova di futuro.Modelli di progettazione per il livello di accesso ai dati

Anche se è molto improbabile che cambierò il database, preferirei qualcosa che non mi leghi troppo a Mongo. Vorrei anche essere in grado di memorizzare i risultati nella cache con l'opzione per aggiornare l'oggetto memorizzato nella cache dal database, ma questo non è essenziale e può essere fatto in un altro posto.

Ho esaminato gli oggetti di accesso ai dati ma non sembrano molto ben definiti e non riesco a trovare alcun buon esempio di implementazione (in Java o in un linguaggio simile). Ho anche molti casi esclusivi, come la ricerca di nomi utente per il completamento di schede che sembrano non adatti e renderebbero il DAO ampio e gonfio.

Esistono modelli di progettazione che faciliterebbero l'acquisizione e il salvataggio di oggetti senza essere specifici del database? Buoni esempi di implementazione sarebbero utili (preferibilmente in Java).

risposta

33

Bene, l'approccio comune alla memorizzazione dei dati in Java è, come hai notato, non molto orientato agli oggetti. Questo di per sé non è né buono né cattivo: "l'orientazione all'oggetto" non è né vantaggio né svantaggio, è solo uno dei molti paradigmi, che a volte aiuta con una buona architettura (e talvolta non lo fa).

La ragione per cui i DAO in Java non sono in genere orientati agli oggetti è esattamente ciò che si desidera ottenere, riducendo la dipendenza dal database specifico. In un linguaggio meglio progettato, che ha permesso l'ereditarietà multipla, questo, o il corso, può essere fatto in modo molto elegante in un modo orientato agli oggetti, ma con java, sembra essere più un problema che non ne vale la pena.

In un senso più ampio, l'approccio non OO consente di separare i dati a livello di applicazione dal modo in cui sono archiviati. Questo è più della (non) dipendenza dalle specifiche di un particolare database, ma anche dagli schemi di archiviazione, che è particolarmente importante quando si usano i database relazionali (non farmi iniziare su ORM): si può avere uno schema relazionale ben progettato perfettamente tradotto nel modello OO dell'applicazione dal tuo DAO.

Quindi, ciò che la maggior parte dei DAO sono in Java al giorno d'oggi sono essenzialmente ciò che hai menzionato all'inizio: classi, piene di metodi statici. Una differenza è che, invece di rendere statici tutti i metodi, è meglio avere un singolo "metodo factory" statico (probabilmente, in una classe diversa), che restituisce un'istanza (singleton) del tuo DAO, che implementa una particolare interfaccia , utilizzato dal codice dell'applicazione per accedere al database:

public interface GreatDAO { 
    User getUser(int id); 
    void saveUser(User u); 
} 
public class TheGreatestDAO implements GreatDAO { 
    protected TheGeatestDAO(){} 
    ... 
} 
public class GreatDAOFactory { 
    private static GreatDAO dao = null; 
    protected static synchronized GreatDao setDAO(GreatDAO d) { 
     GreatDAO old = dao; 
     dao = d; 
     return old; 
    } 
    public static synchronized GreatDAO getDAO() { 
     return dao == null ? dao = new TheGreatestDAO() : dao; 
    } 
} 

public class App { 
    void setUserName(int id, String name) { 
      GreatDAO dao = GreatDAOFactory.getDao(); 
      User u = dao.getUser(id); 
      u.setName(name); 
      dao.saveUser(u); 
    } 
} 

Perché in questo modo rispetto ai metodi statici? Bene, e se decidessi di passare a un altro database? Naturalmente, dovresti creare una nuova classe DAO, implementando la logica per il tuo nuovo spazio di archiviazione. Se stavi usando metodi statici, ora dovresti passare tutto il codice, accedere al DAO e cambiarlo per usare la tua nuova classe, giusto? Questo potrebbe essere un enorme dolore. E se poi cambi idea e vuoi tornare al vecchio db?

Con questo approccio, è sufficiente modificare GreatDAOFactory.getDAO() e creare un'istanza di una classe diversa e tutto il codice dell'applicazione utilizzerà il nuovo database senza alcuna modifica.

Nella vita reale, ciò avviene spesso senza alcuna modifica del codice: il metodo factory ottiene il nome della classe di implementazione tramite un'impostazione di proprietà e lo istanzia con la reflection, quindi tutto ciò che è necessario fare per passare alle implementazioni è modificare un file di proprietà.Ci sono in realtà quadri - come spring o guice - che gestiscono questo meccanismo di "iniezione di dipendenza" per te, ma non entrerò nei dettagli, in primo luogo, perché è davvero al di là della portata della tua domanda, e anche, perché io non sono necessariamente convinto che il vantaggio derivante dall'utilizzo di tali framework valga la pena di integrarsi con loro per la maggior parte delle applicazioni.

Un altro vantaggio (probabilmente più probabilmente da sfruttare) di questo "approccio di fabbrica" ​​rispetto a quello statico è la testabilità. Immagina, che stai scrivendo un test unitario, che dovrebbe testare la logica della tua classe App indipendentemente da qualsiasi DAO sottostante. Non si desidera che utilizzi alcun reale storage sottostante per diversi motivi (velocità, dovendo configurarlo e ripulire afterwords, possibili collisioni con altri test, possibilità di risultati dei test inquinanti con problemi in DAO, non correlati a App, che viene effettivamente testato, ecc.).

Per fare ciò, si desidera un framework di test, come Mockito, che consente di "deridere" la funzionalità di qualsiasi oggetto o metodo, sostituendolo con un oggetto "fittizio", con comportamento predefinito (salterò i dettagli, perché, ancora una volta va oltre lo scopo). Quindi, puoi creare questo oggetto fittizio che sostituisce il tuo DAO e rendere GreatDAOFactory il tuo manichino invece della cosa reale chiamando il GreatDAOFactory.setDAO(dao) prima del test (e ripristinandolo dopo). Se stavi utilizzando metodi statici invece della classe di istanza, ciò non sarebbe possibile.

Un altro vantaggio, che è simile al passaggio da un database che ho descritto sopra è "sfruttamento" del tuo dao con funzionalità aggiuntive. Supponiamo che la tua applicazione diventi più lenta man mano che la quantità di dati nel database aumenta e tu decidi che hai bisogno di un livello di cache. Implementa una classe wrapper, che utilizza l'istanza dao reale (fornita come parametro costruttore) per accedere al database e memorizza nella cache gli oggetti letti in memoria, in modo che possano essere restituiti più rapidamente. È quindi possibile creare il proprio GreatDAOFactory.getDAO istanza in questo wrapper, affinché l'applicazione possa trarne vantaggio.

(Questo è chiamato "schema di delega" ... e sembra un dolore nel sedere, specialmente quando hai molti metodi definiti nel tuo DAO: dovrai implementarli tutti nel wrapper, anche per alterare comportamento di uno solo. In alternativa, potresti semplicemente sottoclassi il tuo dao e aggiungerai il caching in questo modo. Questa sarebbe una codifica molto meno noiosa in anticipo, ma potrebbe diventare problematica quando deciderai di cambiare il database, o, peggio, in avere un'opzione di commutazione delle implementazioni avanti e indietro).

Uno ugualmente ampiamente usato (ma, a mio parere, inferiore) alternativa al metodo "fabbrica" ​​sta facendo la dao una variabile membro in tutte le classi che ne hanno bisogno:

public class App { 
    GreatDao dao; 
    public App(GreatDao d) { dao = d; } 
} 

In questo modo, il codice che istanzia queste classi ha bisogno di istanziare l'oggetto dao (potrebbe ancora usare la fabbrica) e fornirlo come parametro costruttore. I framework di iniezione delle dipendenze che ho menzionato sopra, di solito fanno qualcosa di simile a questo.

Questo fornisce tutti i vantaggi dell'approccio "metodo di produzione", che ho descritto in precedenza, ma, come ho detto, non è buono secondo me. Gli svantaggi qui sono dover scrivere un costruttore per ciascuna delle tue classi di app, fare esattamente la stessa cosa sopra e sopra, e anche non essere in grado di istanziare facilmente le classi quando necessario, e alcune leggibilità perse: con una base di codice abbastanza grande , un lettore del tuo codice, che non ha familiarità con esso, avrà difficoltà a capire quale implementazione effettiva del dao viene utilizzata, come viene istanziata, se è un singleton, un'implementazione thread-safe, se mantiene lo stato, o cache qualsiasi cosa, come vengono prese le decisioni sulla scelta di una particolare implementazione, ecc.

+0

In realtà restituire un '' 'boolean''' per le operazioni di salvataggio/eliminazione per indicare che il persistere su disco/database è stato eseguito correttamente. – ambodi

+1

No, vuoi lanciare un'eccezione se fallisce comunque: ti permette di comunicare molte informazioni preziose sull'errore al chiamante, rispetto alla semplice restituzione di false, e rende anche meno ingombrante la gestione degli errori da parte del chiamante : invece di controllare lo stato di ogni chiamata e di propagarlo in cima allo stack, dovevano solo rilevare l'eccezione una volta, al livello in cui deve essere gestita. – Dima

+1

Questa è una risposta sottovalutata, ben fatta! Una domanda, perché 'setDAO' restituisce il DAO vecchio/precedente piuttosto che non restituire nulla? Come sarebbe usato? –

Problemi correlati